@routstr/cocod 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routstr/cocod",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli-shared.ts CHANGED
@@ -42,6 +42,36 @@ export async function isDaemonRunning(): Promise<boolean> {
42
42
  }
43
43
  }
44
44
 
45
+ const DAEMON_POLL_INTERVAL_MS = 100;
46
+ const DAEMON_SLOW_START_WARNING_MS = 30_000;
47
+
48
+ function sleep(ms: number): Promise<void> {
49
+ return new Promise((resolve) => setTimeout(resolve, ms));
50
+ }
51
+
52
+ async function waitForDaemonReady(startedAt: number, warningShown: { value: boolean }): Promise<void> {
53
+ for (;;) {
54
+ try {
55
+ const result = await callDaemon("/status");
56
+ if (typeof result.output === "string") {
57
+ const status = result.output;
58
+ if (status === "LOCKED" || status === "UNLOCKED" || status === "ERROR") {
59
+ return;
60
+ }
61
+ }
62
+ } catch {
63
+ // Daemon may not be accepting requests yet
64
+ }
65
+
66
+ if (!warningShown.value && Date.now() - startedAt >= DAEMON_SLOW_START_WARNING_MS) {
67
+ warningShown.value = true;
68
+ console.log("Daemon is taking longer than expected, please wait...");
69
+ }
70
+
71
+ await sleep(DAEMON_POLL_INTERVAL_MS);
72
+ }
73
+ }
74
+
45
75
  export async function startDaemonProcess(): Promise<void> {
46
76
  const proc = Bun.spawn({
47
77
  cmd: ["bun", "run", `${import.meta.dir}/index.ts`, "daemon"],
@@ -51,14 +81,21 @@ export async function startDaemonProcess(): Promise<void> {
51
81
  });
52
82
  proc.unref();
53
83
 
54
- for (let i = 0; i < 50; i++) {
55
- await new Promise((resolve) => setTimeout(resolve, 100));
84
+ const startedAt = Date.now();
85
+ const warningShown = { value: false };
86
+
87
+ for (;;) {
88
+ await sleep(DAEMON_POLL_INTERVAL_MS);
56
89
  if (await isDaemonRunning()) {
90
+ await waitForDaemonReady(startedAt, warningShown);
57
91
  return;
58
92
  }
59
- }
60
93
 
61
- throw new Error("Daemon failed to start within 5 seconds");
94
+ if (!warningShown.value && Date.now() - startedAt >= DAEMON_SLOW_START_WARNING_MS) {
95
+ warningShown.value = true;
96
+ console.log("Daemon is taking longer than expected, please wait...");
97
+ }
98
+ }
62
99
  }
63
100
 
64
101
  export async function ensureDaemonRunning(): Promise<void> {
package/src/daemon.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { mnemonicToSeedSync } from "@scure/bip39";
2
+ import { closeSync, openSync, writeFileSync } from "node:fs";
2
3
  import { unlink } from "node:fs/promises";
4
+ import process from "node:process";
3
5
  import { CONFIG_FILE, SOCKET_PATH, PID_FILE } from "./utils/config.js";
4
6
  import { createDaemonLogger, serializeError } from "./utils/logger.js";
5
7
  import { DaemonStateManager } from "./utils/state.js";
@@ -7,6 +9,69 @@ import { initializeWallet } from "./utils/wallet.js";
7
9
  import { createRouteHandlers, buildRoutes } from "./routes.js";
8
10
  import type { WalletConfig } from "./utils/config.js";
9
11
 
12
+ async function isProcessAlive(pid: number): Promise<boolean> {
13
+ if (!Number.isInteger(pid) || pid <= 0) {
14
+ return false;
15
+ }
16
+
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ async function acquirePidLock(logger: ReturnType<typeof createDaemonLogger>): Promise<void> {
26
+ const pidFile = Bun.file(PID_FILE);
27
+ if (await pidFile.exists()) {
28
+ const existingPidText = (await pidFile.text()).trim();
29
+ const existingPid = Number.parseInt(existingPidText, 10);
30
+
31
+ if (await isProcessAlive(existingPid)) {
32
+ logger.warn("daemon.start.skipped", {
33
+ reason: "already_running",
34
+ pid: existingPid,
35
+ pidFile: PID_FILE,
36
+ });
37
+ await logger.flush();
38
+ console.error(`Error: Daemon is already running with PID ${existingPid}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ logger.warn("daemon.pid.stale", {
43
+ pid: existingPidText || null,
44
+ pidFile: PID_FILE,
45
+ });
46
+ try {
47
+ await unlink(PID_FILE);
48
+ } catch {
49
+ // File may already be gone
50
+ }
51
+ }
52
+
53
+ try {
54
+ const fd = openSync(PID_FILE, "wx");
55
+ try {
56
+ writeFileSync(fd, `${process.pid}`);
57
+ } finally {
58
+ closeSync(fd);
59
+ }
60
+ } catch {
61
+ const currentPidText = (await Bun.file(PID_FILE).text()).trim();
62
+ const currentPid = Number.parseInt(currentPidText, 10);
63
+
64
+ logger.warn("daemon.start.skipped", {
65
+ reason: "pid_lock_exists",
66
+ pid: Number.isNaN(currentPid) ? currentPidText : currentPid,
67
+ pidFile: PID_FILE,
68
+ });
69
+ await logger.flush();
70
+ console.error("Error: Daemon is already starting or running");
71
+ process.exit(1);
72
+ }
73
+ }
74
+
10
75
  export async function startDaemon() {
11
76
  const stateManager = new DaemonStateManager();
12
77
  const logger = createDaemonLogger();
@@ -16,6 +81,8 @@ export async function startDaemon() {
16
81
  socketPath: SOCKET_PATH,
17
82
  });
18
83
 
84
+ await acquirePidLock(logger);
85
+
19
86
  try {
20
87
  const testConn = await Bun.connect({
21
88
  unix: SOCKET_PATH,
@@ -31,6 +98,11 @@ export async function startDaemon() {
31
98
  reason: "already_running",
32
99
  socketPath: SOCKET_PATH,
33
100
  });
101
+ try {
102
+ await unlink(PID_FILE);
103
+ } catch {
104
+ // File might not exist
105
+ }
34
106
  await logger.flush();
35
107
  console.error(`Error: Daemon is already running on ${SOCKET_PATH}`);
36
108
  process.exit(1);
@@ -38,25 +110,11 @@ export async function startDaemon() {
38
110
  // Not running, safe to proceed
39
111
  }
40
112
 
41
- try {
42
- await Bun.write(PID_FILE, "");
43
- await unlink(PID_FILE);
44
- } catch {
45
- // Directory creation failed or file didn't exist
46
- }
47
-
48
113
  try {
49
114
  await unlink(SOCKET_PATH);
50
115
  } catch {
51
116
  // File might not exist
52
117
  }
53
- try {
54
- await unlink(PID_FILE);
55
- } catch {
56
- // File might not exist
57
- }
58
-
59
- await Bun.write(PID_FILE, process.pid.toString());
60
118
 
61
119
  try {
62
120
  const configExists = await Bun.file(CONFIG_FILE).exists();
@@ -87,6 +145,7 @@ export async function startDaemon() {
87
145
  }
88
146
  } else {
89
147
  logger.info("wallet.config_missing");
148
+ logger.info("wallet.uninitialized");
90
149
  }
91
150
  } catch (error) {
92
151
  logger.warn("wallet.config_load_failed", { error: serializeError(error) });
@@ -160,9 +219,6 @@ export async function startDaemon() {
160
219
  });
161
220
 
162
221
  logger.info("daemon.started", { socketPath: SOCKET_PATH });
163
- if (stateManager.isUninitialized()) {
164
- logger.info("wallet.uninitialized");
165
- }
166
222
 
167
223
  process.on("unhandledRejection", (error) => {
168
224
  logger.error("daemon.unhandled_rejection", { error: serializeError(error) });