Skip to content

Commit a72b8be

Browse files
committed
Fix desktop startup error (logging + crash handlers)
1 parent bfa8f7f commit a72b8be

1 file changed

Lines changed: 219 additions & 30 deletions

File tree

desktop/main.cjs

Lines changed: 219 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
const crypto = require("crypto");
22
const fs = require("fs");
33
const path = require("path");
4-
const { pathToFileURL } = require("url");
4+
const net = require("net");
5+
const { spawn } = require("child_process");
56
const { app, BrowserWindow, shell, dialog } = require("electron");
67
const { autoUpdater } = require("electron-updater");
78

89
const APP_HOST = "127.0.0.1";
910
const APP_PORT = 5510;
10-
const SERVER_READY_TIMEOUT_MS = 20000;
11+
const SERVER_READY_TIMEOUT_MS = 60000;
12+
const SERVER_READY_RETRY_MS = 300;
1113
const UPDATE_CHECK_DELAY_MS = 5000;
12-
const STARTUP_LOG_RELATIVE_PATH = path.join("logs", "startup.log");
14+
const DESKTOP_STARTUP_LOG_FILENAME = "desktop-startup.log";
15+
const STARTUP_LOG_PREVIEW_LINES = 20;
1316
let updaterInitialized = false;
17+
let selectedPort = APP_PORT;
18+
let startupLogPath = path.resolve(process.cwd(), DESKTOP_STARTUP_LOG_FILENAME);
19+
let embeddedServerProcess = null;
20+
let isQuittingApp = false;
21+
let bootstrapCompleted = false;
22+
const startupDebugEnabled = process.env.SINGBETTER_DEBUG_STARTUP === "1";
1423

