@cello-protocol/daemon 0.0.50 → 0.0.51
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/dist/bin/cello-daemon.js +13 -0
- package/dist/bin/cello-daemon.js.map +1 -1
- package/dist/connect-or-start.d.ts +11 -11
- package/dist/connect-or-start.d.ts.map +1 -1
- package/dist/connect-or-start.js +97 -39
- package/dist/connect-or-start.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +61 -11
- package/dist/daemon.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc-server.d.ts.map +1 -1
- package/dist/ipc-server.js +79 -13
- package/dist/ipc-server.js.map +1 -1
- package/dist/lock-file.d.ts +12 -0
- package/dist/lock-file.d.ts.map +1 -1
- package/dist/lock-file.js +36 -1
- package/dist/lock-file.js.map +1 -1
- package/dist/singleton-lock.d.ts +85 -0
- package/dist/singleton-lock.d.ts.map +1 -0
- package/dist/singleton-lock.js +208 -0
- package/dist/singleton-lock.js.map +1 -0
- package/dist/sqlcipher-db.d.ts +28 -0
- package/dist/sqlcipher-db.d.ts.map +1 -1
- package/dist/sqlcipher-db.js +6 -1
- package/dist/sqlcipher-db.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { Logger, LockFileContent, IpcRequest, IpcResponse, IpcResponseOk, IpcResponseError, IpcNotification, IpcFrame, AgentState, AgentInfo, ConnectionStatus, ConnectionInfo, DirectorySignalingState, DaemonStatusResponse, DaemonConfig, IManifestProvider, IManifestVersionStore, IManifestPollScheduler, IDirectoryChallengeVerifier, } from "./types.js";
|
|
2
2
|
export { ErrorCodes } from "./types.js";
|
|
3
3
|
export { startDaemon, type DaemonHandle } from "./daemon.js";
|
|
4
|
-
export { readLock, acquireLock, removeLock, isProcessAlive } from "./lock-file.js";
|
|
4
|
+
export { readLock, acquireLock, removeLock, removeLockIfOwned, isProcessAlive } from "./lock-file.js";
|
|
5
|
+
export { acquireSingletonLock, probeSingletonLock, DaemonAlreadyRunningError, SINGLETON_LOCK_FILENAME, type SingletonLock, } from "./singleton-lock.js";
|
|
5
6
|
export { loadAgents, type LoadedAgent, type FailedAgent, type AgentLoadResult } from "./agent-loader.js";
|
|
6
7
|
export { createIpcServer, type IpcServer, type IpcHandler, type IpcServerConfig, type IpcDisconnectHandler } from "./ipc-server.js";
|
|
7
8
|
export { connectToDaemon, type IpcClient, IpcError } from "./ipc-client.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,MAAM,EACN,eAAe,EACf,UAAU,EACV,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,MAAM,EACN,eAAe,EACf,UAAU,EACV,WAAW,EACX,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,oBAAoB,EACpB,YAAY,EACZ,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,2BAA2B,GAC5B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACtG,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,yBAAyB,EACzB,uBAAuB,EACvB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACpI,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGpE,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,eAAe,EACf,8BAA8B,EAC9B,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAGjC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,kCAAkC,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AAK7G,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,KAAK,eAAe,EACpB,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ErrorCodes } from "./types.js";
|
|
2
2
|
export { startDaemon } from "./daemon.js";
|
|
3
|
-
export { readLock, acquireLock, removeLock, isProcessAlive } from "./lock-file.js";
|
|
3
|
+
export { readLock, acquireLock, removeLock, removeLockIfOwned, isProcessAlive } from "./lock-file.js";
|
|
4
|
+
export { acquireSingletonLock, probeSingletonLock, DaemonAlreadyRunningError, SINGLETON_LOCK_FILENAME, } from "./singleton-lock.js";
|
|
4
5
|
export { loadAgents } from "./agent-loader.js";
|
|
5
6
|
export { createIpcServer } from "./ipc-server.js";
|
|
6
7
|
export { connectToDaemon, IpcError } from "./ipc-client.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EAAE,WAAW,EAAqB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACtG,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,yBAAyB,EACzB,uBAAuB,GAExB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,UAAU,EAA4D,MAAM,mBAAmB,CAAC;AACzG,OAAO,EAAE,eAAe,EAAoF,MAAM,iBAAiB,CAAC;AACpI,OAAO,EAAE,eAAe,EAAkB,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAsB,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAA0D,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEpE,qFAAqF;AACrF,OAAO,EACL,iBAAiB,EACjB,0BAA0B,EAC1B,eAAe,EACf,8BAA8B,EAC9B,uBAAuB,GACxB,MAAM,yBAAyB,CAAC;AASjC,+DAA+D;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC/F,OAAO,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAC3E,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,kCAAkC,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AAE7G,6FAA6F;AAC7F,+FAA+F;AAC/F,4EAA4E;AAC5E,OAAO,EACL,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,gBAAgB,GAGjB,MAAM,iBAAiB,CAAC"}
|
package/dist/ipc-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipc-server.d.ts","sourceRoot":"","sources":["../src/ipc-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAA2B,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjH,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;AAElE,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,kBAAkB,IAAI,MAAM,CAAC;IAC7B,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD,qFAAqF;IACrF,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC;IAC/E,wCAAwC;IACxC,gBAAgB,IAAI,MAAM,EAAE,CAAC;CAC9B;AASD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAChC,SAAS,
|
|
1
|
+
{"version":3,"file":"ipc-server.d.ts","sourceRoot":"","sources":["../src/ipc-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,MAAM,EAA2B,eAAe,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjH,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,oBAAoB,GAAG,CAAC,YAAY,EAAE,MAAM,KAAK,IAAI,CAAC;AAElE,MAAM,WAAW,SAAS;IACxB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,kBAAkB,IAAI,MAAM,CAAC;IAC7B,YAAY,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAClD,qFAAqF;IACrF,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,GAAG,OAAO,CAAC;IAC/E,wCAAwC;IACxC,gBAAgB,IAAI,MAAM,EAAE,CAAC;CAC9B;AASD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,GAChC,SAAS,CA6TX"}
|
package/dist/ipc-server.js
CHANGED
|
@@ -28,13 +28,42 @@
|
|
|
28
28
|
* e. Unlink socket file
|
|
29
29
|
*/
|
|
30
30
|
import { createServer } from "node:net";
|
|
31
|
-
import { chmod, unlink } from "node:fs/promises";
|
|
31
|
+
import { chmod, stat, unlink } from "node:fs/promises";
|
|
32
32
|
import { randomUUID } from "node:crypto";
|
|
33
33
|
export function createIpcServer(config, handlers) {
|
|
34
34
|
const { socketPath, maxConnections, logger } = config;
|
|
35
35
|
let server = null;
|
|
36
36
|
const connections = new Map();
|
|
37
37
|
let stopping = false;
|
|
38
|
+
// DOD-DAEMON-CLEANUP-1 (AC2): the identity — not the path — of the socket THIS server created.
|
|
39
|
+
// dev+ino is the only thing that survives another daemon rebinding the same path.
|
|
40
|
+
let createdSocket = null;
|
|
41
|
+
/**
|
|
42
|
+
* What is sitting at socketPath right now?
|
|
43
|
+
* "ours" — the socket this server created; we may remove it.
|
|
44
|
+
* "gone" — nothing there; nothing to do.
|
|
45
|
+
* "foreign" — a DIFFERENT socket. Another daemon rebound the path. Hands off.
|
|
46
|
+
*
|
|
47
|
+
* The distinction is by inode, not by path. A path tells you a file exists; only the inode tells
|
|
48
|
+
* you it is the same file you made.
|
|
49
|
+
*/
|
|
50
|
+
async function socketOwnership() {
|
|
51
|
+
let current;
|
|
52
|
+
try {
|
|
53
|
+
current = await stat(socketPath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Nothing at the path (ENOENT is the only realistic case) — there is nothing to own.
|
|
57
|
+
return "gone";
|
|
58
|
+
}
|
|
59
|
+
if (createdSocket === null) {
|
|
60
|
+
// Unreachable: stop() returns early unless the server is listening, and a listening server has
|
|
61
|
+
// been through start(), which sets this or throws. Say so rather than inventing an answer —
|
|
62
|
+
// guessing here is what the whole unit is about.
|
|
63
|
+
throw new Error("ipc server: socket identity unknown — stop() ran without a completed start()");
|
|
64
|
+
}
|
|
65
|
+
return current.dev === createdSocket.dev && current.ino === createdSocket.ino ? "ours" : "foreign";
|
|
66
|
+
}
|
|
38
67
|
let disconnectHandler = null;
|
|
39
68
|
function handleConnection(socket) {
|
|
40
69
|
if (stopping) {
|
|
@@ -186,6 +215,18 @@ export function createIpcServer(config, handlers) {
|
|
|
186
215
|
});
|
|
187
216
|
// SI-001: Set socket permissions to owner-only
|
|
188
217
|
await chmod(socketPath, 0o600);
|
|
218
|
+
// DOD-DAEMON-CLEANUP-1 (AC2): remember WHICH socket we created, by identity — not merely that
|
|
219
|
+
// a file exists at this path. If another daemon later rebinds the path, its listen() replaces
|
|
220
|
+
// the file with a different inode, and stop() must be able to tell the two apart so it does not
|
|
221
|
+
// unlink a socket it does not own.
|
|
222
|
+
//
|
|
223
|
+
// No fallback here, deliberately. Swallowing this stat and leaving the identity unknown would
|
|
224
|
+
// make stop() treat our OWN socket as foreign: it would then never close the server, so the
|
|
225
|
+
// daemon would go on accepting IPC connections into a torn-down handler stack after stop()
|
|
226
|
+
// returned, and would leave its socket behind. A daemon that cannot identify the socket it just
|
|
227
|
+
// bound has no business coming up — and startup failure is already handled cleanly.
|
|
228
|
+
const created = await stat(socketPath);
|
|
229
|
+
createdSocket = { dev: created.dev, ino: created.ino };
|
|
189
230
|
},
|
|
190
231
|
async stop() {
|
|
191
232
|
stopping = true;
|
|
@@ -221,19 +262,44 @@ export function createIpcServer(config, handlers) {
|
|
|
221
262
|
// Connection may already be closed
|
|
222
263
|
}
|
|
223
264
|
}
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
265
|
+
// DOD-DAEMON-CLEANUP-1 (AC2): decide ownership BEFORE closing, because close() is itself a
|
|
266
|
+
// blind unlink.
|
|
267
|
+
//
|
|
268
|
+
// Node's server.close() removes the unix socket path it bound — by PATH, with no check on what
|
|
269
|
+
// is actually there now. If another daemon has since rebound this path, closing our server
|
|
270
|
+
// deletes THEIR socket, and they are left alive, serving, and unreachable. Guarding our own
|
|
271
|
+
// explicit unlink is not enough; the close would already have done the damage.
|
|
272
|
+
const ownership = await socketOwnership();
|
|
273
|
+
if (ownership === "foreign") {
|
|
274
|
+
// We must not close: that would unlink the healthy daemon's socket out from under it. Drop
|
|
275
|
+
// the handle from the event loop instead and let process exit reclaim the fd. The listening
|
|
276
|
+
// socket is already inert — its path was taken from it, so nothing can reach it. Leaking an
|
|
277
|
+
// unreachable fd for the last moments of a dying process is a trade we make happily against
|
|
278
|
+
// disarming a live peer.
|
|
279
|
+
logger.info("daemon.ipc.socket.not_ours", { socketPath, ourIno: createdSocket?.ino ?? null });
|
|
280
|
+
server.unref();
|
|
230
281
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
282
|
+
else {
|
|
283
|
+
await new Promise((resolve) => {
|
|
284
|
+
server.close(() => resolve());
|
|
285
|
+
});
|
|
286
|
+
// close() normally unlinks the path for us. Re-check rather than assume: only remove the file
|
|
287
|
+
// if it is STILL the socket we created. Between the close and here, the singleton lock is
|
|
288
|
+
// still held (the daemon releases it after this returns), so no successor can have bound the
|
|
289
|
+
// path — but the cost of re-checking is one stat, and the cost of being wrong is the bug this
|
|
290
|
+
// whole unit exists to kill.
|
|
291
|
+
if (await socketOwnership() === "ours") {
|
|
292
|
+
try {
|
|
293
|
+
await unlink(socketPath);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (err.code !== "ENOENT") {
|
|
297
|
+
logger.warn("daemon.ipc.socket.unlink.failed", {
|
|
298
|
+
socketPath,
|
|
299
|
+
error: err instanceof Error ? err.message : String(err),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
237
303
|
}
|
|
238
304
|
}
|
|
239
305
|
server = null;
|
package/dist/ipc-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipc-server.js","sourceRoot":"","sources":["../src/ipc-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,YAAY,EAA4B,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"ipc-server.js","sourceRoot":"","sources":["../src/ipc-server.ts"],"names":[],"mappings":"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"}
|
package/dist/lock-file.d.ts
CHANGED
|
@@ -23,5 +23,17 @@ import type { LockFileContent, Logger } from "./types.js";
|
|
|
23
23
|
export declare function readLock(lockPath: string): Promise<LockFileContent | null>;
|
|
24
24
|
export declare function acquireLock(lockPath: string, content: LockFileContent): Promise<void>;
|
|
25
25
|
export declare function removeLock(lockPath: string, logger: Logger): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* DOD-DAEMON-CLEANUP-1 (AC1) — remove the lock ONLY if it is still ours.
|
|
28
|
+
*
|
|
29
|
+
* An exiting daemon must not disarm a daemon it does not own. By the time we shut down, a DIFFERENT
|
|
30
|
+
* daemon may have written this lock; deleting it would leave that healthy daemon unreachable
|
|
31
|
+
* (`cello logout` reports "No daemon running") and the next `cello login` would spawn a third beside
|
|
32
|
+
* it. That is the cascade — the obvious recovery action, killing the orphan, propagates the bug.
|
|
33
|
+
*
|
|
34
|
+
* `removeLock` (unconditional) is still correct for the CLI's stale-lock cleanup, where the lock
|
|
35
|
+
* belongs to a process that is provably gone. It is NOT correct on the way out of a live daemon.
|
|
36
|
+
*/
|
|
37
|
+
export declare function removeLockIfOwned(lockPath: string, ownPid: number, logger: Logger): Promise<void>;
|
|
26
38
|
export declare function isProcessAlive(pid: number): boolean;
|
|
27
39
|
//# sourceMappingURL=lock-file.d.ts.map
|
package/dist/lock-file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-file.d.ts","sourceRoot":"","sources":["../src/lock-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE1D,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAwBhF;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3F;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD"}
|
|
1
|
+
{"version":3,"file":"lock-file.d.ts","sourceRoot":"","sources":["../src/lock-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAE1D,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAwBhF;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3F;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWhF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOnD"}
|
package/dist/lock-file.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* 4. removeLock(path):
|
|
20
20
|
* - Unlink the file, ignoring ENOENT
|
|
21
21
|
*/
|
|
22
|
-
import { readFile, writeFile, rename, unlink } from "node:fs/promises";
|
|
22
|
+
import { readFile, writeFile, rename, stat, unlink } from "node:fs/promises";
|
|
23
23
|
export async function readLock(lockPath) {
|
|
24
24
|
let raw;
|
|
25
25
|
try {
|
|
@@ -72,6 +72,41 @@ export async function removeLock(lockPath, logger) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* DOD-DAEMON-CLEANUP-1 (AC1) — remove the lock ONLY if it is still ours.
|
|
77
|
+
*
|
|
78
|
+
* An exiting daemon must not disarm a daemon it does not own. By the time we shut down, a DIFFERENT
|
|
79
|
+
* daemon may have written this lock; deleting it would leave that healthy daemon unreachable
|
|
80
|
+
* (`cello logout` reports "No daemon running") and the next `cello login` would spawn a third beside
|
|
81
|
+
* it. That is the cascade — the obvious recovery action, killing the orphan, propagates the bug.
|
|
82
|
+
*
|
|
83
|
+
* `removeLock` (unconditional) is still correct for the CLI's stale-lock cleanup, where the lock
|
|
84
|
+
* belongs to a process that is provably gone. It is NOT correct on the way out of a live daemon.
|
|
85
|
+
*/
|
|
86
|
+
export async function removeLockIfOwned(lockPath, ownPid, logger) {
|
|
87
|
+
const lock = await readLock(lockPath);
|
|
88
|
+
if (lock === null) {
|
|
89
|
+
// readLock returns null for BOTH "absent" and "unparseable" — so this is the one place that
|
|
90
|
+
// cannot tell "nothing to do" from "something is wrong". Distinguish them: a lock file that
|
|
91
|
+
// exists but cannot be read (truncated by a full disk, a partial write) is left alone, because we
|
|
92
|
+
// cannot prove it is ours — but it is said out loud rather than passed over in silence.
|
|
93
|
+
try {
|
|
94
|
+
await stat(lockPath);
|
|
95
|
+
logger.warn("daemon.lock.unreadable", { ownPid, lockPath });
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Genuinely absent. Nothing to remove, nothing to report.
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (lock.pid !== ownPid) {
|
|
103
|
+
// SI: a daemon that declines to remove a lock says so. Silence here is what made the live
|
|
104
|
+
// incident so hard to see.
|
|
105
|
+
logger.info("daemon.lock.not_ours", { ownPid, lockPid: lock.pid, lockPath });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await removeLock(lockPath, logger);
|
|
109
|
+
}
|
|
75
110
|
export function isProcessAlive(pid) {
|
|
76
111
|
try {
|
|
77
112
|
process.kill(pid, 0);
|
package/dist/lock-file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lock-file.js","sourceRoot":"","sources":["../src/lock-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"lock-file.js","sourceRoot":"","sources":["../src/lock-file.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-SINGLE-DAEMON-1 — the daemon singleton, enforced by the kernel.
|
|
3
|
+
*
|
|
4
|
+
* `daemon.lock` is a JSON file, and a file cannot answer "is a daemon running?". Anyone may delete
|
|
5
|
+
* it; a `kill -9` leaves it behind naming a pid that is dead — or, worse, one the OS has since REUSED
|
|
6
|
+
* for an unrelated process, which makes `isProcessAlive()` say "yes" forever. Every stale-lock
|
|
7
|
+
* heuristic ever written is an attempt to guess what only the kernel actually knows.
|
|
8
|
+
*
|
|
9
|
+
* So we ask the kernel. The daemon takes a real POSIX write lock (`fcntl`) on a dedicated file and
|
|
10
|
+
* holds it for its entire lifetime. Two processes cannot hold it at once, and the kernel drops it the
|
|
11
|
+
* instant the holder dies — SIGKILL, panic, `kill -9`, however it goes. There is no stale state to
|
|
12
|
+
* clean up and no recovery step to get wrong.
|
|
13
|
+
*
|
|
14
|
+
* ## Why SQLite is the lock
|
|
15
|
+
*
|
|
16
|
+
* Node has no `flock`. `fs.constants.O_EXLOCK` exists only on macOS/BSD — using it would leave every
|
|
17
|
+
* Linux operator (including the EC2 demo agent) with NO lock at all, which is worse than none because
|
|
18
|
+
* it looks like one. A native `flock` addon (`fs-ext`) compiles from source, and install time is a
|
|
19
|
+
* cost every operator pays on every version bump (see CLAUDE.md — the client is a heavy local node,
|
|
20
|
+
* not a thin wrapper).
|
|
21
|
+
*
|
|
22
|
+
* `@signalapp/sqlcipher` is already a daemon dependency, ships prebuilt (no compile), and SQLite's
|
|
23
|
+
* whole business is taking correct `fcntl` locks on every POSIX platform. `locking_mode = exclusive`
|
|
24
|
+
* plus one header write acquires the exclusive lock and holds it until the connection closes. A
|
|
25
|
+
* second process gets SQLITE_BUSY immediately (`busy_timeout = 0`). SQLite also tracks locks per
|
|
26
|
+
* inode inside the process, so this correctly refuses a second daemon in the SAME process too —
|
|
27
|
+
* which raw `fcntl`, whose locks are per-process, would not.
|
|
28
|
+
*
|
|
29
|
+
* The lock database holds no rows and never will. It is a mutex that happens to be spelled in SQL.
|
|
30
|
+
*
|
|
31
|
+
* ## The division of labour with daemon.lock (AC4)
|
|
32
|
+
*
|
|
33
|
+
* The OS lock DECIDES whether a daemon may start. `daemon.lock` only SUPPLIES METADATA — it is how we
|
|
34
|
+
* learn the holder's pid to name in the error, because `fcntl` will not tell us. If the JSON is
|
|
35
|
+
* missing, stale, or lying, the worst outcome is a less specific error message. It can never admit a
|
|
36
|
+
* second daemon, and it can never wedge a legitimate one out.
|
|
37
|
+
*
|
|
38
|
+
* ## The one constraint
|
|
39
|
+
*
|
|
40
|
+
* `~/.cello` must live on a LOCAL filesystem. POSIX `fcntl` locks are advisory-but-honoured on local
|
|
41
|
+
* filesystems (APFS, ext4, xfs — macOS and the Linux hosts we run on); on NFSv3 without a working
|
|
42
|
+
* `lockd`, and on some 9p/WSL2 setups, they can silently degrade to no-ops. There, two daemons would
|
|
43
|
+
* both "acquire" and the singleton would be absent while appearing present. We do not attempt to
|
|
44
|
+
* detect this — a real self-test needs a second process, which is far too much to do at every startup.
|
|
45
|
+
* The lock path is logged in `daemon.singleton.acquired` so an operator debugging a two-daemon
|
|
46
|
+
* situation can see exactly which file the kernel was asked about.
|
|
47
|
+
*/
|
|
48
|
+
import type { Logger } from "./types.js";
|
|
49
|
+
/** The file the kernel lock is taken on. Holds no data — see the note above. */
|
|
50
|
+
export declare const SINGLETON_LOCK_FILENAME = "daemon.singleton";
|
|
51
|
+
export declare class DaemonAlreadyRunningError extends Error {
|
|
52
|
+
/** The pid from `daemon.lock`, or null when that file is absent/unreadable/malformed. */
|
|
53
|
+
readonly holderPid: number | null;
|
|
54
|
+
constructor(holderPid: number | null);
|
|
55
|
+
}
|
|
56
|
+
export interface SingletonLock {
|
|
57
|
+
/** Release the lock. The kernel would do this on process exit anyway; this is for in-process use. */
|
|
58
|
+
release(): void;
|
|
59
|
+
}
|
|
60
|
+
/** The daemon binary's exit code when it loses the singleton race — distinct from 1 = "broken". */
|
|
61
|
+
export declare const EXIT_ALREADY_RUNNING = 3;
|
|
62
|
+
export declare function acquireSingletonLock(celloDir: string, logger: Logger, busyTimeoutMs?: number): SingletonLock;
|
|
63
|
+
/**
|
|
64
|
+
* Is a daemon holding the lock for `celloDir` right now?
|
|
65
|
+
*
|
|
66
|
+
* "held" — a daemon is alive. Connect to it; never spawn beside it.
|
|
67
|
+
* "free" — no daemon is running, whatever `daemon.lock` claims. Spawning is correct.
|
|
68
|
+
* "unknown" — we could not ask the kernel (native module missing, directory unreadable).
|
|
69
|
+
*
|
|
70
|
+
* The third answer is the point. "I could not determine whether a daemon is running" and "no daemon is
|
|
71
|
+
* running" are different facts, and collapsing them into one boolean means the caller spawns on both —
|
|
72
|
+
* so the one case where we are blind becomes the one case we start a second daemon. Callers must
|
|
73
|
+
* refuse to spawn on "unknown" and surface the underlying error instead.
|
|
74
|
+
*
|
|
75
|
+
* Asking the kernel rather than trusting `daemon.lock` is also what makes a REUSED pid harmless: a
|
|
76
|
+
* stale lock file naming a live-but-unrelated process would otherwise wedge `cello login` forever.
|
|
77
|
+
*
|
|
78
|
+
* The probe takes the lock and immediately drops it. The gap between the probe and a subsequent spawn
|
|
79
|
+
* is safe: the daemon we spawn acquires the lock authoritatively and exits EXIT_ALREADY_RUNNING if it
|
|
80
|
+
* loses that race — and the caller reads that exit code as "someone else won, connect to them", never
|
|
81
|
+
* as a failure. Two probes can therefore both say "free" and both spawn, and exactly one daemon
|
|
82
|
+
* survives.
|
|
83
|
+
*/
|
|
84
|
+
export declare function probeSingletonLock(celloDir: string, logger: Logger): "held" | "free" | "unknown";
|
|
85
|
+
//# sourceMappingURL=singleton-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton-lock.d.ts","sourceRoot":"","sources":["../src/singleton-lock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAKH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC,gFAAgF;AAChF,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,yFAAyF;IACzF,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;gBAEtB,SAAS,EAAE,MAAM,GAAG,IAAI;CASrC;AAED,MAAM,WAAW,aAAa;IAC5B,qGAAqG;IACrG,OAAO,IAAI,IAAI,CAAC;CACjB;AA0DD,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAgBtC,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,aAAa,GAAE,MAAuC,GACrD,aAAa,CAyCf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAWhG"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOD-SINGLE-DAEMON-1 — the daemon singleton, enforced by the kernel.
|
|
3
|
+
*
|
|
4
|
+
* `daemon.lock` is a JSON file, and a file cannot answer "is a daemon running?". Anyone may delete
|
|
5
|
+
* it; a `kill -9` leaves it behind naming a pid that is dead — or, worse, one the OS has since REUSED
|
|
6
|
+
* for an unrelated process, which makes `isProcessAlive()` say "yes" forever. Every stale-lock
|
|
7
|
+
* heuristic ever written is an attempt to guess what only the kernel actually knows.
|
|
8
|
+
*
|
|
9
|
+
* So we ask the kernel. The daemon takes a real POSIX write lock (`fcntl`) on a dedicated file and
|
|
10
|
+
* holds it for its entire lifetime. Two processes cannot hold it at once, and the kernel drops it the
|
|
11
|
+
* instant the holder dies — SIGKILL, panic, `kill -9`, however it goes. There is no stale state to
|
|
12
|
+
* clean up and no recovery step to get wrong.
|
|
13
|
+
*
|
|
14
|
+
* ## Why SQLite is the lock
|
|
15
|
+
*
|
|
16
|
+
* Node has no `flock`. `fs.constants.O_EXLOCK` exists only on macOS/BSD — using it would leave every
|
|
17
|
+
* Linux operator (including the EC2 demo agent) with NO lock at all, which is worse than none because
|
|
18
|
+
* it looks like one. A native `flock` addon (`fs-ext`) compiles from source, and install time is a
|
|
19
|
+
* cost every operator pays on every version bump (see CLAUDE.md — the client is a heavy local node,
|
|
20
|
+
* not a thin wrapper).
|
|
21
|
+
*
|
|
22
|
+
* `@signalapp/sqlcipher` is already a daemon dependency, ships prebuilt (no compile), and SQLite's
|
|
23
|
+
* whole business is taking correct `fcntl` locks on every POSIX platform. `locking_mode = exclusive`
|
|
24
|
+
* plus one header write acquires the exclusive lock and holds it until the connection closes. A
|
|
25
|
+
* second process gets SQLITE_BUSY immediately (`busy_timeout = 0`). SQLite also tracks locks per
|
|
26
|
+
* inode inside the process, so this correctly refuses a second daemon in the SAME process too —
|
|
27
|
+
* which raw `fcntl`, whose locks are per-process, would not.
|
|
28
|
+
*
|
|
29
|
+
* The lock database holds no rows and never will. It is a mutex that happens to be spelled in SQL.
|
|
30
|
+
*
|
|
31
|
+
* ## The division of labour with daemon.lock (AC4)
|
|
32
|
+
*
|
|
33
|
+
* The OS lock DECIDES whether a daemon may start. `daemon.lock` only SUPPLIES METADATA — it is how we
|
|
34
|
+
* learn the holder's pid to name in the error, because `fcntl` will not tell us. If the JSON is
|
|
35
|
+
* missing, stale, or lying, the worst outcome is a less specific error message. It can never admit a
|
|
36
|
+
* second daemon, and it can never wedge a legitimate one out.
|
|
37
|
+
*
|
|
38
|
+
* ## The one constraint
|
|
39
|
+
*
|
|
40
|
+
* `~/.cello` must live on a LOCAL filesystem. POSIX `fcntl` locks are advisory-but-honoured on local
|
|
41
|
+
* filesystems (APFS, ext4, xfs — macOS and the Linux hosts we run on); on NFSv3 without a working
|
|
42
|
+
* `lockd`, and on some 9p/WSL2 setups, they can silently degrade to no-ops. There, two daemons would
|
|
43
|
+
* both "acquire" and the singleton would be absent while appearing present. We do not attempt to
|
|
44
|
+
* detect this — a real self-test needs a second process, which is far too much to do at every startup.
|
|
45
|
+
* The lock path is logged in `daemon.singleton.acquired` so an operator debugging a two-daemon
|
|
46
|
+
* situation can see exactly which file the kernel was asked about.
|
|
47
|
+
*/
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
import { readFileSync } from "node:fs";
|
|
50
|
+
import { loadSignalModule } from "./sqlcipher-db.js";
|
|
51
|
+
/** The file the kernel lock is taken on. Holds no data — see the note above. */
|
|
52
|
+
export const SINGLETON_LOCK_FILENAME = "daemon.singleton";
|
|
53
|
+
export class DaemonAlreadyRunningError extends Error {
|
|
54
|
+
/** The pid from `daemon.lock`, or null when that file is absent/unreadable/malformed. */
|
|
55
|
+
holderPid;
|
|
56
|
+
constructor(holderPid) {
|
|
57
|
+
super(holderPid === null
|
|
58
|
+
? "another daemon is already running (pid unknown — daemon.lock is missing or unreadable)"
|
|
59
|
+
: `another daemon is already running (pid ${holderPid})`);
|
|
60
|
+
this.name = "DaemonAlreadyRunningError";
|
|
61
|
+
this.holderPid = holderPid;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the holder's pid from the advisory `daemon.lock`. METADATA ONLY (AC4): this pid is used to
|
|
66
|
+
* write a better error message and for nothing else. It never decides anything, so a stale or absent
|
|
67
|
+
* file costs us a specific error message, never correctness.
|
|
68
|
+
*/
|
|
69
|
+
function readHolderPid(celloDir) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(readFileSync(join(celloDir, "daemon.lock"), "utf-8"));
|
|
72
|
+
return typeof parsed.pid === "number" ? parsed.pid : null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isBusy(err) {
|
|
79
|
+
const code = err?.code;
|
|
80
|
+
return typeof code === "string" && code.startsWith("SQLITE_BUSY");
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Take the exclusive OS lock for `celloDir`, or throw `DaemonAlreadyRunningError` if another process
|
|
84
|
+
* holds it. Returns a handle that holds the lock until `release()` (or process death).
|
|
85
|
+
*
|
|
86
|
+
* Call this BEFORE opening the database, registering agents, or connecting to the directory (AC2):
|
|
87
|
+
* a daemon that loses the race must do NONE of those things.
|
|
88
|
+
*/
|
|
89
|
+
/**
|
|
90
|
+
* How long a STARTING daemon will wait for a contended lock before giving up.
|
|
91
|
+
*
|
|
92
|
+
* Not zero, and the reason is subtle. `probeSingletonLock()` does not read the lock — it TAKES it and
|
|
93
|
+
* drops it. That means a probe (a concurrent `cello login`, a shim, a status check) holds the
|
|
94
|
+
* exclusive lock for the few milliseconds it takes to open the file and write the header. With a zero
|
|
95
|
+
* timeout, a daemon that reaches its acquisition inside that window would get SQLITE_BUSY and exit
|
|
96
|
+
* "another daemon is already running" — when no other daemon exists at all. A read-only question must
|
|
97
|
+
* never be able to kill the thing it is asking about.
|
|
98
|
+
*
|
|
99
|
+
* A genuine second daemon still loses within this bound, which is what AC2's "immediately" is for.
|
|
100
|
+
*/
|
|
101
|
+
const DAEMON_ACQUIRE_BUSY_TIMEOUT_MS = 3000;
|
|
102
|
+
/**
|
|
103
|
+
* How long a PROBE waits before concluding the lock is held.
|
|
104
|
+
*
|
|
105
|
+
* Also not zero, and for the mirror of the reason above. A probe takes the lock too, so two probes
|
|
106
|
+
* can collide with each other: a second `cello login`, or the MCP shim autostarting while the
|
|
107
|
+
* operator types `cello login`. With a zero timeout the loser of that collision would report "held"
|
|
108
|
+
* — i.e. "a daemon is already running" — when there is no daemon at all, and the caller would sit in
|
|
109
|
+
* connectWithRetry for ten seconds before failing with a confident lie. That is precisely the class
|
|
110
|
+
* of wrong-but-certain statement this whole story exists to abolish.
|
|
111
|
+
*
|
|
112
|
+
* A REAL daemon holds the lock forever, so waiting a little costs a probe nothing in the case that
|
|
113
|
+
* matters — it still times out and still correctly answers "held". A transient probe-vs-probe
|
|
114
|
+
* collision, meanwhile, resolves in milliseconds. Strictly better than zero in both directions.
|
|
115
|
+
*/
|
|
116
|
+
const PROBE_BUSY_TIMEOUT_MS = 500;
|
|
117
|
+
/** The daemon binary's exit code when it loses the singleton race — distinct from 1 = "broken". */
|
|
118
|
+
export const EXIT_ALREADY_RUNNING = 3;
|
|
119
|
+
function openLockDb(lockDbPath) {
|
|
120
|
+
const mod = loadSignalModule();
|
|
121
|
+
const Ctor = mod.Database ?? mod.default;
|
|
122
|
+
try {
|
|
123
|
+
return new Ctor(lockDbPath);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// The file could not be opened at all (permissions, unwritable dir). NOT a contended lock —
|
|
127
|
+
// never let this be mistaken for "a daemon is running".
|
|
128
|
+
throw new Error(`Could not open the daemon singleton lock at ${lockDbPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export function acquireSingletonLock(celloDir, logger, busyTimeoutMs = DAEMON_ACQUIRE_BUSY_TIMEOUT_MS) {
|
|
132
|
+
const lockDbPath = join(celloDir, SINGLETON_LOCK_FILENAME);
|
|
133
|
+
const db = openLockDb(lockDbPath);
|
|
134
|
+
try {
|
|
135
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
136
|
+
db.pragma("locking_mode = exclusive");
|
|
137
|
+
// SQLite defers the actual lock acquisition until it has a reason to write. `user_version` is a
|
|
138
|
+
// write to the database header — it takes the exclusive lock and stores nothing.
|
|
139
|
+
db.pragma("user_version = 1");
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
try {
|
|
143
|
+
db.close();
|
|
144
|
+
}
|
|
145
|
+
catch { /* we are already failing; closing is best-effort */ }
|
|
146
|
+
if (isBusy(err)) {
|
|
147
|
+
const holderPid = readHolderPid(celloDir);
|
|
148
|
+
logger.info("daemon.singleton.contended", { holderPid, lockPath: lockDbPath });
|
|
149
|
+
throw new DaemonAlreadyRunningError(holderPid);
|
|
150
|
+
}
|
|
151
|
+
// Some other SQLite failure. Keep the frame — a bare SQLITE_* error here reads as a database
|
|
152
|
+
// problem, when what actually failed was taking the singleton lock.
|
|
153
|
+
throw new Error(`Could not take the daemon singleton lock at ${lockDbPath}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
154
|
+
}
|
|
155
|
+
logger.info("daemon.singleton.acquired", { pid: process.pid, lockPath: lockDbPath });
|
|
156
|
+
let released = false;
|
|
157
|
+
return {
|
|
158
|
+
release() {
|
|
159
|
+
if (released)
|
|
160
|
+
return;
|
|
161
|
+
released = true;
|
|
162
|
+
try {
|
|
163
|
+
db.close();
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
logger.warn("daemon.singleton.release.failed", {
|
|
167
|
+
error: err instanceof Error ? err.message : String(err),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Is a daemon holding the lock for `celloDir` right now?
|
|
175
|
+
*
|
|
176
|
+
* "held" — a daemon is alive. Connect to it; never spawn beside it.
|
|
177
|
+
* "free" — no daemon is running, whatever `daemon.lock` claims. Spawning is correct.
|
|
178
|
+
* "unknown" — we could not ask the kernel (native module missing, directory unreadable).
|
|
179
|
+
*
|
|
180
|
+
* The third answer is the point. "I could not determine whether a daemon is running" and "no daemon is
|
|
181
|
+
* running" are different facts, and collapsing them into one boolean means the caller spawns on both —
|
|
182
|
+
* so the one case where we are blind becomes the one case we start a second daemon. Callers must
|
|
183
|
+
* refuse to spawn on "unknown" and surface the underlying error instead.
|
|
184
|
+
*
|
|
185
|
+
* Asking the kernel rather than trusting `daemon.lock` is also what makes a REUSED pid harmless: a
|
|
186
|
+
* stale lock file naming a live-but-unrelated process would otherwise wedge `cello login` forever.
|
|
187
|
+
*
|
|
188
|
+
* The probe takes the lock and immediately drops it. The gap between the probe and a subsequent spawn
|
|
189
|
+
* is safe: the daemon we spawn acquires the lock authoritatively and exits EXIT_ALREADY_RUNNING if it
|
|
190
|
+
* loses that race — and the caller reads that exit code as "someone else won, connect to them", never
|
|
191
|
+
* as a failure. Two probes can therefore both say "free" and both spawn, and exactly one daemon
|
|
192
|
+
* survives.
|
|
193
|
+
*/
|
|
194
|
+
export function probeSingletonLock(celloDir, logger) {
|
|
195
|
+
try {
|
|
196
|
+
acquireSingletonLock(celloDir, logger, PROBE_BUSY_TIMEOUT_MS).release();
|
|
197
|
+
return "free";
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
if (err instanceof DaemonAlreadyRunningError)
|
|
201
|
+
return "held";
|
|
202
|
+
logger.warn("daemon.singleton.probe.failed", {
|
|
203
|
+
error: err instanceof Error ? err.message : String(err),
|
|
204
|
+
});
|
|
205
|
+
return "unknown";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=singleton-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton-lock.js","sourceRoot":"","sources":["../src/singleton-lock.ts"],"names":[],"mappings":"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"}
|