@parcha/pi-durable-disk 0.1.0-beta.12 → 0.1.0-beta.14

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 (46) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +268 -28
  3. package/dist/app.d.ts +5 -0
  4. package/dist/app.js +3 -2
  5. package/dist/claim.d.ts +34 -2
  6. package/dist/claim.js +86 -5
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.js +98 -33
  9. package/dist/commands.d.ts +70 -0
  10. package/dist/commands.js +257 -0
  11. package/dist/confine.d.ts +47 -7
  12. package/dist/confine.js +148 -19
  13. package/dist/env.d.ts +17 -26
  14. package/dist/env.js +73 -68
  15. package/dist/errors.d.ts +2 -1
  16. package/dist/errors.js +1 -1
  17. package/dist/fork.d.ts +23 -0
  18. package/dist/fork.js +103 -3
  19. package/dist/hosts/daytona-launch.d.ts +3 -1
  20. package/dist/hosts/daytona-launch.js +30 -6
  21. package/dist/hosts/daytona.js +15 -3
  22. package/dist/hosts/local-host.d.ts +7 -1
  23. package/dist/hosts/local-host.js +30 -16
  24. package/dist/index.d.ts +31 -15
  25. package/dist/index.js +23 -15
  26. package/dist/internals.d.ts +3 -0
  27. package/dist/internals.js +6 -0
  28. package/dist/lease.d.ts +5 -2
  29. package/dist/lease.js +3 -1
  30. package/dist/local-run.d.ts +10 -0
  31. package/dist/local-run.js +64 -0
  32. package/dist/park.d.ts +4 -3
  33. package/dist/park.js +4 -3
  34. package/dist/process-table.d.ts +38 -0
  35. package/dist/process-table.js +158 -0
  36. package/dist/run.d.ts +52 -10
  37. package/dist/run.js +42 -7
  38. package/dist/status.d.ts +11 -3
  39. package/dist/status.js +15 -13
  40. package/dist/store.d.ts +14 -2
  41. package/dist/store.js +16 -4
  42. package/dist/supervise.d.ts +22 -3
  43. package/dist/supervise.js +40 -13
  44. package/dist/watchdog.d.ts +3 -1
  45. package/dist/watchdog.js +46 -13
  46. package/package.json +12 -7
package/dist/watchdog.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // The lease watchdog on a thread of its own. A store commit is synchronous, so a FUSE daemon that stops
2
2
  // answering blocks the instance's main thread inside the kernel, and with it every timer the main thread owns. This
3
- // thread never touches the mount: it reads a clock reading the main thread shares, /proc and the cgroup, and sends
4
- // signals. Once the lease has lapsed it kills the instance's commands on every look, until the main thread runs again,
3
+ // thread never touches the mount: it reads a clock reading the main thread shares, /proc and the cgroup (on macOS, one
4
+ // ps listing), and sends signals. Once the lease has lapsed it kills the instance's commands on every look, until the main thread runs again,
5
5
  // sees the flag and takes the normal fence path (exit 75).
6
+ import * as child_process from "node:child_process";
6
7
  import * as fs from "node:fs";
7
8
  import * as path from "node:path";
8
9
  import { Worker } from "node:worker_threads";
@@ -49,20 +50,49 @@ function cgroupPids(instance, proc, sys) {
49
50
  }
50
51
  }
51
52
 
