teamshare-bridge 0.22.1 → 0.22.2
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/bridge/daemon-instance.d.ts +80 -0
- package/dist/bridge/daemon-instance.js +273 -0
- package/dist/bridge/daemon-instance.js.map +1 -1
- package/dist/bridge/daemon.js +15 -0
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/index.js +12 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/service.d.ts +50 -28
- package/dist/bridge/service.js +157 -254
- package/dist/bridge/service.js.map +1 -1
- package/dist/bridge/singleton-policy.d.ts +84 -0
- package/dist/bridge/singleton-policy.js +99 -0
- package/dist/bridge/singleton-policy.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* singleton-policy: the ONE decision behind "exactly one daemon runs here".
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS A SEPARATE, PURE FILE
|
|
5
|
+
* -------------------------------
|
|
6
|
+
* `daemon.lock` cannot enforce the singleton it exists to enforce. It is a
|
|
7
|
+
* FILE, and every way it fails is a way two daemons end up alive:
|
|
8
|
+
*
|
|
9
|
+
* - `isExpired(startedAt)` declares a lock abandoned after 24h, so a daemon
|
|
10
|
+
* that has been up for a day is invisible to the next `acquireDaemonLock()`;
|
|
11
|
+
* - the lock is cleared by a half-written `npm i -g`, a cleanup sweep, a
|
|
12
|
+
* restore-from-backup, or `stopAllDaemons()` clearing it while a survivor
|
|
13
|
+
* was still running;
|
|
14
|
+
* - `readLiveDaemonLock()` clears it whenever `pidAlive(lock.pid)` is false,
|
|
15
|
+
* and a pid we cannot signal reads as dead.
|
|
16
|
+
*
|
|
17
|
+
* Each of those leaves a LIVE daemon holding no lock. The next `start` (human
|
|
18
|
+
* or watchdog) sees a free machine, spawns, and now there are two. On this
|
|
19
|
+
* machine there were three at once — one serving, two wedged or stale — which
|
|
20
|
+
* is what made `npm i -g` fail with EBUSY on a mapped native DLL and left the
|
|
21
|
+
* install half-written.
|
|
22
|
+
*
|
|
23
|
+
* So the authority for "is a daemon already running" is PROCESS ENUMERATION
|
|
24
|
+
* (`listOurDaemonPids`, command-line verified), and the lock is demoted to what
|
|
25
|
+
* it is actually good for: naming WHICH of the live pids is the incumbent.
|
|
26
|
+
*
|
|
27
|
+
* THE RULE
|
|
28
|
+
* --------
|
|
29
|
+
* Of every verified daemon process on this machine, AT MOST ONE survives:
|
|
30
|
+
*
|
|
31
|
+
* 1. none alive -> `start` (spawn one)
|
|
32
|
+
* 2. alive but nothing answers /ping -> `replace` (all of them are wedged;
|
|
33
|
+
* stop every one, spawn one)
|
|
34
|
+
* 3. alive and something answers /ping -> `keep` (exactly one pid survives;
|
|
35
|
+
* every other one is stopped)
|
|
36
|
+
*
|
|
37
|
+
* Rule 3 is the load-bearing one: it is what a file-based lock cannot express,
|
|
38
|
+
* because "several processes match, one of them is real" is not a state any
|
|
39
|
+
* file can represent.
|
|
40
|
+
*
|
|
41
|
+
* WHICH PID SURVIVES (rule 3) — deterministic, never "whoever won the race":
|
|
42
|
+
* 1. the pid `daemon.lock` names, if it is still alive (the incumbent);
|
|
43
|
+
* 2. else the pid `runtime.json` names, if it is still alive;
|
|
44
|
+
* 3. else the LOWEST pid, i.e. the oldest process — on a machine that briefly
|
|
45
|
+
* held two, the older one has the live socket and the agent state.
|
|
46
|
+
*
|
|
47
|
+
* A `blocked` outcome covers the one case where spawning is worse than doing
|
|
48
|
+
* nothing: a bridge answers /ping but NO pid of ours is identifiable (it runs
|
|
49
|
+
* from a dev checkout, or its files were wiped under it). Starting then only
|
|
50
|
+
* produces a loser that exits on the port-occupant check, once per tick.
|
|
51
|
+
*
|
|
52
|
+
* No I/O here on purpose: `tests/daemon-singleton.test.mjs` drives this with
|
|
53
|
+
* plain arrays, and the real machine work lives in `daemon-instance.ts`.
|
|
54
|
+
*/
|
|
55
|
+
/** What the caller must do to converge on one daemon. */
|
|
56
|
+
export type SingletonAction =
|
|
57
|
+
/** No daemon is alive: spawn one. */
|
|
58
|
+
'start'
|
|
59
|
+
/** Exactly `keep` survives; every pid in `stop` must be terminated. */
|
|
60
|
+
| 'keep'
|
|
61
|
+
/** Every live daemon is wedged: terminate all of them, then spawn one. */
|
|
62
|
+
| 'replace'
|
|
63
|
+
/** A bridge answers but is not identifiable: do nothing, say why. */
|
|
64
|
+
| 'blocked';
|
|
65
|
+
export interface SingletonInput {
|
|
66
|
+
/** Pids VERIFIABLY running `…/index.js daemon`, ascending, no duplicates. */
|
|
67
|
+
live: readonly number[];
|
|
68
|
+
/** The pid `daemon.lock` names, or null when it names nothing/already dead. */
|
|
69
|
+
lockPid: number | null;
|
|
70
|
+
/** The pid `runtime.json` names, or null. */
|
|
71
|
+
runtimePid: number | null;
|
|
72
|
+
/** True when some daemon answers `/ping` with ok:true on a candidate port. */
|
|
73
|
+
anyAnswering: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface SingletonDecision {
|
|
76
|
+
action: SingletonAction;
|
|
77
|
+
/** The one pid allowed to keep running; null for `start`/`replace`/`blocked`. */
|
|
78
|
+
keep: number | null;
|
|
79
|
+
/** Verified daemon pids that must be terminated, ascending. */
|
|
80
|
+
stop: number[];
|
|
81
|
+
/** Human-readable justification for the log line. Never empty. */
|
|
82
|
+
reason: string;
|
|
83
|
+
}
|
|
84
|
+
export declare function decideSingleton(input: SingletonInput): SingletonDecision;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* singleton-policy: the ONE decision behind "exactly one daemon runs here".
|
|
4
|
+
*
|
|
5
|
+
* WHY IT IS A SEPARATE, PURE FILE
|
|
6
|
+
* -------------------------------
|
|
7
|
+
* `daemon.lock` cannot enforce the singleton it exists to enforce. It is a
|
|
8
|
+
* FILE, and every way it fails is a way two daemons end up alive:
|
|
9
|
+
*
|
|
10
|
+
* - `isExpired(startedAt)` declares a lock abandoned after 24h, so a daemon
|
|
11
|
+
* that has been up for a day is invisible to the next `acquireDaemonLock()`;
|
|
12
|
+
* - the lock is cleared by a half-written `npm i -g`, a cleanup sweep, a
|
|
13
|
+
* restore-from-backup, or `stopAllDaemons()` clearing it while a survivor
|
|
14
|
+
* was still running;
|
|
15
|
+
* - `readLiveDaemonLock()` clears it whenever `pidAlive(lock.pid)` is false,
|
|
16
|
+
* and a pid we cannot signal reads as dead.
|
|
17
|
+
*
|
|
18
|
+
* Each of those leaves a LIVE daemon holding no lock. The next `start` (human
|
|
19
|
+
* or watchdog) sees a free machine, spawns, and now there are two. On this
|
|
20
|
+
* machine there were three at once — one serving, two wedged or stale — which
|
|
21
|
+
* is what made `npm i -g` fail with EBUSY on a mapped native DLL and left the
|
|
22
|
+
* install half-written.
|
|
23
|
+
*
|
|
24
|
+
* So the authority for "is a daemon already running" is PROCESS ENUMERATION
|
|
25
|
+
* (`listOurDaemonPids`, command-line verified), and the lock is demoted to what
|
|
26
|
+
* it is actually good for: naming WHICH of the live pids is the incumbent.
|
|
27
|
+
*
|
|
28
|
+
* THE RULE
|
|
29
|
+
* --------
|
|
30
|
+
* Of every verified daemon process on this machine, AT MOST ONE survives:
|
|
31
|
+
*
|
|
32
|
+
* 1. none alive -> `start` (spawn one)
|
|
33
|
+
* 2. alive but nothing answers /ping -> `replace` (all of them are wedged;
|
|
34
|
+
* stop every one, spawn one)
|
|
35
|
+
* 3. alive and something answers /ping -> `keep` (exactly one pid survives;
|
|
36
|
+
* every other one is stopped)
|
|
37
|
+
*
|
|
38
|
+
* Rule 3 is the load-bearing one: it is what a file-based lock cannot express,
|
|
39
|
+
* because "several processes match, one of them is real" is not a state any
|
|
40
|
+
* file can represent.
|
|
41
|
+
*
|
|
42
|
+
* WHICH PID SURVIVES (rule 3) — deterministic, never "whoever won the race":
|
|
43
|
+
* 1. the pid `daemon.lock` names, if it is still alive (the incumbent);
|
|
44
|
+
* 2. else the pid `runtime.json` names, if it is still alive;
|
|
45
|
+
* 3. else the LOWEST pid, i.e. the oldest process — on a machine that briefly
|
|
46
|
+
* held two, the older one has the live socket and the agent state.
|
|
47
|
+
*
|
|
48
|
+
* A `blocked` outcome covers the one case where spawning is worse than doing
|
|
49
|
+
* nothing: a bridge answers /ping but NO pid of ours is identifiable (it runs
|
|
50
|
+
* from a dev checkout, or its files were wiped under it). Starting then only
|
|
51
|
+
* produces a loser that exits on the port-occupant check, once per tick.
|
|
52
|
+
*
|
|
53
|
+
* No I/O here on purpose: `tests/daemon-singleton.test.mjs` drives this with
|
|
54
|
+
* plain arrays, and the real machine work lives in `daemon-instance.ts`.
|
|
55
|
+
*/
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.decideSingleton = decideSingleton;
|
|
58
|
+
function listPids(pids) {
|
|
59
|
+
return [...new Set(pids)].filter((p) => Number.isInteger(p) && p > 0).sort((a, b) => a - b);
|
|
60
|
+
}
|
|
61
|
+
function decideSingleton(input) {
|
|
62
|
+
const live = listPids(input.live);
|
|
63
|
+
if (live.length === 0) {
|
|
64
|
+
if (input.anyAnswering) {
|
|
65
|
+
return {
|
|
66
|
+
action: 'blocked',
|
|
67
|
+
keep: null,
|
|
68
|
+
stop: [],
|
|
69
|
+
reason: 'a bridge daemon is answering /ping but no TeamShare daemon process could be ' +
|
|
70
|
+
'identified - not starting a second one (run `teamshare-bridge restart` if this is wrong)',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { action: 'start', keep: null, stop: [], reason: 'no daemon process on this machine' };
|
|
74
|
+
}
|
|
75
|
+
if (!input.anyAnswering) {
|
|
76
|
+
return {
|
|
77
|
+
action: 'replace',
|
|
78
|
+
keep: null,
|
|
79
|
+
stop: live,
|
|
80
|
+
reason: `${live.length} daemon process(es) alive but nothing answers /ping ` +
|
|
81
|
+
`(${live.join(', ')}) - replacing them with one working daemon`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// A bridge is healthy, so exactly one process is allowed to keep running.
|
|
85
|
+
const claimed = [input.lockPid, input.runtimePid].filter((pid) => pid !== null && live.includes(pid));
|
|
86
|
+
const keep = claimed[0] ?? live[0];
|
|
87
|
+
const stop = live.filter((pid) => pid !== keep);
|
|
88
|
+
const source = keep === input.lockPid ? 'daemon.lock' : keep === input.runtimePid ? 'runtime.json' : 'oldest process';
|
|
89
|
+
return {
|
|
90
|
+
action: 'keep',
|
|
91
|
+
keep,
|
|
92
|
+
stop,
|
|
93
|
+
reason: stop.length > 0
|
|
94
|
+
? `${live.length} daemons were running (${live.join(', ')}); keeping pid ${keep} ` +
|
|
95
|
+
`(${source}) and stopping ${stop.join(', ')} - only one daemon may run`
|
|
96
|
+
: `daemon already running (pid ${keep}, ${source})`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=singleton-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"singleton-policy.js","sourceRoot":"","sources":["../../src/bridge/singleton-policy.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;;AAsCH,0CA8CC;AAlDD,SAAS,QAAQ,CAAC,IAAuB;IACvC,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC9F,CAAC;AAED,SAAgB,eAAe,CAAC,KAAqB;IACnD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAElC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC;YACvB,OAAO;gBACL,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,IAAI;gBACV,IAAI,EAAE,EAAE;gBACR,MAAM,EACJ,8EAA8E;oBAC9E,0FAA0F;aAC7F,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IAChG,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;QACxB,OAAO;YACL,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,MAAM,EACJ,GAAG,IAAI,CAAC,MAAM,sDAAsD;gBACpE,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,4CAA4C;SAClE,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,MAAM,CACtD,CAAC,GAAG,EAAiB,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAC3D,CAAC;IACF,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,IAAI,CAAC,CAAC;IAChD,MAAM,MAAM,GACV,IAAI,KAAK,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACzG,OAAO;QACL,MAAM,EAAE,MAAM;QACd,IAAI;QACJ,IAAI;QACJ,MAAM,EACJ,IAAI,CAAC,MAAM,GAAG,CAAC;YACb,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,0BAA0B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,IAAI,GAAG;gBAChF,IAAI,MAAM,kBAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,4BAA4B;YACzE,CAAC,CAAC,+BAA+B,IAAI,KAAK,MAAM,GAAG;KACxD,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED