@parcha/pi-durable-disk 0.1.0-beta.13 → 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.
- package/CHANGELOG.md +96 -0
- package/README.md +102 -35
- package/dist/claim.d.ts +6 -1
- package/dist/claim.js +12 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +96 -32
- package/dist/commands.d.ts +4 -1
- package/dist/commands.js +48 -90
- package/dist/confine.d.ts +36 -7
- package/dist/confine.js +125 -18
- package/dist/env.d.ts +7 -22
- package/dist/env.js +25 -60
- package/dist/fork.d.ts +23 -0
- package/dist/fork.js +103 -3
- package/dist/hosts/daytona-launch.d.ts +3 -1
- package/dist/hosts/daytona-launch.js +27 -5
- package/dist/hosts/daytona.js +15 -3
- package/dist/hosts/local-host.d.ts +7 -1
- package/dist/hosts/local-host.js +25 -16
- package/dist/index.d.ts +31 -16
- package/dist/index.js +23 -16
- package/dist/internals.d.ts +3 -0
- package/dist/internals.js +6 -0
- package/dist/lease.d.ts +3 -0
- package/dist/lease.js +2 -0
- package/dist/local-run.js +3 -2
- package/dist/process-table.d.ts +38 -0
- package/dist/process-table.js +158 -0
- package/dist/run.d.ts +10 -0
- package/dist/run.js +4 -1
- package/dist/status.d.ts +11 -3
- package/dist/status.js +15 -13
- package/dist/store.d.ts +14 -2
- package/dist/store.js +16 -4
- package/dist/supervise.d.ts +19 -0
- package/dist/supervise.js +25 -0
- package/dist/watchdog.d.ts +3 -1
- package/dist/watchdog.js +43 -12
- package/package.json +8 -3
package/dist/status.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export type RunStatus = (typeof RUN_STATUSES)[number];
|
|
|
19
19
|
export interface RunHolder {
|
|
20
20
|
readonly driver: string;
|
|
21
21
|
readonly host: string;
|
|
22
|
-
/** `/proc/sys/kernel/random/boot_id
|
|
22
|
+
/** `/proc/sys/kernel/random/boot_id` (`kern.bootsessionuuid` on macOS); null where it cannot be read. */
|
|
23
23
|
readonly bootId: string | null;
|
|
24
24
|
readonly pid: number;
|
|
25
25
|
/** When this incarnation claimed the run (ISO 8601). */
|
|
@@ -81,12 +81,14 @@ export declare const ARCHIL_FSTYPE = "fuse.archil";
|
|
|
81
81
|
* A run root held open as a directory for the life of the instance. `at` is `/proc/self/fd/<n>`, a path the kernel
|
|
82
82
|
* resolves through the open directory and never through the root's path (Node has no openat; the magic link stands in
|
|
83
83
|
* for it): a file made, written or renamed under `at` is in the directory opened at acquire, or the call fails (ENOTCONN
|
|
84
|
-
* once the FUSE daemon is gone). It never lands on the local disk under a mountpoint whose mount went away.
|
|
84
|
+
* once the FUSE daemon is gone). It never lands on the local disk under a mountpoint whose mount went away. macOS has no
|
|
85
|
+
* such link: there `at` is the root's path (`byPath`), and only the checks below stand between a write and a root that was
|
|
86
|
+
* replaced.
|
|
85
87
|
*/
|
|
86
88
|
export interface ClaimDir {
|
|
87
89
|
/** The root's path, as the claim names it. */
|
|
88
90
|
readonly root: string;
|
|
89
|
-
/** `/proc/self/fd/<n
|
|
91
|
+
/** `/proc/self/fd/<n>`, or the root's path `byPath`. Throws once closed: the number may then name another file. */
|
|
90
92
|
readonly at: string;
|
|
91
93
|
/**
|
|
92
94
|
* Resolves while the root's path still resolves to the directory held open (same device, same inode). A mount that was
|
|
@@ -107,6 +109,12 @@ export interface OpenClaimDirOptions {
|
|
|
107
109
|
readonly fstype?: string | null;
|
|
108
110
|
/** Default `/proc/self/mountinfo`. */
|
|
109
111
|
readonly mountinfo?: string;
|
|
112
|
+
/**
|
|
113
|
+
* `at` is the root's path, not `/proc/self/fd/<n>`: the default on macOS, which has none. A write is checked against the
|
|
114
|
+
* held directory first (`assertMounted`), but a root renamed and replaced between that check and the write gets the
|
|
115
|
+
* write. For a local directory only.
|
|
116
|
+
*/
|
|
117
|
+
readonly byPath?: boolean;
|
|
110
118
|
}
|
|
111
119
|
/** `major:minor` of a device number, as `/proc/self/mountinfo` writes it (glibc's encoding). */
|
|
112
120
|
export declare function deviceName(dev: bigint): string;
|
package/dist/status.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// claim with write-to-temp, fsync, rename, and read by the supervisor (over the S3 API or a local mount). It is a hint
|
|
3
3
|
// for the supervisor, never the source of truth: the store is. Any error writing it is a fence. The instance writes it
|
|
4
4
|
// through its claim directory (`ClaimDir`), never by path, so a write cannot land under a mountpoint whose mount is gone.
|
|
5
|
-
import { constants
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
6
|
import { open, readFile, rename, rm, stat } from "node:fs/promises";
|
|
7
7
|
import { hostname } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { FencedError, ownWriteFenced, PdaError } from "./errors.js";
|
|
10
|
+
import { thisBoot } from "./process-table.js";
|
|
10
11
|
export const RUN_JSON = "run.json";
|
|
11
12
|
/** The temporary file every write goes through; renamed over `run.json` once it is fsynced. */
|
|
12
13
|
export const RUN_JSON_TEMP = "run.json.tmp";
|
|
@@ -204,7 +205,8 @@ export async function openClaimDir(root, options = {}) {
|
|
|
204
205
|
await handle.close().catch(() => undefined);
|
|
205
206
|
throw error instanceof FencedError ? error : unmounted(`checking the claim root ${root} failed: ${error.message}`, error);
|
|
206
207
|
}
|
|
207
|
-
const
|
|
208
|
+
const byPath = options.byPath ?? process.platform === "darwin";
|
|
209
|
+
const at = byPath ? root : `/proc/self/fd/${handle.fd}`;
|
|
208
210
|
let closed = false;
|
|
209
211
|
const same = (path, now, claims) => {
|
|
210
212
|
if (now.dev === claims.dev && now.ino === claims.ino)
|
|
@@ -234,10 +236,18 @@ export async function openClaimDir(root, options = {}) {
|
|
|
234
236
|
async assertSame(name) {
|
|
235
237
|
if (closed)
|
|
236
238
|
throw unmounted(`the claim directory of ${root} is closed`);
|
|
237
|
-
|
|
239
|
+
if (byPath) {
|
|
240
|
+
// No second way to name the entry: the root must still be the directory held, and the entry on its device.
|
|
241
|
+
const [dir, entry] = await Promise.all([statOf(root), statOf(join(root, name))]);
|
|
242
|
+
same(root, dir, held);
|
|
243
|
+
if (entry.dev !== held.dev)
|
|
244
|
+
throw unmounted(`${name} in the claim directory is on device ${deviceName(entry.dev)}, not the claim's ${deviceName(held.dev)}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const [named, inDir] = await Promise.all([statOf(join(root, name)), statOf(join(at, name))]);
|
|
238
248
|
if (inDir.dev !== held.dev)
|
|
239
249
|
throw unmounted(`${name} in the claim directory is on device ${deviceName(inDir.dev)}, not the claim's ${deviceName(held.dev)}`);
|
|
240
|
-
same(join(root, name),
|
|
250
|
+
same(join(root, name), named, inDir);
|
|
241
251
|
},
|
|
242
252
|
async close() {
|
|
243
253
|
if (closed)
|
|
@@ -247,18 +257,10 @@ export async function openClaimDir(root, options = {}) {
|
|
|
247
257
|
},
|
|
248
258
|
};
|
|
249
259
|
}
|
|
250
|
-
function bootId() {
|
|
251
|
-
try {
|
|
252
|
-
return readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim() || null;
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
260
|
/**
|
|
259
261
|
* This process as a holder: the host driver's `handle` (default driver `local`, this machine's host name and boot id)
|
|
260
262
|
* with this process's pid and `since`. A handle whose named fields have the wrong type is refused (RunRecordError).
|
|
261
263
|
*/
|
|
262
264
|
export function currentHolder(handle = {}, since = new Date()) {
|
|
263
|
-
return holderOf({ driver: "local", host: hostname(), bootId:
|
|
265
|
+
return holderOf({ driver: "local", host: hostname(), bootId: thisBoot(), ...handle, pid: process.pid, since: since.toISOString() });
|
|
264
266
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -39,8 +39,18 @@ export declare const PROFILE_PRAGMAS: {
|
|
|
39
39
|
readonly mmap_size: 0;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* On macOS, fsync does not flush the drive's cache, so a commit FULL syncs could still be lost to a power cut. These make
|
|
44
|
+
* SQLite sync with F_FULLFSYNC instead, at every commit and every checkpoint.
|
|
45
|
+
*/
|
|
46
|
+
export declare const FULLFSYNC_PRAGMAS: {
|
|
47
|
+
readonly fullfsync: 1;
|
|
48
|
+
readonly checkpoint_fullfsync: 1;
|
|
49
|
+
};
|
|
50
|
+
/** What `profile` must read back on `platform`: its own pragmas, and on macOS `FULLFSYNC_PRAGMAS` too. */
|
|
51
|
+
export declare function profilePragmas(profile: Profile, platform?: NodeJS.Platform): Readonly<Record<string, string | number>>;
|
|
52
|
+
/** Read every pragma of `profile` on `platform` (default this one) back and refuse (StorePragmaError) when any differs. */
|
|
53
|
+
export declare function assertPragmas(database: SqliteExecutor, profile: Profile, platform?: NodeJS.Platform): Promise<void>;
|
|
44
54
|
/**
|
|
45
55
|
* The fenced state of one handle. The first error whose result code is in the I/O, full or not-a-database class marks it
|
|
46
56
|
* fenced, runs `onFenced` once (before the error propagates), and from then on every operation fails with the same
|
|
@@ -84,6 +94,8 @@ export type OpenOptions = {
|
|
|
84
94
|
readonly onFenced?: (error: StoreFencedError) => void;
|
|
85
95
|
/** Wraps the raw database inside the fence, for instrumentation and fault injection. */
|
|
86
96
|
readonly decorate?: (database: SqliteDatabase) => SqliteDatabase;
|
|
97
|
+
/** The platform whose pragmas the store takes (`profilePragmas`); default this one. For tests. */
|
|
98
|
+
readonly platform?: NodeJS.Platform;
|
|
87
99
|
};
|
|
88
100
|
export type ArchilStore = {
|
|
89
101
|
readonly storage: SqliteStorage;
|
package/dist/store.js
CHANGED
|
@@ -64,10 +64,19 @@ export const PROFILE_PRAGMAS = {
|
|
|
64
64
|
exclusive: { locking_mode: "exclusive", journal_mode: "wal", synchronous: 2, mmap_size: 0 },
|
|
65
65
|
shared: { locking_mode: "normal", journal_mode: "wal", synchronous: 2, mmap_size: 0 },
|
|
66
66
|
};
|
|
67
|
-
/**
|
|
68
|
-
|
|
67
|
+
/**
|
|
68
|
+
* On macOS, fsync does not flush the drive's cache, so a commit FULL syncs could still be lost to a power cut. These make
|
|
69
|
+
* SQLite sync with F_FULLFSYNC instead, at every commit and every checkpoint.
|
|
70
|
+
*/
|
|
71
|
+
export const FULLFSYNC_PRAGMAS = { fullfsync: 1, checkpoint_fullfsync: 1 };
|
|
72
|
+
/** What `profile` must read back on `platform`: its own pragmas, and on macOS `FULLFSYNC_PRAGMAS` too. */
|
|
73
|
+
export function profilePragmas(profile, platform = process.platform) {
|
|
74
|
+
return platform === "darwin" ? { ...PROFILE_PRAGMAS[profile], ...FULLFSYNC_PRAGMAS } : PROFILE_PRAGMAS[profile];
|
|
75
|
+
}
|
|
76
|
+
/** Read every pragma of `profile` on `platform` (default this one) back and refuse (StorePragmaError) when any differs. */
|
|
77
|
+
export async function assertPragmas(database, profile, platform = process.platform) {
|
|
69
78
|
const mismatches = [];
|
|
70
|
-
for (const [pragma, expected] of Object.entries(
|
|
79
|
+
for (const [pragma, expected] of Object.entries(profilePragmas(profile, platform))) {
|
|
71
80
|
const row = await database.get(`PRAGMA ${pragma}`);
|
|
72
81
|
const actual = row === undefined ? undefined : Object.values(row)[0];
|
|
73
82
|
if (actual !== expected)
|
|
@@ -203,7 +212,10 @@ export async function openArchilStore(file, profile = "exclusive", options = {})
|
|
|
203
212
|
}
|
|
204
213
|
await database.exec("PRAGMA synchronous = FULL"); // fsync the WAL on every commit
|
|
205
214
|
await database.exec("PRAGMA mmap_size = 0"); // no mmap I/O of the database file on a network filesystem
|
|
206
|
-
|
|
215
|
+
const platform = options.platform ?? process.platform;
|
|
216
|
+
for (const pragma of platform === "darwin" ? Object.keys(FULLFSYNC_PRAGMAS) : [])
|
|
217
|
+
await database.exec(`PRAGMA ${pragma} = ON`);
|
|
218
|
+
await assertPragmas(database, profile, platform);
|
|
207
219
|
const storage = await SqliteStorage.open(database);
|
|
208
220
|
return { storage, database, profile, file };
|
|
209
221
|
}
|
package/dist/supervise.d.ts
CHANGED
|
@@ -241,6 +241,25 @@ export type TokenUser = {
|
|
|
241
241
|
expiresAt?: string;
|
|
242
242
|
createdAt?: string;
|
|
243
243
|
};
|
|
244
|
+
/** What `archilControl` returns: everything `ensureRunning`, `superviseRuns`, `checkHost`, `fork` and the token janitor use. */
|
|
245
|
+
export type ArchilControl = SupervisorControl & CheckControl & {
|
|
246
|
+
listUsers(): Promise<TokenUser[]>;
|
|
247
|
+
};
|
|
248
|
+
/** The two calls of the `disk` client (this package's dependency) that `archilControl` makes. */
|
|
249
|
+
export type DiskClient = Pick<typeof import("disk"), "configure" | "getDisk">;
|
|
250
|
+
/**
|
|
251
|
+
* One Archil disk's control API from an API key, the object `pi-durable-disk supervise` builds for itself: a library
|
|
252
|
+
* caller that composes its own supervisor (with `daytonaHost`, or a driver of its own) passes it as `control`. The S3 and
|
|
253
|
+
* control calls go to the disk the key reaches; `listUsers` reads the disk record again on every call (the list lags by
|
|
254
|
+
* seconds). `configure` is the `disk` client's process-wide setting, so one process controls disks of one key and region.
|
|
255
|
+
* `client` replaces the `disk` client (tests).
|
|
256
|
+
*/
|
|
257
|
+
export declare function archilControl(options: {
|
|
258
|
+
readonly apiKey: string;
|
|
259
|
+
readonly disk: string;
|
|
260
|
+
readonly region: string;
|
|
261
|
+
readonly client?: DiskClient;
|
|
262
|
+
}): Promise<ArchilControl>;
|
|
244
263
|
export interface TokenSweepOptions {
|
|
245
264
|
/** The disk's token users (`getDisk(id).authorizedUsers`; the list lags by seconds). */
|
|
246
265
|
listUsers(): Promise<TokenUser[]>;
|
package/dist/supervise.js
CHANGED
|
@@ -347,6 +347,31 @@ export async function superviseRuns(refs, host, opts) {
|
|
|
347
347
|
}
|
|
348
348
|
return out;
|
|
349
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* One Archil disk's control API from an API key, the object `pi-durable-disk supervise` builds for itself: a library
|
|
352
|
+
* caller that composes its own supervisor (with `daytonaHost`, or a driver of its own) passes it as `control`. The S3 and
|
|
353
|
+
* control calls go to the disk the key reaches; `listUsers` reads the disk record again on every call (the list lags by
|
|
354
|
+
* seconds). `configure` is the `disk` client's process-wide setting, so one process controls disks of one key and region.
|
|
355
|
+
* `client` replaces the `disk` client (tests).
|
|
356
|
+
*/
|
|
357
|
+
export async function archilControl(options) {
|
|
358
|
+
const { configure, getDisk } = options.client ?? (await import("disk"));
|
|
359
|
+
configure({ apiKey: options.apiKey, region: options.region });
|
|
360
|
+
const disk = await getDisk(options.disk);
|
|
361
|
+
return {
|
|
362
|
+
getObject: (key) => disk.getObject(key),
|
|
363
|
+
headObject: (key) => disk.headObject(key),
|
|
364
|
+
putObject: (key, body, putOptions) => disk.putObject(key, body, putOptions),
|
|
365
|
+
addUser: (user) => disk.addUser(user),
|
|
366
|
+
removeUser: (type, identifier) => disk.removeUser(type, identifier),
|
|
367
|
+
listDelegations: () => disk.listDelegations(),
|
|
368
|
+
revokeDelegation: (d) => disk.revokeDelegation(d),
|
|
369
|
+
exec: (command) => disk.exec(command),
|
|
370
|
+
listObjects: (prefix, listOptions) => disk.listObjects(prefix, listOptions),
|
|
371
|
+
deleteObjects: (keys, deleteOptions) => disk.deleteObjects(keys, deleteOptions),
|
|
372
|
+
listUsers: async () => (await getDisk(options.disk)).authorizedUsers ?? [],
|
|
373
|
+
};
|
|
374
|
+
}
|
|
350
375
|
const RELEASED = ["paused", "sleeping", "done", "failed"];
|
|
351
376
|
/**
|
|
352
377
|
* Remove mount-token users that no live mount needs. Removing a token user under a live mount makes that mount lose its
|
package/dist/watchdog.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SweepOptions {
|
|
|
9
9
|
readonly proc?: string;
|
|
10
10
|
readonly sys?: string;
|
|
11
11
|
readonly kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
12
|
+
/** Lists processes with this ps(1) instead of reading `proc`: the default on macOS, which has no /proc, unless `proc` is given. */
|
|
13
|
+
readonly ps?: (file: string, args: readonly string[]) => string;
|
|
12
14
|
}
|
|
13
15
|
/** One look at the shared state at `now`. True once the lease has lapsed, for good; the first look records the age. */
|
|
14
16
|
export declare const lapsed: (shared: BigInt64Array, now: bigint) => boolean;
|
|
@@ -16,7 +18,7 @@ export declare const lapsed: (shared: BigInt64Array, now: bigint) => boolean;
|
|
|
16
18
|
* SIGKILL the instance's commands: the process group of each direct child (pi starts each command as its own group's
|
|
17
19
|
* leader), or the child alone when it shares the instance's group; with `ownCgroup`, every other process of the cgroup.
|
|
18
20
|
* Never the instance or its group. A process this user may not signal (sudo's archil helpers) is skipped. Returns the
|
|
19
|
-
* number of signals sent. `stat` lines are `pid (comm) state ppid pgrp ...`, read after the last `)` since comm may hold
|
|
21
|
+
* 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
|
|
20
22
|
* spaces and parentheses.
|
|
21
23
|
*/
|
|
22
24
|
export declare const sweep: (instance: number, options?: SweepOptions) => number;
|
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,
|
|
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
|
-
|
|
53
|
-
|
|
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
|
|
57
|
-
const
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parcha/pi-durable-disk",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
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
|
},
|
|
@@ -58,12 +62,13 @@
|
|
|
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 <1.
|
|
66
|
-
"@earendil-works/pi-durable": ">=1.0.4 <1.
|
|
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"
|