@parcha/pi-durable-disk 0.1.0-beta.19 → 0.1.0-beta.21
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 +9 -0
- package/dist/claim.d.ts +5 -5
- package/dist/claim.js +7 -7
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +5 -3
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +4 -0
- package/dist/hosts/daytona.js +1 -1
- package/dist/hosts/local-host.js +1 -5
- package/dist/place.d.ts +0 -1
- package/dist/run.d.ts +2 -5
- package/dist/run.js +2 -5
- package/dist/status.js +21 -10
- package/dist/watchdog.d.ts +1 -2
- package/dist/watchdog.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.0-beta.21 (2026-10-21)
|
|
6
|
+
|
|
7
|
+
- `supervise --no-restart` is in the usage text and the reference: it turns off the local driver's restart on failure (`localHost({ restart: false })`), so an instance that exits stays down until the supervisor's next pass.
|
|
8
|
+
|
|
9
|
+
## 0.1.0-beta.20 (2026-10-20)
|
|
10
|
+
|
|
11
|
+
- A start mark keeper whose rewrite fails because the run's directory moved after its check now warns that the directory is no longer the one it started on, and stops. Before, the `PDA_START_MARK` warning said the rewrite failed (ENOENT or ENOTDIR). Nothing is written in either case; only the stated reason changes.
|
|
12
|
+
- The local-disk env conformance cases watch with a 100 ms poll, so pi's fixed 3 s wait for a report keeps seconds of slack on a loaded runner; the product's 1 s watch default is unchanged.
|
|
13
|
+
|
|
5
14
|
## 0.1.0-beta.19 (2026-10-20)
|
|
6
15
|
|
|
7
16
|
- An instance whose app released the run ends by itself: `pi-durable-disk run --app` exits 0 once `onOpen` has returned and the run is released, so the unit does not stay up with no mount.
|
package/dist/claim.d.ts
CHANGED
|
@@ -234,6 +234,11 @@ export declare function writtenSince(dir: string, since: number, limit?: number)
|
|
|
234
234
|
fresh: string[];
|
|
235
235
|
unchecked: string | null;
|
|
236
236
|
}>;
|
|
237
|
+
/**
|
|
238
|
+
* The token an instance holds when it was given none (`--token-stdin` read nothing): enough to reuse its live mount, never
|
|
239
|
+
* to mount. A restart in place gets it from a driver that hands the token to the first incarnation only (localHost).
|
|
240
|
+
*/
|
|
241
|
+
export declare const NO_TOKEN = "none";
|
|
237
242
|
/**
|
|
238
243
|
* Take the run's claim: mount `<disk>:/runs/<id>` exclusively at `<mountRoot>/runs/<id>` and verify it. A live mount of
|
|
239
244
|
* the same run already there is kept (in-place restart); a dead one is cleaned first. With no mount there, a stale archil
|
|
@@ -242,11 +247,6 @@ export declare function writtenSince(dir: string, since: number, limit?: number)
|
|
|
242
247
|
* verifying write is refused, MOUNTPOINT_NOT_EMPTY when the mountpoint cannot be emptied or archil still finds it not
|
|
243
248
|
* empty, MOUNT_FAILED (exit 1) for a daemon of this host that is still running there.
|
|
244
249
|
*/
|
|
245
|
-
/**
|
|
246
|
-
* The token an instance holds when it was given none (`--token-stdin` read nothing): enough to reuse its live mount, never
|
|
247
|
-
* to mount. A restart in place gets it from a driver that hands the token to the first incarnation only (localHost).
|
|
248
|
-
*/
|
|
249
|
-
export declare const NO_TOKEN = "none";
|
|
250
250
|
export declare function acquire(opts: AcquireOptions): Promise<Claim>;
|
|
251
251
|
/** One exclusive mount of a path, as `withClaim` holds it: its own mountpoint, never reused. */
|
|
252
252
|
export interface ExclusiveMount {
|
package/dist/claim.js
CHANGED
|
@@ -7,7 +7,7 @@ import { lstat, mkdir, open, readdir, readFile, rmdir, stat } from "node:fs/prom
|
|
|
7
7
|
import { basename, isAbsolute, join } from "node:path";
|
|
8
8
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { ClaimError, FencedError, HeldError, ownWriteFenced } from "./errors.js";
|
|
10
|
+
import { ClaimError, EXIT_DATAERR, FencedError, HeldError, ownWriteFenced } from "./errors.js";
|
|
11
11
|
export const DEFAULT_MOUNT_ROOT = "/mnt/archil";
|
|
12
12
|
/** This package's `bin/archil-scoped`. A deployment installs a root-owned copy and passes its path as `archil`. */
|
|
13
13
|
export const ARCHIL_SCOPED = fileURLToPath(new URL("../bin/archil-scoped", import.meta.url));
|
|
@@ -620,7 +620,7 @@ export async function writtenSince(dir, since, limit = STRAY_CHECK_LIMIT) {
|
|
|
620
620
|
return { fresh, unchecked: null };
|
|
621
621
|
}
|
|
622
622
|
/** The exit of a start refused for the app's own writes before its claim (EX_DATAERR): a restart would write them again. */
|
|
623
|
-
const EXIT_WRITTEN_BEFORE_MOUNT =
|
|
623
|
+
const EXIT_WRITTEN_BEFORE_MOUNT = EXIT_DATAERR;
|
|
624
624
|
/** How many entries a directory holds; 0 when it cannot be read. */
|
|
625
625
|
const entries = (dir) => readdir(dir).then((names) => names.length, () => 0);
|
|
626
626
|
/**
|
|
@@ -701,6 +701,11 @@ async function verifyTarget(host, path, mountpoint, probe) {
|
|
|
701
701
|
throw ownWriteFenced(join(mountpoint, CLAIM_PROBE), err);
|
|
702
702
|
}
|
|
703
703
|
}
|
|
704
|
+
/**
|
|
705
|
+
* The token an instance holds when it was given none (`--token-stdin` read nothing): enough to reuse its live mount, never
|
|
706
|
+
* to mount. A restart in place gets it from a driver that hands the token to the first incarnation only (localHost).
|
|
707
|
+
*/
|
|
708
|
+
export const NO_TOKEN = "none";
|
|
704
709
|
/**
|
|
705
710
|
* Take the run's claim: mount `<disk>:/runs/<id>` exclusively at `<mountRoot>/runs/<id>` and verify it. A live mount of
|
|
706
711
|
* the same run already there is kept (in-place restart); a dead one is cleaned first. With no mount there, a stale archil
|
|
@@ -709,11 +714,6 @@ async function verifyTarget(host, path, mountpoint, probe) {
|
|
|
709
714
|
* verifying write is refused, MOUNTPOINT_NOT_EMPTY when the mountpoint cannot be emptied or archil still finds it not
|
|
710
715
|
* empty, MOUNT_FAILED (exit 1) for a daemon of this host that is still running there.
|
|
711
716
|
*/
|
|
712
|
-
/**
|
|
713
|
-
* The token an instance holds when it was given none (`--token-stdin` read nothing): enough to reuse its live mount, never
|
|
714
|
-
* to mount. A restart in place gets it from a driver that hands the token to the first incarnation only (localHost).
|
|
715
|
-
*/
|
|
716
|
-
export const NO_TOKEN = "none";
|
|
717
717
|
export async function acquire(opts) {
|
|
718
718
|
const { ref, token } = opts;
|
|
719
719
|
const path = runPath(ref.id);
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { type Parking } from "./park.js";
|
|
|
3
3
|
import { type RunServer } from "./serve.js";
|
|
4
4
|
import { type ArchilControl, type DiskClient } from "./supervise.js";
|
|
5
5
|
import { type DurableRun } from "./run.js";
|
|
6
|
-
/** "30s", "500ms", "2m", "1h", or plain milliseconds. */
|
|
7
6
|
/**
|
|
8
7
|
* Whether the lease watchdog may kill every other process in the instance's cgroup: only inside its own systemd unit,
|
|
9
8
|
* whose cgroup holds nothing but the instance and its commands. Elsewhere the cgroup may be shared (a login session, an
|
|
@@ -21,6 +20,7 @@ export declare function watchdogOwnsCgroup(holder: Record<string, unknown>, opti
|
|
|
21
20
|
owns: boolean;
|
|
22
21
|
warning?: string;
|
|
23
22
|
};
|
|
23
|
+
/** "30s", "500ms", "2m", "1h", or plain milliseconds. */
|
|
24
24
|
export declare function parseDuration(text: string): number;
|
|
25
25
|
/** `--run-as NAME|UID[:GID]`: the user the agent's commands run as, with its group and home from /etc/passwd when listed. */
|
|
26
26
|
export declare function runUser(spec: string, passwd?: string): {
|
package/dist/cli.js
CHANGED
|
@@ -42,7 +42,7 @@ const USAGE = `usage:
|
|
|
42
42
|
[--user U] [--group G] [--app MODULE] [--run-arg ARG ...] [--lease-expiry 90s] [--stonith-timeout 30s]
|
|
43
43
|
[--start-grace 90s] [--start-backoff-max 10m] [--stop-timeout 30s] [--token-ttl 24h] [--token-prefix P] [--demand] [--create] [--env K=V ...] [--log-dir DIR]
|
|
44
44
|
[--archil BIN] [--control-timeout 10s] [--sweep-tokens] [--token-grace 15m]
|
|
45
|
-
[--park-threshold 60s] [--api-key-env NAME]
|
|
45
|
+
[--park-threshold 60s] [--no-restart] [--api-key-env NAME]
|
|
46
46
|
with --host docker: --image IMAGE [--app-root DIR] [--fleet F] [--name-prefix P] [--run-as USER]
|
|
47
47
|
[--docker-arg ARG ...] [--apparmor auto|PROFILE|none] [--docker BIN]
|
|
48
48
|
pi-durable-disk supervise --check --disk D --region R [--mount-root DIR] [--user U] [--group G] [--check-id-prefix P]
|
|
@@ -51,7 +51,6 @@ const USAGE = `usage:
|
|
|
51
51
|
pi-durable-disk release --id ID [--mount-root DIR]
|
|
52
52
|
pi-durable-disk fork --disk D --region R --id ID --new-id NEW [--mount-root DIR] [--archil BIN] [--token-prefix P]
|
|
53
53
|
[--api-key-env NAME]`;
|
|
54
|
-
/** "30s", "500ms", "2m", "1h", or plain milliseconds. */
|
|
55
54
|
/**
|
|
56
55
|
* Whether the lease watchdog may kill every other process in the instance's cgroup: only inside its own systemd unit,
|
|
57
56
|
* whose cgroup holds nothing but the instance and its commands. Elsewhere the cgroup may be shared (a login session, an
|
|
@@ -90,6 +89,7 @@ export function watchdogOwnsCgroup(holder, options = {}) {
|
|
|
90
89
|
return refuse(`the parent shares cgroup ${own}, so this process only inherited the handle`);
|
|
91
90
|
return { owns: true };
|
|
92
91
|
}
|
|
92
|
+
/** "30s", "500ms", "2m", "1h", or plain milliseconds. */
|
|
93
93
|
export function parseDuration(text) {
|
|
94
94
|
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(text.trim());
|
|
95
95
|
if (!m)
|
|
@@ -219,7 +219,9 @@ async function runInstance(values) {
|
|
|
219
219
|
throw new UsageError(`--serve-host ${serveHost} is not loopback: name a --serve-token-file`);
|
|
220
220
|
const serveToken = tokenFile === undefined ? undefined : readServeToken(tokenFile);
|
|
221
221
|
const mountRoot = str(values["mount-root"], DEFAULT_MOUNT_ROOT);
|
|
222
|
-
|
|
222
|
+
// On a disk the id names the run's directory: an unsafe one stops the instance here (INVALID_ARGUMENT), before it serves.
|
|
223
|
+
if (dir === undefined)
|
|
224
|
+
runPath(ref.id);
|
|
223
225
|
const log = (event, extra = {}) => process.stderr.write(`${JSON.stringify({ at: new Date().toISOString(), event, run: ref.id, ...extra })}\n`);
|
|
224
226
|
// The app's factory runs once the claim is held (`afterClaim`), so `ctx.root` is the live directory whenever app code
|
|
225
227
|
// runs; what it returns opens the Harness. Serve starts before the open, with the root options the factory fills in.
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** EX_DATAERR: the run's data refuses to open (a store behind its seal, an unreadable `run.json`); a restart cannot help. */
|
|
2
|
+
export declare const EXIT_DATAERR = 65;
|
|
3
|
+
/** EX_SOFTWARE: this package cannot read the head of pi's store (pi's schema moved); a restart cannot help. */
|
|
4
|
+
export declare const EXIT_SOFTWARE = 70;
|
|
1
5
|
/** EX_TEMPFAIL: the claim was revoked or its mount failed; nothing this process writes can become durable. */
|
|
2
6
|
export declare const EXIT_FENCED = 75;
|
|
3
7
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
// another); both are terminal for the host's process supervisor, which must not restart into a run it lost.
|
|
3
3
|
// A module's own errors extend FencedError or HeldError (overriding `code`) so one `instanceof` or `exitCodeFor` covers
|
|
4
4
|
// them all, e.g. `class StoreFencedError extends FencedError` with `{ code: "STORE_FENCED" }`.
|
|
5
|
+
/** EX_DATAERR: the run's data refuses to open (a store behind its seal, an unreadable `run.json`); a restart cannot help. */
|
|
6
|
+
export const EXIT_DATAERR = 65;
|
|
7
|
+
/** EX_SOFTWARE: this package cannot read the head of pi's store (pi's schema moved); a restart cannot help. */
|
|
8
|
+
export const EXIT_SOFTWARE = 70;
|
|
5
9
|
/** EX_TEMPFAIL: the claim was revoked or its mount failed; nothing this process writes can become durable. */
|
|
6
10
|
export const EXIT_FENCED = 75;
|
|
7
11
|
/**
|
package/dist/hosts/daytona.js
CHANGED
|
@@ -182,7 +182,6 @@ export function sandboxName(prefix, runId, now) {
|
|
|
182
182
|
const stamp = `${now.toString(36)}${Math.floor(Math.random() * 1296).toString(36).padStart(2, "0")}`;
|
|
183
183
|
return `${prefix}${slug}-${stamp}`.slice(0, 63);
|
|
184
184
|
}
|
|
185
|
-
/** The last line of a command's output that parses as JSON, or null. */
|
|
186
185
|
/**
|
|
187
186
|
* What the launcher printed, for a start it gave no reason for: the last 4 KB, or that it printed nothing. It prints only
|
|
188
187
|
* its state; the mount token is in a root-only file it reads, never in its output.
|
|
@@ -191,6 +190,7 @@ function launcherOutput(text) {
|
|
|
191
190
|
const out = text.trim();
|
|
192
191
|
return out ? `the launcher printed: ${out.slice(-4096)}` : "the launcher printed nothing and exited 0";
|
|
193
192
|
}
|
|
193
|
+
/** The last line of a command's output that parses as JSON, or null. */
|
|
194
194
|
function lastJson(text) {
|
|
195
195
|
for (const line of text.trim().split("\n").reverse()) {
|
|
196
196
|
try {
|
package/dist/hosts/local-host.js
CHANGED
|
@@ -17,7 +17,7 @@ import { hostname } from "node:os";
|
|
|
17
17
|
import { extname, join } from "node:path";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
import { ARCHIL_SCOPED, DEAD_CONNECTION, DEFAULT_MOUNT_ROOT, runPath } from "../claim.js";
|
|
20
|
-
import { EXIT_FENCED, EXIT_HELD, PdaError } from "../errors.js";
|
|
20
|
+
import { EXIT_DATAERR, EXIT_FENCED, EXIT_HELD, EXIT_SOFTWARE, PdaError } from "../errors.js";
|
|
21
21
|
import { LOCAL_PARK_THRESHOLD_MS } from "../park.js";
|
|
22
22
|
import { processTable, thisBoot } from "../process-table.js";
|
|
23
23
|
import { attemptEnv } from "./attempt-env.js";
|
|
@@ -44,10 +44,6 @@ const FUSERMOUNT = "/usr/bin/fusermount";
|
|
|
44
44
|
const UMOUNT = "/usr/bin/umount";
|
|
45
45
|
/** How long a stat of the mountpoint may take before its daemon counts as alive but stuck (never removed). */
|
|
46
46
|
const STAT_TIMEOUT_MS = 3_000;
|
|
47
|
-
/** EX_DATAERR: a store behind its seal or a bad run.json (run.ts names it EXIT_DATAERR). */
|
|
48
|
-
const EXIT_DATAERR = 65;
|
|
49
|
-
/** EX_SOFTWARE: the store's head is unreadable (pi's schema moved); run.json says failed, STORE_HEAD_UNREADABLE. */
|
|
50
|
-
const EXIT_SOFTWARE = 70;
|
|
51
47
|
/** Exits a restart cannot fix: 65 data error, 70 store head unreadable, 75 fenced, 76 held. */
|
|
52
48
|
export const TERMINAL_EXITS = [EXIT_DATAERR, EXIT_SOFTWARE, EXIT_FENCED, EXIT_HELD];
|
|
53
49
|
// The package's own CLI, with the extension this module has: `.ts` from a checkout, `.js` once built.
|
package/dist/place.d.ts
CHANGED
|
@@ -43,7 +43,6 @@ export type RunnerOrder = {
|
|
|
43
43
|
env: Record<string, string>;
|
|
44
44
|
stdin?: string;
|
|
45
45
|
};
|
|
46
|
-
/** What the place reads of the run, the way it reads it: `run.json`, and on a disk the run's delegations. */
|
|
47
46
|
/**
|
|
48
47
|
* What the place reads of the run, the way it reads it: `run.json`; on a disk, the run's delegations; and the boxes the
|
|
49
48
|
* place made for its runners, each as a receipt (`HostBox`: provider, kind, from, to, list price per second and where
|
package/dist/run.d.ts
CHANGED
|
@@ -3,17 +3,14 @@ import { Harness } from "@earendil-works/pi-durable";
|
|
|
3
3
|
import type { HarnessOptions, Seq, Storage, ToolRegistration } from "@earendil-works/pi-durable";
|
|
4
4
|
import type { SqliteDatabase, SqliteExecutor } from "@earendil-works/pi-durable/storage/sqlite";
|
|
5
5
|
import { type AcquireOptions, type ArchilHost, type Claim, type RunRef } from "./claim.js";
|
|
6
|
-
import { FencedError, PdaError } from "./errors.js";
|
|
6
|
+
import { EXIT_DATAERR, EXIT_SOFTWARE, FencedError, PdaError } from "./errors.js";
|
|
7
7
|
import { type ArchilStore, type Profile } from "./store.js";
|
|
8
8
|
import { type ClaimDir, type PersistRecord, type RunRecord, type RunStatus } from "./status.js";
|
|
9
9
|
/** The same-host owner lock under the run's root. */
|
|
10
10
|
export declare const OWNER_LOCK = "owner.lock";
|
|
11
11
|
/** pi's store under the claim's `store/` directory. */
|
|
12
12
|
export declare const STORE_FILE = "run.sqlite";
|
|
13
|
-
|
|
14
|
-
export declare const EXIT_DATAERR = 65;
|
|
15
|
-
/** EX_SOFTWARE: this package cannot read the head of pi's store (pi's schema moved); a restart cannot help. */
|
|
16
|
-
export declare const EXIT_SOFTWARE = 70;
|
|
13
|
+
export { EXIT_DATAERR, EXIT_SOFTWARE };
|
|
17
14
|
export type RunErrorCode = "INVALID_ARGUMENT" | "STORE_HEAD_UNREADABLE" | "RUN_RELEASED";
|
|
18
15
|
/**
|
|
19
16
|
* Bad options, a call on a released run, or a store whose head cannot be read (STORE_HEAD_UNREADABLE: the run is marked
|
package/dist/run.js
CHANGED
|
@@ -12,7 +12,7 @@ import { Harness } from "@earendil-works/pi-durable";
|
|
|
12
12
|
import { acquire } from "./claim.js";
|
|
13
13
|
import { reapOrRefuse } from "./commands.js";
|
|
14
14
|
import { archilEnv } from "./env.js";
|
|
15
|
-
import { EXIT_FENCED, FencedError, HeldError, ownWriteFenced, PdaError } from "./errors.js";
|
|
15
|
+
import { EXIT_DATAERR, EXIT_FENCED, EXIT_SOFTWARE, FencedError, HeldError, ownWriteFenced, PdaError } from "./errors.js";
|
|
16
16
|
import { openArchilStore } from "./store.js";
|
|
17
17
|
import { LeaseWatchdog } from "./watchdog.js";
|
|
18
18
|
import { currentHolder, DEFAULT_HEARTBEAT_MS, DEFAULT_LEASE_EXPIRY_MS, DEFAULT_LEASE_MARGIN_MS, keepStartMark, openClaimDir, persistRecord, readRunRecord, RUN_JSON, RunRecordError, writeRunRecord, } from "./status.js";
|
|
@@ -20,10 +20,7 @@ import { currentHolder, DEFAULT_HEARTBEAT_MS, DEFAULT_LEASE_EXPIRY_MS, DEFAULT_L
|
|
|
20
20
|
export const OWNER_LOCK = "owner.lock";
|
|
21
21
|
/** pi's store under the claim's `store/` directory. */
|
|
22
22
|
export const STORE_FILE = "run.sqlite";
|
|
23
|
-
|
|
24
|
-
export const EXIT_DATAERR = 65;
|
|
25
|
-
/** EX_SOFTWARE: this package cannot read the head of pi's store (pi's schema moved); a restart cannot help. */
|
|
26
|
-
export const EXIT_SOFTWARE = 70;
|
|
23
|
+
export { EXIT_DATAERR, EXIT_SOFTWARE };
|
|
27
24
|
/**
|
|
28
25
|
* Bad options, a call on a released run, or a store whose head cannot be read (STORE_HEAD_UNREADABLE: the run is marked
|
|
29
26
|
* `failed` and released, exit 70).
|
package/dist/status.js
CHANGED
|
@@ -7,7 +7,7 @@ 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 { Worker } from "node:worker_threads";
|
|
10
|
-
import { FencedError, ownWriteFenced, PdaError } from "./errors.js";
|
|
10
|
+
import { EXIT_DATAERR, FencedError, ownWriteFenced, PdaError } from "./errors.js";
|
|
11
11
|
import { fdPath, hold } from "./held-fd.js";
|
|
12
12
|
import { thisBoot } from "./process-table.js";
|
|
13
13
|
export const RUN_JSON = "run.json";
|
|
@@ -23,7 +23,7 @@ export const RUN_STATUSES = ["running", "sleeping", "paused", "done", "failed"];
|
|
|
23
23
|
/** `run.json` is missing a field, has one of the wrong type, or is not JSON. Exit 65 (EX_DATAERR): a restart cannot fix it. */
|
|
24
24
|
export class RunRecordError extends PdaError {
|
|
25
25
|
constructor(message, options = {}) {
|
|
26
|
-
super("RUN_JSON_INVALID", message, { cause: options.cause, exitCode:
|
|
26
|
+
super("RUN_JSON_INVALID", message, { cause: options.cause, exitCode: EXIT_DATAERR });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const isInstant = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
|
|
@@ -184,8 +184,9 @@ export function parseStartMark(text) {
|
|
|
184
184
|
// watchdog's is (src/watchdog.ts says why). One refresh at a time, all synchronous: a stop (the message) is handled only
|
|
185
185
|
// between two refreshes, so none is still writing once the thread has exited. It writes only while `dir` is still the
|
|
186
186
|
// directory it first saw (its device and inode, read on this thread, so a stalled mount never holds the caller's): a path
|
|
187
|
-
// that names another (a descriptor number reused) or none is never written, and the keeper stops.
|
|
188
|
-
//
|
|
187
|
+
// that names another (a descriptor number reused) or none is never written, and the keeper stops. A rewrite that fails is
|
|
188
|
+
// checked the same way, so a directory that moved between the check and the write is reported as moved, not as a failed
|
|
189
|
+
// write. Its first failure goes to the main thread, once.
|
|
189
190
|
const KEEPER_SOURCE = `"use strict";
|
|
190
191
|
const fs = require("node:fs");
|
|
191
192
|
const path = require("node:path");
|
|
@@ -200,19 +201,23 @@ const report = (message) => {
|
|
|
200
201
|
reported = true;
|
|
201
202
|
parentPort.postMessage(message);
|
|
202
203
|
};
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
// Why dir is no longer the directory the keeper first saw, or null while it is (the first call records it).
|
|
205
|
+
function moved() {
|
|
205
206
|
let now;
|
|
206
207
|
try {
|
|
207
208
|
now = fs.statSync(dir, { bigint: true });
|
|
208
209
|
} catch (error) {
|
|
209
|
-
|
|
210
|
-
return report("its directory cannot be reached (" + (error.code || error.message) + "), so it is no longer the directory the keeper started on");
|
|
210
|
+
return "its directory cannot be reached (" + (error.code || error.message) + "), so it is no longer the directory the keeper started on";
|
|
211
211
|
}
|
|
212
212
|
first ??= { dev: now.dev, ino: now.ino };
|
|
213
|
-
|
|
213
|
+
return now.dev === first.dev && now.ino === first.ino ? null : "its directory is no longer the directory the keeper started on";
|
|
214
|
+
}
|
|
215
|
+
function refresh() {
|
|
216
|
+
if (stopped) return;
|
|
217
|
+
const away = moved();
|
|
218
|
+
if (away) {
|
|
214
219
|
stopped = true;
|
|
215
|
-
return report(
|
|
220
|
+
return report(away);
|
|
216
221
|
}
|
|
217
222
|
let text;
|
|
218
223
|
try {
|
|
@@ -245,6 +250,12 @@ function refresh() {
|
|
|
245
250
|
fs.closeSync(handle);
|
|
246
251
|
}
|
|
247
252
|
} catch (error) {
|
|
253
|
+
// The directory may have moved after the check above: then that is the reason, and the keeper stops.
|
|
254
|
+
const away = moved();
|
|
255
|
+
if (away) {
|
|
256
|
+
stopped = true;
|
|
257
|
+
return report(away);
|
|
258
|
+
}
|
|
248
259
|
report("rewriting it failed (" + (error.code || error.message) + ")");
|
|
249
260
|
}
|
|
250
261
|
}
|
package/dist/watchdog.d.ts
CHANGED
|
@@ -18,8 +18,7 @@ export declare const lapsed: (shared: BigInt64Array, now: bigint) => boolean;
|
|
|
18
18
|
* SIGKILL the instance's commands: the process group of each direct child (pi starts each command as its own group's
|
|
19
19
|
* leader), or the child alone when it shares the instance's group; with `ownCgroup`, every other process of the cgroup.
|
|
20
20
|
* Never the instance or its group. A process this user may not signal (sudo's archil helpers) is skipped. Returns the
|
|
21
|
-
* number of signals sent. Where there is no /proc (macOS) the processes come from one ps listing, and there is no cgroup.
|
|
22
|
-
* spaces and parentheses.
|
|
21
|
+
* number of signals sent. Where there is no /proc (macOS) the processes come from one ps listing, and there is no cgroup.
|
|
23
22
|
*/
|
|
24
23
|
export declare const sweep: (instance: number, options?: SweepOptions) => number;
|
|
25
24
|
/** The main thread's handle on the watchdog thread. */
|
package/dist/watchdog.js
CHANGED
|
@@ -30,6 +30,8 @@ function lapsed(shared, now) {
|
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// A stat line is "pid (comm) state ppid pgrp ...": its fields are read after the last ")", since comm may hold spaces and
|
|
34
|
+
// parentheses.
|
|
33
35
|
function stat(proc, pid) {
|
|
34
36
|
try {
|
|
35
37
|
const text = fs.readFileSync(path.join(proc, String(pid), "stat"), "utf8");
|
|
@@ -126,8 +128,7 @@ export const lapsed = compiled.lapsed;
|
|
|
126
128
|
* SIGKILL the instance's commands: the process group of each direct child (pi starts each command as its own group's
|
|
127
129
|
* leader), or the child alone when it shares the instance's group; with `ownCgroup`, every other process of the cgroup.
|
|
128
130
|
* Never the instance or its group. A process this user may not signal (sudo's archil helpers) is skipped. Returns the
|
|
129
|
-
* number of signals sent. Where there is no /proc (macOS) the processes come from one ps listing, and there is no cgroup.
|
|
130
|
-
* spaces and parentheses.
|
|
131
|
+
* number of signals sent. Where there is no /proc (macOS) the processes come from one ps listing, and there is no cgroup.
|
|
131
132
|
*/
|
|
132
133
|
export const sweep = compiled.sweep;
|
|
133
134
|
/** The main thread's handle on the watchdog thread. */
|
package/package.json
CHANGED