Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/code/src/main/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import dns from "node:dns";
import { mkdirSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { app, protocol } from "electron";
import { app, crashReporter, protocol } from "electron";
import { fixPath } from "./utils/fixPath";

const isDev = !app.isPackaged;
Expand Down Expand Up @@ -57,6 +57,8 @@ app.commandLine.appendSwitch("enable-logging", "file");
app.commandLine.appendSwitch("log-file", chromiumLogPath);
app.commandLine.appendSwitch("log-level", "0");

crashReporter.start({ uploadToServer: false });

// Force IPv4 resolution when "localhost" is used so the agent hits 127.0.0.1
// instead of ::1. This matches how the renderer already reaches the PostHog API.
dns.setDefaultResultOrder("ipv4first");
Expand Down
110 changes: 98 additions & 12 deletions apps/code/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "reflect-metadata";
import os from "node:os";
import { app } from "electron";
import { app, BrowserWindow } from "electron";
import log from "electron-log/main";
import "./utils/logger";
import "./services/index.js";
Expand All @@ -19,6 +19,7 @@ import type { NotificationService } from "./services/notification/service";
import type { OAuthService } from "./services/oauth/service";
import {
captureException,
getPostHogClient,
initializePostHog,
trackAppEvent,
} from "./services/posthog-analytics";
Expand All @@ -43,6 +44,102 @@ if (!gotTheLock) {
process.exit(0);
}

const RECOVERABLE_RENDER_REASONS = new Set([
"abnormal-exit",
"killed",
"crashed",
"oom",
"integrity-failure",
"memory-eviction",
]);
const CRASH_LOOP_WINDOW_MS = 30_000;
const CRASH_LOOP_THRESHOLD = 3;
const recentCrashTimestamps: number[] = [];

function isCrashLoop(): boolean {
const now = Date.now();
while (
recentCrashTimestamps.length > 0 &&
now - recentCrashTimestamps[0] > CRASH_LOOP_WINDOW_MS
) {
recentCrashTimestamps.shift();
}
recentCrashTimestamps.push(now);
return recentCrashTimestamps.length >= CRASH_LOOP_THRESHOLD;
}

app.on("render-process-gone", (_event, webContents, details) => {
const props = {
source: "main",
type: "render-process-gone",
reason: details.reason,
exitCode: String(details.exitCode),
url: webContents.getURL(),
title: webContents.getTitle(),
webContentsId: String(webContents.id),
};
log.error("Renderer process gone", {
...props,
chromiumLogTail: readChromiumLogTail(),
});
captureException(
new Error(`Renderer process gone: ${details.reason}`),
props,
);
getPostHogClient()
?.flush()
.catch(() => {});

if (RECOVERABLE_RENDER_REASONS.has(details.reason)) {
if (isCrashLoop()) {
log.error("Crash loop detected, stopping auto-recovery", {
crashesInWindow: recentCrashTimestamps.length,
windowMs: CRASH_LOOP_WINDOW_MS,
});
return;
}
log.info("Recovering from renderer crash", { reason: details.reason });
const win = BrowserWindow.fromWebContents(webContents);
if (!win || win.isDestroyed()) {
log.warn("No window to recover");
return;
}
setImmediate(() => {
if (win.isDestroyed()) return;
log.info("Reloading webContents");
win.webContents.reload();
log.info("Bringing window to foreground");
win.show();
win.moveTop();
Comment thread
jonathanlab marked this conversation as resolved.
win.focus();
app.focus({ steal: true });
});
}
});

app.on("child-process-gone", (_event, details) => {
const props = {
source: "main",
type: "child-process-gone",
processType: details.type,
reason: details.reason,
exitCode: String(details.exitCode),
serviceName: details.serviceName ?? "",
name: details.name ?? "",
};
log.error("Child process gone", {
...props,
chromiumLogTail: readChromiumLogTail(),
});
captureException(
new Error(`Child process gone (${details.type}): ${details.reason}`),
props,
);
getPostHogClient()
?.flush()
.catch(() => {});
});

async function initializeServices(): Promise<void> {
container.get<DatabaseService>(MAIN_TOKENS.DatabaseService);
container.get<OAuthService>(MAIN_TOKENS.OAuthService);
Expand Down Expand Up @@ -111,17 +208,6 @@ app.on("window-all-closed", () => {
app.quit();
});

app.on("child-process-gone", (_event, details) => {
log.error("Child process gone", {
type: details.type,
reason: details.reason,
exitCode: details.exitCode,
serviceName: details.serviceName,
name: details.name,
chromiumLogTail: readChromiumLogTail(),
});
});

app.on("before-quit", async (event) => {
let lifecycleService: AppLifecycleService;
try {
Expand Down
85 changes: 85 additions & 0 deletions apps/code/src/main/menu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { readdirSync, statSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import {
app,
BrowserWindow,
Expand All @@ -17,6 +19,31 @@ import type { UpdatesService } from "./services/updates/service";
import { isDevBuild } from "./utils/env";
import { getLogFilePath } from "./utils/logger";

function findLatestCrashDump(): string | null {
const pendingDir = path.join(app.getPath("crashDumps"), "pending");
let entries: string[];
try {
entries = readdirSync(pendingDir);
} catch {
return null;
}
let latest: { file: string; mtimeMs: number } | null = null;
for (const name of entries) {
if (!name.endsWith(".dmp")) continue;
const full = path.join(pendingDir, name);
let mtimeMs: number;
try {
mtimeMs = statSync(full).mtimeMs;
} catch {
continue;
}
if (!latest || mtimeMs > latest.mtimeMs) {
latest = { file: full, mtimeMs };
}
}
return latest?.file ?? null;
}

function getSystemInfo(): string {
const commit = __BUILD_COMMIT__ ?? "dev";
const buildDate = __BUILD_DATE__ ?? "dev";
Expand Down Expand Up @@ -124,6 +151,64 @@ function buildFileMenu(): MenuItemConstructorOptions {
shell.showItemInFolder(getLogFilePath());
},
},
{
label:
process.platform === "darwin"
? "Show crash dumps in Finder"
: "Show crash dumps in file manager",
click: () => {
const latest = findLatestCrashDump();
if (latest) {
shell.showItemInFolder(latest);
return;
}
const pendingDir = path.join(
app.getPath("crashDumps"),
"pending",
);
void shell.openPath(pendingDir).then((err) => {
if (err) void shell.openPath(app.getPath("crashDumps"));
});
},
},
...(isDevBuild()
? [
{
label: "Test: terminate renderer (forced shutdown, no fault)",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (!win) return;
win.webContents.forcefullyCrashRenderer();
},
},
{
label: "Test: crash renderer (in-process, EXC_BAD_ACCESS)",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (!win) return;
void win.webContents.executeJavaScript(
"window.__posthogCodeTest.crash()",
);
},
},
{
label: "Test: abort renderer (in-process, SIGABRT)",
click: () => {
const win = BrowserWindow.getFocusedWindow();
if (!win) return;
void win.webContents.executeJavaScript(
"window.__posthogCodeTest.abort()",
);
},
},
{
label: "Test: crash main process (SIGABRT)",
click: () => {
process.crash();
},
},
]
: []),
{ type: "separator" },
{
label: "Invalidate OAuth token",
Expand Down
11 changes: 11 additions & 0 deletions apps/code/src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ contextBridge.exposeInMainWorld("electronUtils", {
getPathForFile: (file: File) => webUtils.getPathForFile(file),
});

if (process.argv.includes("--posthog-code-dev")) {
contextBridge.exposeInMainWorld("__posthogCodeTest", {
crash: () => {
process.crash();
},
abort: () => {
process.abort();
},
});
}

process.once("loaded", async () => {
exposeElectronTRPC();
});
7 changes: 6 additions & 1 deletion apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { createIPCHandler } from "@posthog/electron-trpc/main";
import {
app,
BrowserWindow,
Menu,
type MenuItemConstructorOptions,
Expand Down Expand Up @@ -53,7 +54,7 @@ function getSavedWindowState(): WindowStateSchema {
return state;
}

function saveWindowState(window: BrowserWindow): void {
export function saveWindowState(window: BrowserWindow): void {
const isMaximized = window.isMaximized();
windowStateStore.set("isMaximized", isMaximized);

Expand Down Expand Up @@ -192,6 +193,7 @@ export function createWindow(): void {
preload: path.join(__dirname, "preload.js"),
enableBlinkFeatures: "GetDisplayMedia",
partition: "persist:main",
additionalArguments: isDev ? ["--posthog-code-dev"] : [],
...(isDev && { webSecurity: false }),
},
});
Expand All @@ -205,6 +207,9 @@ export function createWindow(): void {
mainWindow?.maximize();
}
mainWindow?.show();
mainWindow?.moveTop();
mainWindow?.focus();
app.focus({ steal: true });
};

mainWindow.once("ready-to-show", showWindow);
Expand Down
3 changes: 3 additions & 0 deletions mprocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ procs:
6-mobile-ios:
shell: 'node scripts/pnpm-run.mjs --filter @posthog/mobile run ios'
autostart: false

7-chromium-log:
shell: 'tail -F ~/.posthog-code/logs-dev/chromium.log'
Loading