diff --git a/src/daemon/index.ts b/src/daemon/index.ts index 37d3515..713f204 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -335,6 +335,12 @@ async function main(): Promise { if (import.meta.main) { main().catch((error) => { logger.error("Failed to start Routstr daemon:", error); + // Also write to stderr so the spawning CLI can surface the real error + // (stdout/stderr are redirected to debug.log by start-daemon.ts). + console.error( + "Failed to start Routstr daemon:", + error instanceof Error ? error.message : error, + ); process.exit(1); }); } diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index f4c10c5..303c00a 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -142,7 +142,7 @@ export async function assertLegacyCocodNotRunning( throw new Error( `Legacy cocod daemon is still running with PID ${runningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + - "Run 'cocod stop' and try again.", + `Run 'cocod stop' or 'kill ${runningPid}' and try again.`, ); } @@ -169,7 +169,7 @@ export async function assertLegacyCocodNotRunning( throw new Error( `Legacy cocod daemon is still running with PID ${newlyRunningPid}. ` + "Refusing to open the wallet database because cocod and coco-core cannot safely use it at the same time. " + - "Run 'cocod stop' and try again.", + `Run 'cocod stop' or 'kill ${newlyRunningPid}' and try again.`, { cause: error }, ); } diff --git a/src/start-daemon.ts b/src/start-daemon.ts index f807886..b20d150 100644 --- a/src/start-daemon.ts +++ b/src/start-daemon.ts @@ -1,4 +1,4 @@ -import { openSync } from "fs"; +import { openSync, existsSync, readFileSync } from "fs"; import { logger } from "./utils/logger"; import { CONFIG_DIR, LOGS_DIR } from "./utils/config"; import { withCrossProcessLock } from "./utils/process-lock"; @@ -6,6 +6,27 @@ import { withCrossProcessLock } from "./utils/process-lock"; const DAEMON_STARTUP_LOCK_PATH = `${CONFIG_DIR}/routstrd-startup.lock`; const DEBUG_LOG_PATH = `${CONFIG_DIR}/debug.log`; +/** + * The spawned daemon's stdout/stderr is redirected to DEBUG_LOG_PATH, so when + * it exits early we can surface its actual error from there instead of just + * telling the user to go read logs. + */ +function readDaemonOutput(offset: number): string { + try { + if (!existsSync(DEBUG_LOG_PATH)) return ""; + const content = readFileSync(DEBUG_LOG_PATH, "utf-8"); + return content + .slice(offset) + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .slice(-30) + .join("\n"); + } catch { + return ""; + } +} + async function isDaemonHealthy(port: string): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); @@ -44,6 +65,9 @@ async function startDaemonUnlocked( const daemonScript = new URL("./daemon/index.js", import.meta.url).pathname; const shellCmd = `bun run "${daemonScript}" ${args.map((a) => `'${a}'`).join(" ")}`; + const debugLogOffset = existsSync(DEBUG_LOG_PATH) + ? readFileSync(DEBUG_LOG_PATH, "utf-8").length + : 0; const debugLogFd = openSync(DEBUG_LOG_PATH, "a"); const proc = Bun.spawn(["sh", "-c", shellCmd], { @@ -65,8 +89,12 @@ async function startDaemonUnlocked( await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); if (exitCode !== null) { + const daemonOutput = readDaemonOutput(debugLogOffset); throw new Error( - `Daemon process exited early with code ${exitCode}. Check logs in ${LOGS_DIR}`, + `Daemon process exited early with code ${exitCode}.` + + (daemonOutput + ? `\n\nDaemon output:\n${daemonOutput}` + : ` Check logs in ${LOGS_DIR}`), ); }