fix: call stopLegacyCocod in restart/update/service paths and wait for pidfile release

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.
This commit is contained in:
redshift
2026-07-23 21:05:05 +01:00
parent 95641e812f
commit 154f36d8d2
+118 -8
View File
@@ -146,16 +146,25 @@ async function restartDaemonsAfterUpdate(): Promise<void> {
// (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 <baseUrl>",
"Delete the API key stored for the given provider base URL (refunds balance first)",
)
.option(
"--mint-url <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 <baseUrl>: 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),