52
- function sweep(instance, options = {}) {
53
- const proc = options.proc ?? "/proc";
54
- const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
53
+ /** The instance's group and its direct children, from /proc. */
54
+ function procTree(instance, proc) {
55
55
  const own = stat(proc, instance);
56
- if (own === undefined) return 0;
57
- const targets = new Set();
56
+ if (own === undefined) return undefined;
57
+ const children = [];
58
58
  for (const entry of fs.readdirSync(proc)) {
59
59
  const pid = Number(entry);
60
60
  if (!Number.isInteger(pid) || pid <= 0 || pid === instance) continue;
61
61
  const child = stat(proc, pid);
62
- if (child === undefined || child.ppid !== instance) continue;
63
- targets.add(child.pgrp === own.pgrp || child.pgrp <= 0 ? pid : -child.pgrp);
62
+ if (child !== undefined && child.ppid === instance) children.push({ pid, pgrp: child.pgrp });
63
+ }
64
+ return { pgrp: own.pgrp, children };
65
+ }
66
+
67
+ /** The same from one ps listing, where there is no /proc (macOS). */
68
+ function psTree(instance, ps) {
69
+ let text;
70
+ try {
71
+ text = ps("/bin/ps", ["-A", "-o", "pid=,ppid=,pgid="]);
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ const rows = [];
76
+ for (const line of text.split("\n")) {
77
+ const f = line.trim().split(/\s+/).map(Number);
78
+ if (f.length === 3 && f.every(Number.isInteger)) rows.push({ pid: f[0], ppid: f[1], pgrp: f[2] });
64
79
  }
65
- if (options.ownCgroup) {
80
+ const own = rows.find((row) => row.pid === instance);
81
+ if (own === undefined) return undefined;
82
+ return { pgrp: own.pgrp, children: rows.filter((row) => row.ppid === instance && row.pid > 0 && row.pid !== instance) };
83
+ }
84
+
85
+ const runPs = (file, args) => child_process.execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 10000 });
86
+
87
+ function sweep(instance, options = {}) {
88
+ const proc = options.proc ?? "/proc";
89
+ const kill = options.kill ?? ((pid, signal) => process.kill(pid, signal));
90
+ const ps = options.ps ?? (options.proc === undefined && process.platform === "darwin" ? runPs : undefined);
91
+ const tree = ps === undefined ? procTree(instance, proc) : psTree(instance, ps);
92
+ if (tree === undefined) return 0;
93
+ const targets = new Set();
94
+ for (const child of tree.children) targets.add(child.pgrp === tree.pgrp || child.pgrp <= 1 ? child.pid : -child.pgrp);
95
+ if (options.ownCgroup && ps === undefined) {
66
96
  for (const pid of cgroupPids(instance, proc, options.sys ?? "/sys/fs/cgroup")) if (pid !== instance && pid > 0) targets.add(pid);
67
97
  }
68
98
  let sent = 0;
@@ -78,6 +108,7 @@ function sweep(instance, options = {}) {
78
108
  }
79
109
  `;
80
110
  const WORKER_SOURCE = `"use strict";
111
+ const child_process = require("node:child_process");
81
112
  const fs = require("node:fs");
82
113
  const path = require("node:path");
83
114
  const { workerData } = require("node:worker_threads");
@@ -88,14 +119,14 @@ setInterval(() => {
88
119
  Atomics.add(shared, SLOT_KILLED, BigInt(sweep(workerData.instance, { ownCgroup: workerData.ownCgroup })));
89
120
  }, workerData.checkMs);
90
121
  `;
91
- const compiled = new Function("fs", "path", `"use strict";${LIB}\nreturn { lapsed, sweep };`)(fs, path);
122
+ const compiled = new Function("child_process", "fs", "path", `"use strict";${LIB}\nreturn { lapsed, sweep };`)(child_process, fs, path);
92
123
  /** One look at the shared state at `now`. True once the lease has lapsed, for good; the first look records the age. */
93
124
  export const lapsed = compiled.lapsed;
94
125
  /**
95
126
  * SIGKILL the instance's commands: the process group of each direct child (pi starts each command as its own group's
96
127
  * leader), or the child alone when it shares the instance's group; with `ownCgroup`, every other process of the cgroup.
97
128
  * Never the instance or its group. A process this user may not signal (sudo's archil helpers) is skipped. Returns the
98
- * number of signals sent. `stat` lines are `pid (comm) state ppid pgrp ...`, read after the last `)` since comm may hold
129
+ * number of signals sent. Where there is no /proc (macOS) the processes come from one ps listing, and there is no cgroup. `stat` lines are `pid (comm) state ppid pgrp ...`, read after the last `)` since comm may hold
99
130
  * spaces and parentheses.
100
131
  */
101
132
  export const sweep = compiled.sweep;
@@ -110,7 +141,9 @@ export class LeaseWatchdog {
110
141
  Atomics.store(this.#shared, SLOT_BEAT, options.beatNs);
111
142
  Atomics.store(this.#shared, SLOT_LIMIT, BigInt(Math.round(options.limitMs * 1e6)));
112
143
  const data = { buffer, checkMs: options.checkMs, instance: process.pid, ownCgroup: options.ownCgroup === true };
113
- this.#worker = new Worker(WORKER_SOURCE, { eval: true, workerData: data });
144
+ // No inherited Node flags: the source is self-contained CommonJS and needs none of the host's, and some would make Node
145
+ // read it as ESM (`--input-type=module`, a host running inline ESM through -e or stdin), where `require` is undefined.
146
+ this.#worker = new Worker(WORKER_SOURCE, { eval: true, workerData: data, execArgv: [] });
114
147
  this.#worker.unref();
115
148
  }
116
149
  /** A heartbeat write that started at `startNs` succeeded. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcha/pi-durable-disk",
3
- "version": "0.1.0-beta.12",
3
+ "version": "0.1.0-beta.14",
4
4
  "description": "A durable pi host on a network disk (Archil): one owner per run, a fence, durable commits, and a supervisor.",
5
5
  "keywords": [
6
6
  "pi",
@@ -30,6 +30,10 @@
30
30
  "types": "./dist/lease.d.ts",
31
31
  "default": "./dist/lease.js"
32
32
  },
33
+ "./internals": {
34
+ "types": "./dist/internals.d.ts",
35
+ "default": "./dist/internals.js"
36
+ },
33
37
  "./bin/archil-scoped": "./bin/archil-scoped",
34
38
  "./package.json": "./package.json"
35
39
  },
@@ -49,7 +53,7 @@
49
53
  "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/rewrite-dts.mjs dist",
50
54
  "prepack": "npm run build",
51
55
  "typecheck": "tsc --noEmit && tsc --noEmit -p examples",
52
- "test": "node --test 'test/*.test.ts'",
56
+ "test": "node --test --test-timeout=240000 'test/*.test.ts' 'test/examples/*.test.ts'",
53
57
  "test:acceptance": "node --test --test-concurrency=1 'test/acceptance/*.test.ts'",
54
58
  "test:live": "node --test --test-concurrency=1 'test/live/*.test.ts'",
55
59
  "test:live:claim": "node --test --test-concurrency=1 test/live/claim.live.test.ts",
@@ -58,20 +62,21 @@
58
62
  "test:live:run": "node --test --test-concurrency=1 test/live/run.live.test.ts",
59
63
  "test:live:store": "node --test --test-concurrency=1 test/live/store.live.test.ts",
60
64
  "test:live:supervise": "node --test --test-concurrency=1 test/live/supervise.live.test.ts",
65
+ "test:live:supervise-slow": "node --test --test-concurrency=1 test/live/supervise-slow.live.test.ts",
61
66
  "test:live:docker": "PDA_LIVE_DOCKER=1 node --test --test-concurrency=1 test/live/docker.live.test.ts test/live/docker-park.live.test.ts",
62
67
  "verify:tarball": "node scripts/verify-tarball.mjs"
63
68
  },
64
69
  "peerDependencies": {
65
- "@earendil-works/chord": "~1.0.4",
66
- "@earendil-works/pi-durable": "~1.0.4"
70
+ "@earendil-works/chord": ">=1.0.4 <1.4.0",
71
+ "@earendil-works/pi-durable": ">=1.0.4 <1.4.0"
67
72
  },
68
73
  "dependencies": {
69
74
  "disk": "1.7.0"
70
75
  },
71
76
  "devDependencies": {
72
- "@earendil-works/chord": "1.0.4",
73
- "@earendil-works/pi-ai": "1.0.4",
74
- "@earendil-works/pi-durable": "1.0.4"
77
+ "@earendil-works/chord": "1.1.0",
78
+ "@earendil-works/pi-ai": "1.1.0",
79
+ "@earendil-works/pi-durable": "1.1.0"
75
80
  },
76
81
  "repository": {
77
82
  "type": "git",