@serfab/cadre-host 0.13.0 → 1.1.0
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/README.md +6 -4
- package/dist/bin/host.js +9 -1
- package/dist/bin/host.js.map +1 -1
- package/dist/donation/__tests__/fake-orchestrator.js +1 -1
- package/dist/donation/__tests__/fake-orchestrator.js.map +1 -1
- package/dist/donation/donation-service.d.ts +7 -2
- package/dist/donation/donation-service.js +16 -2
- package/dist/donation/donation-service.js.map +1 -1
- package/dist/donation/donation-supervisor.d.ts +12 -2
- package/dist/donation/donation-supervisor.js +25 -2
- package/dist/donation/donation-supervisor.js.map +1 -1
- package/dist/donation/types.d.ts +2 -1
- package/dist/donation/types.js.map +1 -1
- package/dist/installer/index.d.ts +9 -1
- package/dist/installer/index.js +5 -1
- package/dist/installer/index.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +71 -11
- package/dist/orchestrator/host-process-orchestrator.js +173 -80
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/orchestrator/port-allocator.d.ts +44 -2
- package/dist/orchestrator/port-allocator.js +63 -5
- package/dist/orchestrator/port-allocator.js.map +1 -1
- package/dist/orchestrator/port-probe.d.ts +15 -0
- package/dist/orchestrator/port-probe.js +22 -0
- package/dist/orchestrator/port-probe.js.map +1 -0
- package/dist/orchestrator/types.d.ts +13 -1
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/server/routes/bootstrap-node-validation.d.ts +30 -11
- package/dist/server/routes/bootstrap-node-validation.js +32 -13
- package/dist/server/routes/bootstrap-node-validation.js.map +1 -1
- package/dist/server/server.d.ts +4 -1
- package/dist/server/server.js +36 -2
- package/dist/server/server.js.map +1 -1
- package/dist/ui/assets/{index-C-iks41M.js → index-CEoNRwM_.js} +3 -3
- package/dist/ui/assets/{index-C-iks41M.js.map → index-CEoNRwM_.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/package.json +4 -4
- package/src/__tests__/orchestrator-env-scrub.test.ts +22 -0
- package/src/__tests__/orchestrator-node-identity.test.ts +4 -4
- package/src/__tests__/orchestrator-owner.test.ts +45 -2
- package/src/__tests__/orchestrator-ports.test.ts +172 -0
- package/src/__tests__/orchestrator.test.ts +190 -79
- package/src/bin/host.ts +9 -1
- package/src/donation/__tests__/donation-service.test.ts +1155 -1068
- package/src/donation/__tests__/donation-supervisor.test.ts +548 -510
- package/src/donation/__tests__/fake-orchestrator.ts +1 -1
- package/src/donation/donation-service.ts +22 -3
- package/src/donation/donation-supervisor.ts +25 -2
- package/src/donation/types.ts +2 -1
- package/src/installer/__tests__/installer.smoke.test.ts +263 -218
- package/src/installer/index.ts +327 -314
- package/src/orchestrator/host-process-orchestrator.ts +1250 -1143
- package/src/orchestrator/port-allocator.ts +148 -91
- package/src/orchestrator/port-probe.ts +31 -0
- package/src/orchestrator/types.ts +13 -1
- package/src/server/__tests__/bootstrap-node-validation.test.ts +12 -8
- package/src/server/__tests__/grants-route.test.ts +72 -4
- package/src/server/__tests__/nodes-route.test.ts +2 -2
- package/src/server/__tests__/publishers.test.ts +1 -1
- package/src/server/__tests__/status-route.test.ts +1 -1
- package/src/server/routes/bootstrap-node-validation.ts +32 -13
- package/src/server/server.ts +157 -122
package/dist/installer/index.js
CHANGED
|
@@ -63,7 +63,12 @@ export class Installer {
|
|
|
63
63
|
// 4. Seed nat.json with the chosen libp2p port + UPnP flag so the
|
|
64
64
|
// NatService picks them up on first start.
|
|
65
65
|
seedNatSettings(answers.dataDir, answers.libp2pPort, answers.upnpEnabled);
|
|
66
|
+
const uiUrl = `http://127.0.0.1:${answers.uiPort}/`;
|
|
66
67
|
// 5. Service-host registration.
|
|
68
|
+
if (opts.noService) {
|
|
69
|
+
log('--no-service: skipping service registration, browser open and enrollment invite');
|
|
70
|
+
return { dataDir: answers.dataDir, uiUrl, configPath: cfgPath };
|
|
71
|
+
}
|
|
67
72
|
const serviceHost = opts.serviceHost ?? createServiceHost(this.platform);
|
|
68
73
|
const ctx = {
|
|
69
74
|
nodePath: opts.nodePath ?? process.execPath,
|
|
@@ -73,7 +78,6 @@ export class Installer {
|
|
|
73
78
|
};
|
|
74
79
|
await serviceHost.install(ctx);
|
|
75
80
|
// 6. Browser open (best-effort).
|
|
76
|
-
const uiUrl = `http://127.0.0.1:${answers.uiPort}/`;
|
|
77
81
|
const shouldOpenBrowser = opts.openBrowser !== false && !opts.nonInteractive && process.stdout.isTTY;
|
|
78
82
|
if (shouldOpenBrowser) {
|
|
79
83
|
openBrowser(uiUrl);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installer/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAuB,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EACL,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,OAAO,GACR,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,cAAc,GAEf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,iBAAiB,EAA6C,MAAM,yBAAyB,CAAC;AACvG,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,aAAa,EACb,eAAe,EACf,mBAAmB,GAGpB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhF,MAAM,GAAG,GAAG,KAAK,CAAC,sBAAsB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/installer/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { Orchestrator, OrchestratorCreateRequest, OrchestratorCreateResult, OrchestratorStats } from '@serfab/cadre-provider';
|
|
2
|
-
import { type OwnerSpawnConfig, type HostProcessConfig, type ManagedNodeInfo, type NodeStateListener } from './types.js';
|
|
2
|
+
import { type OwnerSpawnConfig, type HostProcessConfig, type ManagedNodeInfo, type NodePorts, type NodeStateListener } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
|
|
5
|
+
* entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
|
|
6
|
+
* every interface.
|
|
7
|
+
*
|
|
8
|
+
* The WebSocket entry is what lets a phone reach the child. A phone's node carries
|
|
9
|
+
* WebSocket, circuit-relay and WebRTC transports but no TCP
|
|
10
|
+
* (`reference-app-rn/src/phone-node-config.ts`), and it listens on nothing, so the
|
|
11
|
+
* phone has to be the one that dials. Naming a `/ws` address is all the child needs:
|
|
12
|
+
* cadre-core adds the WebSocket transport for any listen entry that names one
|
|
13
|
+
* (`resolveTransportOptions` in `cadre-core/src/relay-addrs.ts`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function childListenAddrs(ports: Pick<NodePorts, 'p2p' | 'ws'>): string[];
|
|
3
16
|
/** Fixed friendly id for the admin's owner node. */
|
|
4
17
|
export declare const OWNER_CONTAINER_ID = "owner";
|
|
5
18
|
/** Endpoint + bearer for the owner node's loopback admin channel (6.6). */
|
|
@@ -168,11 +181,51 @@ export declare class HostProcessOrchestrator implements Orchestrator {
|
|
|
168
181
|
* `await` here.
|
|
169
182
|
*/
|
|
170
183
|
private launchChild;
|
|
184
|
+
/**
|
|
185
|
+
* Refuse to re-spawn `containerId` while a previous child of it may still be
|
|
186
|
+
* running. Both spawn paths call this as their last `await` before the handle
|
|
187
|
+
* drop, so a refusal happens while nothing has been released, no token rotated
|
|
188
|
+
* and no child launched — the caller sees an ordinary failed spawn.
|
|
189
|
+
*
|
|
190
|
+
* Why it exists: the drop hands the old handle's ports back and the launch takes
|
|
191
|
+
* those same ports (`reusedNodePorts`), so a child started over a live one dies
|
|
192
|
+
* on a port clash — but only after `launchChild` has returned success with a
|
|
193
|
+
* fresh seed token. The caller then records a dead child, and the live one
|
|
194
|
+
* refuses the new token.
|
|
195
|
+
*
|
|
196
|
+
* - A handle this process spawned is checked exactly ({@link isHandleLive}); a
|
|
197
|
+
* live one refuses outright.
|
|
198
|
+
* - A handle re-attached from `state.json` whose pid is alive but whose token
|
|
199
|
+
* does not match is either our child still starting or an unrelated process
|
|
200
|
+
* that inherited the pid. The ports decide: if any the re-spawn would take back
|
|
201
|
+
* is already bound, refuse. The pid is never killed on that evidence — it may
|
|
202
|
+
* not be ours.
|
|
203
|
+
*
|
|
204
|
+
* `overrides` are the ports the caller will override on top of the reused ones
|
|
205
|
+
* (the owner node's configured p2p port); `owner` adds the admin channel.
|
|
206
|
+
*
|
|
207
|
+
* NOTE: the probe cannot see a re-attached child still loading its modules —
|
|
208
|
+
* `cadre-cli start` writes its token and only then binds, so a child in that
|
|
209
|
+
* window shows neither, and a re-spawn launched then races it for the ports.
|
|
210
|
+
* Reachable only when the host restarts within a child's first seconds. If it
|
|
211
|
+
* is ever seen, treat a re-attached handle with a live pid as starting for a
|
|
212
|
+
* grace period after its `spawnedAt`.
|
|
213
|
+
*
|
|
214
|
+
* NOTE: the probe holds each port for an instant, so a child binding that port
|
|
215
|
+
* in the same instant fails. It would have had to write its token between the
|
|
216
|
+
* token check above and the probe, against the several awaited steps
|
|
217
|
+
* `cadre-cli start` takes between writing its token and binding; if a re-attached
|
|
218
|
+
* child is ever seen dying on its own port right after a host restart, probe by
|
|
219
|
+
* connecting instead of binding.
|
|
220
|
+
*/
|
|
221
|
+
private refuseRespawnOverLiveChild;
|
|
222
|
+
/** Every handle for `containerId`, in map order — the order `dropStaleHandle` drops them in. */
|
|
223
|
+
private handlesFor;
|
|
171
224
|
/**
|
|
172
225
|
* Drop any handle left over from a previous spawn of the same `containerId`
|
|
173
226
|
* and release its ports. Handles are keyed by the per-spawn `dockerId`, so
|
|
174
227
|
* without this a re-spawn (donated-node respawn) would strand the prior
|
|
175
|
-
* handle in the map forever, leaking
|
|
228
|
+
* handle in the map forever, leaking its ports from a bounded range each
|
|
176
229
|
* time. Mirrors the same cleanup in `ensureOwnerNode`.
|
|
177
230
|
*
|
|
178
231
|
* DO NOT delete the workdir: the identity key and node-local stores
|
|
@@ -180,11 +233,12 @@ export declare class HostProcessOrchestrator implements Orchestrator {
|
|
|
180
233
|
* the whole reason a respawn comes back as the same node. `launchChild`
|
|
181
234
|
* reuses the same workdir.
|
|
182
235
|
*
|
|
183
|
-
*
|
|
184
|
-
* re-spawn
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
236
|
+
* Releasing the ports hands them straight back to the allocator, and the
|
|
237
|
+
* re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
|
|
238
|
+
* re-spawn while the *previous* child is still listening would bind-clash.
|
|
239
|
+
* Callers must therefore never drop a live child, and both do so only after
|
|
240
|
+
* {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
|
|
241
|
+
* a live child must stop it first.
|
|
188
242
|
*
|
|
189
243
|
* **The drop is reversible.** It happens before the launch can fail, so the
|
|
190
244
|
* dropped handles are returned and every caller restores them via
|
|
@@ -197,7 +251,7 @@ export declare class HostProcessOrchestrator implements Orchestrator {
|
|
|
197
251
|
private dropStaleHandle;
|
|
198
252
|
/**
|
|
199
253
|
* Inverse of {@link dropStaleHandle}, for a spawn that then failed: put each
|
|
200
|
-
* handle back in the map and re-reserve its
|
|
254
|
+
* handle back in the map and re-reserve its ports, leaving host state
|
|
201
255
|
* exactly as the failed attempt found it.
|
|
202
256
|
*
|
|
203
257
|
* **Precondition: the drop → launch window is synchronous.** Restoring is
|
|
@@ -215,9 +269,10 @@ export declare class HostProcessOrchestrator implements Orchestrator {
|
|
|
215
269
|
*
|
|
216
270
|
* NOTE: a restored handle keeps `alive: true` even if its child exited while
|
|
217
271
|
* it was out of the map (`launchChild`'s exit listener looks the handle up by
|
|
218
|
-
* dockerId and finds nothing). Only reachable by
|
|
219
|
-
*
|
|
220
|
-
*
|
|
272
|
+
* dockerId and finds nothing). Only reachable by dropping a handle whose child
|
|
273
|
+
* is still live, which {@link refuseRespawnOverLiveChild} refuses before any
|
|
274
|
+
* drop. (Its start-up blind spot concerns re-attached handles only, and those
|
|
275
|
+
* have no exit listener to miss.)
|
|
221
276
|
*/
|
|
222
277
|
private restoreDroppedHandles;
|
|
223
278
|
/** A managed node's own directory under `rootDir` — config, log, storage, identity. */
|
|
@@ -233,6 +288,11 @@ export declare class HostProcessOrchestrator implements Orchestrator {
|
|
|
233
288
|
private releasePorts;
|
|
234
289
|
stopContainer(dockerId: string): Promise<void>;
|
|
235
290
|
removeContainer(dockerId: string): Promise<void>;
|
|
291
|
+
/**
|
|
292
|
+
* Whether the child `dockerId` names is running — see {@link isHandleLive}. An
|
|
293
|
+
* id with no handle is not running: without the handle's workdir there is no
|
|
294
|
+
* token to verify a live pid against.
|
|
295
|
+
*/
|
|
236
296
|
isRunning(dockerId: string): Promise<boolean>;
|
|
237
297
|
getStats(dockerId: string): Promise<OrchestratorStats>;
|
|
238
298
|
getLogs(dockerId: string, tail?: number): Promise<string>;
|
|
@@ -7,10 +7,11 @@ import debug from 'debug';
|
|
|
7
7
|
import pidusage from 'pidusage';
|
|
8
8
|
import { defaultLogPath, rotateOnDisk } from './log-rotator.js';
|
|
9
9
|
import { ensureNodeIdentity } from './node-identity.js';
|
|
10
|
-
import { allocateNodePorts, PortAllocator } from './port-allocator.js';
|
|
10
|
+
import { allocateNodePorts, PortAllocator, releaseNodePorts, reserveNodePorts, reusedNodePorts, } from './port-allocator.js';
|
|
11
11
|
import { StateStore } from './state-store.js';
|
|
12
12
|
import { isPidAlive } from './pid-liveness.js';
|
|
13
|
-
import {
|
|
13
|
+
import { assertPortFree } from './port-probe.js';
|
|
14
|
+
import { encodeDockerId, } from './types.js';
|
|
14
15
|
const log = debug('cadre:host:orchestrator');
|
|
15
16
|
const DEFAULTS = {
|
|
16
17
|
portRangeStart: 10000,
|
|
@@ -42,6 +43,38 @@ function scrubbedParentEnv() {
|
|
|
42
43
|
}
|
|
43
44
|
return env;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The libp2p listen addresses a managed child binds — its `CADRE_LISTEN_ADDRS`, one
|
|
48
|
+
* entry per element: TCP on the `p2p` port and WebSocket on the `ws` port, both on
|
|
49
|
+
* every interface.
|
|
50
|
+
*
|
|
51
|
+
* The WebSocket entry is what lets a phone reach the child. A phone's node carries
|
|
52
|
+
* WebSocket, circuit-relay and WebRTC transports but no TCP
|
|
53
|
+
* (`reference-app-rn/src/phone-node-config.ts`), and it listens on nothing, so the
|
|
54
|
+
* phone has to be the one that dials. Naming a `/ws` address is all the child needs:
|
|
55
|
+
* cadre-core adds the WebSocket transport for any listen entry that names one
|
|
56
|
+
* (`resolveTransportOptions` in `cadre-core/src/relay-addrs.ts`).
|
|
57
|
+
*/
|
|
58
|
+
export function childListenAddrs(ports) {
|
|
59
|
+
return [`/ip4/0.0.0.0/tcp/${ports.p2p}`, `/ip4/0.0.0.0/tcp/${ports.ws}/ws`];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Every TCP port a managed child binds, on the address it binds it to: health and
|
|
63
|
+
* metrics (`cadre-cli`'s `HealthServer`) and the libp2p TCP and WebSocket listeners
|
|
64
|
+
* ({@link childListenAddrs}) on every interface, plus the loopback admin channel —
|
|
65
|
+
* for the owner node only, the one child started with `--admin-port`. A key the
|
|
66
|
+
* set lacks (a handle persisted by an older build) is skipped.
|
|
67
|
+
*/
|
|
68
|
+
function childPortBindings(ports, owner) {
|
|
69
|
+
const bindings = [];
|
|
70
|
+
for (const port of [ports.health, ports.metrics, ports.p2p, ports.ws]) {
|
|
71
|
+
if (port !== undefined)
|
|
72
|
+
bindings.push({ port, host: '0.0.0.0' });
|
|
73
|
+
}
|
|
74
|
+
if (owner && ports.admin !== undefined)
|
|
75
|
+
bindings.push({ port: ports.admin, host: '127.0.0.1' });
|
|
76
|
+
return bindings;
|
|
77
|
+
}
|
|
45
78
|
/** Fixed friendly id for the admin's owner node. */
|
|
46
79
|
export const OWNER_CONTAINER_ID = 'owner';
|
|
47
80
|
/**
|
|
@@ -101,7 +134,6 @@ export class HostProcessOrchestrator {
|
|
|
101
134
|
const state = this.stateStore.load();
|
|
102
135
|
this.ownerConfig = state.ownerConfig;
|
|
103
136
|
for (const persisted of state.handles) {
|
|
104
|
-
const alive = isPidAlive(persisted.pid) && tokenMatches(persisted.workdir, persisted.startupToken);
|
|
105
137
|
const handle = {
|
|
106
138
|
containerId: persisted.containerId,
|
|
107
139
|
dockerId: persisted.dockerId,
|
|
@@ -113,8 +145,9 @@ export class HostProcessOrchestrator {
|
|
|
113
145
|
partyId: persisted.partyId,
|
|
114
146
|
profile: persisted.profile,
|
|
115
147
|
...(persisted.owner ? { owner: true } : {}),
|
|
116
|
-
alive,
|
|
148
|
+
alive: false,
|
|
117
149
|
};
|
|
150
|
+
handle.alive = isHandleLive(handle);
|
|
118
151
|
this.handles.set(handle.dockerId, handle);
|
|
119
152
|
// Always reserve ports — even for dead handles, until the caller cleans them up
|
|
120
153
|
this.reservePorts(persisted.ports);
|
|
@@ -191,8 +224,8 @@ export class HostProcessOrchestrator {
|
|
|
191
224
|
// Identity BEFORE any port is reserved: it is the one step here that can
|
|
192
225
|
// fail on its own (an unreadable or undecodable identity.key throws rather
|
|
193
226
|
// than silently re-keying), and everything past the allocation is either
|
|
194
|
-
// infallible or releases the ports itself. Allocating first would leak
|
|
195
|
-
// ports from a bounded range on every failed provision attempt.
|
|
227
|
+
// infallible or releases the ports itself. Allocating first would leak a
|
|
228
|
+
// node's worth of ports from a bounded range on every failed provision attempt.
|
|
196
229
|
const identity = await ensureNodeIdentity(workdir);
|
|
197
230
|
log('container %s identity peerId=%s', request.containerId, identity.peerId);
|
|
198
231
|
// A node started with pinned owner keys belongs to a FOREIGN cadre (the
|
|
@@ -203,8 +236,8 @@ export class HostProcessOrchestrator {
|
|
|
203
236
|
const foreignParty = (request.pinnedOwnerKeys?.length ?? 0) > 0;
|
|
204
237
|
// Only storage-profile nodes participate in strands and thus fan out push
|
|
205
238
|
// wakes — a transaction-only node need not carry credentials. Resolved
|
|
206
|
-
// BEFORE the drop below
|
|
207
|
-
//
|
|
239
|
+
// BEFORE the drop below, which keeps the drop → launch window synchronous
|
|
240
|
+
// (see restoreDroppedHandles).
|
|
208
241
|
const push = request.profile === 'storage' && !foreignParty ? await this.resolvePush() : undefined;
|
|
209
242
|
// Pinned owner key(s) reach the child via CADRE_OWNER_KEYS (comma-separated);
|
|
210
243
|
// `cadre-cli start` unions it into its cold-start pinnedKeyTrustPolicy so the
|
|
@@ -212,16 +245,17 @@ export class HostProcessOrchestrator {
|
|
|
212
245
|
const extraEnv = request.pinnedOwnerKeys?.length
|
|
213
246
|
? { CADRE_OWNER_KEYS: request.pinnedOwnerKeys.join(',') }
|
|
214
247
|
: undefined;
|
|
248
|
+
// The last `await` on this path: checked as close to the drop as it can be.
|
|
249
|
+
await this.refuseRespawnOverLiveChild(request.containerId, {}, false);
|
|
215
250
|
// Nothing from here to the launch is `await`ed — which is what keeps
|
|
216
251
|
// `restoreDroppedHandles`'s documented precondition true even though the
|
|
217
252
|
// `try` now opens further up. Do not add an `await` past this line.
|
|
218
253
|
dropped = this.dropStaleHandle(request.containerId);
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
ports = allocateNodePorts(this.portAllocator);
|
|
254
|
+
// A re-spawn comes back on the ports the dropped handle held, not merely the
|
|
255
|
+
// lowest free ones: a requester that dials the node itself (a phone) knows it
|
|
256
|
+
// only by the address it was given. The drop above just released them, and
|
|
257
|
+
// nothing can have taken them since — see reusedNodePorts.
|
|
258
|
+
ports = allocateNodePorts(this.portAllocator, reusedNodePorts(dropped));
|
|
225
259
|
return this.launchChild({
|
|
226
260
|
containerId: request.containerId,
|
|
227
261
|
partyId: request.partyId,
|
|
@@ -355,7 +389,7 @@ export class HostProcessOrchestrator {
|
|
|
355
389
|
}
|
|
356
390
|
this.ownerConfig = config;
|
|
357
391
|
const existing = this.findOwnerHandle();
|
|
358
|
-
if (existing &&
|
|
392
|
+
if (existing && isHandleLive(existing)) {
|
|
359
393
|
this.persist();
|
|
360
394
|
return toNodeInfo(existing);
|
|
361
395
|
}
|
|
@@ -365,6 +399,8 @@ export class HostProcessOrchestrator {
|
|
|
365
399
|
// Resolved BEFORE the drop below so the drop → launch window stays
|
|
366
400
|
// synchronous (see restoreDroppedHandles).
|
|
367
401
|
const push = await this.resolvePush();
|
|
402
|
+
// The last `await` before the drop — same guard as in createContainer.
|
|
403
|
+
await this.refuseRespawnOverLiveChild(OWNER_CONTAINER_ID, { p2p: config.libp2pPort }, true);
|
|
368
404
|
// NOTE: the short-circuit above matches via `findOwnerHandle` (owner flag OR
|
|
369
405
|
// containerId), this drop only by containerId. Equivalent today — every
|
|
370
406
|
// owner handle is launched with OWNER_CONTAINER_ID. If an owner spawn ever
|
|
@@ -374,8 +410,10 @@ export class HostProcessOrchestrator {
|
|
|
374
410
|
let ports;
|
|
375
411
|
try {
|
|
376
412
|
// The owner node must listen on the configured libp2p port so the NAT
|
|
377
|
-
// mapping (external → internal) lands on it — hence the p2p override
|
|
378
|
-
|
|
413
|
+
// mapping (external → internal) lands on it — hence the p2p override, which
|
|
414
|
+
// wins over the dropped handle's. Every other port comes back as it was, for
|
|
415
|
+
// the same reason as in createContainer.
|
|
416
|
+
ports = allocateNodePorts(this.portAllocator, { ...reusedNodePorts(dropped), p2p: config.libp2pPort });
|
|
379
417
|
const result = this.launchChild({
|
|
380
418
|
containerId: OWNER_CONTAINER_ID,
|
|
381
419
|
partyId: config.partyId,
|
|
@@ -493,17 +531,19 @@ export class HostProcessOrchestrator {
|
|
|
493
531
|
CADRE_STARTUP_TOKEN: token,
|
|
494
532
|
CADRE_HEALTH_PORT: String(ports.health),
|
|
495
533
|
CADRE_METRICS_PORT: String(ports.metrics),
|
|
496
|
-
CADRE_LISTEN_ADDRS:
|
|
534
|
+
CADRE_LISTEN_ADDRS: childListenAddrs(ports).join(','),
|
|
497
535
|
// NOTE: the scrub above drops every CADRE_* var and `extraEnv` is built only from
|
|
498
|
-
// pinnedOwnerKeys, so a managed child advertises only the
|
|
536
|
+
// pinnedOwnerKeys, so a managed child advertises only the ports assigned to it here
|
|
499
537
|
// — `CADRE_ANNOUNCE_ADDRS`/`CADRE_APPEND_ANNOUNCE_ADDRS` cannot reach it. Fine while
|
|
500
|
-
// children are reached at
|
|
501
|
-
// a proxy or DNS name, plumb an announce var through from host config. The
|
|
502
|
-
// the CHILD'S CONTROL NODE's alone: each strand node the
|
|
503
|
-
//
|
|
504
|
-
// (`cadre-core/src/strand-network-config.ts`). A NAT
|
|
505
|
-
//
|
|
506
|
-
//
|
|
538
|
+
// children are reached at those ports or through a relay; if a host is ever fronted
|
|
539
|
+
// by a proxy or DNS name, plumb an announce var through from host config. The TCP
|
|
540
|
+
// and WebSocket ports are the CHILD'S CONTROL NODE's alone: each strand node the
|
|
541
|
+
// child runs binds the same two entries with OS-assigned ports instead, since one
|
|
542
|
+
// port cannot be held twice (`cadre-core/src/strand-network-config.ts`). A NAT
|
|
543
|
+
// forward (the owner node's, via `NatService`) covers the TCP port only, so it
|
|
544
|
+
// reaches the control node over TCP alone; a phone outside the LAN, which needs the
|
|
545
|
+
// WebSocket port, and every strand node are reached through observed addresses or
|
|
546
|
+
// a relay.
|
|
507
547
|
CADRE_SEED_TOKEN: seedToken,
|
|
508
548
|
// Pin each child's node-local state (trusted-owner anchor, retained
|
|
509
549
|
// cold-start dial targets) to its OWN workdir. This is the same value the
|
|
@@ -604,11 +644,67 @@ export class HostProcessOrchestrator {
|
|
|
604
644
|
p2pPort: ports.p2p,
|
|
605
645
|
};
|
|
606
646
|
}
|
|
647
|
+
/**
|
|
648
|
+
* Refuse to re-spawn `containerId` while a previous child of it may still be
|
|
649
|
+
* running. Both spawn paths call this as their last `await` before the handle
|
|
650
|
+
* drop, so a refusal happens while nothing has been released, no token rotated
|
|
651
|
+
* and no child launched — the caller sees an ordinary failed spawn.
|
|
652
|
+
*
|
|
653
|
+
* Why it exists: the drop hands the old handle's ports back and the launch takes
|
|
654
|
+
* those same ports (`reusedNodePorts`), so a child started over a live one dies
|
|
655
|
+
* on a port clash — but only after `launchChild` has returned success with a
|
|
656
|
+
* fresh seed token. The caller then records a dead child, and the live one
|
|
657
|
+
* refuses the new token.
|
|
658
|
+
*
|
|
659
|
+
* - A handle this process spawned is checked exactly ({@link isHandleLive}); a
|
|
660
|
+
* live one refuses outright.
|
|
661
|
+
* - A handle re-attached from `state.json` whose pid is alive but whose token
|
|
662
|
+
* does not match is either our child still starting or an unrelated process
|
|
663
|
+
* that inherited the pid. The ports decide: if any the re-spawn would take back
|
|
664
|
+
* is already bound, refuse. The pid is never killed on that evidence — it may
|
|
665
|
+
* not be ours.
|
|
666
|
+
*
|
|
667
|
+
* `overrides` are the ports the caller will override on top of the reused ones
|
|
668
|
+
* (the owner node's configured p2p port); `owner` adds the admin channel.
|
|
669
|
+
*
|
|
670
|
+
* NOTE: the probe cannot see a re-attached child still loading its modules —
|
|
671
|
+
* `cadre-cli start` writes its token and only then binds, so a child in that
|
|
672
|
+
* window shows neither, and a re-spawn launched then races it for the ports.
|
|
673
|
+
* Reachable only when the host restarts within a child's first seconds. If it
|
|
674
|
+
* is ever seen, treat a re-attached handle with a live pid as starting for a
|
|
675
|
+
* grace period after its `spawnedAt`.
|
|
676
|
+
*
|
|
677
|
+
* NOTE: the probe holds each port for an instant, so a child binding that port
|
|
678
|
+
* in the same instant fails. It would have had to write its token between the
|
|
679
|
+
* token check above and the probe, against the several awaited steps
|
|
680
|
+
* `cadre-cli start` takes between writing its token and binding; if a re-attached
|
|
681
|
+
* child is ever seen dying on its own port right after a host restart, probe by
|
|
682
|
+
* connecting instead of binding.
|
|
683
|
+
*/
|
|
684
|
+
async refuseRespawnOverLiveChild(containerId, overrides, owner) {
|
|
685
|
+
const previous = this.handlesFor(containerId);
|
|
686
|
+
if (previous.some(isHandleLive)) {
|
|
687
|
+
throw new Error(`container ${containerId} is still running`);
|
|
688
|
+
}
|
|
689
|
+
if (!previous.some((h) => isPidAlive(h.pid)))
|
|
690
|
+
return;
|
|
691
|
+
const bindings = childPortBindings({ ...reusedNodePorts(previous), ...overrides }, owner);
|
|
692
|
+
try {
|
|
693
|
+
await Promise.all(bindings.map(assertPortFree));
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
throw new Error(`container ${containerId} may still be running: ${err.message}`, { cause: err });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** Every handle for `containerId`, in map order — the order `dropStaleHandle` drops them in. */
|
|
700
|
+
handlesFor(containerId) {
|
|
701
|
+
return [...this.handles.values()].filter((h) => h.containerId === containerId);
|
|
702
|
+
}
|
|
607
703
|
/**
|
|
608
704
|
* Drop any handle left over from a previous spawn of the same `containerId`
|
|
609
705
|
* and release its ports. Handles are keyed by the per-spawn `dockerId`, so
|
|
610
706
|
* without this a re-spawn (donated-node respawn) would strand the prior
|
|
611
|
-
* handle in the map forever, leaking
|
|
707
|
+
* handle in the map forever, leaking its ports from a bounded range each
|
|
612
708
|
* time. Mirrors the same cleanup in `ensureOwnerNode`.
|
|
613
709
|
*
|
|
614
710
|
* DO NOT delete the workdir: the identity key and node-local stores
|
|
@@ -616,11 +712,12 @@ export class HostProcessOrchestrator {
|
|
|
616
712
|
* the whole reason a respawn comes back as the same node. `launchChild`
|
|
617
713
|
* reuses the same workdir.
|
|
618
714
|
*
|
|
619
|
-
*
|
|
620
|
-
* re-spawn
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
715
|
+
* Releasing the ports hands them straight back to the allocator, and the
|
|
716
|
+
* re-spawn then takes those same ports as overrides (`reusedNodePorts`), so a
|
|
717
|
+
* re-spawn while the *previous* child is still listening would bind-clash.
|
|
718
|
+
* Callers must therefore never drop a live child, and both do so only after
|
|
719
|
+
* {@link refuseRespawnOverLiveChild} has passed. A caller that needs to replace
|
|
720
|
+
* a live child must stop it first.
|
|
624
721
|
*
|
|
625
722
|
* **The drop is reversible.** It happens before the launch can fail, so the
|
|
626
723
|
* dropped handles are returned and every caller restores them via
|
|
@@ -631,20 +728,17 @@ export class HostProcessOrchestrator {
|
|
|
631
728
|
* gone by then.
|
|
632
729
|
*/
|
|
633
730
|
dropStaleHandle(containerId) {
|
|
634
|
-
const dropped =
|
|
635
|
-
for (const h of
|
|
636
|
-
if (h.containerId !== containerId)
|
|
637
|
-
continue;
|
|
731
|
+
const dropped = this.handlesFor(containerId);
|
|
732
|
+
for (const h of dropped) {
|
|
638
733
|
log('dropping stale handle %s for container %s', h.dockerId, containerId);
|
|
639
734
|
this.releasePorts(h.ports);
|
|
640
735
|
this.handles.delete(h.dockerId);
|
|
641
|
-
dropped.push(h);
|
|
642
736
|
}
|
|
643
737
|
return dropped;
|
|
644
738
|
}
|
|
645
739
|
/**
|
|
646
740
|
* Inverse of {@link dropStaleHandle}, for a spawn that then failed: put each
|
|
647
|
-
* handle back in the map and re-reserve its
|
|
741
|
+
* handle back in the map and re-reserve its ports, leaving host state
|
|
648
742
|
* exactly as the failed attempt found it.
|
|
649
743
|
*
|
|
650
744
|
* **Precondition: the drop → launch window is synchronous.** Restoring is
|
|
@@ -662,9 +756,10 @@ export class HostProcessOrchestrator {
|
|
|
662
756
|
*
|
|
663
757
|
* NOTE: a restored handle keeps `alive: true` even if its child exited while
|
|
664
758
|
* it was out of the map (`launchChild`'s exit listener looks the handle up by
|
|
665
|
-
* dockerId and finds nothing). Only reachable by
|
|
666
|
-
*
|
|
667
|
-
*
|
|
759
|
+
* dockerId and finds nothing). Only reachable by dropping a handle whose child
|
|
760
|
+
* is still live, which {@link refuseRespawnOverLiveChild} refuses before any
|
|
761
|
+
* drop. (Its start-up blind spot concerns re-attached handles only, and those
|
|
762
|
+
* have no exit listener to miss.)
|
|
668
763
|
*/
|
|
669
764
|
restoreDroppedHandles(dropped) {
|
|
670
765
|
for (const h of dropped) {
|
|
@@ -691,19 +786,10 @@ export class HostProcessOrchestrator {
|
|
|
691
786
|
* launch then failed.
|
|
692
787
|
*/
|
|
693
788
|
reservePorts(ports) {
|
|
694
|
-
this.portAllocator
|
|
695
|
-
this.portAllocator.markUsed(ports.metrics);
|
|
696
|
-
this.portAllocator.markUsed(ports.p2p);
|
|
697
|
-
// `admin` is absent on handles persisted before this build; markUsed would
|
|
698
|
-
// otherwise put `undefined` in the used-set.
|
|
699
|
-
if (typeof ports.admin === 'number')
|
|
700
|
-
this.portAllocator.markUsed(ports.admin);
|
|
789
|
+
reserveNodePorts(this.portAllocator, ports);
|
|
701
790
|
}
|
|
702
791
|
releasePorts(ports) {
|
|
703
|
-
this.portAllocator
|
|
704
|
-
this.portAllocator.release(ports.metrics);
|
|
705
|
-
this.portAllocator.release(ports.p2p);
|
|
706
|
-
this.portAllocator.release(ports.admin);
|
|
792
|
+
releaseNodePorts(this.portAllocator, ports);
|
|
707
793
|
}
|
|
708
794
|
async stopContainer(dockerId) {
|
|
709
795
|
const handle = this.requireHandle(dockerId);
|
|
@@ -770,37 +856,20 @@ export class HostProcessOrchestrator {
|
|
|
770
856
|
this.handles.delete(dockerId);
|
|
771
857
|
this.persist();
|
|
772
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Whether the child `dockerId` names is running — see {@link isHandleLive}. An
|
|
861
|
+
* id with no handle is not running: without the handle's workdir there is no
|
|
862
|
+
* token to verify a live pid against.
|
|
863
|
+
*/
|
|
773
864
|
async isRunning(dockerId) {
|
|
774
865
|
const handle = this.handles.get(dockerId);
|
|
775
|
-
|
|
776
|
-
let token;
|
|
777
|
-
let workdir;
|
|
778
|
-
if (handle) {
|
|
779
|
-
pid = handle.pid;
|
|
780
|
-
token = handle.startupToken;
|
|
781
|
-
workdir = handle.workdir;
|
|
782
|
-
}
|
|
783
|
-
else {
|
|
784
|
-
try {
|
|
785
|
-
const decoded = decodeDockerId(dockerId);
|
|
786
|
-
pid = decoded.pid;
|
|
787
|
-
token = decoded.token;
|
|
788
|
-
}
|
|
789
|
-
catch {
|
|
790
|
-
return false;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
if (!isPidAlive(pid)) {
|
|
794
|
-
if (handle)
|
|
795
|
-
handle.alive = false;
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
if (workdir === undefined) {
|
|
799
|
-
// We have no record — without a workdir we can't verify the token,
|
|
800
|
-
// so we must treat this as unknown rather than running.
|
|
866
|
+
if (!handle)
|
|
801
867
|
return false;
|
|
802
|
-
|
|
803
|
-
|
|
868
|
+
if (isHandleLive(handle))
|
|
869
|
+
return true;
|
|
870
|
+
if (!isPidAlive(handle.pid))
|
|
871
|
+
handle.alive = false;
|
|
872
|
+
return false;
|
|
804
873
|
}
|
|
805
874
|
async getStats(dockerId) {
|
|
806
875
|
const handle = this.requireHandle(dockerId);
|
|
@@ -958,6 +1027,30 @@ function resolveCadreCliBin() {
|
|
|
958
1027
|
`and listed as a dependency. Underlying error: ${err.message}`, { cause: err });
|
|
959
1028
|
}
|
|
960
1029
|
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Whether the process a handle names is still the child it was launched as — the
|
|
1032
|
+
* one liveness rule behind `init`, `isRunning`, `ensureOwnerNode`'s short-circuit
|
|
1033
|
+
* and the pre-spawn guard, so they cannot drift apart.
|
|
1034
|
+
*
|
|
1035
|
+
* A handle this process spawned carries its `ChildProcess`, and that answer is
|
|
1036
|
+
* exact from the moment of spawn: no exit seen, and the pid still answering. (The
|
|
1037
|
+
* pid check covers the moment between the OS ending the process and Node
|
|
1038
|
+
* delivering its exit; the pid cannot have been reused in that moment, since the
|
|
1039
|
+
* OS holds it until Node has reaped the child.) The startup-token file is no use
|
|
1040
|
+
* here — a child writes it only once it is running its own code, so reading it
|
|
1041
|
+
* would report every child dead for its first moments.
|
|
1042
|
+
*
|
|
1043
|
+
* A handle re-attached from `state.json` after a host restart has no
|
|
1044
|
+
* `ChildProcess`, so it falls back to the pid plus that token file, which tells our
|
|
1045
|
+
* child apart from an unrelated process that inherited its pid.
|
|
1046
|
+
*/
|
|
1047
|
+
function isHandleLive(handle) {
|
|
1048
|
+
const { child } = handle;
|
|
1049
|
+
if (child) {
|
|
1050
|
+
return child.exitCode === null && child.signalCode === null && isPidAlive(handle.pid);
|
|
1051
|
+
}
|
|
1052
|
+
return isPidAlive(handle.pid) && tokenMatches(handle.workdir, handle.startupToken);
|
|
1053
|
+
}
|
|
961
1054
|
function tokenMatches(workdir, expected) {
|
|
962
1055
|
const tokenPath = join(workdir, STARTUP_TOKEN_FILE);
|
|
963
1056
|
try {
|