1524
const logDesktop = (message) => {
1625
console.log(`[desktop] ${message}`);
@@ -20,16 +29,29 @@ const ensureDir = (targetPath) => {
2029
fs.mkdirSync(targetPath, { recursive: true });
2130
};
2231

23-
const appendStartupLog = (userDataDir, message, error) => {
32+
const getStartupLogPath = () => {
33+
if (startupLogPath) return startupLogPath;
2434
try {
25-
const logsDir = path.join(userDataDir, "logs");
26-
ensureDir(logsDir);
27-
const startupLogPath = path.join(logsDir, "startup.log");
35+
const userDataDir = app.getPath("userData");
36+
ensureDir(userDataDir);
37+
startupLogPath = path.join(userDataDir, DESKTOP_STARTUP_LOG_FILENAME);
38+
return startupLogPath;
39+
} catch (_error) {
40+
// ignore and fallback
41+
}
42+
startupLogPath = path.resolve(process.cwd(), DESKTOP_STARTUP_LOG_FILENAME);
43+
return startupLogPath;
44+
};
45+
46+
const appendStartupLog = (message, error) => {
47+
try {
48+
const logPath = getStartupLogPath();
49+
ensureDir(path.dirname(logPath));
2850
const errorSuffix = error
2951
? ` | error=${error instanceof Error ? error.stack || error.message : String(error)}`
3052
: "";
3153
fs.appendFileSync(
32-
startupLogPath,
54+
logPath,
3355
`[${new Date().toISOString()}] ${message}${errorSuffix}\n`,
3456
"utf8",
3557
);
@@ -38,6 +60,53 @@ const appendStartupLog = (userDataDir, message, error) => {
3860
}
3961
};
4062

63+
const readStartupLogTail = (lineCount = STARTUP_LOG_PREVIEW_LINES) => {
64+
try {
65+
const logPath = getStartupLogPath();
66+
if (!fs.existsSync(logPath)) return "(log file does not exist yet)";
67+
const lines = fs
68+
.readFileSync(logPath, "utf8")
69+
.split(/\r?\n/)
70+
.filter(Boolean);
71+
if (lines.length === 0) return "(log file is empty)";
72+
return lines.slice(-lineCount).join("\n");
73+
} catch (error) {
74+
return `Unable to read startup log tail: ${error instanceof Error ? error.message : String(error)}`;
75+
}
76+
};
77+
78+
const showStartupFailureDialog = (error, title = "Desktop failed to start embedded server") => {
79+
const message = error instanceof Error ? error.stack || error.message : String(error);
80+
appendStartupLog(title, error);
81+
const details = [
82+
title,
83+
`Log file: ${getStartupLogPath()}`,
84+
"",
85+
"Recent log lines:",
86+
readStartupLogTail(),
87+
].join("\n");
88+
dialog.showErrorBox("Desktop Startup Error", `${message}\n\n${details}`);
89+
};
90+
91+
const installGlobalCrashHandlers = () => {
92+
process.on("uncaughtException", (error) => {
93+
appendStartupLog("Main process uncaughtException", error);
94+
showStartupFailureDialog(error);
95+
if (!bootstrapCompleted) {
96+
app.quit();
97+
}
98+
});
99+
100+
process.on("unhandledRejection", (reason) => {
101+
const error = reason instanceof Error ? reason : new Error(String(reason));
102+
appendStartupLog("Main process unhandledRejection", error);
103+
showStartupFailureDialog(error);
104+
if (!bootstrapCompleted) {
105+
app.quit();
106+
}
107+
});
108+
};
109+
41110
const resolveFileDatabasePath = (databaseUrl) => {
42111
if (!databaseUrl || typeof databaseUrl !== "string") return null;
43112
if (!databaseUrl.startsWith("file:") && !databaseUrl.startsWith("sqlite:")) return null;
@@ -90,22 +159,57 @@ const loadOrCreateSessionSecret = (secretPath) => {
90159

91160
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
92161

93-
const waitForServer = async (url) => {
162+
const isPortAvailable = (host, port) =>
163+
new Promise((resolve) => {
164+
const tester = net.createServer();
165+
tester.once("error", () => resolve(false));
166+
tester.once("listening", () => {
167+
tester.close(() => resolve(true));
168+
});
169+
tester.listen({ host, port });
170+
});
171+
172+
const resolveDesktopPort = async (host, preferredPort) => {
173+
if (await isPortAvailable(host, preferredPort)) return preferredPort;
174+
for (let offset = 1; offset <= 25; offset += 1) {
175+
const candidatePort = preferredPort + offset;
176+
if (await isPortAvailable(host, candidatePort)) {
177+
appendStartupLog(`Port ${preferredPort} is busy, using ${candidatePort} instead`);
178+
return candidatePort;
179+
}
180+
}
181+
throw new Error(
182+
`No free port found near ${preferredPort}. Close any process using localhost ports ${preferredPort}-${preferredPort + 25}.`,
183+
);
184+
};
185+
186+
const waitForServer = async (url, serverProcess) => {
94187
const start = Date.now();
188+
appendStartupLog(`Waiting for server readiness at ${url}/api/health`);
95189
while (Date.now() - start < SERVER_READY_TIMEOUT_MS) {
190+
if (serverProcess && serverProcess.exitCode !== null) {
191+
throw new Error(
192+
`Embedded server exited before readiness (code=${serverProcess.exitCode}, signal=${serverProcess.signalCode || "none"})`,
193+
);
194+
}
96195
try {
97196
const response = await fetch(`${url}/api/health`, {
98197
method: "GET",
99198
});
100199
if (response.ok || response.status === 401 || response.status === 403) {
200+
appendStartupLog(`Embedded server ready at ${url}/api/health (status ${response.status})`);
101201
return;
102202
}
103-
} catch (_error) {
104-
// Ignore startup race and retry.
203+
} catch (error) {
204+
if (startupDebugEnabled) {
205+
appendStartupLog("Server health probe failed; retrying", error);
206+
}
105207
}
106-
await wait(300);
208+
await wait(SERVER_READY_RETRY_MS);
107209
}
108-
throw new Error("Timed out while waiting for desktop server startup");
210+
throw new Error(
211+
`Timed out while waiting for desktop server startup at ${url}/api/health after ${SERVER_READY_TIMEOUT_MS}ms`,
212+
);
109213
};
110214

111215
const readUpdatePublishTarget = () => {
@@ -228,10 +332,11 @@ const configureDesktopEnvironment = () => {
228332
const dbPath = path.join(dataDir, "singbetter.db");
229333
const legacyDesktopDbPath = path.join(dataDir, "desktop.db");
230334
const secretPath = path.join(dataDir, "session.secret");
231-
const startupLogPath = path.join(dataDir, STARTUP_LOG_RELATIVE_PATH);
335+
startupLogPath = path.join(dataDir, DESKTOP_STARTUP_LOG_FILENAME);
232336

233337
ensureDir(dataDir);
234338
ensureDir(uploadsDir);
339+
appendStartupLog(`Desktop startup initialized userData=${dataDir}`);
235340

236341
const envDatabasePath = resolveFileDatabasePath(process.env.DATABASE_URL);
237342
const legacyProjectDbPath = path.resolve(process.cwd(), "dev.db");
@@ -249,20 +354,20 @@ const configureDesktopEnvironment = () => {
249354
dbSource = resolved.source;
250355
migratedFrom = resolved.migratedFrom;
251356
} catch (error) {
252-
appendStartupLog(dataDir, "DB source resolution failed; continuing with fresh DB path", error);
357+
appendStartupLog("DB source resolution failed; continuing with fresh DB path", error);
253358
try {
254359
if (!fs.existsSync(dbPath)) {
255360
fs.closeSync(fs.openSync(dbPath, "w"));
256361
}
257362
} catch (fileError) {
258-
appendStartupLog(dataDir, "Failed to create fresh DB placeholder file", fileError);
363+
appendStartupLog("Failed to create fresh DB placeholder file", fileError);
259364
}
260365
}
261366

262367
process.env.NODE_ENV = "production";
263368
process.env.DESKTOP_APP = "1";
264369
process.env.HOST = APP_HOST;
265-
process.env.PORT = String(APP_PORT);
370+
process.env.PORT = String(selectedPort);
266371
process.env.DATABASE_URL = `file:${dbPath}`;
267372
process.env.USE_JSON_DB = "false";
268373
process.env.AUTH_PROVIDER = process.env.AUTH_PROVIDER || "local";
@@ -274,6 +379,7 @@ const configureDesktopEnvironment = () => {
274379
process.env.UPLOADS_DIR = uploadsDir;
275380
process.env.SESSION_SECRET = loadOrCreateSessionSecret(secretPath);
276381
process.env.STARTUP_LOG_PATH = startupLogPath;
382+
process.env.CORS_ALLOWED_ORIGINS = `http://${APP_HOST}:${selectedPort}`;
277383

278384
if (migratedFrom) {
279385
logDesktop(`DB resolved at: ${dbPath} (source: migrated)`);
@@ -282,8 +388,17 @@ const configureDesktopEnvironment = () => {
282388
logDesktop(`DB resolved at: ${dbPath} (source: ${dbSource})`);
283389
}
284390
logDesktop(
285-
`Runtime paths desktopApp=${process.env.DESKTOP_APP} userData=${dataDir} db=${dbPath} uploads=${uploadsDir}`,
391+
`Runtime paths desktopApp=${process.env.DESKTOP_APP} userData=${dataDir} db=${dbPath} uploads=${uploadsDir} port=${selectedPort}`,
392+
);
393+
appendStartupLog(
394+
`Runtime configured host=${APP_HOST} port=${selectedPort} db=${dbPath} uploads=${uploadsDir} debug=${startupDebugEnabled ? "1" : "0"}`,
286395
);
396+
397+
if (startupDebugEnabled) {
398+
appendStartupLog(
399+
`Debug startup details cwd=${process.cwd()} resourcesPath=${process.resourcesPath} appPath=${app.getAppPath()}`,
400+
);
401+
}
287402
};
288403

289404
const getServerEntryPath = () => path.resolve(__dirname, "..", "dist", "index.js");
@@ -295,10 +410,66 @@ const startEmbeddedServer = async () => {
295410
`Desktop bundle is missing server build at: ${serverEntry}. Run 'npm run build' first.`,
296411
);
297412
}
298-
await import(pathToFileURL(serverEntry).href);
413+
414+
const serverEnv = {
415+
...process.env,
416+
NODE_ENV: "production",
417+
DESKTOP_APP: "1",
418+
HOST: APP_HOST,
419+
PORT: String(selectedPort),
420+
ELECTRON_RUN_AS_NODE: "1",
421+
};
422+
423+
appendStartupLog(
424+
`Spawning embedded server command="${process.execPath}" args="${serverEntry}" host=${APP_HOST} port=${selectedPort}`,
425+
);
426+
427+
const child = spawn(process.execPath, [serverEntry], {
428+
cwd: path.resolve(__dirname, ".."),
429+
env: serverEnv,
430+
stdio: ["ignore", "pipe", "pipe"],
431+
windowsHide: true,
432+
});
433+
embeddedServerProcess = child;
434+
435+
child.stdout.on("data", (chunk) => {
436+
const output = chunk.toString().trim();
437+
if (!output) return;
438+
appendStartupLog(`[server:stdout] ${output}`);
439+
if (startupDebugEnabled) {
440+
logDesktop(`[server:stdout] ${output}`);
441+
}
442+
});
443+
444+
child.stderr.on("data", (chunk) => {
445+
const output = chunk.toString().trim();
446+
if (!output) return;
447+
appendStartupLog(`[server:stderr] ${output}`);
448+
logDesktop(`[server:stderr] ${output}`);
449+
});
450+
451+
child.on("error", (error) => {
452+
appendStartupLog("Embedded server child process error", error);
453+
if (!bootstrapCompleted) {
454+
showStartupFailureDialog(error);
455+
app.quit();
456+
}
457+
});
458+
459+
child.on("exit", (code, signal) => {
460+
appendStartupLog(`Embedded server child exited code=${code ?? "null"} signal=${signal ?? "null"}`);
461+
if (!bootstrapCompleted && !isQuittingApp) {
462+
showStartupFailureDialog(
463+
new Error(`Embedded server exited before startup completed (code=${code ?? "null"}, signal=${signal ?? "null"})`),
464+
);
465+
app.quit();
466+
}
467+
});
468+
469+
return child;
299470
};
300471

301-
const createMainWindow = async () => {
472+
const createMainWindow = async (serverUrl) => {
302473
const windowIconPath = resolveDesktopIconPath();
303474
const mainWindow = new BrowserWindow({
304475
width: 1360,
@@ -314,14 +485,16 @@ const createMainWindow = async () => {
314485
sandbox: true,
315486
webSecurity: true,
316487
allowRunningInsecureContent: false,
317-
devTools: !app.isPackaged,
488+
devTools: !app.isPackaged || startupDebugEnabled,
318489
},
319490
});
320491

321-
const appUrl = `http://${APP_HOST}:${APP_PORT}`;
322-
const appOrigin = new URL(appUrl).origin;
323-
await waitForServer(appUrl);
324-
await mainWindow.loadURL(appUrl);
492+
const appOrigin = new URL(serverUrl).origin;
493+
await mainWindow.loadURL(serverUrl);
494+
495+
if (startupDebugEnabled) {
496+
mainWindow.webContents.openDevTools({ mode: "detach" });
497+
}
325498

326499
mainWindow.webContents.on("will-navigate", (event, url) => {
327500
const targetOrigin = (() => {
@@ -351,22 +524,36 @@ const createMainWindow = async () => {
351524
};
352525

353526
const bootstrap = async () => {
527+
selectedPort = await resolveDesktopPort(APP_HOST, APP_PORT);
354528
configureDesktopEnvironment();
355-
await startEmbeddedServer();
356-
const mainWindow = await createMainWindow();
529+
const serverUrl = `http://${APP_HOST}:${selectedPort}`;
530+
const child = await startEmbeddedServer();
531+
await waitForServer(serverUrl, child);
532+
const mainWindow = await createMainWindow(serverUrl);
533+
bootstrapCompleted = true;
534+
appendStartupLog(`Desktop startup completed successfully at ${serverUrl}`);
357535
initAutoUpdates(mainWindow);
358536
};
359537

360538
app.whenReady().then(async () => {
361539
try {
362540
await bootstrap();
363541
} catch (error) {
364-
const message = error instanceof Error ? error.message : String(error);
365-
dialog.showErrorBox("Desktop Startup Error", message);
542+
showStartupFailureDialog(error);
366543
app.quit();
367544
}
368545
});
369546

547+
installGlobalCrashHandlers();
548+
549+
app.on("before-quit", () => {
550+
isQuittingApp = true;
551+
if (embeddedServerProcess && embeddedServerProcess.exitCode === null) {
552+
appendStartupLog("Stopping embedded server process");
553+
embeddedServerProcess.kill();
554+
}
555+
});
556+
370557
app.on("window-all-closed", () => {
371558
if (process.platform !== "darwin") {
372559
app.quit();
@@ -376,7 +563,9 @@ app.on("window-all-closed", () => {
376563
app.on("activate", async () => {
377564
if (BrowserWindow.getAllWindows().length === 0) {
378565
try {
379-
await createMainWindow();
566+
const serverUrl = `http://${APP_HOST}:${selectedPort}`;
567+
await waitForServer(serverUrl, embeddedServerProcess);
568+
await createMainWindow(serverUrl);
380569
} catch (_error) {
381570
app.quit();
382571
}

0 commit comments

Comments
 (0)