@quolu/lattice 0.79.3 → 0.79.5
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 +8 -2
- package/bin/lattice-bridge.mjs +31 -24
- package/bin/lattice-dashboard-supervisor.mjs +18 -0
- package/bin/lattice-dashboard.mjs +37 -14
- package/bin/lattice-mcp.mjs +2 -1
- package/docs/bridge-setup.md +37 -0
- package/package.json +2 -2
- package/sensor/dist/bin/lattice-sensor.js +6 -26
- package/sensor/dist/bin/node-version-check.d.ts +10 -1
- package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
- package/sensor/dist/bin/node-version-check.js +43 -1
- package/sensor/dist/bin/node-version-check.js.map +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +4 -0
- package/sensor/dist/mcp/daemon-paths.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon-paths.js +12 -0
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/index.d.ts +3 -1
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +59 -30
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/proxy.d.ts +2 -0
- package/sensor/dist/mcp/proxy.d.ts.map +1 -1
- package/sensor/dist/mcp/proxy.js +9 -3
- package/sensor/dist/mcp/proxy.js.map +1 -1
- package/sensor/dist/mcp/session.d.ts +7 -0
- package/sensor/dist/mcp/session.d.ts.map +1 -1
- package/sensor/dist/mcp/session.js +4 -1
- package/sensor/dist/mcp/session.js.map +1 -1
- package/sensor/dist/mcp/transport.d.ts +5 -5
- package/sensor/dist/mcp/transport.d.ts.map +1 -1
- package/sensor/dist/mcp/transport.js +14 -6
- package/sensor/dist/mcp/transport.js.map +1 -1
- package/sensor/package.json +1 -1
- package/src/bridge-cli.mjs +6 -2
- package/src/bridge-config.mjs +8 -0
- package/src/bridge-daemon.mjs +30 -2
- package/src/dashboard-delivery.mjs +14 -2
- package/src/dashboard-lifecycle-ipc.mjs +5 -0
- package/src/dashboard-lifecycle-journal.mjs +237 -0
- package/src/dashboard-process-control.mjs +74 -0
- package/src/dashboard-supervisor.mjs +180 -0
- package/src/dashboard-windows-process-control.ps1 +40 -0
- package/src/factory-diagnostics.mjs +22 -9
- package/src/mcp-startup-receipt.mjs +99 -0
- package/src/node-version-guard.mjs +3 -42
- package/src/setup-config.mjs +7 -3
- package/src/todo-dashboard-registry.mjs +24 -3
- package/src/todo-store-cache.mjs +50 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/mcp/transport.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEH,mDAAqC;AAyCrC,gCAAgC;AACnB,QAAA,UAAU,GAAG;IACxB,UAAU,EAAE,CAAC,KAAK;IAClB,cAAc,EAAE,CAAC,KAAK;IACtB,cAAc,EAAE,CAAC,KAAK;IACtB,aAAa,EAAE,CAAC,KAAK;IACrB,aAAa,EAAE,CAAC,KAAK;CACb,CAAC;AAmBX;;;;;GAKG;AACH,MAAe,yBAAyB;IAC5B,cAAc,GAA0B,IAAI,CAAC;IACvD,2EAA2E;IAC3E,4DAA4D;IAClD,OAAO,GAAG,IAAI,GAAG,EAGvB,CAAC;IACK,aAAa,GAAG,CAAC,CAAC;IAClB,OAAO,GAAG,KAAK,CAAC;IAO1B;;;;;;;OAOG;IACH,OAAO,CAAC,MAAc,EAAE,MAAgB,EAAE,SAAS,GAAG,IAAI;QACxD,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;QACxD,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC9C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACxB,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,SAAS,mBAAmB,MAAM,YAAY,CAAC,CAAC,CAAC;YACvF,CAAC,EAAE,SAAS,CAAC,CAAC;YACd,kEAAkE;YAClE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;YAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;gBACnB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;gBAC5D,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;aAC3D,CAAC,CAAC;YACH,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,CAAC,QAAyB;QAC5B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,MAAM,CAAC,MAAc,EAAE,MAAgB;QACrC,MAAM,YAAY,GAAwB,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAC7E,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,UAAU,CAAC,EAAmB,EAAE,MAAe;
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/mcp/transport.ts"],"names":[],"mappings":"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"}
|
package/sensor/package.json
CHANGED
package/src/bridge-cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import * as clack from '@clack/prompts';
|
|
|
6
6
|
import packageJson from '../package.json' with { type: 'json' };
|
|
7
7
|
import { resolveBridgeListenAddress } from './bridge-address.mjs';
|
|
8
8
|
import { registerBridgeUpstream } from './bridge-registrar.mjs';
|
|
9
|
-
import { ensureTodoDashboardDaemon } from './todo-dashboard-registry.mjs';
|
|
9
|
+
import { ensureTodoDashboardDaemon, readTodoDashboardLifecycleStatus } from './todo-dashboard-registry.mjs';
|
|
10
10
|
import { verifyBridgeHubDelivery } from './dashboard-delivery.mjs';
|
|
11
11
|
import {
|
|
12
12
|
BridgeConfigError, configureBridge, disableBridge, readBridgeConfig, restoreBridgeConfig,
|
|
@@ -472,7 +472,11 @@ export async function runBridgeCli({ argv, stdout, stderr, env = process.env,
|
|
|
472
472
|
if (wizard) {
|
|
473
473
|
stdout.write(`Lattice bridgeを${config.listen.address}:${config.listen.port}で有効にしました。\n`);
|
|
474
474
|
for (const warning of warnings ?? []) stdout.write(`警告: ${warning.message}\n`);
|
|
475
|
-
} else
|
|
475
|
+
} else {
|
|
476
|
+
const output = result(command, config, recovery, liveness, diagnostics, warnings);
|
|
477
|
+
if (command === 'status') output.dashboard_lifecycle = await readTodoDashboardLifecycleStatus({ env });
|
|
478
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
479
|
+
}
|
|
476
480
|
return 0;
|
|
477
481
|
} catch (error) {
|
|
478
482
|
const payload = { schema: 'lattice.cli_error.v2',
|
package/src/bridge-config.mjs
CHANGED
|
@@ -270,6 +270,14 @@ export async function withBridgeOperationLock({ env = process.env } = {}, action
|
|
|
270
270
|
return withLock(refs.operationLock, action);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/** Serialize request publication with daemon shutdown; never hold this while waiting for a receipt. */
|
|
274
|
+
export async function withBridgeStopTransition({ env = process.env } = {}, action) {
|
|
275
|
+
if (typeof action !== 'function') throw new TypeError('bridge stop action required');
|
|
276
|
+
const refs = bridgeConfigPaths(env);
|
|
277
|
+
await prepareRoot(refs.root);
|
|
278
|
+
return withLock(path.join(refs.root, 'bridge-stop-transition.lock'), action);
|
|
279
|
+
}
|
|
280
|
+
|
|
273
281
|
function closeServer(server) {
|
|
274
282
|
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
275
283
|
}
|
package/src/bridge-daemon.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { parseTree } from 'jsonc-parser';
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
BRIDGE_PORT_MAX, BRIDGE_PORT_MIN, BridgeConfigError, bridgeConfigPaths, readBridgeConfig,
|
|
12
|
+
withBridgeStopTransition,
|
|
12
13
|
} from './bridge-config.mjs';
|
|
13
14
|
import { resolveBridgeListenAddress } from './bridge-address.mjs';
|
|
14
15
|
import packageJson from '../package.json' with { type: 'json' };
|
|
@@ -201,9 +202,24 @@ export async function clearBridgeStopControl({ env = process.env } = {}) {
|
|
|
201
202
|
await rm(refs.receipt, { force: true });
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
/** Close the data plane before acknowledging the latest published request. */
|
|
206
|
+
export async function completeBridgeDaemonStop({ env = process.env, close } = {}) {
|
|
207
|
+
if (typeof close !== 'function') throw new TypeError('bridge close action required');
|
|
208
|
+
return withBridgeStopTransition({ env }, async () => {
|
|
209
|
+
await close();
|
|
210
|
+
const request = await readBridgeStopRequest({ env });
|
|
211
|
+
if (request !== null) await writeBridgeStopReceipt({ nonce: request.nonce, env });
|
|
212
|
+
await removeBridgeDaemonDescriptor({ env });
|
|
213
|
+
await removeBridgeDaemonActiveMarker({ env });
|
|
214
|
+
return { nonce: request?.nonce ?? null };
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function publishBridgeStopRequest({ env, listen }) {
|
|
205
219
|
let descriptor = null;
|
|
206
|
-
try { descriptor = await readBridgeDaemonDescriptor({ env }); } catch {
|
|
220
|
+
try { descriptor = await readBridgeDaemonDescriptor({ env }); } catch (error) {
|
|
221
|
+
if (error?.code !== 'BRIDGE_DAEMON_DESCRIPTOR_INVALID') throw error;
|
|
222
|
+
}
|
|
207
223
|
let config = null;
|
|
208
224
|
try { config = await readBridgeConfig({ env }); } catch (error) {
|
|
209
225
|
if (!['BRIDGE_CONFIG_INVALID', 'BRIDGE_CONFIG_MODE_INVALID'].includes(error?.code)) throw error;
|
|
@@ -234,11 +250,23 @@ export async function requestBridgeDaemonStop({ env = process.env, listen = null
|
|
|
234
250
|
}
|
|
235
251
|
}
|
|
236
252
|
const refs = bridgeStopPaths(env);
|
|
253
|
+
// Concurrent callers join the published request instead of overwriting its nonce.
|
|
254
|
+
const pending = await readBridgeStopRequest({ env });
|
|
255
|
+
if (pending !== null) return { state: 'requested', nonce: pending.nonce };
|
|
237
256
|
const nonce = randomBytes(32).toString('hex');
|
|
238
257
|
await rm(refs.receipt, { force: true });
|
|
239
258
|
await atomicDescriptor(refs.request, {
|
|
240
259
|
schema: STOP_REQUEST_SCHEMA, nonce, requested_at: new Date().toISOString(),
|
|
241
260
|
});
|
|
261
|
+
return { state: 'requested', nonce };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function requestBridgeDaemonStop({ env = process.env, listen = null } = {}) {
|
|
265
|
+
const published = await withBridgeStopTransition({ env }, () => publishBridgeStopRequest({ env, listen }));
|
|
266
|
+
if (published.state === 'not_running') return published;
|
|
267
|
+
const { nonce } = published;
|
|
268
|
+
const refs = bridgeStopPaths(env);
|
|
269
|
+
// Wait outside the transition lock: the daemon needs it to close and acknowledge.
|
|
242
270
|
const deadline = Date.now() + STOP_TIMEOUT_MS;
|
|
243
271
|
while (Date.now() < deadline) {
|
|
244
272
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
@@ -5,6 +5,12 @@ import { createBridgeHubHeartbeatController } from './bridge-hub-heartbeat.mjs';
|
|
|
5
5
|
|
|
6
6
|
const SCHEMA = 'lattice.dashboard_delivery.v1';
|
|
7
7
|
const SETUP = 'lattice bridge setup --listen <IP> --dashboard --hub <URL> --json';
|
|
8
|
+
const FAILURE_STAGES = new Map([
|
|
9
|
+
...['heartbeat_missing', 'heartbeat_invalid', 'unreachable', 'rejected', 'skipped_no_dashboard',
|
|
10
|
+
'skipped_no_projects', 'project_not_registered'].map((reason) => [reason, 'heartbeat_registration']),
|
|
11
|
+
...['hub_listing_unavailable', 'project_not_visible'].map((reason) => [reason, 'hub_listing']),
|
|
12
|
+
...['project_unreachable', 'project_response_invalid'].map((reason) => [reason, 'project_route']),
|
|
13
|
+
]);
|
|
8
14
|
|
|
9
15
|
function receipt(state, reason, projectIds = [], urls = [], nextAction = null) {
|
|
10
16
|
return { schema: SCHEMA, state, reason, project_ids: projectIds, urls, next_action: nextAction };
|
|
@@ -13,7 +19,8 @@ function receipt(state, reason, projectIds = [], urls = [], nextAction = null) {
|
|
|
13
19
|
function unconfirmed(reason, projectIds = []) {
|
|
14
20
|
return new BridgeConfigError('BRIDGE_HUB_DELIVERY_UNCONFIRMED',
|
|
15
21
|
'工程の公開配信を確認できません。接続設定は保存済みです。', {
|
|
16
|
-
reason,
|
|
22
|
+
reason, stage: FAILURE_STAGES.get(reason), project_ids: projectIds,
|
|
23
|
+
next_action: 'lattice todo dashboard ensure --json',
|
|
17
24
|
});
|
|
18
25
|
}
|
|
19
26
|
|
|
@@ -29,7 +36,8 @@ export async function verifyBridgeHubDelivery({ config, env = process.env, proje
|
|
|
29
36
|
|| result?.schema !== 'lattice.bridge_hub_registration_result.v2'
|
|
30
37
|
|| !Array.isArray(registered) || !registered.every((id) => typeof id === 'string')
|
|
31
38
|
|| !Array.isArray(result.rejected)) {
|
|
32
|
-
throw unconfirmed(delivered?.state
|
|
39
|
+
throw unconfirmed(FAILURE_STAGES.has(delivered?.state) ? delivered.state
|
|
40
|
+
: delivered?.state == null ? 'heartbeat_missing' : 'heartbeat_invalid');
|
|
33
41
|
}
|
|
34
42
|
const projectIds = projectId === null ? registered : [projectId];
|
|
35
43
|
if (projectIds.length === 0 || projectIds.some((id) => !registered.includes(id))
|
|
@@ -97,6 +105,10 @@ export async function reportProjectDashboardDelivery({ projectId, env, stderr,
|
|
|
97
105
|
if (typeof error?.code !== 'string') throw error;
|
|
98
106
|
delivery = receipt('failed', error.code, [projectId], [],
|
|
99
107
|
error.detail?.next_action ?? 'lattice bridge status --json');
|
|
108
|
+
const subreason = error.detail?.reason;
|
|
109
|
+
delivery.failure = error.code === 'BRIDGE_HUB_DELIVERY_UNCONFIRMED' && FAILURE_STAGES.has(subreason)
|
|
110
|
+
? { stage: FAILURE_STAGES.get(subreason), subreason }
|
|
111
|
+
: { stage: 'unknown', subreason: 'unavailable' };
|
|
100
112
|
}
|
|
101
113
|
stderr?.write(`${JSON.stringify(delivery)}\n`);
|
|
102
114
|
return delivery;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readdir, readFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'lattice.dashboard_lifecycle.v1';
|
|
7
|
+
export const LIFECYCLE_MAX_RUNS = 64;
|
|
8
|
+
export const LIFECYCLE_MAX_BYTES = 16 * 1024;
|
|
9
|
+
const PHASES = new Set(['spawn', 'import', 'registry', 'store', 'listen', 'ready',
|
|
10
|
+
'periodic_sync', 'empty_shutdown', 'shutdown_request', 'sync_failed', 'store_read_failed',
|
|
11
|
+
'store_manifest_missing', 'store_root_missing', 'store_access_denied', 'store_io_failed',
|
|
12
|
+
'store_manifest_invalid', 'store_artifact_missing',
|
|
13
|
+
'cleanup_failed', 'close_failed', 'close', 'closed']);
|
|
14
|
+
const RUN_ID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u;
|
|
15
|
+
const WINDOWS_REPLACE_WAIT_MS = 500;
|
|
16
|
+
const REPLACE_MAX_ATTEMPTS = 32;
|
|
17
|
+
|
|
18
|
+
export function lifecycleError(code) {
|
|
19
|
+
return Object.assign(new Error(code), { code,
|
|
20
|
+
detail: { reason: code.toLowerCase(), next_action: 'lattice todo dashboard ensure --json' } });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function directory(root) { return path.join(root, 'lifecycle'); }
|
|
24
|
+
function terminationTime(run) {
|
|
25
|
+
return run.exit_observed_at ?? run.observer_loss_observed_at ?? run.started_at;
|
|
26
|
+
}
|
|
27
|
+
function reference(root, id) {
|
|
28
|
+
if (!RUN_ID.test(id)) throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID');
|
|
29
|
+
return path.join(directory(root), `${id}.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function checkOwned(ref, type) {
|
|
33
|
+
const info = await lstat(ref);
|
|
34
|
+
if (info.isSymbolicLink() || (type === 'directory' ? !info.isDirectory() : !info.isFile())
|
|
35
|
+
|| (typeof process.getuid === 'function' && info.uid !== process.getuid())
|
|
36
|
+
|| (process.platform !== 'win32' && (info.mode & 0o077) !== 0)) {
|
|
37
|
+
throw lifecycleError('DASHBOARD_LIFECYCLE_UNSAFE_PATH');
|
|
38
|
+
}
|
|
39
|
+
return info;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function prepareLifecycleDirectory(root) {
|
|
43
|
+
// The runtime root is already private and owned by the dashboard registry.
|
|
44
|
+
await checkOwned(root, 'directory');
|
|
45
|
+
await mkdir(directory(root), { mode: 0o700 }).catch((error) => {
|
|
46
|
+
if (error.code !== 'EEXIST') throw error;
|
|
47
|
+
});
|
|
48
|
+
await checkOwned(directory(root), 'directory');
|
|
49
|
+
// Interrupted atomic writes contain no body data either, but must not grow forever.
|
|
50
|
+
const pending = /^(?:[a-f0-9-]{36}\.json|ensure\.json)\.([1-9][0-9]*)\.[a-f0-9-]{36}\.tmp$/u;
|
|
51
|
+
for (const name of await readdir(directory(root))) {
|
|
52
|
+
const match = pending.exec(name);
|
|
53
|
+
if (!match) continue;
|
|
54
|
+
let alive = true;
|
|
55
|
+
try { process.kill(Number(match[1]), 0); } catch (error) {
|
|
56
|
+
if (error.code !== 'ESRCH') throw error;
|
|
57
|
+
alive = false;
|
|
58
|
+
}
|
|
59
|
+
if (!alive) {
|
|
60
|
+
const ref = path.join(directory(root), name);
|
|
61
|
+
await checkOwned(ref, 'file');
|
|
62
|
+
await rm(ref);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function readLifecycleRuns(root) {
|
|
68
|
+
try { await checkOwned(directory(root), 'directory'); } catch (error) {
|
|
69
|
+
if (error.code === 'ENOENT') return [];
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
const runs = [];
|
|
73
|
+
for (const name of (await readdir(directory(root))).sort()) {
|
|
74
|
+
if (!name.endsWith('.json') || !RUN_ID.test(name.slice(0, -5))) continue;
|
|
75
|
+
const ref = path.join(directory(root), name);
|
|
76
|
+
const info = await checkOwned(ref, 'file');
|
|
77
|
+
if (info.size > LIFECYCLE_MAX_BYTES) throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID');
|
|
78
|
+
const handle = await open(ref, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
79
|
+
let value;
|
|
80
|
+
try { value = JSON.parse(await handle.readFile('utf8')); }
|
|
81
|
+
catch { throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID'); }
|
|
82
|
+
finally { await handle.close(); }
|
|
83
|
+
if (value?.schema !== SCHEMA || value.run_id !== name.slice(0, -5)
|
|
84
|
+
|| !['active', 'closed', 'supervisor_lost'].includes(value.state)) {
|
|
85
|
+
throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID');
|
|
86
|
+
}
|
|
87
|
+
runs.push(value);
|
|
88
|
+
}
|
|
89
|
+
return runs.sort((a, b) => a.started_at.localeCompare(b.started_at));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Publish only after a real atomic replacement; Windows delete-sharing can be transient. */
|
|
93
|
+
async function publishLifecycleJson(ref, bytes) {
|
|
94
|
+
const parent = path.dirname(ref);
|
|
95
|
+
await checkOwned(parent, 'directory');
|
|
96
|
+
// At most 64 live writers can race this check, so even orphaned temporary files
|
|
97
|
+
// have a finite bound. Refuse another write rather than discard an active run.
|
|
98
|
+
if ((await readdir(parent)).length >= 256) throw lifecycleError('DASHBOARD_LIFECYCLE_CAPACITY');
|
|
99
|
+
if (Buffer.byteLength(bytes) > LIFECYCLE_MAX_BYTES) throw lifecycleError('DASHBOARD_LIFECYCLE_CAPACITY');
|
|
100
|
+
try { await checkOwned(ref, 'file'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
101
|
+
const temporary = `${ref}.${process.pid}.${randomUUID()}.tmp`;
|
|
102
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
103
|
+
try {
|
|
104
|
+
await handle.writeFile(bytes, 'utf8');
|
|
105
|
+
await handle.sync();
|
|
106
|
+
} finally { await handle.close(); }
|
|
107
|
+
// Failed private temporaries stay bounded for dead-writer cleanup. Never delete
|
|
108
|
+
// the old receipt to make replacement succeed, or let cleanup mask its error.
|
|
109
|
+
const deadline = performance.now() + WINDOWS_REPLACE_WAIT_MS;
|
|
110
|
+
let attempts = 0;
|
|
111
|
+
for (;;) {
|
|
112
|
+
await checkOwned(parent, 'directory');
|
|
113
|
+
await checkOwned(temporary, 'file');
|
|
114
|
+
try { await checkOwned(ref, 'file'); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
115
|
+
attempts++;
|
|
116
|
+
try {
|
|
117
|
+
await rename(temporary, ref);
|
|
118
|
+
return;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const busy = process.platform === 'win32' && ['EPERM', 'EACCES', 'EBUSY'].includes(error.code);
|
|
121
|
+
if (!busy) throw error;
|
|
122
|
+
if (performance.now() >= deadline || attempts >= REPLACE_MAX_ATTEMPTS) {
|
|
123
|
+
const failure = lifecycleError('DASHBOARD_LIFECYCLE_PUBLISH_FAILED');
|
|
124
|
+
failure.cause = error;
|
|
125
|
+
failure.detail = { ...failure.detail, operation: 'atomic_replace',
|
|
126
|
+
fs_code: error.code, attempts, temporary_retained: true };
|
|
127
|
+
throw failure;
|
|
128
|
+
}
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve,
|
|
130
|
+
Math.min(10 * 2 ** Math.min(attempts - 1, 3), 50, Math.max(1, deadline - performance.now()))));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function writeLifecycleRun(root, run) {
|
|
136
|
+
await publishLifecycleJson(reference(root, run.run_id), `${JSON.stringify(run)}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Call under the existing dashboard startup lock, before spawning anything. */
|
|
140
|
+
export async function createLifecycleRun(root, version) {
|
|
141
|
+
await prepareLifecycleDirectory(root);
|
|
142
|
+
const runs = await readLifecycleRuns(root);
|
|
143
|
+
while (runs.length >= LIFECYCLE_MAX_RUNS) {
|
|
144
|
+
const oldest = runs.filter((run) => run.state !== 'active'
|
|
145
|
+
&& !run.child_at_reconciliation?.startsWith('present'))
|
|
146
|
+
.sort((a, b) => terminationTime(a).localeCompare(terminationTime(b)))[0];
|
|
147
|
+
if (!oldest) throw lifecycleError('DASHBOARD_LIFECYCLE_CAPACITY');
|
|
148
|
+
const index = runs.findIndex((run) => run.run_id === oldest.run_id);
|
|
149
|
+
const [old] = runs.splice(index, 1);
|
|
150
|
+
await rm(reference(root, old.run_id));
|
|
151
|
+
}
|
|
152
|
+
const run = { schema: SCHEMA, run_id: randomUUID(), version, platform: process.platform,
|
|
153
|
+
state: 'active', started_at: new Date().toISOString(), supervisor: null, child: null,
|
|
154
|
+
phase: 'spawn', ready_at: null, exit_observed_at: null,
|
|
155
|
+
observed_exit: { status: 'unknown' }, termination_request: null,
|
|
156
|
+
stderr: { bytes: 0, hashed_bytes: 0, sha256: null, truncated: false }, events: [] };
|
|
157
|
+
await writeLifecycleRun(root, run);
|
|
158
|
+
return run;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function recordLifecyclePhase(run, phase) {
|
|
162
|
+
if (!PHASES.has(phase) || phase === run.phase) return false;
|
|
163
|
+
run.phase = phase;
|
|
164
|
+
if (['empty_shutdown', 'shutdown_request', 'sync_failed', 'cleanup_failed', 'close_failed'].includes(phase)) {
|
|
165
|
+
run.exit_stage = phase;
|
|
166
|
+
}
|
|
167
|
+
const at = new Date().toISOString();
|
|
168
|
+
if (phase === 'ready' && run.ready_at === null) run.ready_at = at;
|
|
169
|
+
if (phase.startsWith('store_') || ['sync_failed', 'cleanup_failed', 'close_failed'].includes(phase)) {
|
|
170
|
+
run.last_failure_observed = { at, phase };
|
|
171
|
+
}
|
|
172
|
+
run.events.push({ at, phase });
|
|
173
|
+
if (run.events.length > 64) run.events.shift();
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** No fabricated exit time/value when the observer itself disappeared. */
|
|
178
|
+
export async function reconcileLifecycleRuns(root, observeIdentity) {
|
|
179
|
+
const runs = await readLifecycleRuns(root);
|
|
180
|
+
for (const run of runs) {
|
|
181
|
+
if (run.state === 'closed') continue;
|
|
182
|
+
const supervisor = run.supervisor === null ? null : await observeIdentity(run.supervisor.pid);
|
|
183
|
+
if (run.state === 'active' && supervisor !== null && supervisor.birth !== null
|
|
184
|
+
&& supervisor.birth === run.supervisor.birth) continue;
|
|
185
|
+
// Unknown birth cannot establish observer loss. Do not overwrite its receipt.
|
|
186
|
+
if (run.state === 'active' && supervisor !== null
|
|
187
|
+
&& (supervisor.birth === null || run.supervisor.birth === null)) continue;
|
|
188
|
+
const previousState = run.state;
|
|
189
|
+
const previousPresence = run.child_at_reconciliation;
|
|
190
|
+
run.state = 'supervisor_lost';
|
|
191
|
+
run.observer_loss_observed_at ??= new Date().toISOString();
|
|
192
|
+
const child = run.child === null ? null : await observeIdentity(run.child.pid);
|
|
193
|
+
run.child_at_reconciliation = run.child === null ? 'not_observed' : child === null ? 'absent'
|
|
194
|
+
: child.birth !== null && child.birth === run.child.birth ? 'present_identity_matched'
|
|
195
|
+
: child.birth !== null && run.child.birth !== null ? 'absent_identity_reused'
|
|
196
|
+
: 'present_identity_unconfirmed';
|
|
197
|
+
run.observed_exit = { status: 'unknown', reason: 'supervisor_lost' };
|
|
198
|
+
if (previousState !== run.state || previousPresence !== run.child_at_reconciliation) {
|
|
199
|
+
run.child_presence_observed_at = new Date().toISOString();
|
|
200
|
+
await writeLifecycleRun(root, run);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return runs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** One bounded decision receipt, serialized by the registry's existing startup lock. */
|
|
207
|
+
export async function recordDashboardEnsureDecision(root, decision, childPid) {
|
|
208
|
+
if (!['reuse_current', 'adopt_current', 'replace_legacy', 'start_missing',
|
|
209
|
+
'pid_reused', 'alive_unresponsive'].includes(decision)) throw new TypeError('invalid dashboard decision');
|
|
210
|
+
await prepareLifecycleDirectory(root);
|
|
211
|
+
const matching = (await readLifecycleRuns(root)).filter((run) => run.child?.pid === childPid
|
|
212
|
+
&& (run.state === 'active' || run.child_at_reconciliation?.startsWith('present'))).at(-1);
|
|
213
|
+
const supervision = childPid === null ? 'not_started' : matching?.state === 'active' ? 'supervised'
|
|
214
|
+
: matching?.state === 'supervisor_lost' ? 'supervisor_lost' : 'legacy_unsupervised';
|
|
215
|
+
const ref = path.join(directory(root), 'ensure.json');
|
|
216
|
+
await publishLifecycleJson(ref, `${JSON.stringify({ schema: 'lattice.dashboard_ensure.v1',
|
|
217
|
+
observed_at: new Date().toISOString(), decision, child_pid: childPid,
|
|
218
|
+
supervision, run_id: matching?.run_id ?? null })}\n`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function readDashboardLifecycleStatus(root) {
|
|
222
|
+
const runs = await readLifecycleRuns(root);
|
|
223
|
+
const terminal = runs.filter((run) => run.state !== 'active').sort((a, b) =>
|
|
224
|
+
terminationTime(a).localeCompare(terminationTime(b)));
|
|
225
|
+
let ensure = null;
|
|
226
|
+
try {
|
|
227
|
+
const ref = path.join(directory(root), 'ensure.json');
|
|
228
|
+
const info = await checkOwned(ref, 'file');
|
|
229
|
+
if (info.size > LIFECYCLE_MAX_BYTES) throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID');
|
|
230
|
+
try { ensure = JSON.parse(await readFile(ref, 'utf8')); }
|
|
231
|
+
catch { throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID'); }
|
|
232
|
+
if (ensure.schema !== 'lattice.dashboard_ensure.v1') throw lifecycleError('DASHBOARD_LIFECYCLE_INVALID');
|
|
233
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
234
|
+
return { schema: 'lattice.dashboard_lifecycle_status.v1',
|
|
235
|
+
last_ensure: ensure, active: runs.filter((run) => run.state === 'active'),
|
|
236
|
+
last_termination: terminal.at(-1) ?? null };
|
|
237
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const execute = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
/** A missing process and an unavailable birth observation are different. */
|
|
9
|
+
export async function observeDashboardProcess(pid) {
|
|
10
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new TypeError('invalid dashboard pid');
|
|
11
|
+
try { process.kill(pid, 0); } catch (error) {
|
|
12
|
+
if (error.code === 'ESRCH') return null;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform === 'linux') {
|
|
17
|
+
const [stat, boot] = await Promise.all([readFile(`/proc/${pid}/stat`, 'utf8'),
|
|
18
|
+
readFile('/proc/sys/kernel/random/boot_id', 'utf8')]);
|
|
19
|
+
const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
|
|
20
|
+
if (fields[0] === 'Z') return null;
|
|
21
|
+
return { pid, birth: `linux:${boot.trim()}:${fields[19]}` };
|
|
22
|
+
}
|
|
23
|
+
const { stdout } = process.platform === 'win32'
|
|
24
|
+
? await execute('pwsh.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
25
|
+
`[System.Diagnostics.Process]::GetProcessById(${pid}).StartTime.ToUniversalTime().ToFileTimeUtc()`],
|
|
26
|
+
{ encoding: 'utf8', timeout: 5_000, windowsHide: true })
|
|
27
|
+
: await execute('/bin/ps', ['-o', 'lstart=', '-p', String(pid)],
|
|
28
|
+
{ encoding: 'utf8', timeout: 5_000, env: { ...process.env, LC_ALL: 'C' } });
|
|
29
|
+
return { pid, birth: stdout.trim() || null };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Recheck death after a racing exit. An observation failure never licenses a kill.
|
|
32
|
+
try { process.kill(pid, 0); } catch (probeError) {
|
|
33
|
+
if (probeError.code === 'ESRCH') return null;
|
|
34
|
+
throw probeError;
|
|
35
|
+
}
|
|
36
|
+
return { pid, birth: null, observation: 'unavailable' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function observedDashboardExit(code, signal, platform = process.platform) {
|
|
41
|
+
if (platform === 'win32') {
|
|
42
|
+
if (!Number.isInteger(code)) return { status: 'unknown', signal: 'not_applicable' };
|
|
43
|
+
const uint32 = code >>> 0;
|
|
44
|
+
return { status: 'observed', uint32, hex: `0x${uint32.toString(16).padStart(8, '0').toUpperCase()}`,
|
|
45
|
+
signal: 'not_applicable', source: 'node_child_wait', origin: 'unknown' };
|
|
46
|
+
}
|
|
47
|
+
if (Number.isInteger(code)) return { status: 'observed', code, signal: null, source: 'node_child_wait' };
|
|
48
|
+
if (typeof signal === 'string') return { status: 'observed', code: null, signal, source: 'node_child_wait' };
|
|
49
|
+
return { status: 'unknown' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Use the same authenticated handle for birth check, termination and exit read. */
|
|
53
|
+
export async function terminateOwnedWindowsDashboard(identity, exitCode = 1) {
|
|
54
|
+
if (!identity || !Number.isSafeInteger(identity.pid) || !/^[0-9]+$/u.test(identity.birth ?? '')) {
|
|
55
|
+
throw Object.assign(new Error('dashboard process birth is unavailable'),
|
|
56
|
+
{ code: 'DASHBOARD_CHILD_IDENTITY_UNAVAILABLE' });
|
|
57
|
+
}
|
|
58
|
+
const script = fileURLToPath(new URL('./dashboard-windows-process-control.ps1', import.meta.url));
|
|
59
|
+
const { stdout } = await execute('pwsh.exe', ['-NoProfile', '-NonInteractive', '-File', script,
|
|
60
|
+
'-ChildPid', String(identity.pid), '-BirthFileTime', identity.birth, '-RequestedExitCode', String(exitCode)],
|
|
61
|
+
{ encoding: 'utf8', timeout: 12_000, windowsHide: true, maxBuffer: 16_384 });
|
|
62
|
+
const result = JSON.parse(stdout.trim());
|
|
63
|
+
if (result.schema !== 'lattice.dashboard_process_control.v1'
|
|
64
|
+
|| result.pid !== identity.pid || result.birth_filetime !== identity.birth
|
|
65
|
+
|| typeof result.termination_requested !== 'boolean' || result.requested_exit_code !== exitCode
|
|
66
|
+
|| !Number.isInteger(result.actual_exit_code) || result.actual_exit_code < 0
|
|
67
|
+
|| result.actual_exit_code > 0xFFFFFFFF
|
|
68
|
+
|| result.actual_exit_hex !== `0x${result.actual_exit_code.toString(16).padStart(8, '0').toUpperCase()}`
|
|
69
|
+
|| result.source !== 'GetExitCodeProcess') {
|
|
70
|
+
throw Object.assign(new Error('dashboard process control result invalid'),
|
|
71
|
+
{ code: 'DASHBOARD_CHILD_CONTROL_INVALID' });
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|