stitchkit 0.103.9 → 0.103.10
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 +8 -0
- package/dist/agent-runtime/store-conformance-lifecycle.d.ts +10 -10
- package/dist/entrypoints/agent-runtime/sandbox.js +1 -1
- package/dist/entrypoints/application/diagnostic-journal.js +2 -1
- package/dist/entrypoints/files.js +8 -4
- package/dist/entrypoints/process.d.ts +2 -1
- package/dist/entrypoints/process.d.ts.map +1 -1
- package/dist/entrypoints/process.js +9 -2
- package/dist/{index-eeqmg7qm.js → index-6m6a9ty4.js} +92 -9
- package/dist/index-ga597cvy.js +184 -0
- package/dist/index-nybn2err.js +213 -0
- package/dist/internal/exclusive-lock.d.ts +2 -1
- package/dist/internal/exclusive-lock.d.ts.map +1 -1
- package/dist/internal/process-instance.d.ts +46 -9
- package/dist/internal/process-instance.d.ts.map +1 -1
- package/dist/internal/with-exclusive-lock.d.ts +4 -3
- package/dist/internal/with-exclusive-lock.d.ts.map +1 -1
- package/dist/process/command-owner.d.ts.map +1 -1
- package/dist/process/contract.d.ts +10 -0
- package/dist/process/contract.d.ts.map +1 -1
- package/dist/process/group.d.ts +1 -1
- package/dist/process/group.d.ts.map +1 -1
- package/dist/process/leader-settlement.d.ts +4 -0
- package/dist/process/leader-settlement.d.ts.map +1 -0
- package/llms/files.txt +1 -1
- package/llms/process.txt +76 -1
- package/llms-full.txt +77 -2
- package/llms.txt +2 -2
- package/native/darwin-arm64.node +0 -0
- package/native/darwin-x64.node +0 -0
- package/package.json +1 -1
- package/dist/index-7kxw1awn.js +0 -328
package/llms-full.txt
CHANGED
|
@@ -8031,12 +8031,16 @@ description: Native IO для Bun и Node, с явно выбранной durabi
|
|
|
8031
8031
|
type: guide
|
|
8032
8032
|
status: active
|
|
8033
8033
|
created: 2026-10-01 20:44 +07:00
|
|
8034
|
-
updated: 2026-10-
|
|
8034
|
+
updated: 2026-10-02 00:37 +07:00
|
|
8035
8035
|
participants:
|
|
8036
8036
|
- role: authored
|
|
8037
8037
|
harness: Codex
|
|
8038
8038
|
model: GPT-6
|
|
8039
8039
|
at: 2026-10-01 20:44 +07:00
|
|
8040
|
+
- role: implemented
|
|
8041
|
+
harness: Codex
|
|
8042
|
+
model: GPT-6
|
|
8043
|
+
at: 2026-10-02 00:08 +07:00
|
|
8040
8044
|
---
|
|
8041
8045
|
|
|
8042
8046
|
# Native IO
|
|
@@ -8152,6 +8156,10 @@ callbacks owner не запускает. Normal completion ждёт всех byt
|
|
|
8152
8156
|
|
|
8153
8157
|
Cleanup: POSIX process group, TERM, `killGraceMs` (default 100, maximum 10 000), затем KILL;
|
|
8154
8158
|
`cleanupTimeoutMs` (default 2000, maximum 30 000) ограничивает ожидание close после сигналов.
|
|
8159
|
+
На Darwin временный `EPERM` при сигнале группе может означать zombies, ещё не убранные
|
|
8160
|
+
OS reaper. Повтор ограничен `cleanupTimeoutMs`; успех требует доставленного сигнала или
|
|
8161
|
+
`ESRCH`, постоянный отказ сохраняется в cause. Закрытие каждого owned pipe и выход leader
|
|
8162
|
+
наблюдаются отдельно, включая отсутствие общего `child.close` при teardown.
|
|
8155
8163
|
Сохраняется caller/sink причина; failure cleanup — `NativeCommandError` с `COMMAND_CLEANUP` и
|
|
8156
8164
|
AggregateError cause. Даже вышедший parent не исключает helper, удерживающий pipe. Windows
|
|
8157
8165
|
отказывает с `COMMAND_UNAVAILABLE`; потомок, самостоятельно покинувший группу через setsid,
|
|
@@ -8166,6 +8174,68 @@ shell policy, command admission, логирование, redaction, bytes→text
|
|
|
8166
8174
|
Длительные signal-only команды остаются streaming; bounded tar/capture получают свои явные limits.
|
|
8167
8175
|
Импорт leaf не делает установку всего npm package маленькой; выбирайте dependency после измерения
|
|
8168
8176
|
packed размера и dependency closure.
|
|
8177
|
+
## Resource-scoped launchers and shared owners
|
|
8178
|
+
|
|
8179
|
+
`onLeaderSettled(event, signal)` observes leader exit before inherited stdout/stderr pipes
|
|
8180
|
+
close. A resource owner can stop its external scope there, allowing pipe drain to complete.
|
|
8181
|
+
The callback runs once: `event.kind === 'exit'` carries observed `exitCode`/`signal`; a terminal
|
|
8182
|
+
failure uses `kind: 'error'` and preserves its cause. Unavailable executables, output sink
|
|
8183
|
+
failure and caller cancellation settle through the same owner. `cleanupTimeoutMs` bounds
|
|
8184
|
+
the callback after settlement starts; it never becomes a deadline for signal-only execution.
|
|
8185
|
+
Honor the settlement signal; arbitrary user promises cannot be forcibly cancelled.
|
|
8186
|
+
|
|
8187
|
+
```ts
|
|
8188
|
+
await runNativeCommand({
|
|
8189
|
+
executable: launcher,
|
|
8190
|
+
args,
|
|
8191
|
+
signal: scope.signal,
|
|
8192
|
+
onOutput: (bytes, channel, signal) => scope.write(bytes, channel, signal),
|
|
8193
|
+
onLeaderSettled: (event, signal) => scope.settle(event, signal),
|
|
8194
|
+
});
|
|
8195
|
+
```
|
|
8196
|
+
|
|
8197
|
+
`observeProcessInstance(pid)` returns `observed` with a `ProcessInstance`, or `unavailable`
|
|
8198
|
+
with its cause. `probeProcessOwner(pid, recorded)` compares that evidence on the machine the
|
|
8199
|
+
caller established: `matched`, `different-boot`, `reused-pid`, `pid-gone`, `legacy` or
|
|
8200
|
+
`unavailable`. Missing/partial/denied evidence is not death. Supervisor tree fencing remains
|
|
8201
|
+
the caller's job; a PID lifetime is not proof that its whole workload stopped.
|
|
8202
|
+
|
|
8203
|
+
An exclusive lock applies exact requested permissions through its creating descriptor,
|
|
8204
|
+
independent of umask. Default `0600` stays private. Shared `0640` requires a common group and
|
|
8205
|
+
traversable directories; another UID also needs directory write permission to reclaim a
|
|
8206
|
+
proven-dead owner's file. Set `ownerlessGraceMs: null` to refuse every age-only reclaim,
|
|
8207
|
+
including ownerless reclaim guards. The default remains 5000 ms for existing clients.
|
|
8208
|
+
Восстановление stale reclaim guard использует тот же lock owner и следующий guard:
|
|
8209
|
+
проверка inode и удаление выполняются под ним. Конкурирующий writer не обходит guard,
|
|
8210
|
+
пока прежнее удаление приостановлено; live и unknown lifetime evidence остаются отказом.
|
|
8211
|
+
|
|
8212
|
+
## Libraries with a Zod-only runtime
|
|
8213
|
+
|
|
8214
|
+
Public leaves keep optional peers out of their import closure. Installing `stitchkit` still
|
|
8215
|
+
installs the complete package and ky; selective imports alone do not avoid that installation.
|
|
8216
|
+
A library can use Stitchkit as a build dependency, bundle its selected public native imports,
|
|
8217
|
+
keep Zod external, and publish the resulting implementation with its library artifact.
|
|
8218
|
+
|
|
8219
|
+
```ts
|
|
8220
|
+
// The library's build entry; all mechanisms remain owned by Stitchkit.
|
|
8221
|
+
export { canonicalJson } from 'stitchkit/primitives';
|
|
8222
|
+
export { writeFileAtomic, withExclusiveLock } from 'stitchkit/files';
|
|
8223
|
+
export { observeProcessInstance, probeProcessOwner, runNativeCommand } from 'stitchkit/process';
|
|
8224
|
+
```
|
|
8225
|
+
|
|
8226
|
+
Bundle for Node-compatible execution with Zod external. Publish declarations from the same
|
|
8227
|
+
version: carry their reachable relative `.d.ts` closure, rather than emitting references to
|
|
8228
|
+
`stitchkit` that force clients to install it for types. A library exposing its own DTOs may
|
|
8229
|
+
instead emit declarations for only that DTO contract. Keep the library's manifest at runtime
|
|
8230
|
+
`dependencies: {}` and `peerDependencies: { zod: ... }` when that is its declared promise.
|
|
8231
|
+
|
|
8232
|
+
Darwin consumers need the published native binaries at the bundled loader's relative native
|
|
8233
|
+
directory. A JS-only bundle is not portable evidence. Qualify the packed library outside its
|
|
8234
|
+
build tree in Bun and Node, test its declarations, and check that importing an unbundled
|
|
8235
|
+
Stitchkit leaf fails there. A separate schema-only entry must not import the native entry.
|
|
8236
|
+
|
|
8237
|
+
This delivery removes a runtime kernel installation; it does not remove the build-time
|
|
8238
|
+
Stitchkit installation or create an independent lightweight npm package.
|
|
8169
8239
|
|
|
8170
8240
|
|
|
8171
8241
|
==============================================================================
|
|
@@ -20568,7 +20638,7 @@ available from `stitchkit/contract`.
|
|
|
20568
20638
|
| `ManagedFileInspector` | _type_ | bounded-prefix read/write inspection callback with a finite cancellation signal that cannot own path or size |
|
|
20569
20639
|
| `ManagedFileInspectionInput` / `ManagedFileInspection` | _type_ | inspector prefix/name/declared media/signal input and validated metadata-only result |
|
|
20570
20640
|
| `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running |
|
|
20571
|
-
| `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` |
|
|
20641
|
+
| `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` (default 5000 ms, `null` disables age-based reclaim including guards); exact `mode` is independent of umask; shared readers need a common group and traversable directories |
|
|
20572
20642
|
| `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
|
|
20573
20643
|
| `createChunkSpool` | function | the server half of a chunked upload: parts on disk under `owner` + client-minted `uploadId`, each with a size+sha256 receipt; idempotent `open` and `put`, the first writer of a part wins across processes, `assemble` in order once every part is in, `sweep` of untouched uploads — [guide](../guide/client.md#chunked-uploads) |
|
|
20574
20644
|
| `ChunkSpool` / `ChunkSpoolConfig` / `ChunkSpoolKey` / `ChunkSpoolOpen` / `ChunkSpoolPart` / `ChunkSpoolAssembly` | _type_ | the spool, its directory, part size, file limit, meta schema and age; an upload's key; its declaration; one part; the assembled parts with a concatenating `stream()` |
|
|
@@ -21122,3 +21192,8 @@ Native POSIX Bun/Node one-shot command execution. See [native IO](../guide/nativ
|
|
|
21122
21192
|
| `NativeCommandOptions` | type | executable/args, env policy, signal/deadline, bytes sink and finite cleanup limits |
|
|
21123
21193
|
| `NativeCommandResult` | type | observed nullable exit code/signal and captured stdout/stderr bytes |
|
|
21124
21194
|
| `NativeCommandError` | class | COMMAND_LIMIT, COMMAND_UNAVAILABLE or COMMAND_CLEANUP with internal cause |
|
|
21195
|
+
|
|
21196
|
+
| `NativeCommandSettlement` | type | observed leader `exit` or terminal `error`; `onLeaderSettled` runs once before drain, bounded by cleanupTimeoutMs, with a cancellable settlement signal |
|
|
21197
|
+
| `observeProcessInstance` / `ProcessInstanceObservation` | function / type | Linux/Darwin lifetime observation; observed identity or unavailable with its original cause, never guessed absence |
|
|
21198
|
+
| `ProcessInstanceSchema` / `ProcessInstance` | schema / type | platform, boot ID, namespace and process start identity from the shared native reader |
|
|
21199
|
+
| `probeProcessOwner` / `ProcessOwnerEvidence` | function / type | compare lifetimes on one caller-established machine; matched, boot change, reused PID, gone, legacy or unavailable with cause |
|
package/llms.txt
CHANGED
|
@@ -39,7 +39,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
39
39
|
- **stitchkit/application/schemas**: [llms/application-schemas.txt](llms/application-schemas.txt) 1.8 KB
|
|
40
40
|
- **stitchkit/testing**: [llms/testing.txt](llms/testing.txt) 30.8 KB
|
|
41
41
|
- **stitchkit/agent-runtime/sandbox**: [llms/agent-runtime-sandbox.txt](llms/agent-runtime-sandbox.txt) 8.1 KB
|
|
42
|
-
- **stitchkit/files**: [llms/files.txt](llms/files.txt) 4.
|
|
42
|
+
- **stitchkit/files**: [llms/files.txt](llms/files.txt) 4.5 KB
|
|
43
43
|
- **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 26.9 KB
|
|
44
44
|
- **stitchkit/telegram/html**: [llms/telegram-html.txt](llms/telegram-html.txt) 2.7 KB
|
|
45
45
|
- **stitchkit/telegram/screens**: [llms/telegram-screens.txt](llms/telegram-screens.txt) 21.2 KB
|
|
@@ -55,7 +55,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
|
|
|
55
55
|
- **stitchkit/agent-runtime/react**: [llms/agent-runtime-react.txt](llms/agent-runtime-react.txt) 1.0 KB
|
|
56
56
|
- **stitchkit/agent-runtime/realtime**: [llms/agent-runtime-realtime.txt](llms/agent-runtime-realtime.txt) 1.8 KB
|
|
57
57
|
- **stitchkit/agent-runtime/harness-tools**: [llms/agent-runtime-harness-tools.txt](llms/agent-runtime-harness-tools.txt) 1.3 KB
|
|
58
|
-
- **stitchkit/process**: [llms/process.txt](llms/process.txt)
|
|
58
|
+
- **stitchkit/process**: [llms/process.txt](llms/process.txt) 15.9 KB
|
|
59
59
|
|
|
60
60
|
## Upgrading slices
|
|
61
61
|
- **Upgrading — how to move across versions**: [llms/upgrading.txt](llms/upgrading.txt) 10.3 KB
|
package/native/darwin-arm64.node
CHANGED
|
Binary file
|
package/native/darwin-x64.node
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/dist/index-7kxw1awn.js
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
loadDarwinBinding
|
|
3
|
-
} from "./index-d4pa2bgs.js";
|
|
4
|
-
import {
|
|
5
|
-
isRecord
|
|
6
|
-
} from "./index-9jfdm5tg.js";
|
|
7
|
-
|
|
8
|
-
// src/internal/exclusive-lock.ts
|
|
9
|
-
import { constants } from "node:fs";
|
|
10
|
-
import { lstat, open, unlink } from "node:fs/promises";
|
|
11
|
-
import { hostname } from "node:os";
|
|
12
|
-
import { z as z2 } from "zod";
|
|
13
|
-
|
|
14
|
-
// src/internal/process-identity.ts
|
|
15
|
-
import { execFile } from "node:child_process";
|
|
16
|
-
import { readFile } from "node:fs/promises";
|
|
17
|
-
import { platform } from "node:os";
|
|
18
|
-
import { promisify } from "node:util";
|
|
19
|
-
var detectedIdentity;
|
|
20
|
-
async function firstReadableLine(paths) {
|
|
21
|
-
for (const path of paths) {
|
|
22
|
-
try {
|
|
23
|
-
const text = (await readFile(path, "utf8")).trim();
|
|
24
|
-
if (text.length > 0)
|
|
25
|
-
return text;
|
|
26
|
-
} catch {}
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
var run = promisify(execFile);
|
|
31
|
-
function parsePlatformUuid(dump) {
|
|
32
|
-
return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(dump)?.[1] ?? null;
|
|
33
|
-
}
|
|
34
|
-
async function readDarwinPlatformUuid(command = "ioreg") {
|
|
35
|
-
try {
|
|
36
|
-
const { stdout } = await run(command, ["-rd1", "-c", "IOPlatformExpertDevice"], {
|
|
37
|
-
timeout: 2000,
|
|
38
|
-
maxBuffer: 1 << 20,
|
|
39
|
-
encoding: "utf8"
|
|
40
|
-
});
|
|
41
|
-
return parsePlatformUuid(stdout);
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
async function machineIdentity(declared) {
|
|
47
|
-
if (declared !== undefined)
|
|
48
|
-
return declared;
|
|
49
|
-
if (detectedIdentity !== undefined)
|
|
50
|
-
return detectedIdentity;
|
|
51
|
-
detectedIdentity = platform() === "darwin" ? await readDarwinPlatformUuid() : await firstReadableLine(["/etc/machine-id", "/var/lib/dbus/machine-id"]);
|
|
52
|
-
return detectedIdentity;
|
|
53
|
-
}
|
|
54
|
-
async function probeLiveness(pid, reach = (target) => process.kill(target, 0)) {
|
|
55
|
-
try {
|
|
56
|
-
reach(pid);
|
|
57
|
-
} catch (error) {
|
|
58
|
-
return isRecord(error) && error.code === "ESRCH" ? "gone" : "alive";
|
|
59
|
-
}
|
|
60
|
-
return await isZombieProcess(pid) ? "gone" : "alive";
|
|
61
|
-
}
|
|
62
|
-
async function isZombieProcess(pid, seam = {}) {
|
|
63
|
-
const { procRoot = "/proc", psCommand = "ps" } = seam;
|
|
64
|
-
try {
|
|
65
|
-
const stat = await readFile(`${procRoot}/${pid}/stat`, "utf8");
|
|
66
|
-
return stat.slice(stat.lastIndexOf(")") + 1).trimStart().startsWith("Z");
|
|
67
|
-
} catch {}
|
|
68
|
-
try {
|
|
69
|
-
const { stdout } = await run(psCommand, ["-o", "state=", "-p", String(pid)], {
|
|
70
|
-
timeout: 2000,
|
|
71
|
-
maxBuffer: 1 << 16,
|
|
72
|
-
encoding: "utf8"
|
|
73
|
-
});
|
|
74
|
-
return stdout.trim().startsWith("Z");
|
|
75
|
-
} catch {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// src/internal/process-instance.ts
|
|
81
|
-
import { readFile as readFile2, readlink } from "node:fs/promises";
|
|
82
|
-
import { platform as platform2 } from "node:os";
|
|
83
|
-
import { z } from "zod";
|
|
84
|
-
var ProcessInstanceSchema = z.object({
|
|
85
|
-
platform: z.enum(["linux", "darwin"]),
|
|
86
|
-
bootId: z.string().min(1),
|
|
87
|
-
namespace: z.string().min(1),
|
|
88
|
-
startId: z.string().regex(/^\d+$/)
|
|
89
|
-
}).strict();
|
|
90
|
-
function linuxProcessStart(stat, pid) {
|
|
91
|
-
if (!stat.startsWith(`${pid} (`))
|
|
92
|
-
return null;
|
|
93
|
-
const end = stat.lastIndexOf(")");
|
|
94
|
-
if (end < 0)
|
|
95
|
-
return null;
|
|
96
|
-
const start = stat.slice(end + 1).trim().split(/\s+/)[19];
|
|
97
|
-
return start !== undefined && /^\d+$/.test(start) ? start : null;
|
|
98
|
-
}
|
|
99
|
-
async function readProcessInstance(pid, procRoot = "/proc") {
|
|
100
|
-
try {
|
|
101
|
-
if (platform2() === "darwin") {
|
|
102
|
-
const result = ProcessInstanceSchema.safeParse(loadDarwinBinding().processIdentity(pid));
|
|
103
|
-
return result.success ? result.data : null;
|
|
104
|
-
}
|
|
105
|
-
if (platform2() !== "linux")
|
|
106
|
-
return null;
|
|
107
|
-
const [bootId, namespace, ownStatus, timeNamespace, ownStat, stat] = await Promise.all([
|
|
108
|
-
readFile2(`${procRoot}/sys/kernel/random/boot_id`, "utf8"),
|
|
109
|
-
readlink(`${procRoot}/self/ns/pid`),
|
|
110
|
-
readFile2(`${procRoot}/self/status`, "utf8"),
|
|
111
|
-
readlink(`${procRoot}/self/ns/time`).catch((error) => {
|
|
112
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
113
|
-
return "";
|
|
114
|
-
throw error;
|
|
115
|
-
}),
|
|
116
|
-
readFile2(`${procRoot}/self/stat`, "utf8"),
|
|
117
|
-
readFile2(`${procRoot}/${pid}/stat`, "utf8")
|
|
118
|
-
]);
|
|
119
|
-
const namespacePids = ownStatus.match(/^NStgid:[ \t]*(.*)$/m)?.[1]?.trim().split(/\s+/);
|
|
120
|
-
if (namespacePids?.length !== 1 || namespacePids[0] !== String(process.pid) || linuxProcessStart(ownStat, process.pid) === null)
|
|
121
|
-
return null;
|
|
122
|
-
const startId = linuxProcessStart(stat, pid);
|
|
123
|
-
if (startId === null || !bootId.trim())
|
|
124
|
-
return null;
|
|
125
|
-
return {
|
|
126
|
-
platform: "linux",
|
|
127
|
-
bootId: bootId.trim(),
|
|
128
|
-
namespace: `${namespace};${timeNamespace}`,
|
|
129
|
-
startId
|
|
130
|
-
};
|
|
131
|
-
} catch {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
async function probeProcessOwner(pid, recorded, probes = { read: readProcessInstance, liveness: probeLiveness }) {
|
|
136
|
-
if (recorded === undefined)
|
|
137
|
-
return { liveness: await probes.liveness(pid), identity: "legacy" };
|
|
138
|
-
const unavailable = {
|
|
139
|
-
liveness: "not-probed",
|
|
140
|
-
identity: "unavailable"
|
|
141
|
-
};
|
|
142
|
-
if (recorded === null)
|
|
143
|
-
return unavailable;
|
|
144
|
-
const current = await probes.read(process.pid);
|
|
145
|
-
if (!current || recorded.platform !== current.platform)
|
|
146
|
-
return unavailable;
|
|
147
|
-
if (recorded.bootId !== current.bootId)
|
|
148
|
-
return { liveness: "gone", identity: "different-boot" };
|
|
149
|
-
if (recorded.namespace !== current.namespace)
|
|
150
|
-
return unavailable;
|
|
151
|
-
const liveness = await probes.liveness(pid);
|
|
152
|
-
if (liveness === "gone")
|
|
153
|
-
return { liveness, identity: "pid-gone" };
|
|
154
|
-
const observed = await probes.read(pid);
|
|
155
|
-
if (!observed || observed.bootId !== current.bootId || observed.namespace !== current.namespace)
|
|
156
|
-
return unavailable;
|
|
157
|
-
if (recorded.startId !== observed.startId)
|
|
158
|
-
return { liveness: "gone", identity: "reused-pid" };
|
|
159
|
-
return { liveness: "alive", identity: "matched" };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// src/internal/exclusive-lock.ts
|
|
163
|
-
var ExclusiveLockOwnerSchema = z2.object({
|
|
164
|
-
pid: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
165
|
-
host: z2.string().min(1),
|
|
166
|
-
acquiredAt: z2.string(),
|
|
167
|
-
machine: z2.string().min(1).optional().catch(undefined),
|
|
168
|
-
process: ProcessInstanceSchema.nullable().optional().catch(null)
|
|
169
|
-
});
|
|
170
|
-
function isCode(error, code) {
|
|
171
|
-
return isRecord(error) && error.code === code;
|
|
172
|
-
}
|
|
173
|
-
function readOwner(text) {
|
|
174
|
-
let parsed;
|
|
175
|
-
try {
|
|
176
|
-
parsed = JSON.parse(text);
|
|
177
|
-
} catch {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const owner = ExclusiveLockOwnerSchema.safeParse(parsed);
|
|
181
|
-
return owner.success ? owner.data : undefined;
|
|
182
|
-
}
|
|
183
|
-
function attribute(owner, identity) {
|
|
184
|
-
if (identity !== null && owner.machine !== undefined) {
|
|
185
|
-
return owner.machine === identity ? "this-machine" : "another-machine";
|
|
186
|
-
}
|
|
187
|
-
return owner.host === hostname() ? "this-machine" : "unattributable";
|
|
188
|
-
}
|
|
189
|
-
async function diagnose(owner, declaredIdentity) {
|
|
190
|
-
if (!owner)
|
|
191
|
-
return { attribution: "unattributable", liveness: "not-probed", owner: null };
|
|
192
|
-
const attribution = attribute(owner, await machineIdentity(declaredIdentity));
|
|
193
|
-
if (attribution !== "this-machine")
|
|
194
|
-
return { attribution, liveness: "not-probed", owner };
|
|
195
|
-
return { attribution, ...await probeProcessOwner(owner.pid, owner.process), owner };
|
|
196
|
-
}
|
|
197
|
-
async function readLockFile(path) {
|
|
198
|
-
let handle;
|
|
199
|
-
try {
|
|
200
|
-
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
201
|
-
const info = await handle.stat();
|
|
202
|
-
return {
|
|
203
|
-
owner: readOwner(await handle.readFile("utf8")),
|
|
204
|
-
ino: info.ino,
|
|
205
|
-
mtimeMs: info.mtimeMs
|
|
206
|
-
};
|
|
207
|
-
} catch {
|
|
208
|
-
return;
|
|
209
|
-
} finally {
|
|
210
|
-
await handle?.close().catch(() => {
|
|
211
|
-
return;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
async function unlinkIfSame(path, ino) {
|
|
216
|
-
try {
|
|
217
|
-
if ((await lstat(path)).ino !== ino)
|
|
218
|
-
return;
|
|
219
|
-
await unlink(path);
|
|
220
|
-
} catch (error) {
|
|
221
|
-
if (!isCode(error, "ENOENT"))
|
|
222
|
-
throw error;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
async function createOwned(path, options, reclaimed) {
|
|
226
|
-
const identity = await machineIdentity(options.machineIdentity);
|
|
227
|
-
const instance = await readProcessInstance(process.pid);
|
|
228
|
-
const handle = await open(path, "wx", options.mode);
|
|
229
|
-
const owner = {
|
|
230
|
-
pid: process.pid,
|
|
231
|
-
process: instance,
|
|
232
|
-
host: hostname(),
|
|
233
|
-
acquiredAt: new Date().toISOString(),
|
|
234
|
-
...identity !== null && { machine: identity }
|
|
235
|
-
};
|
|
236
|
-
let ino;
|
|
237
|
-
try {
|
|
238
|
-
await handle.writeFile(`${JSON.stringify(owner)}
|
|
239
|
-
`, "utf8");
|
|
240
|
-
ino = (await handle.stat()).ino;
|
|
241
|
-
} catch (error) {
|
|
242
|
-
await handle.close().catch(() => {
|
|
243
|
-
return;
|
|
244
|
-
});
|
|
245
|
-
await unlink(path).catch(() => {
|
|
246
|
-
return;
|
|
247
|
-
});
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
250
|
-
let released = false;
|
|
251
|
-
return {
|
|
252
|
-
path,
|
|
253
|
-
owner,
|
|
254
|
-
reclaimed,
|
|
255
|
-
async release() {
|
|
256
|
-
if (released)
|
|
257
|
-
return;
|
|
258
|
-
released = true;
|
|
259
|
-
try {
|
|
260
|
-
await handle.close();
|
|
261
|
-
} finally {
|
|
262
|
-
await unlinkIfSame(path, ino);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
function reclaimable(state, diagnosis, graceMs) {
|
|
268
|
-
if (state.owner)
|
|
269
|
-
return diagnosis.liveness === "gone";
|
|
270
|
-
return graceMs !== undefined && Date.now() - state.mtimeMs >= graceMs;
|
|
271
|
-
}
|
|
272
|
-
async function reclaimUnderGuard(path, options) {
|
|
273
|
-
const guardPath = `${path}.reclaim`;
|
|
274
|
-
let guard;
|
|
275
|
-
try {
|
|
276
|
-
guard = await createOwned(guardPath, options, false);
|
|
277
|
-
} catch (error) {
|
|
278
|
-
if (!isCode(error, "EEXIST"))
|
|
279
|
-
throw error;
|
|
280
|
-
const stale = await readLockFile(guardPath);
|
|
281
|
-
if (stale) {
|
|
282
|
-
const verdict = await diagnose(stale.owner, options.machineIdentity);
|
|
283
|
-
if (reclaimable(stale, verdict, options.ownerlessGraceMs ?? 5000)) {
|
|
284
|
-
await unlinkIfSame(guardPath, stale.ino);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const state = await readLockFile(path);
|
|
291
|
-
if (state) {
|
|
292
|
-
const diagnosis = await diagnose(state.owner, options.machineIdentity);
|
|
293
|
-
if (!reclaimable(state, diagnosis, options.ownerlessGraceMs))
|
|
294
|
-
return "refused";
|
|
295
|
-
await unlinkIfSame(path, state.ino);
|
|
296
|
-
}
|
|
297
|
-
return await createOwned(path, options, state !== undefined).catch((error) => {
|
|
298
|
-
if (isCode(error, "EEXIST"))
|
|
299
|
-
return "refused";
|
|
300
|
-
throw error;
|
|
301
|
-
});
|
|
302
|
-
} finally {
|
|
303
|
-
await guard.release().catch(() => {
|
|
304
|
-
return;
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
async function attemptExclusiveLock(path, options) {
|
|
309
|
-
try {
|
|
310
|
-
return { held: await createOwned(path, options, false) };
|
|
311
|
-
} catch (error) {
|
|
312
|
-
if (!isCode(error, "EEXIST"))
|
|
313
|
-
throw error;
|
|
314
|
-
if (!options.reclaim)
|
|
315
|
-
return { error };
|
|
316
|
-
const state = await readLockFile(path);
|
|
317
|
-
const diagnosis = await diagnose(state?.owner, options.machineIdentity);
|
|
318
|
-
if (!state || !reclaimable(state, diagnosis, options.ownerlessGraceMs)) {
|
|
319
|
-
return { error, diagnosis };
|
|
320
|
-
}
|
|
321
|
-
const reclaimed = await reclaimUnderGuard(path, options);
|
|
322
|
-
if (reclaimed !== undefined && reclaimed !== "refused")
|
|
323
|
-
return { held: reclaimed };
|
|
324
|
-
return { error, diagnosis };
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export { attemptExclusiveLock };
|