@omnitron-dev/omnitron 0.1.3 → 0.2.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/dist/cli/omnitron.js +32 -1
- package/dist/cli/omnitron.js.map +1 -1
- package/dist/cluster/leader-election.d.ts.map +1 -1
- package/dist/cluster/leader-election.js +0 -2
- package/dist/cluster/leader-election.js.map +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +10 -2
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +10 -2
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/inspect-graph.d.ts +20 -0
- package/dist/commands/inspect-graph.d.ts.map +1 -0
- package/dist/commands/inspect-graph.js +78 -0
- package/dist/commands/inspect-graph.js.map +1 -0
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +10 -2
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +11 -1
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +17 -3
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +10 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/monit.d.ts.map +1 -1
- package/dist/commands/monit.js +9 -0
- package/dist/commands/monit.js.map +1 -1
- package/dist/commands/output.d.ts +35 -0
- package/dist/commands/output.d.ts.map +1 -0
- package/dist/commands/output.js +63 -0
- package/dist/commands/output.js.map +1 -0
- package/dist/commands/stack.d.ts.map +1 -1
- package/dist/commands/stack.js +22 -7
- package/dist/commands/stack.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +17 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/tor.d.ts +13 -0
- package/dist/commands/tor.d.ts.map +1 -0
- package/dist/commands/tor.js +100 -0
- package/dist/commands/tor.js.map +1 -0
- package/dist/commands/up.js +1 -1
- package/dist/commands/up.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +29 -5
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +14 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/daemon/daemon-client.d.ts +14 -0
- package/dist/daemon/daemon-client.d.ts.map +1 -1
- package/dist/daemon/daemon-client.js +4 -0
- package/dist/daemon/daemon-client.js.map +1 -1
- package/dist/daemon/daemon-entry.js +17 -23
- package/dist/daemon/daemon-entry.js.map +1 -1
- package/dist/daemon/daemon.d.ts +2 -0
- package/dist/daemon/daemon.d.ts.map +1 -1
- package/dist/daemon/daemon.js +118 -7
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/daemon.module.d.ts.map +1 -1
- package/dist/daemon/daemon.module.js +41 -14
- package/dist/daemon/daemon.module.js.map +1 -1
- package/dist/daemon/daemon.rpc-service.d.ts +19 -0
- package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
- package/dist/daemon/daemon.rpc-service.js +14 -0
- package/dist/daemon/daemon.rpc-service.js.map +1 -1
- package/dist/daemon/pid-manager.d.ts +55 -4
- package/dist/daemon/pid-manager.d.ts.map +1 -1
- package/dist/daemon/pid-manager.js +123 -16
- package/dist/daemon/pid-manager.js.map +1 -1
- package/dist/daemon/state-store.d.ts.map +1 -1
- package/dist/daemon/state-store.js +43 -1
- package/dist/daemon/state-store.js.map +1 -1
- package/dist/infrastructure/container-runtime.d.ts +10 -0
- package/dist/infrastructure/container-runtime.d.ts.map +1 -1
- package/dist/infrastructure/container-runtime.js +111 -36
- package/dist/infrastructure/container-runtime.js.map +1 -1
- package/dist/infrastructure/infrastructure.service.d.ts +41 -0
- package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
- package/dist/infrastructure/infrastructure.service.js +157 -8
- package/dist/infrastructure/infrastructure.service.js.map +1 -1
- package/dist/infrastructure/phantom-endpoint-janitor.d.ts +72 -0
- package/dist/infrastructure/phantom-endpoint-janitor.d.ts.map +1 -0
- package/dist/infrastructure/phantom-endpoint-janitor.js +149 -0
- package/dist/infrastructure/phantom-endpoint-janitor.js.map +1 -0
- package/dist/infrastructure/presets/index.d.ts.map +1 -1
- package/dist/infrastructure/presets/index.js +2 -0
- package/dist/infrastructure/presets/index.js.map +1 -1
- package/dist/infrastructure/presets/postgres.d.ts.map +1 -1
- package/dist/infrastructure/presets/postgres.js +9 -3
- package/dist/infrastructure/presets/postgres.js.map +1 -1
- package/dist/infrastructure/presets/redis.d.ts.map +1 -1
- package/dist/infrastructure/presets/redis.js +10 -4
- package/dist/infrastructure/presets/redis.js.map +1 -1
- package/dist/infrastructure/presets/registry.d.ts.map +1 -1
- package/dist/infrastructure/presets/registry.js +12 -0
- package/dist/infrastructure/presets/registry.js.map +1 -1
- package/dist/infrastructure/presets/tor.d.ts +74 -0
- package/dist/infrastructure/presets/tor.d.ts.map +1 -0
- package/dist/infrastructure/presets/tor.js +176 -0
- package/dist/infrastructure/presets/tor.js.map +1 -0
- package/dist/infrastructure/service-resolver.d.ts +31 -0
- package/dist/infrastructure/service-resolver.d.ts.map +1 -1
- package/dist/infrastructure/service-resolver.js +60 -9
- package/dist/infrastructure/service-resolver.js.map +1 -1
- package/dist/infrastructure/stack-infra-manager.d.ts +1 -1
- package/dist/infrastructure/stack-infra-manager.js +1 -1
- package/dist/infrastructure/types.d.ts +17 -0
- package/dist/infrastructure/types.d.ts.map +1 -1
- package/dist/observability/index.d.ts +4 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +3 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/metrics-bridge.d.ts +87 -0
- package/dist/observability/metrics-bridge.d.ts.map +1 -0
- package/dist/observability/metrics-bridge.js +295 -0
- package/dist/observability/metrics-bridge.js.map +1 -0
- package/dist/observability/metrics-server.d.ts +68 -0
- package/dist/observability/metrics-server.d.ts.map +1 -0
- package/dist/observability/metrics-server.js +146 -0
- package/dist/observability/metrics-server.js.map +1 -0
- package/dist/orchestrator/app-handle.d.ts +13 -1
- package/dist/orchestrator/app-handle.d.ts.map +1 -1
- package/dist/orchestrator/app-handle.js +12 -0
- package/dist/orchestrator/app-handle.js.map +1 -1
- package/dist/orchestrator/bootstrap-process.d.ts +49 -0
- package/dist/orchestrator/bootstrap-process.d.ts.map +1 -1
- package/dist/orchestrator/bootstrap-process.js +167 -0
- package/dist/orchestrator/bootstrap-process.js.map +1 -1
- package/dist/orchestrator/build-service.d.ts +50 -3
- package/dist/orchestrator/build-service.d.ts.map +1 -1
- package/dist/orchestrator/build-service.js +357 -85
- package/dist/orchestrator/build-service.js.map +1 -1
- package/dist/orchestrator/file-watcher.d.ts.map +1 -1
- package/dist/orchestrator/file-watcher.js +60 -38
- package/dist/orchestrator/file-watcher.js.map +1 -1
- package/dist/orchestrator/orchestrator.service.d.ts +176 -3
- package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator.service.js +587 -56
- package/dist/orchestrator/orchestrator.service.js.map +1 -1
- package/dist/orchestrator/process-janitor.d.ts +153 -0
- package/dist/orchestrator/process-janitor.d.ts.map +1 -0
- package/dist/orchestrator/process-janitor.js +279 -0
- package/dist/orchestrator/process-janitor.js.map +1 -0
- package/dist/project/config-resolver.d.ts.map +1 -1
- package/dist/project/config-resolver.js +38 -12
- package/dist/project/config-resolver.js.map +1 -1
- package/dist/project/registry.d.ts +1 -0
- package/dist/project/registry.d.ts.map +1 -1
- package/dist/project/registry.js +30 -0
- package/dist/project/registry.js.map +1 -1
- package/dist/services/auth.service.d.ts.map +1 -1
- package/dist/services/auth.service.js +9 -2
- package/dist/services/auth.service.js.map +1 -1
- package/dist/services/netron-telemetry-transport.d.ts.map +1 -1
- package/dist/services/netron-telemetry-transport.js +0 -4
- package/dist/services/netron-telemetry-transport.js.map +1 -1
- package/dist/services/node-manager.service.js +1 -1
- package/dist/services/node-manager.service.js.map +1 -1
- package/dist/services/pipeline.service.d.ts +15 -0
- package/dist/services/pipeline.service.d.ts.map +1 -1
- package/dist/services/pipeline.service.js +53 -2
- package/dist/services/pipeline.service.js.map +1 -1
- package/dist/services/project.rpc-service.d.ts +4 -0
- package/dist/services/project.rpc-service.d.ts.map +1 -1
- package/dist/services/project.rpc-service.js +9 -0
- package/dist/services/project.rpc-service.js.map +1 -1
- package/dist/services/project.service.d.ts +9 -0
- package/dist/services/project.service.d.ts.map +1 -1
- package/dist/services/project.service.js +148 -21
- package/dist/services/project.service.js.map +1 -1
- package/dist/shared/dto/project.d.ts +1 -1
- package/dist/shared/dto/project.d.ts.map +1 -1
- package/dist/shared/dto/services.d.ts +24 -0
- package/dist/shared/dto/services.d.ts.map +1 -1
- package/dist/workers/health-monitor.service.d.ts.map +1 -1
- package/dist/workers/health-monitor.service.js +4 -1
- package/dist/workers/health-monitor.service.js.map +1 -1
- package/dist/workers/types.d.ts +3 -0
- package/dist/workers/types.d.ts.map +1 -1
- package/package.json +19 -18
- package/webapp/dist/assets/Badge-D2ZBP5iK.js +1 -0
- package/webapp/dist/assets/Card-vO88g06B.js +1 -0
- package/webapp/dist/assets/CardActions-C9h7Q2wW.js +1 -0
- package/webapp/dist/assets/CardContent-BA_Xmmhk.js +1 -0
- package/webapp/dist/assets/CardHeader-ceIs__6x.js +1 -0
- package/webapp/dist/assets/Chip-Ckk0xUhK.js +1 -0
- package/webapp/dist/assets/DialogActions-BhHM7hov.js +1 -0
- package/webapp/dist/assets/DialogContentText-BT2JQRmK.js +1 -0
- package/webapp/dist/assets/FormControlLabel-CcQByVS8.js +1 -0
- package/webapp/dist/assets/Grid-14PETRQW.js +1 -0
- package/webapp/dist/assets/InputAdornment-B8aHIsQ7.js +1 -0
- package/webapp/dist/assets/Skeleton-B74-Ds6O.js +37 -0
- package/webapp/dist/assets/Switch-DvWD2wh4.js +1 -0
- package/webapp/dist/assets/SwitchBase-CDU4nY8O.js +1 -0
- package/webapp/dist/assets/Tab-DcCOYYp5.js +1 -0
- package/webapp/dist/assets/TableContainer-CzTh4mu-.js +1 -0
- package/webapp/dist/assets/TableRow-BySEreZR.js +2 -0
- package/webapp/dist/assets/TextField-BLf-BHz2.js +1 -0
- package/webapp/dist/assets/alerts-DnRbT41C.js +1 -0
- package/webapp/dist/assets/apps-CdR0VH2d.js +1 -0
- package/webapp/dist/assets/chunk-DtcAfS_W.js +1 -0
- package/webapp/dist/assets/client-DU8I_lDc.js +2 -0
- package/webapp/dist/assets/containers-Dw-rRXV1.js +1 -0
- package/webapp/dist/assets/dashboard-CzgB0uZz.js +1 -0
- package/webapp/dist/assets/dashboard-builder-DsA0yg33.js +1 -0
- package/webapp/dist/assets/deployments-DtQAoIcQ.js +1 -0
- package/webapp/dist/assets/detail-BJR-G7JG.js +1 -0
- package/webapp/dist/assets/detail-nlCeSltl.js +8 -0
- package/webapp/dist/assets/index-ByQDwp2r.js +566 -0
- package/webapp/dist/assets/isMuiElement-DwF2naaF.js +1 -0
- package/webapp/dist/assets/logs-Kn8o4H7k.js +11 -0
- package/webapp/dist/assets/metrics-DN8Zu5wT.js +2 -0
- package/webapp/dist/assets/nodes-Bgch7AtN.js +7 -0
- package/webapp/dist/assets/pipelines-Cry3qm-S.js +4 -0
- package/webapp/dist/assets/popper-BJFSj6rc.js +1 -0
- package/webapp/dist/assets/preload-helper-DSXbuxSR.js +1 -0
- package/webapp/dist/assets/projects-Dym2GI1U.js +1 -0
- package/webapp/dist/assets/react-apexcharts.esm-CSGK5Cxn.js +958 -0
- package/webapp/dist/assets/react-apexcharts.esm-DEAWOwx5.js +1003 -0
- package/webapp/dist/assets/settings-va3zTcow.js +1 -0
- package/webapp/dist/assets/sign-in-ik35umbp.js +1 -0
- package/webapp/dist/assets/stacks-CVFOnxbF.js +1 -0
- package/webapp/dist/assets/system-info-npIXAjX2.js +1 -0
- package/webapp/dist/assets/topology-bDe70vLs.js +12 -0
- package/webapp/dist/assets/traces-DIIcT8DF.js +1 -0
- package/webapp/dist/assets/use-stack-context-DHLwZv1r.js +1 -0
- package/webapp/dist/index.html +5 -6
- package/webapp/dist/assets/Chip-BySxTjyN.js +0 -1
- package/webapp/dist/assets/DialogContentText-YQWiAVrU.js +0 -1
- package/webapp/dist/assets/DialogTitle-CDkgUIwp.js +0 -1
- package/webapp/dist/assets/FormControlLabel-Bu3CCsBM.js +0 -1
- package/webapp/dist/assets/Grid-BFY8hAzL.js +0 -1
- package/webapp/dist/assets/InputAdornment-_Dayk-3y.js +0 -3
- package/webapp/dist/assets/Skeleton-BGj6Sw8m.js +0 -37
- package/webapp/dist/assets/Switch-CA6682vv.js +0 -1
- package/webapp/dist/assets/TableContainer-4epq10KJ.js +0 -1
- package/webapp/dist/assets/TextField-C73KHMVN.js +0 -1
- package/webapp/dist/assets/alerts-JEQALS7Y.js +0 -1
- package/webapp/dist/assets/apps-DMPysEKB.js +0 -1
- package/webapp/dist/assets/client-DlGPter5.js +0 -110
- package/webapp/dist/assets/containers-CvkC7do8.js +0 -1
- package/webapp/dist/assets/dashboard-C0Fre9pC.js +0 -1
- package/webapp/dist/assets/dashboard-builder-DAghqdpE.js +0 -1
- package/webapp/dist/assets/deployments-Cg1lI1uT.js +0 -1
- package/webapp/dist/assets/detail-CyFtEson.js +0 -1
- package/webapp/dist/assets/detail-DqoSGKT1.js +0 -8
- package/webapp/dist/assets/icons-ynIWYfBP.js +0 -3
- package/webapp/dist/assets/index-vKrDmEA_.js +0 -259
- package/webapp/dist/assets/isMuiElement-BRL3YuCT.js +0 -1
- package/webapp/dist/assets/logs-IovIij1C.js +0 -11
- package/webapp/dist/assets/metrics-Dufwtwjq.js +0 -1
- package/webapp/dist/assets/nodes-Brvu8W3j.js +0 -7
- package/webapp/dist/assets/pipelines-t40X4MuC.js +0 -4
- package/webapp/dist/assets/prop-types-CvVtHxp4.js +0 -1
- package/webapp/dist/assets/react-apexcharts.esm-CLO6gala.js +0 -954
- package/webapp/dist/assets/settings-DTAIST3v.js +0 -1
- package/webapp/dist/assets/sign-in-DQEAkJum.js +0 -1
- package/webapp/dist/assets/stacks-BjM1f45N.js +0 -1
- package/webapp/dist/assets/system-info-BQSbSEK-.js +0 -1
- package/webapp/dist/assets/topology-6N8-Az_Z.js +0 -12
- package/webapp/dist/assets/traces-DVXyb-4m.js +0 -1
- package/webapp/dist/assets/use-stack-context-CbE8fxBA.js +0 -1
- /package/webapp/dist/assets/{constants-DqFQJ27U.js → constants-BgnT-Xdy.js} +0 -0
- /package/webapp/dist/assets/{formatters-J3Xqn6BQ.js → formatters-Do41SRmE.js} +0 -0
- /package/webapp/dist/assets/{fs-uP8eiZAR.js → fs-CgZLtHiQ.js} +0 -0
|
@@ -3,13 +3,32 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prevents duplicate daemons and enables daemon discovery.
|
|
5
5
|
* Also handles stale PID/socket cleanup for robust restart.
|
|
6
|
+
*
|
|
7
|
+
* T#56 hardening — PID reuse detection
|
|
8
|
+
* ------------------------------------
|
|
9
|
+
* `process.kill(pid, 0)` only answers "is SOMETHING alive at this
|
|
10
|
+
* pid", not "is OUR daemon alive at this pid". After a daemon crash
|
|
11
|
+
* the kernel will eventually recycle the pid for an unrelated
|
|
12
|
+
* process (a Chrome tab, a Node REPL, whatever). The old pid file
|
|
13
|
+
* still pointed at it, so `omnitron daemon start` falsely reported
|
|
14
|
+
* "daemon already running" and refused to come up.
|
|
15
|
+
*
|
|
16
|
+
* Fix: write a signature alongside the pid (the daemon's argv[1] —
|
|
17
|
+
* the entry-point script path). On read, after pid-alive, verify
|
|
18
|
+
* the process at that pid actually has a matching argv. We read
|
|
19
|
+
* argv via `/proc/<pid>/cmdline` on Linux and `ps -p <pid> -o args=`
|
|
20
|
+
* on macOS / BSD; both are read-only and fast. If we can't tell
|
|
21
|
+
* either way (privilege issue, exotic platform), fall back to the
|
|
22
|
+
* pid-only check — strictly an improvement over the historical
|
|
23
|
+
* behaviour.
|
|
6
24
|
*/
|
|
7
25
|
export declare class PidManager {
|
|
8
26
|
private readonly pidFile;
|
|
9
27
|
constructor(pidFile: string);
|
|
10
28
|
/**
|
|
11
29
|
* Check if another daemon is already running.
|
|
12
|
-
* Uses process.kill(pid, 0) to verify liveness
|
|
30
|
+
* Uses process.kill(pid, 0) to verify liveness AND argv signature
|
|
31
|
+
* to verify the live process is actually the same daemon.
|
|
13
32
|
*/
|
|
14
33
|
isRunning(): boolean;
|
|
15
34
|
/**
|
|
@@ -22,7 +41,12 @@ export declare class PidManager {
|
|
|
22
41
|
*/
|
|
23
42
|
readPid(): number | null;
|
|
24
43
|
/**
|
|
25
|
-
*
|
|
44
|
+
* Read the signature line from the pidfile, if present.
|
|
45
|
+
* Returns null for legacy (pid-only) files.
|
|
46
|
+
*/
|
|
47
|
+
private readSignature;
|
|
48
|
+
/**
|
|
49
|
+
* Write current process PID + signature.
|
|
26
50
|
*/
|
|
27
51
|
write(): void;
|
|
28
52
|
/**
|
|
@@ -31,13 +55,40 @@ export declare class PidManager {
|
|
|
31
55
|
remove(): void;
|
|
32
56
|
/**
|
|
33
57
|
* Clean up stale PID file and optionally a socket file.
|
|
34
|
-
* Called when PID file exists but process is dead
|
|
58
|
+
* Called when PID file exists but process is dead OR the live
|
|
59
|
+
* process is a pid-reused stranger.
|
|
35
60
|
* Returns true if stale state was found and cleaned.
|
|
36
61
|
*/
|
|
37
62
|
cleanupStale(socketPath?: string): boolean;
|
|
38
63
|
/**
|
|
39
|
-
* Check if a process with given PID is alive.
|
|
64
|
+
* Check if a process with given PID is alive. Thin delegate to
|
|
65
|
+
* the shared helper in `@omnitron-dev/titan/utils` — kept on
|
|
66
|
+
* `PidManager` as a static so existing call sites continue to
|
|
67
|
+
* work, but the actual semantic (EPERM-as-alive,
|
|
68
|
+
* invalid-pid-as-dead) lives in one place. Pre-fix this
|
|
69
|
+
* version caught ALL errors as dead and so mis-classified
|
|
70
|
+
* processes owned by another uid (EPERM) as crashed —
|
|
71
|
+
* potentially triggering false-positive restarts in
|
|
72
|
+
* multi-tenant supervisors.
|
|
40
73
|
*/
|
|
41
74
|
static isProcessAlive(pid: number): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The signature this daemon will record in its pidfile. Currently
|
|
77
|
+
* the absolute entry-point script path (argv[1]). Two daemon
|
|
78
|
+
* processes with the same script path are treated as the same
|
|
79
|
+
* service for pid-reuse purposes, which is the right granularity
|
|
80
|
+
* for omnitron.
|
|
81
|
+
*/
|
|
82
|
+
static currentSignature(): string;
|
|
83
|
+
/**
|
|
84
|
+
* Best-effort argv check against the recorded signature.
|
|
85
|
+
*
|
|
86
|
+
* Returns true if we positively verified a match, false if we
|
|
87
|
+
* positively verified a mismatch (pid was reused), and — to err
|
|
88
|
+
* on the safe side — true if we couldn't read argv at all. The
|
|
89
|
+
* "unknown → assume same" fallback preserves the legacy behaviour
|
|
90
|
+
* for platforms where we can't introspect the live process.
|
|
91
|
+
*/
|
|
92
|
+
static processArgvMatches(pid: number, expectedSignature: string): boolean;
|
|
42
93
|
}
|
|
43
94
|
//# sourceMappingURL=pid-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pid-manager.d.ts","sourceRoot":"","sources":["../../src/daemon/pid-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pid-manager.d.ts","sourceRoot":"","sources":["../../src/daemon/pid-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,MAAM;IAE5C;;;;OAIG;IACH,SAAS,IAAI,OAAO;IAapB;;OAEG;IACH,MAAM,IAAI,MAAM,GAAG,IAAI;IAMvB;;;OAGG;IACH,OAAO,IAAI,MAAM,GAAG,IAAI;IAcxB;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,MAAM,IAAI,IAAI;IAQd;;;;;OAKG;IACH,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO;IAuB1C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAI3C;;;;;;OAMG;IACH,MAAM,CAAC,gBAAgB,IAAI,MAAM;IAIjC;;;;;;;;OAQG;IACH,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,OAAO;CAmC3E"}
|
|
@@ -3,9 +3,29 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Prevents duplicate daemons and enables daemon discovery.
|
|
5
5
|
* Also handles stale PID/socket cleanup for robust restart.
|
|
6
|
+
*
|
|
7
|
+
* T#56 hardening — PID reuse detection
|
|
8
|
+
* ------------------------------------
|
|
9
|
+
* `process.kill(pid, 0)` only answers "is SOMETHING alive at this
|
|
10
|
+
* pid", not "is OUR daemon alive at this pid". After a daemon crash
|
|
11
|
+
* the kernel will eventually recycle the pid for an unrelated
|
|
12
|
+
* process (a Chrome tab, a Node REPL, whatever). The old pid file
|
|
13
|
+
* still pointed at it, so `omnitron daemon start` falsely reported
|
|
14
|
+
* "daemon already running" and refused to come up.
|
|
15
|
+
*
|
|
16
|
+
* Fix: write a signature alongside the pid (the daemon's argv[1] —
|
|
17
|
+
* the entry-point script path). On read, after pid-alive, verify
|
|
18
|
+
* the process at that pid actually has a matching argv. We read
|
|
19
|
+
* argv via `/proc/<pid>/cmdline` on Linux and `ps -p <pid> -o args=`
|
|
20
|
+
* on macOS / BSD; both are read-only and fast. If we can't tell
|
|
21
|
+
* either way (privilege issue, exotic platform), fall back to the
|
|
22
|
+
* pid-only check — strictly an improvement over the historical
|
|
23
|
+
* behaviour.
|
|
6
24
|
*/
|
|
7
25
|
import fs from 'node:fs';
|
|
8
26
|
import path from 'node:path';
|
|
27
|
+
import { spawnSync } from 'node:child_process';
|
|
28
|
+
import { isProcessAlive } from '@omnitron-dev/titan/utils';
|
|
9
29
|
export class PidManager {
|
|
10
30
|
pidFile;
|
|
11
31
|
constructor(pidFile) {
|
|
@@ -13,13 +33,22 @@ export class PidManager {
|
|
|
13
33
|
}
|
|
14
34
|
/**
|
|
15
35
|
* Check if another daemon is already running.
|
|
16
|
-
* Uses process.kill(pid, 0) to verify liveness
|
|
36
|
+
* Uses process.kill(pid, 0) to verify liveness AND argv signature
|
|
37
|
+
* to verify the live process is actually the same daemon.
|
|
17
38
|
*/
|
|
18
39
|
isRunning() {
|
|
19
40
|
const pid = this.readPid();
|
|
20
41
|
if (pid === null)
|
|
21
42
|
return false;
|
|
22
|
-
|
|
43
|
+
if (!PidManager.isProcessAlive(pid))
|
|
44
|
+
return false;
|
|
45
|
+
// T#56: pid is alive — but is it actually OUR daemon? If we
|
|
46
|
+
// recorded a signature and it doesn't match, treat as stale.
|
|
47
|
+
const recordedSignature = this.readSignature();
|
|
48
|
+
if (recordedSignature && !PidManager.processArgvMatches(pid, recordedSignature)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
23
52
|
}
|
|
24
53
|
/**
|
|
25
54
|
* Get the PID of the running daemon (only if alive).
|
|
@@ -28,7 +57,7 @@ export class PidManager {
|
|
|
28
57
|
const pid = this.readPid();
|
|
29
58
|
if (pid === null)
|
|
30
59
|
return null;
|
|
31
|
-
return
|
|
60
|
+
return this.isRunning() ? pid : null;
|
|
32
61
|
}
|
|
33
62
|
/**
|
|
34
63
|
* Get the raw PID from file (even if process is dead).
|
|
@@ -36,8 +65,12 @@ export class PidManager {
|
|
|
36
65
|
*/
|
|
37
66
|
readPid() {
|
|
38
67
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
68
|
+
const raw = fs.readFileSync(this.pidFile, 'utf-8').trim();
|
|
69
|
+
// Backward-compat: legacy pidfiles contain only the pid; the
|
|
70
|
+
// T#56 format is `<pid>\n<signature>`. Either way `parseInt`
|
|
71
|
+
// on the first non-empty line returns the right thing.
|
|
72
|
+
const firstLine = raw.split('\n')[0]?.trim() ?? '';
|
|
73
|
+
const pid = parseInt(firstLine, 10);
|
|
41
74
|
return isNaN(pid) ? null : pid;
|
|
42
75
|
}
|
|
43
76
|
catch {
|
|
@@ -45,12 +78,27 @@ export class PidManager {
|
|
|
45
78
|
}
|
|
46
79
|
}
|
|
47
80
|
/**
|
|
48
|
-
*
|
|
81
|
+
* Read the signature line from the pidfile, if present.
|
|
82
|
+
* Returns null for legacy (pid-only) files.
|
|
83
|
+
*/
|
|
84
|
+
readSignature() {
|
|
85
|
+
try {
|
|
86
|
+
const raw = fs.readFileSync(this.pidFile, 'utf-8');
|
|
87
|
+
const second = raw.split('\n')[1]?.trim();
|
|
88
|
+
return second && second.length > 0 ? second : null;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Write current process PID + signature.
|
|
49
96
|
*/
|
|
50
97
|
write() {
|
|
51
98
|
const dir = path.dirname(this.pidFile);
|
|
52
99
|
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
-
|
|
100
|
+
const signature = PidManager.currentSignature();
|
|
101
|
+
fs.writeFileSync(this.pidFile, `${process.pid}\n${signature}\n`, 'utf-8');
|
|
54
102
|
}
|
|
55
103
|
/**
|
|
56
104
|
* Remove PID file
|
|
@@ -65,17 +113,20 @@ export class PidManager {
|
|
|
65
113
|
}
|
|
66
114
|
/**
|
|
67
115
|
* Clean up stale PID file and optionally a socket file.
|
|
68
|
-
* Called when PID file exists but process is dead
|
|
116
|
+
* Called when PID file exists but process is dead OR the live
|
|
117
|
+
* process is a pid-reused stranger.
|
|
69
118
|
* Returns true if stale state was found and cleaned.
|
|
70
119
|
*/
|
|
71
120
|
cleanupStale(socketPath) {
|
|
72
121
|
const rawPid = this.readPid();
|
|
73
122
|
if (rawPid === null)
|
|
74
123
|
return false;
|
|
75
|
-
//
|
|
76
|
-
|
|
124
|
+
// T#56: treat both "no process alive" and "different process is
|
|
125
|
+
// alive at this pid" as stale. The old logic only handled the
|
|
126
|
+
// dead-process case, so a recycled pid sat in the file forever.
|
|
127
|
+
if (this.isRunning())
|
|
77
128
|
return false;
|
|
78
|
-
//
|
|
129
|
+
// Stale — clean up.
|
|
79
130
|
this.remove();
|
|
80
131
|
if (socketPath) {
|
|
81
132
|
try {
|
|
@@ -88,16 +139,72 @@ export class PidManager {
|
|
|
88
139
|
return true;
|
|
89
140
|
}
|
|
90
141
|
/**
|
|
91
|
-
* Check if a process with given PID is alive.
|
|
142
|
+
* Check if a process with given PID is alive. Thin delegate to
|
|
143
|
+
* the shared helper in `@omnitron-dev/titan/utils` — kept on
|
|
144
|
+
* `PidManager` as a static so existing call sites continue to
|
|
145
|
+
* work, but the actual semantic (EPERM-as-alive,
|
|
146
|
+
* invalid-pid-as-dead) lives in one place. Pre-fix this
|
|
147
|
+
* version caught ALL errors as dead and so mis-classified
|
|
148
|
+
* processes owned by another uid (EPERM) as crashed —
|
|
149
|
+
* potentially triggering false-positive restarts in
|
|
150
|
+
* multi-tenant supervisors.
|
|
92
151
|
*/
|
|
93
152
|
static isProcessAlive(pid) {
|
|
94
|
-
|
|
95
|
-
|
|
153
|
+
return isProcessAlive(pid);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The signature this daemon will record in its pidfile. Currently
|
|
157
|
+
* the absolute entry-point script path (argv[1]). Two daemon
|
|
158
|
+
* processes with the same script path are treated as the same
|
|
159
|
+
* service for pid-reuse purposes, which is the right granularity
|
|
160
|
+
* for omnitron.
|
|
161
|
+
*/
|
|
162
|
+
static currentSignature() {
|
|
163
|
+
return process.argv[1] ?? '';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Best-effort argv check against the recorded signature.
|
|
167
|
+
*
|
|
168
|
+
* Returns true if we positively verified a match, false if we
|
|
169
|
+
* positively verified a mismatch (pid was reused), and — to err
|
|
170
|
+
* on the safe side — true if we couldn't read argv at all. The
|
|
171
|
+
* "unknown → assume same" fallback preserves the legacy behaviour
|
|
172
|
+
* for platforms where we can't introspect the live process.
|
|
173
|
+
*/
|
|
174
|
+
static processArgvMatches(pid, expectedSignature) {
|
|
175
|
+
if (!expectedSignature)
|
|
96
176
|
return true;
|
|
177
|
+
// Linux: /proc/<pid>/cmdline is null-separated argv.
|
|
178
|
+
if (process.platform === 'linux') {
|
|
179
|
+
try {
|
|
180
|
+
const buf = fs.readFileSync(`/proc/${pid}/cmdline`);
|
|
181
|
+
const argv = buf.toString('utf-8').split('\0').filter(Boolean);
|
|
182
|
+
return argv.includes(expectedSignature);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return true; // unknown — assume the same daemon
|
|
186
|
+
}
|
|
97
187
|
}
|
|
98
|
-
|
|
99
|
-
|
|
188
|
+
// macOS / BSD: shell out to `ps`. -o args= prints the command
|
|
189
|
+
// line; bounded output, ms-scale, safe to call in this rare path.
|
|
190
|
+
if (process.platform === 'darwin' || process.platform.includes('bsd')) {
|
|
191
|
+
try {
|
|
192
|
+
const res = spawnSync('ps', ['-p', String(pid), '-o', 'args='], {
|
|
193
|
+
encoding: 'utf-8',
|
|
194
|
+
timeout: 2_000,
|
|
195
|
+
});
|
|
196
|
+
if (res.status === 0 && typeof res.stdout === 'string') {
|
|
197
|
+
return res.stdout.includes(expectedSignature);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
/* fall through */
|
|
202
|
+
}
|
|
203
|
+
return true;
|
|
100
204
|
}
|
|
205
|
+
// Windows / other — no portable cheap check; preserve legacy
|
|
206
|
+
// pid-only semantics.
|
|
207
|
+
return true;
|
|
101
208
|
}
|
|
102
209
|
}
|
|
103
210
|
//# sourceMappingURL=pid-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pid-manager.js","sourceRoot":"","sources":["../../src/daemon/pid-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"pid-manager.js","sourceRoot":"","sources":["../../src/daemon/pid-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/daemon/state-store.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/daemon/state-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,iBAAiB,EAAE,CAAC;CAC3B;AAED,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM;IAE9C,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAwCjC,IAAI,IAAI,cAAc,GAAG,IAAI;IAS7B,KAAK,IAAI,IAAI;CAOd"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
6
7
|
export class StateStore {
|
|
7
8
|
stateFile;
|
|
8
9
|
constructor(stateFile) {
|
|
@@ -11,7 +12,48 @@ export class StateStore {
|
|
|
11
12
|
save(state) {
|
|
12
13
|
const dir = path.dirname(this.stateFile);
|
|
13
14
|
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
-
|
|
15
|
+
// T#55: write atomically — `writeFileSync(stateFile, ...)` was a
|
|
16
|
+
// direct overwrite that left the file half-written if the
|
|
17
|
+
// daemon was SIGKILL'd mid-flush. The next boot's `load()` then
|
|
18
|
+
// saw truncated JSON, `JSON.parse` threw, and the daemon lost
|
|
19
|
+
// the entire process registry (returned `null` and started from
|
|
20
|
+
// scratch — every supervised app was treated as new).
|
|
21
|
+
//
|
|
22
|
+
// The standard POSIX recipe is:
|
|
23
|
+
// 1. write payload to a SIBLING temp file in the same dir;
|
|
24
|
+
// 2. fsync(tmp) so the bytes reach disk, not just page cache;
|
|
25
|
+
// 3. rename(tmp, stateFile) — POSIX-atomic on one filesystem.
|
|
26
|
+
//
|
|
27
|
+
// Pick a unique temp suffix per call so concurrent calls (and
|
|
28
|
+
// stale temp files from a crashed earlier write) don't collide.
|
|
29
|
+
const payload = JSON.stringify(state, null, 2);
|
|
30
|
+
const tmp = `${this.stateFile}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`;
|
|
31
|
+
let fd;
|
|
32
|
+
try {
|
|
33
|
+
fd = fs.openSync(tmp, 'w');
|
|
34
|
+
fs.writeFileSync(fd, payload, 'utf-8');
|
|
35
|
+
// Force the data + metadata to disk before the rename so the
|
|
36
|
+
// post-crash visible state is "either old contents or new
|
|
37
|
+
// contents", never "renamed link to half-written bytes".
|
|
38
|
+
fs.fsyncSync(fd);
|
|
39
|
+
fs.closeSync(fd);
|
|
40
|
+
fd = undefined;
|
|
41
|
+
fs.renameSync(tmp, this.stateFile);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
// Best-effort cleanup of the temp file if anything went wrong;
|
|
45
|
+
// we already failed the save, no point hiding the failure.
|
|
46
|
+
if (fd !== undefined)
|
|
47
|
+
try {
|
|
48
|
+
fs.closeSync(fd);
|
|
49
|
+
}
|
|
50
|
+
catch { /* */ }
|
|
51
|
+
try {
|
|
52
|
+
fs.unlinkSync(tmp);
|
|
53
|
+
}
|
|
54
|
+
catch { /* already gone */ }
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
15
57
|
}
|
|
16
58
|
load() {
|
|
17
59
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state-store.js","sourceRoot":"","sources":["../../src/daemon/state-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"state-store.js","sourceRoot":"","sources":["../../src/daemon/state-store.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAmB1C,MAAM,OAAO,UAAU;IACQ;IAA7B,YAA6B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;IAElD,IAAI,CAAC,KAAqB;QACxB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACzC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEvC,iEAAiE;QACjE,0DAA0D;QAC1D,gEAAgE;QAChE,8DAA8D;QAC9D,gEAAgE;QAChE,sDAAsD;QACtD,EAAE;QACF,gCAAgC;QAChC,6DAA6D;QAC7D,gEAAgE;QAChE,gEAAgE;QAChE,EAAE;QACF,8DAA8D;QAC9D,gEAAgE;QAChE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,SAAS,QAAQ,OAAO,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACrF,IAAI,EAAsB,CAAC;QAC3B,IAAI,CAAC;YACH,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAC3B,EAAE,CAAC,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;YACvC,6DAA6D;YAC7D,0DAA0D;YAC1D,yDAAyD;YACzD,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACjB,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;YACjB,EAAE,GAAG,SAAS,CAAC;YACf,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+DAA+D;YAC/D,2DAA2D;YAC3D,IAAI,EAAE,KAAK,SAAS;gBAAE,IAAI,CAAC;oBAAC,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC;YAC/D,IAAI,CAAC;gBAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAC;YACxD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACtD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAmB,CAAC;QAC5C,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC;YACH,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;CACF"}
|
|
@@ -48,6 +48,16 @@ export declare function execInContainer(nameOrId: string, command: string[]): Pr
|
|
|
48
48
|
* Wait for container health check to pass.
|
|
49
49
|
*/
|
|
50
50
|
export declare function waitForHealthy(name: string, timeoutMs?: number): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Create a Docker bridge network if it doesn't exist.
|
|
53
|
+
*
|
|
54
|
+
* Idempotent — `docker network create` exits non-zero with a recognisable
|
|
55
|
+
* "already exists" message when the network is present; we swallow that
|
|
56
|
+
* specific case and propagate everything else. Done via execFile rather
|
|
57
|
+
* than the adapter because the adapter doesn't expose network APIs and
|
|
58
|
+
* the call is rare (once per daemon startup per network).
|
|
59
|
+
*/
|
|
60
|
+
export declare function ensureNetwork(name: string): Promise<void>;
|
|
51
61
|
/**
|
|
52
62
|
* Create a Docker volume if it doesn't exist.
|
|
53
63
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"container-runtime.d.ts","sourceRoot":"","sources":["../../src/infrastructure/container-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAmB,iBAAiB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"container-runtime.d.ts","sourceRoot":"","sources":["../../src/infrastructure/container-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAmB,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAoDrF;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAM1D;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CA6BvE;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkBpF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CA0GhF;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpE;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrE;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9D;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ1F;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvF;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO9D;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,SAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGpF;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAKnD"}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* All Omnitron-managed containers are labeled with `omnitron.managed=true`.
|
|
9
9
|
*/
|
|
10
10
|
// =============================================================================
|
|
11
|
-
// Singleton DockerAdapter (lazy-loaded)
|
|
11
|
+
// Singleton DockerAdapter (lazy-loaded, self-healing)
|
|
12
12
|
// =============================================================================
|
|
13
13
|
let _adapter = null;
|
|
14
14
|
async function getAdapter() {
|
|
@@ -18,6 +18,39 @@ async function getAdapter() {
|
|
|
18
18
|
_adapter = new DockerAdapter();
|
|
19
19
|
return _adapter;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Patterns that mean "the docker daemon connection is gone" rather than
|
|
23
|
+
* "this specific operation failed". When we see one, we invalidate the
|
|
24
|
+
* cached adapter so the next call gets a fresh instance — without this,
|
|
25
|
+
* a docker-daemon restart would leave us forever trying to talk to a
|
|
26
|
+
* dead socket.
|
|
27
|
+
*/
|
|
28
|
+
const ADAPTER_DEAD_PATTERNS = /(?:cannot connect to the docker daemon|docker daemon is not running|connect ENOENT|connect ECONNREFUSED.*docker|EOF$|connection reset)/i;
|
|
29
|
+
function isAdapterDeadError(err) {
|
|
30
|
+
if (!err)
|
|
31
|
+
return false;
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return ADAPTER_DEAD_PATTERNS.test(msg);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run `op` against the docker adapter. If it fails with an error that
|
|
37
|
+
* indicates the daemon connection is dead, invalidate the cached adapter
|
|
38
|
+
* and retry exactly once with a fresh one — recovering automatically
|
|
39
|
+
* from docker daemon restarts.
|
|
40
|
+
*/
|
|
41
|
+
async function withAdapter(op) {
|
|
42
|
+
const adapter = await getAdapter();
|
|
43
|
+
try {
|
|
44
|
+
return await op(adapter);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (!isAdapterDeadError(err))
|
|
48
|
+
throw err;
|
|
49
|
+
_adapter = null;
|
|
50
|
+
const fresh = await getAdapter();
|
|
51
|
+
return await op(fresh);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
21
54
|
// =============================================================================
|
|
22
55
|
// Public API
|
|
23
56
|
// =============================================================================
|
|
@@ -26,8 +59,7 @@ async function getAdapter() {
|
|
|
26
59
|
*/
|
|
27
60
|
export async function isDockerAvailable() {
|
|
28
61
|
try {
|
|
29
|
-
|
|
30
|
-
return adapter.isAvailable();
|
|
62
|
+
return await withAdapter((adapter) => adapter.isAvailable());
|
|
31
63
|
}
|
|
32
64
|
catch {
|
|
33
65
|
return false;
|
|
@@ -37,30 +69,30 @@ export async function isDockerAvailable() {
|
|
|
37
69
|
* List all Omnitron-managed containers.
|
|
38
70
|
*/
|
|
39
71
|
export async function listManagedContainers() {
|
|
40
|
-
const adapter = await getAdapter();
|
|
41
72
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
73
|
+
return await withAdapter(async (adapter) => {
|
|
74
|
+
const allNames = await adapter.listContainers(true);
|
|
75
|
+
const managed = [];
|
|
76
|
+
for (const name of allNames) {
|
|
77
|
+
try {
|
|
78
|
+
const info = await adapter.inspectContainer(name);
|
|
79
|
+
const labels = info?.Config?.Labels ?? {};
|
|
80
|
+
if (labels['omnitron.managed'] === 'true') {
|
|
81
|
+
managed.push({
|
|
82
|
+
name: (info.Name ?? name).replace(/^\//, ''),
|
|
83
|
+
image: info.Config?.Image ?? '',
|
|
84
|
+
status: mapInspectStatus(info.State?.Status),
|
|
85
|
+
containerId: info.Id?.slice(0, 12),
|
|
86
|
+
health: mapInspectHealth(info.State?.Health?.Status),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Container may have been removed between list and inspect
|
|
57
92
|
}
|
|
58
93
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return managed;
|
|
94
|
+
return managed;
|
|
95
|
+
});
|
|
64
96
|
}
|
|
65
97
|
catch {
|
|
66
98
|
return [];
|
|
@@ -70,19 +102,20 @@ export async function listManagedContainers() {
|
|
|
70
102
|
* Get state of a specific container by name.
|
|
71
103
|
*/
|
|
72
104
|
export async function getContainerState(name) {
|
|
73
|
-
const adapter = await getAdapter();
|
|
74
105
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
return await withAdapter(async (adapter) => {
|
|
107
|
+
const exists = await adapter.containerExists(name);
|
|
108
|
+
if (!exists)
|
|
109
|
+
return null;
|
|
110
|
+
const info = await adapter.inspectContainer(name);
|
|
111
|
+
return {
|
|
112
|
+
name: (info.Name ?? name).replace(/^\//, ''),
|
|
113
|
+
image: info.Config?.Image ?? '',
|
|
114
|
+
status: mapInspectStatus(info.State?.Status),
|
|
115
|
+
containerId: info.Id?.slice(0, 12),
|
|
116
|
+
health: mapInspectHealth(info.State?.Health?.Status),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
86
119
|
}
|
|
87
120
|
catch {
|
|
88
121
|
return null;
|
|
@@ -95,6 +128,14 @@ export async function createContainer(config) {
|
|
|
95
128
|
const { execFile } = await import('node:child_process');
|
|
96
129
|
const { promisify } = await import('node:util');
|
|
97
130
|
const execFileAsync = promisify(execFile);
|
|
131
|
+
// Ensure the managed network exists before we try to attach to it.
|
|
132
|
+
// Idempotent — `docker network create` errors with "already exists"
|
|
133
|
+
// when present, which we treat as success. Doing this here (rather
|
|
134
|
+
// than at daemon start) keeps the provisioner self-sufficient and
|
|
135
|
+
// means single-container test setups work without a manual prep step.
|
|
136
|
+
if (config.network) {
|
|
137
|
+
await ensureNetwork(config.network);
|
|
138
|
+
}
|
|
98
139
|
// Ensure volumes exist
|
|
99
140
|
for (const vol of config.volumes) {
|
|
100
141
|
if (!vol.source.startsWith('/') && !vol.source.startsWith('.')) {
|
|
@@ -105,6 +146,14 @@ export async function createContainer(config) {
|
|
|
105
146
|
await ensureImage(config.image);
|
|
106
147
|
// Build docker run command directly — xec adapter can hang
|
|
107
148
|
const args = ['docker', 'run', '-d', '--name', config.name];
|
|
149
|
+
// Network — explicit network avoids the default bridge, which
|
|
150
|
+
// accumulates phantom endpoints after dockerd restarts on macOS and
|
|
151
|
+
// blocks container recreation with "endpoint with name X already
|
|
152
|
+
// exists in network bridge". Falls back to docker's default when
|
|
153
|
+
// unspecified so single-container / no-stack usage still works.
|
|
154
|
+
if (config.network) {
|
|
155
|
+
args.push('--network', config.network);
|
|
156
|
+
}
|
|
108
157
|
// Port mappings
|
|
109
158
|
for (const p of config.ports) {
|
|
110
159
|
args.push('-p', `${p.host}:${p.container}`);
|
|
@@ -241,6 +290,32 @@ export async function waitForHealthy(name, timeoutMs = 60_000) {
|
|
|
241
290
|
return false;
|
|
242
291
|
}
|
|
243
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Create a Docker bridge network if it doesn't exist.
|
|
295
|
+
*
|
|
296
|
+
* Idempotent — `docker network create` exits non-zero with a recognisable
|
|
297
|
+
* "already exists" message when the network is present; we swallow that
|
|
298
|
+
* specific case and propagate everything else. Done via execFile rather
|
|
299
|
+
* than the adapter because the adapter doesn't expose network APIs and
|
|
300
|
+
* the call is rare (once per daemon startup per network).
|
|
301
|
+
*/
|
|
302
|
+
export async function ensureNetwork(name) {
|
|
303
|
+
const { execFile } = await import('node:child_process');
|
|
304
|
+
const { promisify } = await import('node:util');
|
|
305
|
+
const execFileAsync = promisify(execFile);
|
|
306
|
+
try {
|
|
307
|
+
await execFileAsync('docker', ['network', 'create', name, '--driver', 'bridge'], {
|
|
308
|
+
encoding: 'utf-8',
|
|
309
|
+
timeout: 15_000,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
314
|
+
if (/already exists/i.test(msg))
|
|
315
|
+
return; // idempotent
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
244
319
|
/**
|
|
245
320
|
* Create a Docker volume if it doesn't exist.
|
|
246
321
|
*/
|