@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.
Files changed (268) hide show
  1. package/dist/cli/omnitron.js +32 -1
  2. package/dist/cli/omnitron.js.map +1 -1
  3. package/dist/cluster/leader-election.d.ts.map +1 -1
  4. package/dist/cluster/leader-election.js +0 -2
  5. package/dist/cluster/leader-election.js.map +1 -1
  6. package/dist/commands/env.d.ts.map +1 -1
  7. package/dist/commands/env.js +10 -2
  8. package/dist/commands/env.js.map +1 -1
  9. package/dist/commands/health.d.ts.map +1 -1
  10. package/dist/commands/health.js +10 -2
  11. package/dist/commands/health.js.map +1 -1
  12. package/dist/commands/inspect-graph.d.ts +20 -0
  13. package/dist/commands/inspect-graph.d.ts.map +1 -0
  14. package/dist/commands/inspect-graph.js +78 -0
  15. package/dist/commands/inspect-graph.js.map +1 -0
  16. package/dist/commands/inspect.d.ts.map +1 -1
  17. package/dist/commands/inspect.js +10 -2
  18. package/dist/commands/inspect.js.map +1 -1
  19. package/dist/commands/list.d.ts.map +1 -1
  20. package/dist/commands/list.js +11 -1
  21. package/dist/commands/list.js.map +1 -1
  22. package/dist/commands/logs.d.ts.map +1 -1
  23. package/dist/commands/logs.js +17 -3
  24. package/dist/commands/logs.js.map +1 -1
  25. package/dist/commands/metrics.d.ts.map +1 -1
  26. package/dist/commands/metrics.js +10 -2
  27. package/dist/commands/metrics.js.map +1 -1
  28. package/dist/commands/monit.d.ts.map +1 -1
  29. package/dist/commands/monit.js +9 -0
  30. package/dist/commands/monit.js.map +1 -1
  31. package/dist/commands/output.d.ts +35 -0
  32. package/dist/commands/output.d.ts.map +1 -0
  33. package/dist/commands/output.js +63 -0
  34. package/dist/commands/output.js.map +1 -0
  35. package/dist/commands/stack.d.ts.map +1 -1
  36. package/dist/commands/stack.js +22 -7
  37. package/dist/commands/stack.js.map +1 -1
  38. package/dist/commands/status.d.ts.map +1 -1
  39. package/dist/commands/status.js +17 -0
  40. package/dist/commands/status.js.map +1 -1
  41. package/dist/commands/tor.d.ts +13 -0
  42. package/dist/commands/tor.d.ts.map +1 -0
  43. package/dist/commands/tor.js +100 -0
  44. package/dist/commands/tor.js.map +1 -0
  45. package/dist/commands/up.js +1 -1
  46. package/dist/commands/up.js.map +1 -1
  47. package/dist/config/loader.d.ts.map +1 -1
  48. package/dist/config/loader.js +29 -5
  49. package/dist/config/loader.js.map +1 -1
  50. package/dist/config/types.d.ts +14 -0
  51. package/dist/config/types.d.ts.map +1 -1
  52. package/dist/daemon/daemon-client.d.ts +14 -0
  53. package/dist/daemon/daemon-client.d.ts.map +1 -1
  54. package/dist/daemon/daemon-client.js +4 -0
  55. package/dist/daemon/daemon-client.js.map +1 -1
  56. package/dist/daemon/daemon-entry.js +17 -23
  57. package/dist/daemon/daemon-entry.js.map +1 -1
  58. package/dist/daemon/daemon.d.ts +2 -0
  59. package/dist/daemon/daemon.d.ts.map +1 -1
  60. package/dist/daemon/daemon.js +118 -7
  61. package/dist/daemon/daemon.js.map +1 -1
  62. package/dist/daemon/daemon.module.d.ts.map +1 -1
  63. package/dist/daemon/daemon.module.js +41 -14
  64. package/dist/daemon/daemon.module.js.map +1 -1
  65. package/dist/daemon/daemon.rpc-service.d.ts +19 -0
  66. package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
  67. package/dist/daemon/daemon.rpc-service.js +14 -0
  68. package/dist/daemon/daemon.rpc-service.js.map +1 -1
  69. package/dist/daemon/pid-manager.d.ts +55 -4
  70. package/dist/daemon/pid-manager.d.ts.map +1 -1
  71. package/dist/daemon/pid-manager.js +123 -16
  72. package/dist/daemon/pid-manager.js.map +1 -1
  73. package/dist/daemon/state-store.d.ts.map +1 -1
  74. package/dist/daemon/state-store.js +43 -1
  75. package/dist/daemon/state-store.js.map +1 -1
  76. package/dist/infrastructure/container-runtime.d.ts +10 -0
  77. package/dist/infrastructure/container-runtime.d.ts.map +1 -1
  78. package/dist/infrastructure/container-runtime.js +111 -36
  79. package/dist/infrastructure/container-runtime.js.map +1 -1
  80. package/dist/infrastructure/infrastructure.service.d.ts +41 -0
  81. package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
  82. package/dist/infrastructure/infrastructure.service.js +157 -8
  83. package/dist/infrastructure/infrastructure.service.js.map +1 -1
  84. package/dist/infrastructure/phantom-endpoint-janitor.d.ts +72 -0
  85. package/dist/infrastructure/phantom-endpoint-janitor.d.ts.map +1 -0
  86. package/dist/infrastructure/phantom-endpoint-janitor.js +149 -0
  87. package/dist/infrastructure/phantom-endpoint-janitor.js.map +1 -0
  88. package/dist/infrastructure/presets/index.d.ts.map +1 -1
  89. package/dist/infrastructure/presets/index.js +2 -0
  90. package/dist/infrastructure/presets/index.js.map +1 -1
  91. package/dist/infrastructure/presets/postgres.d.ts.map +1 -1
  92. package/dist/infrastructure/presets/postgres.js +9 -3
  93. package/dist/infrastructure/presets/postgres.js.map +1 -1
  94. package/dist/infrastructure/presets/redis.d.ts.map +1 -1
  95. package/dist/infrastructure/presets/redis.js +10 -4
  96. package/dist/infrastructure/presets/redis.js.map +1 -1
  97. package/dist/infrastructure/presets/registry.d.ts.map +1 -1
  98. package/dist/infrastructure/presets/registry.js +12 -0
  99. package/dist/infrastructure/presets/registry.js.map +1 -1
  100. package/dist/infrastructure/presets/tor.d.ts +74 -0
  101. package/dist/infrastructure/presets/tor.d.ts.map +1 -0
  102. package/dist/infrastructure/presets/tor.js +176 -0
  103. package/dist/infrastructure/presets/tor.js.map +1 -0
  104. package/dist/infrastructure/service-resolver.d.ts +31 -0
  105. package/dist/infrastructure/service-resolver.d.ts.map +1 -1
  106. package/dist/infrastructure/service-resolver.js +60 -9
  107. package/dist/infrastructure/service-resolver.js.map +1 -1
  108. package/dist/infrastructure/stack-infra-manager.d.ts +1 -1
  109. package/dist/infrastructure/stack-infra-manager.js +1 -1
  110. package/dist/infrastructure/types.d.ts +17 -0
  111. package/dist/infrastructure/types.d.ts.map +1 -1
  112. package/dist/observability/index.d.ts +4 -0
  113. package/dist/observability/index.d.ts.map +1 -0
  114. package/dist/observability/index.js +3 -0
  115. package/dist/observability/index.js.map +1 -0
  116. package/dist/observability/metrics-bridge.d.ts +87 -0
  117. package/dist/observability/metrics-bridge.d.ts.map +1 -0
  118. package/dist/observability/metrics-bridge.js +295 -0
  119. package/dist/observability/metrics-bridge.js.map +1 -0
  120. package/dist/observability/metrics-server.d.ts +68 -0
  121. package/dist/observability/metrics-server.d.ts.map +1 -0
  122. package/dist/observability/metrics-server.js +146 -0
  123. package/dist/observability/metrics-server.js.map +1 -0
  124. package/dist/orchestrator/app-handle.d.ts +13 -1
  125. package/dist/orchestrator/app-handle.d.ts.map +1 -1
  126. package/dist/orchestrator/app-handle.js +12 -0
  127. package/dist/orchestrator/app-handle.js.map +1 -1
  128. package/dist/orchestrator/bootstrap-process.d.ts +49 -0
  129. package/dist/orchestrator/bootstrap-process.d.ts.map +1 -1
  130. package/dist/orchestrator/bootstrap-process.js +167 -0
  131. package/dist/orchestrator/bootstrap-process.js.map +1 -1
  132. package/dist/orchestrator/build-service.d.ts +50 -3
  133. package/dist/orchestrator/build-service.d.ts.map +1 -1
  134. package/dist/orchestrator/build-service.js +357 -85
  135. package/dist/orchestrator/build-service.js.map +1 -1
  136. package/dist/orchestrator/file-watcher.d.ts.map +1 -1
  137. package/dist/orchestrator/file-watcher.js +60 -38
  138. package/dist/orchestrator/file-watcher.js.map +1 -1
  139. package/dist/orchestrator/orchestrator.service.d.ts +176 -3
  140. package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
  141. package/dist/orchestrator/orchestrator.service.js +587 -56
  142. package/dist/orchestrator/orchestrator.service.js.map +1 -1
  143. package/dist/orchestrator/process-janitor.d.ts +153 -0
  144. package/dist/orchestrator/process-janitor.d.ts.map +1 -0
  145. package/dist/orchestrator/process-janitor.js +279 -0
  146. package/dist/orchestrator/process-janitor.js.map +1 -0
  147. package/dist/project/config-resolver.d.ts.map +1 -1
  148. package/dist/project/config-resolver.js +38 -12
  149. package/dist/project/config-resolver.js.map +1 -1
  150. package/dist/project/registry.d.ts +1 -0
  151. package/dist/project/registry.d.ts.map +1 -1
  152. package/dist/project/registry.js +30 -0
  153. package/dist/project/registry.js.map +1 -1
  154. package/dist/services/auth.service.d.ts.map +1 -1
  155. package/dist/services/auth.service.js +9 -2
  156. package/dist/services/auth.service.js.map +1 -1
  157. package/dist/services/netron-telemetry-transport.d.ts.map +1 -1
  158. package/dist/services/netron-telemetry-transport.js +0 -4
  159. package/dist/services/netron-telemetry-transport.js.map +1 -1
  160. package/dist/services/node-manager.service.js +1 -1
  161. package/dist/services/node-manager.service.js.map +1 -1
  162. package/dist/services/pipeline.service.d.ts +15 -0
  163. package/dist/services/pipeline.service.d.ts.map +1 -1
  164. package/dist/services/pipeline.service.js +53 -2
  165. package/dist/services/pipeline.service.js.map +1 -1
  166. package/dist/services/project.rpc-service.d.ts +4 -0
  167. package/dist/services/project.rpc-service.d.ts.map +1 -1
  168. package/dist/services/project.rpc-service.js +9 -0
  169. package/dist/services/project.rpc-service.js.map +1 -1
  170. package/dist/services/project.service.d.ts +9 -0
  171. package/dist/services/project.service.d.ts.map +1 -1
  172. package/dist/services/project.service.js +148 -21
  173. package/dist/services/project.service.js.map +1 -1
  174. package/dist/shared/dto/project.d.ts +1 -1
  175. package/dist/shared/dto/project.d.ts.map +1 -1
  176. package/dist/shared/dto/services.d.ts +24 -0
  177. package/dist/shared/dto/services.d.ts.map +1 -1
  178. package/dist/workers/health-monitor.service.d.ts.map +1 -1
  179. package/dist/workers/health-monitor.service.js +4 -1
  180. package/dist/workers/health-monitor.service.js.map +1 -1
  181. package/dist/workers/types.d.ts +3 -0
  182. package/dist/workers/types.d.ts.map +1 -1
  183. package/package.json +19 -18
  184. package/webapp/dist/assets/Badge-D2ZBP5iK.js +1 -0
  185. package/webapp/dist/assets/Card-vO88g06B.js +1 -0
  186. package/webapp/dist/assets/CardActions-C9h7Q2wW.js +1 -0
  187. package/webapp/dist/assets/CardContent-BA_Xmmhk.js +1 -0
  188. package/webapp/dist/assets/CardHeader-ceIs__6x.js +1 -0
  189. package/webapp/dist/assets/Chip-Ckk0xUhK.js +1 -0
  190. package/webapp/dist/assets/DialogActions-BhHM7hov.js +1 -0
  191. package/webapp/dist/assets/DialogContentText-BT2JQRmK.js +1 -0
  192. package/webapp/dist/assets/FormControlLabel-CcQByVS8.js +1 -0
  193. package/webapp/dist/assets/Grid-14PETRQW.js +1 -0
  194. package/webapp/dist/assets/InputAdornment-B8aHIsQ7.js +1 -0
  195. package/webapp/dist/assets/Skeleton-B74-Ds6O.js +37 -0
  196. package/webapp/dist/assets/Switch-DvWD2wh4.js +1 -0
  197. package/webapp/dist/assets/SwitchBase-CDU4nY8O.js +1 -0
  198. package/webapp/dist/assets/Tab-DcCOYYp5.js +1 -0
  199. package/webapp/dist/assets/TableContainer-CzTh4mu-.js +1 -0
  200. package/webapp/dist/assets/TableRow-BySEreZR.js +2 -0
  201. package/webapp/dist/assets/TextField-BLf-BHz2.js +1 -0
  202. package/webapp/dist/assets/alerts-DnRbT41C.js +1 -0
  203. package/webapp/dist/assets/apps-CdR0VH2d.js +1 -0
  204. package/webapp/dist/assets/chunk-DtcAfS_W.js +1 -0
  205. package/webapp/dist/assets/client-DU8I_lDc.js +2 -0
  206. package/webapp/dist/assets/containers-Dw-rRXV1.js +1 -0
  207. package/webapp/dist/assets/dashboard-CzgB0uZz.js +1 -0
  208. package/webapp/dist/assets/dashboard-builder-DsA0yg33.js +1 -0
  209. package/webapp/dist/assets/deployments-DtQAoIcQ.js +1 -0
  210. package/webapp/dist/assets/detail-BJR-G7JG.js +1 -0
  211. package/webapp/dist/assets/detail-nlCeSltl.js +8 -0
  212. package/webapp/dist/assets/index-ByQDwp2r.js +566 -0
  213. package/webapp/dist/assets/isMuiElement-DwF2naaF.js +1 -0
  214. package/webapp/dist/assets/logs-Kn8o4H7k.js +11 -0
  215. package/webapp/dist/assets/metrics-DN8Zu5wT.js +2 -0
  216. package/webapp/dist/assets/nodes-Bgch7AtN.js +7 -0
  217. package/webapp/dist/assets/pipelines-Cry3qm-S.js +4 -0
  218. package/webapp/dist/assets/popper-BJFSj6rc.js +1 -0
  219. package/webapp/dist/assets/preload-helper-DSXbuxSR.js +1 -0
  220. package/webapp/dist/assets/projects-Dym2GI1U.js +1 -0
  221. package/webapp/dist/assets/react-apexcharts.esm-CSGK5Cxn.js +958 -0
  222. package/webapp/dist/assets/react-apexcharts.esm-DEAWOwx5.js +1003 -0
  223. package/webapp/dist/assets/settings-va3zTcow.js +1 -0
  224. package/webapp/dist/assets/sign-in-ik35umbp.js +1 -0
  225. package/webapp/dist/assets/stacks-CVFOnxbF.js +1 -0
  226. package/webapp/dist/assets/system-info-npIXAjX2.js +1 -0
  227. package/webapp/dist/assets/topology-bDe70vLs.js +12 -0
  228. package/webapp/dist/assets/traces-DIIcT8DF.js +1 -0
  229. package/webapp/dist/assets/use-stack-context-DHLwZv1r.js +1 -0
  230. package/webapp/dist/index.html +5 -6
  231. package/webapp/dist/assets/Chip-BySxTjyN.js +0 -1
  232. package/webapp/dist/assets/DialogContentText-YQWiAVrU.js +0 -1
  233. package/webapp/dist/assets/DialogTitle-CDkgUIwp.js +0 -1
  234. package/webapp/dist/assets/FormControlLabel-Bu3CCsBM.js +0 -1
  235. package/webapp/dist/assets/Grid-BFY8hAzL.js +0 -1
  236. package/webapp/dist/assets/InputAdornment-_Dayk-3y.js +0 -3
  237. package/webapp/dist/assets/Skeleton-BGj6Sw8m.js +0 -37
  238. package/webapp/dist/assets/Switch-CA6682vv.js +0 -1
  239. package/webapp/dist/assets/TableContainer-4epq10KJ.js +0 -1
  240. package/webapp/dist/assets/TextField-C73KHMVN.js +0 -1
  241. package/webapp/dist/assets/alerts-JEQALS7Y.js +0 -1
  242. package/webapp/dist/assets/apps-DMPysEKB.js +0 -1
  243. package/webapp/dist/assets/client-DlGPter5.js +0 -110
  244. package/webapp/dist/assets/containers-CvkC7do8.js +0 -1
  245. package/webapp/dist/assets/dashboard-C0Fre9pC.js +0 -1
  246. package/webapp/dist/assets/dashboard-builder-DAghqdpE.js +0 -1
  247. package/webapp/dist/assets/deployments-Cg1lI1uT.js +0 -1
  248. package/webapp/dist/assets/detail-CyFtEson.js +0 -1
  249. package/webapp/dist/assets/detail-DqoSGKT1.js +0 -8
  250. package/webapp/dist/assets/icons-ynIWYfBP.js +0 -3
  251. package/webapp/dist/assets/index-vKrDmEA_.js +0 -259
  252. package/webapp/dist/assets/isMuiElement-BRL3YuCT.js +0 -1
  253. package/webapp/dist/assets/logs-IovIij1C.js +0 -11
  254. package/webapp/dist/assets/metrics-Dufwtwjq.js +0 -1
  255. package/webapp/dist/assets/nodes-Brvu8W3j.js +0 -7
  256. package/webapp/dist/assets/pipelines-t40X4MuC.js +0 -4
  257. package/webapp/dist/assets/prop-types-CvVtHxp4.js +0 -1
  258. package/webapp/dist/assets/react-apexcharts.esm-CLO6gala.js +0 -954
  259. package/webapp/dist/assets/settings-DTAIST3v.js +0 -1
  260. package/webapp/dist/assets/sign-in-DQEAkJum.js +0 -1
  261. package/webapp/dist/assets/stacks-BjM1f45N.js +0 -1
  262. package/webapp/dist/assets/system-info-BQSbSEK-.js +0 -1
  263. package/webapp/dist/assets/topology-6N8-Az_Z.js +0 -12
  264. package/webapp/dist/assets/traces-DVXyb-4m.js +0 -1
  265. package/webapp/dist/assets/use-stack-context-CbE8fxBA.js +0 -1
  266. /package/webapp/dist/assets/{constants-DqFQJ27U.js → constants-BgnT-Xdy.js} +0 -0
  267. /package/webapp/dist/assets/{formatters-J3Xqn6BQ.js → formatters-Do41SRmE.js} +0 -0
  268. /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
