fix: surface real daemon error on startup failure and hint kill PID

The CLI spawned the daemon detached with stdout/stderr redirected to
debug.log, so when the daemon exited early the user only saw a useless
'Check logs in ~/.routstrd/logs' message while the actual error sat
silently in the log file.

- daemon/index.ts: print fatal startup error to stderr (captured by the
  spawn redirect) in addition to the dated logger.
- start-daemon.ts: record debug.log offset before spawning; on early exit
  read the newly appended output and include it in the thrown error.
- coco-client.ts: add 'kill <PID>' alternative to the legacy-cocod guard
  error message alongside 'cocod stop'.
This commit is contained in:
redshift
2026-07-23 12:25:56 +01:00
parent a583aaa482
commit e46c3fa37f
3 changed files with 38 additions and 4 deletions
+6
View File
@@ -335,6 +335,12 @@ async function main(): Promise<void> {
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);
});
}
+2 -2
View File
@@ -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 },
);
}
+30 -2
View File
@@ -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<boolean> {
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}`),
);
}