@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 +1 -1
- package/src/cli-shared.ts +41 -4
- package/src/daemon.ts +73 -17
package/package.json
CHANGED
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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) });
|