- * Write current process PID
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;;;;;GAKG;AAKH,qBAAa,UAAU;IACT,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,MAAM;IAE5C;;;OAGG;IACH,SAAS,IAAI,OAAO;IAMpB;;OAEG;IACH,MAAM,IAAI,MAAM,GAAG,IAAI;IAMvB;;;OAGG;IACH,OAAO,IAAI,MAAM,GAAG,IAAI;IAUxB;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;OAEG;IACH,MAAM,IAAI,IAAI;IAQd;;;;OAIG;IACH,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO;IAqB1C;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;CAQ5C"}
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
- return PidManager.isProcessAlive(pid);
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 PidManager.isProcessAlive(pid) ? pid : null;
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 pidStr = fs.readFileSync(this.pidFile, 'utf-8').trim();
40
- const pid = parseInt(pidStr, 10);
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
- * Write current process PID
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
- fs.writeFileSync(this.pidFile, String(process.pid), 'utf-8');
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
- // If process is still alive, not stale
76
- if (PidManager.isProcessAlive(rawPid))
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
- // Process is dead — clean up stale files
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
- try {
95
- process.kill(pid, 0);
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
- catch {
99
- return false;
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;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,OAAO,UAAU;IACQ;IAA7B,YAA6B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEhD;;;OAGG;IACH,SAAS;QACP,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC/B,OAAO,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED;;OAEG;IACH,MAAM;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC3B,IAAI,GAAG,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAC9B,OAAO,UAAU,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;YAC7D,MAAM,GAAG,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YACjC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;QACjC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACH,KAAK;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvC,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC;IAC/D,CAAC;IAED;;OAEG;IACH,MAAM;QACJ,IAAI,CAAC;YACH,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACP,qBAAqB;QACvB,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,YAAY,CAAC,UAAmB;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QAC9B,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAElC,uCAAuC;QACvC,IAAI,UAAU,CAAC,cAAc,CAAC,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;QAEpD,yCAAyC;QACzC,IAAI,CAAC,MAAM,EAAE,CAAC;QAEd,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC;gBACH,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,uBAAuB;YACzB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,MAAM,CAAC,cAAc,CAAC,GAAW;QAC/B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACrB,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF"}
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;AAIH,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;IAMjC,IAAI,IAAI,cAAc,GAAG,IAAI;IAS7B,KAAK,IAAI,IAAI;CAOd"}
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
- fs.writeFileSync(this.stateFile, JSON.stringify(state, null, 2), 'utf-8');
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;AAmB7B,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;QACvC,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC5E,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"}
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;AAmBrF;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO1D;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,CAiBpF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAwFhF;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;;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"}
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
- const adapter = await getAdapter();
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
- // listContainers returns names; filter by inspecting labels
43
- const allNames = await adapter.listContainers(true);
44
- const managed = [];
45
- for (const name of allNames) {
46
- try {
47
- const info = await adapter.inspectContainer(name);
48
- const labels = info?.Config?.Labels ?? {};
49
- if (labels['omnitron.managed'] === 'true') {
50
- managed.push({
51
- name: (info.Name ?? name).replace(/^\//, ''),
52
- image: info.Config?.Image ?? '',
53
- status: mapInspectStatus(info.State?.Status),
54
- containerId: info.Id?.slice(0, 12),
55
- health: mapInspectHealth(info.State?.Health?.Status),
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
- catch {
60
- // Container may have been removed between list and inspect
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
- const exists = await adapter.containerExists(name);
76
- if (!exists)
77
- return null;
78
- const info = await adapter.inspectContainer(name);
79
- return {
80
- name: (info.Name ?? name).replace(/^\//, ''),
81
- image: info.Config?.Image ?? '',
82
- status: mapInspectStatus(info.State?.Status),
83
- containerId: info.Id?.slice(0, 12),
84
- health: mapInspectHealth(info.State?.Health?.Status),
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
  */