From 154f36d8d2875eaa317ef331b2257d39671f4c52 Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:05:05 +0100 Subject: [PATCH] fix: call stopLegacyCocod in restart/update/service paths and wait for pidfile release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'restart' command failed when the old daemon still held the cocod pidfile during wallet disposal — assertLegacyCocodNotRunning detected the PID as 'cocod running' and refused to start. - Call stopLegacyCocod() in restart, update restart, and service install (previously only in 'start'). - After /stop, poll for both health-down AND pidfile-released before spawning the new daemon. The pidfile is released during wallet disposal, which happens after server.close(), so health check alone was insufficient. --- src/cli.ts | 126 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 76b3f5d..ec5da58 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -146,16 +146,25 @@ async function restartDaemonsAfterUpdate(): Promise { // (draining active connections) then exits. await callDaemon("/stop", { method: "POST" }); - for (let i = 0; i < 50; i++) { + // Wait for daemon to fully stop — both the HTTP health check to fail + // AND the legacy cocod pidfile to be released. + const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); - if (!(await isDaemonRunning())) break; + const healthDown = !(await isDaemonRunning()); + const pidFileReleased = !existsSync(pidFilePath); + if (healthDown && pidFileReleased) break; } if (await isDaemonRunning()) { - throw new Error("routstrd did not stop within 5 seconds"); + throw new Error("routstrd did not stop within 10 seconds"); } console.log("routstrd daemon stopped."); + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet. + await stopLegacyCocod(); + console.log("Starting routstrd daemon..."); await startDaemon({ port: String(config.port || 8008), @@ -536,9 +545,97 @@ program program .command("balance") .description("Get wallet and API key balances") - .action(async () => { + .option("--api-keys", "List all stored API keys (baseUrl + key + balance)", false) + .option( + "--delete-api-keys ", + "Delete the API key stored for the given provider base URL (refunds balance first)", + ) + .option( + "--mint-url ", + "Mint URL to refund the deleted API key balance to (defaults to first mint in wallet)", + ) + .action(async (options: { apiKeys: boolean; deleteApiKeys?: string; mintUrl?: string }) => { await ensureDaemonRunning(); + // --delete-api-keys : refund then remove the API key for a + // single provider. + if (options.deleteApiKeys) { + const baseUrl = options.deleteApiKeys; + const queryParts = [`baseUrl=${encodeURIComponent(baseUrl)}`]; + if (options.mintUrl) { + queryParts.push(`mintUrl=${encodeURIComponent(options.mintUrl)}`); + } + const result = await callDaemon( + `/keys/api/delete?${queryParts.join("&")}`, + { method: "DELETE" }, + ); + if (result.error) { + console.log(result.error); + process.exit(1); + } + const out = result.output as + | { + baseUrl?: string; + removed?: boolean; + refunded?: boolean; + refundedAmount?: number; + refundMessage?: string; + message?: string; + } + | undefined; + if (out) { + console.log(out.message ?? `Removed API key for ${baseUrl}`); + if (out.refunded && out.refundedAmount !== undefined) { + console.log(` Refunded: ${out.refundedAmount} sats`); + } else if (out.refundMessage) { + console.log(` Refund: ${out.refundMessage}`); + } + } + return; + } + + // --api-keys: list every stored API key with full details. + if (options.apiKeys) { + const result = await callDaemon("/keys/api"); + if (result.error) { + console.log(result.error); + process.exit(1); + } + + const data = result.output as + | { + apiKeys: Array<{ + baseUrl: string; + key: string; + balance: number; + lastUsed: number | null; + }>; + count: number; + total: number; + unit: string; + } + | undefined; + + console.log("=== API Keys ===\n"); + if (!data || data.apiKeys.length === 0) { + console.log(" No API keys stored."); + return; + } + for (const k of data.apiKeys) { + const lastUsed = k.lastUsed + ? new Date(k.lastUsed).toISOString() + : "never"; + console.log(` ${k.baseUrl}`); + console.log(` key: ${k.key}`); + console.log(` balance: ${k.balance} ${data.unit}`); + console.log(` lastUsed: ${lastUsed}`); + console.log(""); + } + console.log(` Total: ${data.apiKeys.length} key(s), ${data.total} ${data.unit}`); + return; + } + + // Default: show the full wallet + API key balance summary. const [walletResult, keysResult] = await Promise.all([ callDaemon("/balance"), callDaemon("/keys/balance"), @@ -1645,6 +1742,10 @@ serviceCmd console.log("Starting routstrd via PM2..."); try { + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet. + await stopLegacyCocod(); + // Use --interpreter bun to ensure it runs with bun execSync(`pm2 start "${daemonPath}" --name routstrd --interpreter bun`, { stdio: "inherit", @@ -1702,16 +1803,21 @@ program console.log("Stopping daemon..."); await callDaemon("/stop", { method: "POST" }); - // Wait for daemon to fully stop - for (let i = 0; i < 50; i++) { + // Wait for daemon to fully stop — both the HTTP health check to fail + // AND the legacy cocod pidfile to be released (the daemon releases + // it during wallet disposal, which happens after server.close()). + const pidFilePath = `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod/cocod.pid`; + for (let i = 0; i < 100; i++) { await new Promise((resolve) => setTimeout(resolve, 100)); - if (!(await isDaemonRunning())) { + const healthDown = !(await isDaemonRunning()); + const pidFileReleased = !existsSync(pidFilePath); + if (healthDown && pidFileReleased) { break; } } if (await isDaemonRunning()) { - logger.error("Daemon failed to stop within 5 seconds"); + logger.error("Daemon failed to stop within 10 seconds"); process.exit(1); } console.log("Daemon stopped."); @@ -1719,6 +1825,10 @@ program console.log("Daemon was not running."); } + // Stop a legacy cocod daemon (from older routstrd versions) before + // starting the new in-process coco wallet — both cannot share coco.db. + await stopLegacyCocod(); + console.log("Starting daemon..."); await startDaemon({ port: options.port || String(config.port || 8008),