@crouton-kit/capture 1.3.8 → 1.3.9
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/bin/capture +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1478 -105
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +279 -51
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -79
- package/src/tab-session.ts +0 -91
package/src/session/artifacts.ts
CHANGED
|
@@ -1,229 +1,481 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* writes are atomic (temp file + rename) and never follow a symlink planted
|
|
7
|
-
* at the destination, and every helper refuses to operate outside
|
|
8
|
-
* `CAPTURE_ROOT`. Downstream units (snapshot collectors, the recorder
|
|
9
|
-
* bridge, motion query leaves) write ALL artifacts through these helpers
|
|
10
|
-
* instead of ad-hoc `fs.writeFile`/`fs.mkdirSync`.
|
|
11
|
-
*/
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import * as path from 'node:path';
|
|
12
6
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export const CAPTURE_ROOT = path.join(os.tmpdir(), 'capture-sessions');
|
|
19
|
-
|
|
20
|
-
/** Owner rwx only. */
|
|
7
|
+
const configuredRoot = process.env.CAPTURE_ROOT;
|
|
8
|
+
if (configuredRoot !== undefined && !path.isAbsolute(configuredRoot)) throw new Error('CAPTURE_ROOT must be an absolute path');
|
|
9
|
+
/** The private artifact root pathname, frozen at process start; the directory itself is
|
|
10
|
+
* established lazily by the first artifact transaction. Intentionally not realpathed. */
|
|
11
|
+
export const CAPTURE_ROOT = path.resolve(configuredRoot ?? path.join(os.tmpdir(), 'capture-sessions'));
|
|
21
12
|
export const DIR_MODE = 0o700;
|
|
22
|
-
/** Owner rw only. */
|
|
23
13
|
export const FILE_MODE = 0o600;
|
|
14
|
+
const MAX_TIMING_MS = 86_400_000;
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
settled: boolean;
|
|
37
|
-
capturedAt: string;
|
|
16
|
+
type Identity = { dev: number; ino: number };
|
|
17
|
+
export type ArtifactHookOperation = 'root-bootstrap' | 'traversal' | 'final-file' | 'recursive-removal' | 'lock';
|
|
18
|
+
export type ArtifactHookPhase = 'afterComponentLstat' | 'beforeComponentCreate' | 'beforeComponentChdir' | 'afterComponentChdirBeforeIdentityCheck' | 'afterRootPinned' | 'afterParentPinned' | 'afterFinalOpen' | 'afterDescriptorValidated' | 'beforeTempCreate' | 'afterTempCreate' | 'beforeRename' | 'beforeUnlink' | 'beforeOwnedCleanupUnlink' | 'afterChildLstat' | 'beforeChildChdir' | 'afterChildChdirBeforeIdentityCheck' | 'beforeChildUnlink' | 'beforeDirectoryRmdir' | 'beforePublishAttempt' | 'afterStageMkdirPinned' | 'beforeStageChdir' | 'afterStageChdirBeforeIdentityCheck' | 'afterOwnerWrite' | 'beforePublishRename' | 'beforeStageCleanup' | 'afterCanonicalOwnerValidation' | 'afterOwnerRemoval' | 'beforeCanonicalRmdir';
|
|
19
|
+
export interface ArtifactHookDetail { operation: ArtifactHookOperation; phase: ArtifactHookPhase; path: string; component?: string; }
|
|
20
|
+
type Hook = (detail: ArtifactHookDetail) => void;
|
|
21
|
+
export interface ArtifactTestHooks {
|
|
22
|
+
onHook?: Hook;
|
|
23
|
+
afterComponentLstat?: Hook; beforeComponentCreate?: Hook; beforeComponentChdir?: Hook; afterComponentChdirBeforeIdentityCheck?: Hook; afterRootPinned?: Hook; afterParentPinned?: Hook;
|
|
24
|
+
afterFinalOpen?: Hook; afterDescriptorValidated?: Hook; beforeTempCreate?: Hook; afterTempCreate?: Hook; beforeRename?: Hook; beforeUnlink?: Hook; beforeOwnedCleanupUnlink?: Hook;
|
|
25
|
+
afterChildLstat?: Hook; beforeChildChdir?: Hook; afterChildChdirBeforeIdentityCheck?: Hook; beforeChildUnlink?: Hook; beforeDirectoryRmdir?: Hook;
|
|
26
|
+
beforePublishAttempt?: Hook; afterStageMkdirPinned?: Hook; beforeStageChdir?: Hook; afterStageChdirBeforeIdentityCheck?: Hook; afterOwnerWrite?: Hook; beforePublishRename?: Hook; beforeStageCleanup?: Hook; afterCanonicalOwnerValidation?: Hook; afterOwnerRemoval?: Hook; beforeCanonicalRmdir?: Hook;
|
|
38
27
|
}
|
|
28
|
+
/** A hook may be installed via this global symbol before importing this module, so the first transaction's root bootstrap is observable. */
|
|
29
|
+
export const ARTIFACT_TEST_HOOKS_SYMBOL = Symbol.for('capture.artifacts.test-hooks');
|
|
30
|
+
let hooks = globalThis[ARTIFACT_TEST_HOOKS_SYMBOL as keyof typeof globalThis] as ArtifactTestHooks | undefined;
|
|
31
|
+
function hook(operation: ArtifactHookOperation, phase: ArtifactHookPhase, path: string, component?: string): void { const detail = { operation, phase, path, component }; hooks?.onHook?.(detail); hooks?.[phase]?.(detail); }
|
|
32
|
+
/** Test-only synchronous race seams. Production callers must not configure these. */
|
|
33
|
+
export function __setArtifactTestHooks(next?: ArtifactTestHooks): void { hooks = next; }
|
|
34
|
+
export type ArtifactSyscallRole = 'artifact-data-write' | 'lock-owner-write' | 'artifact-temp-fsync' | 'lock-owner-fsync' | 'lock-stage-dir-fsync' | 'artifact-temp-close' | 'lock-owner-close' | 'artifact-rename' | 'lock-publish-rename' | 'artifact-cleanup-unlink' | 'lock-canonical-rmdir' | 'artifact-open' | 'artifact-fstat' | 'artifact-fchmod';
|
|
35
|
+
export interface ArtifactTestFaults { before?: (role: ArtifactSyscallRole) => void; after?: (role: ArtifactSyscallRole) => void; write?: (role: Extract<ArtifactSyscallRole, 'artifact-data-write' | 'lock-owner-write'>, real: () => number) => number; }
|
|
36
|
+
let faults: ArtifactTestFaults | undefined;
|
|
37
|
+
/** Test-only syscall adapter. Calls are always real by default and this is reset by passing undefined. */
|
|
38
|
+
export function __setArtifactTestFaults(next?: ArtifactTestFaults): void { faults = next; }
|
|
39
|
+
let artifactTempToken = () => crypto.randomBytes(12).toString('hex');
|
|
40
|
+
let lockToken = () => crypto.randomBytes(24).toString('hex');
|
|
41
|
+
/** Deterministic test-only sources for nameable collision and publication cases. */
|
|
42
|
+
export function __setArtifactTestTokens(next?: { artifactTemp?: () => string; lock?: () => string }): void { artifactTempToken = next?.artifactTemp ?? (() => crypto.randomBytes(12).toString('hex')); lockToken = next?.lock ?? (() => crypto.randomBytes(24).toString('hex')); }
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
type RuntimeContext = {
|
|
45
|
+
hooks: ArtifactTestHooks | undefined;
|
|
46
|
+
faults: ArtifactTestFaults | undefined;
|
|
47
|
+
artifactTempToken: () => string;
|
|
48
|
+
lockToken: () => string;
|
|
49
|
+
execFileSyncForProvider: ExecFileSync;
|
|
50
|
+
linuxProviderRead: (file: '/proc/sys/kernel/random/boot_id' | `/proc/${number}/stat`) => string;
|
|
51
|
+
};
|
|
52
|
+
function snapshotRuntime(): RuntimeContext { return { hooks, faults, artifactTempToken, lockToken, execFileSyncForProvider, linuxProviderRead }; }
|
|
53
|
+
function withRuntime<T>(runtime: RuntimeContext, action: () => T): T {
|
|
54
|
+
const prior = { hooks, faults, artifactTempToken, lockToken, execFileSyncForProvider, linuxProviderRead };
|
|
55
|
+
hooks = runtime.hooks;
|
|
56
|
+
faults = runtime.faults;
|
|
57
|
+
artifactTempToken = runtime.artifactTempToken;
|
|
58
|
+
lockToken = runtime.lockToken;
|
|
59
|
+
execFileSyncForProvider = runtime.execFileSyncForProvider;
|
|
60
|
+
linuxProviderRead = runtime.linuxProviderRead;
|
|
61
|
+
try { return action(); }
|
|
62
|
+
finally {
|
|
63
|
+
hooks = prior.hooks;
|
|
64
|
+
faults = prior.faults;
|
|
65
|
+
artifactTempToken = prior.artifactTempToken;
|
|
66
|
+
lockToken = prior.lockToken;
|
|
67
|
+
execFileSyncForProvider = prior.execFileSyncForProvider;
|
|
68
|
+
linuxProviderRead = prior.linuxProviderRead;
|
|
69
|
+
}
|
|
46
70
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new Error(`artifact path escapes capture root ${root}: ${targetPath}`);
|
|
71
|
+
async function withRuntimeAsync<T>(runtime: RuntimeContext, action: () => Promise<T>): Promise<T> {
|
|
72
|
+
const prior = { hooks, faults, artifactTempToken, lockToken, execFileSyncForProvider, linuxProviderRead };
|
|
73
|
+
hooks = runtime.hooks;
|
|
74
|
+
faults = runtime.faults;
|
|
75
|
+
artifactTempToken = runtime.artifactTempToken;
|
|
76
|
+
lockToken = runtime.lockToken;
|
|
77
|
+
execFileSyncForProvider = runtime.execFileSyncForProvider;
|
|
78
|
+
linuxProviderRead = runtime.linuxProviderRead;
|
|
79
|
+
try { return await action(); }
|
|
80
|
+
finally {
|
|
81
|
+
hooks = prior.hooks;
|
|
82
|
+
faults = prior.faults;
|
|
83
|
+
artifactTempToken = prior.artifactTempToken;
|
|
84
|
+
lockToken = prior.lockToken;
|
|
85
|
+
execFileSyncForProvider = prior.execFileSyncForProvider;
|
|
86
|
+
linuxProviderRead = prior.linuxProviderRead;
|
|
64
87
|
}
|
|
65
|
-
return resolved;
|
|
66
88
|
}
|
|
89
|
+
let privateLockGate: Promise<unknown> = Promise.resolve();
|
|
90
|
+
function withPrivateLockSerial<T>(task: () => Promise<T>): Promise<T> {
|
|
91
|
+
const run = privateLockGate.then(() => task());
|
|
92
|
+
privateLockGate = run.then(() => undefined, () => undefined);
|
|
93
|
+
return run;
|
|
94
|
+
}
|
|
95
|
+
function syscall<T>(role: ArtifactSyscallRole, real: () => T): T { faults?.before?.(role); return real(); }
|
|
96
|
+
function closeDescriptor(fd: number, role: Extract<ArtifactSyscallRole, 'artifact-temp-close' | 'lock-owner-close'>, afterRealClose?: () => void): void {
|
|
97
|
+
faults?.before?.(role);
|
|
98
|
+
fs.closeSync(fd);
|
|
99
|
+
afterRealClose?.();
|
|
100
|
+
// Close is the sole after-real-call fault seam: the descriptor is definitely closed
|
|
101
|
+
// before this callback can throw.
|
|
102
|
+
faults?.after?.(role);
|
|
103
|
+
}
|
|
104
|
+
function combineFailure(primary: unknown, secondary: unknown, message: string): never {
|
|
105
|
+
throw new AggregateError([primary, secondary], message);
|
|
106
|
+
}
|
|
107
|
+
// The root is established lazily by the first pinned artifact transaction: every
|
|
108
|
+
// transaction traverses it, pinning its identity on first use and verifying it
|
|
109
|
+
// on every later traversal. Non-artifact invocations touch no filesystem state.
|
|
110
|
+
let captureRootIdentity: Identity | undefined;
|
|
111
|
+
let cwdPinned = false;
|
|
67
112
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
113
|
+
export interface SnapMeta { id: string; url: string | null; viewport: string | null; settled: boolean; capturedAt: string; }
|
|
114
|
+
export interface RecMeta { id: string; action: string | null; frames: number; durationMs: number; state: string; viewportRestored?: boolean | null; }
|
|
115
|
+
function identity(stat: fs.Stats): Identity { return { dev: stat.dev, ino: stat.ino }; }
|
|
116
|
+
function sameIdentity(a: Identity, b: Identity): boolean { return a.dev === b.dev && a.ino === b.ino; }
|
|
117
|
+
function errno(error: unknown): string | undefined { return (error as NodeJS.ErrnoException).code; }
|
|
118
|
+
function isMissing(error: unknown): boolean { return errno(error) === 'ENOENT'; }
|
|
119
|
+
function assertName(name: string): void { if (!name || name === '.' || name === '..' || name.includes(path.sep)) throw new Error(`invalid artifact path component: ${name}`); }
|
|
120
|
+
function writeAll(fd: number, data: string | Buffer, role: Extract<ArtifactSyscallRole, 'artifact-data-write' | 'lock-owner-write'> = 'artifact-data-write'): void { const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data); for (let off = 0; off < bytes.length;) { const remaining = bytes.length - off; faults?.before?.(role); if (!faults?.write) { const wrote = fs.writeSync(fd, bytes, off, remaining); if (!Number.isSafeInteger(wrote) || wrote <= 0 || wrote > remaining) throw new Error('short private artifact write'); off += wrote; continue; }
|
|
121
|
+
// A fault callback prescribes the segment to persist. Its `real` callback reports
|
|
122
|
+
// the unmodified syscall capacity; delaying the actual write avoids writing a full
|
|
123
|
+
// buffer and then falsely reporting a partial result.
|
|
124
|
+
const prescribed = faults.write(role, () => remaining); if (!Number.isSafeInteger(prescribed) || prescribed <= 0 || prescribed > remaining) throw new Error('short private artifact write');
|
|
125
|
+
const wrote = fs.writeSync(fd, bytes, off, prescribed); if (!Number.isSafeInteger(wrote) || wrote <= 0 || wrote > prescribed) throw new Error('short private artifact write'); off += wrote;
|
|
126
|
+
} }
|
|
127
|
+
function privateDirHere(): void { const fd = fs.openSync('.', fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW); try { fs.fchmodSync(fd, DIR_MODE); } finally { fs.closeSync(fd); } }
|
|
128
|
+
function verifyRoot(actual: Identity): void { if (captureRootIdentity) { if (!sameIdentity(captureRootIdentity, actual)) throw new Error('capture root changed since it was first established'); } else captureRootIdentity = actual; }
|
|
129
|
+
function rootOrAncestor(current: string): boolean { return current === CAPTURE_ROOT || CAPTURE_ROOT.startsWith(`${current}${path.sep}`); }
|
|
71
130
|
|
|
72
|
-
/**
|
|
73
|
-
function
|
|
74
|
-
|
|
131
|
+
/** Performs only synchronous single-component operations while cwd is inode-pinned. */
|
|
132
|
+
function inPinnedDirectory<T>(absoluteDir: string, create: boolean, operation: () => T, operationKind: ArtifactHookOperation = 'traversal'): T {
|
|
133
|
+
if (cwdPinned) throw new Error('nested private artifact cwd transaction');
|
|
134
|
+
if (!path.isAbsolute(absoluteDir)) throw new Error(`private artifact directory must be absolute: ${absoluteDir}`);
|
|
135
|
+
cwdPinned = true;
|
|
136
|
+
const original = process.cwd();
|
|
137
|
+
const originalIdentity = identity(fs.statSync('.'));
|
|
138
|
+
let result: T | undefined; let failure: unknown;
|
|
75
139
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
140
|
+
const root = path.parse(absoluteDir).root; process.chdir(root); let current = root;
|
|
141
|
+
for (const component of absoluteDir.slice(root.length).split(path.sep).filter(Boolean)) {
|
|
142
|
+
assertName(component); current = path.join(current, component);
|
|
143
|
+
const privateComponent = current === CAPTURE_ROOT || current.startsWith(`${CAPTURE_ROOT}${path.sep}`);
|
|
144
|
+
const darwinVarAlias = process.platform === 'darwin' && current === '/var' && CAPTURE_ROOT.startsWith('/var/');
|
|
145
|
+
const hookOperation: ArtifactHookOperation = current === CAPTURE_ROOT || darwinVarAlias ? 'root-bootstrap' : operationKind;
|
|
146
|
+
let before: fs.Stats;
|
|
147
|
+
try { before = fs.lstatSync(component); hook(hookOperation, 'afterComponentLstat', current, component); }
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!create || !(privateComponent || rootOrAncestor(current)) || !isMissing(error)) throw error;
|
|
150
|
+
hook(hookOperation, 'beforeComponentCreate', current, component);
|
|
151
|
+
// A concurrent honest bootstrap can create this component between the
|
|
152
|
+
// ENOENT lstat and this mkdir. Tolerate exactly EEXIST, then re-lstat the
|
|
153
|
+
// same name: the symlink/non-directory/identity/mode checks below remain
|
|
154
|
+
// authoritative over whatever won the create.
|
|
155
|
+
try { fs.mkdirSync(component, { mode: DIR_MODE }); }
|
|
156
|
+
catch (createError) { if (errno(createError) !== 'EEXIST') throw createError; }
|
|
157
|
+
before = fs.lstatSync(component);
|
|
158
|
+
}
|
|
159
|
+
// Darwin exposes /var as a kernel-owned alias for /private/var. It is the only
|
|
160
|
+
// host alias accepted before the configured root; all user-controlled components
|
|
161
|
+
// including the configured root itself must be real directories.
|
|
162
|
+
// Pin and compare the followed vnode, not the alias directory entry.
|
|
163
|
+
if (before.isSymbolicLink() && darwinVarAlias) {
|
|
164
|
+
const expected = identity(fs.statSync(component));
|
|
165
|
+
hook(hookOperation, 'beforeComponentChdir', current, component);
|
|
166
|
+
process.chdir(component);
|
|
167
|
+
hook(hookOperation, 'afterComponentChdirBeforeIdentityCheck', current, component);
|
|
168
|
+
if (!sameIdentity(expected, identity(fs.statSync('.')))) throw new Error(`artifact directory changed while pinning: ${component}`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (before.isSymbolicLink()) throw new Error(`refusing symlinked artifact directory component: ${component}`);
|
|
172
|
+
if (!before.isDirectory()) throw new Error(`refusing non-directory artifact component: ${component}`);
|
|
173
|
+
const expected = identity(before); hook(hookOperation, 'beforeComponentChdir', current, component); process.chdir(component); hook(hookOperation, 'afterComponentChdirBeforeIdentityCheck', current, component);
|
|
174
|
+
if (!sameIdentity(expected, identity(fs.statSync('.')))) throw new Error(`artifact directory changed while pinning: ${component}`);
|
|
175
|
+
if (current === CAPTURE_ROOT) { verifyRoot(expected); hook('root-bootstrap', 'afterRootPinned', current, component); }
|
|
176
|
+
if (privateComponent) privateDirHere();
|
|
177
|
+
}
|
|
178
|
+
hook(absoluteDir === CAPTURE_ROOT ? 'root-bootstrap' : operationKind, 'afterParentPinned', absoluteDir); result = operation();
|
|
179
|
+
} catch (error) { failure = error; }
|
|
180
|
+
let restoreFailure: unknown;
|
|
181
|
+
try { process.chdir(original); if (!sameIdentity(originalIdentity, identity(fs.statSync('.')))) restoreFailure = new Error('private artifact cwd restoration changed directory identity'); }
|
|
182
|
+
catch (error) { restoreFailure = error; }
|
|
183
|
+
cwdPinned = false;
|
|
184
|
+
if (restoreFailure) { if (failure) throw new AggregateError([failure, restoreFailure], 'private artifact cwd restoration failed'); throw restoreFailure; }
|
|
185
|
+
if (failure) throw failure;
|
|
186
|
+
return result as T;
|
|
91
187
|
}
|
|
92
188
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*/
|
|
101
|
-
export function ensurePrivateDir(dirPath: string): string {
|
|
102
|
-
const resolved = assertUnderCaptureRoot(dirPath);
|
|
103
|
-
const root = path.resolve(CAPTURE_ROOT);
|
|
104
|
-
|
|
105
|
-
ensureSegmentPrivate(root);
|
|
106
|
-
const rel = path.relative(root, resolved);
|
|
107
|
-
let current = root;
|
|
108
|
-
for (const part of rel.split(path.sep).filter(Boolean)) {
|
|
109
|
-
current = path.join(current, part);
|
|
110
|
-
ensureSegmentPrivate(current);
|
|
111
|
-
}
|
|
112
|
-
return resolved;
|
|
189
|
+
export function assertUnderCaptureRoot(targetPath: string): string { const resolved = path.resolve(targetPath); const relative = path.relative(CAPTURE_ROOT, resolved); if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`artifact path escapes capture root ${CAPTURE_ROOT}: ${targetPath}`); return resolved; }
|
|
190
|
+
function pinnedParent<T>(target: string, operation: (name: string) => T, create = true, operationKind: ArtifactHookOperation = 'traversal'): T { const resolved = assertUnderCaptureRoot(target); const name = path.basename(resolved); assertName(name); return inPinnedDirectory(path.dirname(resolved), create, () => operation(name), operationKind); }
|
|
191
|
+
export function ensurePrivateDir(dirPath: string): string { const resolved = assertUnderCaptureRoot(dirPath); inPinnedDirectory(resolved, true, () => undefined); return resolved; }
|
|
192
|
+
function openRegular(name: string, flags: number, create = false, onOpened?: (fd: number) => void, closeRole: Extract<ArtifactSyscallRole, 'artifact-temp-close' | 'lock-owner-close'> = 'artifact-temp-close', hookOperation: ArtifactHookOperation = 'final-file'): number {
|
|
193
|
+
let fd: number | undefined;
|
|
194
|
+
try { fd = syscall('artifact-open', () => fs.openSync(name, flags | fs.constants.O_NOFOLLOW, create ? FILE_MODE : undefined)); onOpened?.(fd); hook(hookOperation, 'afterFinalOpen', path.resolve(process.cwd(), name), name); const stat = syscall('artifact-fstat', () => fs.fstatSync(fd!)); if (!stat.isFile()) throw new Error(`artifact is not a regular file: ${name}`); syscall('artifact-fchmod', () => fs.fchmodSync(fd!, FILE_MODE)); hook(hookOperation, 'afterDescriptorValidated', path.resolve(process.cwd(), name), name); return fd; }
|
|
195
|
+
catch (error) { if (fd !== undefined) { try { closeDescriptor(fd, closeRole); } catch (closeError) { combineFailure(error, closeError, 'private artifact descriptor setup failed'); } } throw error; }
|
|
113
196
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// ============================================================================
|
|
118
|
-
|
|
119
|
-
function refuseExistingSymlink(resolved: string): void {
|
|
197
|
+
export function createPrivateFile(filePath: string, data: string | Buffer = ''): void { pinnedParent(filePath, name => {
|
|
198
|
+
let fd: number | undefined; let created: Identity | undefined; let closed = false;
|
|
199
|
+
let primary: unknown;
|
|
120
200
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
201
|
+
fd = openRegular(name, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, true, opened => { created = identity(fs.fstatSync(opened)); });
|
|
202
|
+
writeAll(fd, data); syscall('artifact-temp-fsync', () => fs.fsyncSync(fd));
|
|
203
|
+
closeDescriptor(fd, 'artifact-temp-close', () => { closed = true; });
|
|
204
|
+
} catch (error) { primary = error; }
|
|
205
|
+
if (fd !== undefined && !closed) { try { closeDescriptor(fd, 'artifact-temp-close', () => { closed = true; }); } catch (closeError) { if (primary) combineFailure(primary, closeError, 'private artifact creation failed'); throw closeError; } }
|
|
206
|
+
if (primary) { try { cleanupOwnedFile(name, created); } catch (cleanupError) { combineFailure(primary, cleanupError, 'private artifact creation cleanup failed'); } throw primary; }
|
|
207
|
+
}); }
|
|
208
|
+
function readFd(fd: number): Buffer { const chunks: Buffer[] = []; const chunk = Buffer.allocUnsafe(65536); for (;;) { const n = fs.readSync(fd, chunk, 0, chunk.length, null); if (!n) return Buffer.concat(chunks); chunks.push(Buffer.from(chunk.subarray(0, n))); } }
|
|
209
|
+
export function readPrivateFile(filePath: string): Buffer { return pinnedParent(filePath, name => { const fd = openRegular(name, fs.constants.O_RDONLY); let value: Buffer | undefined; let primary: unknown; try { value = readFd(fd); } catch (error) { primary = error; } try { closeDescriptor(fd, 'artifact-temp-close'); } catch (closeError) { if (primary) combineFailure(primary, closeError, 'private artifact read failed'); throw closeError; } if (primary) throw primary; return value!; }); }
|
|
210
|
+
/** Opens a contained, no-follow, append descriptor the caller owns and must close.
|
|
211
|
+
* Reuses the pinned-parent transaction and fchmod-on-descriptor private mode, so a
|
|
212
|
+
* planted symlink at the final component fails the open (O_NOFOLLOW) and the target's
|
|
213
|
+
* bytes/mode are never touched. */
|
|
214
|
+
export function openPrivateAppendFd(filePath: string): number {
|
|
215
|
+
return pinnedParent(filePath, name => openRegular(name, fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT, true));
|
|
216
|
+
}
|
|
217
|
+
export function appendPrivateFile(filePath: string, data: string | Buffer): void { pinnedParent(filePath, name => { const fd = openRegular(name, fs.constants.O_WRONLY | fs.constants.O_APPEND); let primary: unknown; try { writeAll(fd, data); syscall('artifact-temp-fsync', () => fs.fsyncSync(fd)); } catch (error) { primary = error; } try { closeDescriptor(fd, 'artifact-temp-close'); } catch (closeError) { if (primary) combineFailure(primary, closeError, 'private artifact append failed'); throw closeError; } if (primary) throw primary; }); }
|
|
218
|
+
function cleanupOwnedFile(name: string, expected?: Identity): void {
|
|
219
|
+
if (!expected) return;
|
|
220
|
+
let lastFailure: unknown;
|
|
221
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
222
|
+
try {
|
|
223
|
+
const current = fs.lstatSync(name);
|
|
224
|
+
if (!sameIdentity(expected, identity(current)) || !current.isFile()) return;
|
|
225
|
+
hook('final-file', 'beforeOwnedCleanupUnlink', path.resolve(process.cwd(), name), name);
|
|
226
|
+
const rechecked = fs.lstatSync(name);
|
|
227
|
+
if (!sameIdentity(expected, identity(rechecked)) || !rechecked.isFile()) return;
|
|
228
|
+
syscall('artifact-cleanup-unlink', () => fs.unlinkSync(name));
|
|
229
|
+
return;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (isMissing(error)) return;
|
|
232
|
+
lastFailure = error;
|
|
124
233
|
}
|
|
125
|
-
} catch (err) {
|
|
126
|
-
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
127
234
|
}
|
|
235
|
+
throw lastFailure;
|
|
128
236
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
* `O_NOFOLLOW`, so a pre-planted symlink at the temp name can't be written
|
|
134
|
-
* through) and renames it over the destination. `rename(2)` replaces
|
|
135
|
-
* whatever is at the destination — including a symlink — without ever
|
|
136
|
-
* following it, so a symlink swapped in at `filePath` between calls is
|
|
137
|
-
* unlinked, never written through. Ensures the parent directory exists
|
|
138
|
-
* (and is private) first. This is the one primitive every other write
|
|
139
|
-
* helper in this module is built on.
|
|
140
|
-
*/
|
|
141
|
-
export function writePrivateFile(filePath: string, data: string | Buffer): void {
|
|
142
|
-
const resolved = assertUnderCaptureRoot(filePath);
|
|
143
|
-
const dir = path.dirname(resolved);
|
|
144
|
-
ensurePrivateDir(dir);
|
|
145
|
-
refuseExistingSymlink(resolved);
|
|
146
|
-
|
|
147
|
-
const tmpPath = path.join(dir, `.${path.basename(resolved)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`);
|
|
148
|
-
const fd = fs.openSync(
|
|
149
|
-
tmpPath,
|
|
150
|
-
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_NOFOLLOW,
|
|
151
|
-
FILE_MODE,
|
|
152
|
-
);
|
|
237
|
+
export function writePrivateFile(filePath: string, data: string | Buffer): void { pinnedParent(filePath, name => {
|
|
238
|
+
try { if (fs.lstatSync(name).isSymbolicLink()) throw new Error(`refusing to replace symlinked private artifact: ${name}`); } catch (error) { if (!isMissing(error)) throw error; }
|
|
239
|
+
const temporary = `.${name}.${process.pid}.${artifactTempToken()}.tmp`; let fd: number | undefined; let created: Identity | undefined; let closed = false; let published = false;
|
|
240
|
+
let primary: unknown;
|
|
153
241
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
hook('final-file', 'beforeTempCreate', path.resolve(process.cwd(), temporary), temporary);
|
|
243
|
+
fd = openRegular(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, true, opened => { created = identity(fs.fstatSync(opened)); });
|
|
244
|
+
hook('final-file', 'afterTempCreate', path.resolve(process.cwd(), temporary), temporary);
|
|
245
|
+
writeAll(fd, data); syscall('artifact-temp-fsync', () => fs.fsyncSync(fd)); closeDescriptor(fd, 'artifact-temp-close', () => { closed = true; });
|
|
246
|
+
hook('final-file', 'beforeRename', path.resolve(process.cwd(), name), name);
|
|
247
|
+
const temporaryState = fs.lstatSync(temporary);
|
|
248
|
+
if (!created || !temporaryState.isFile() || !sameIdentity(created, identity(temporaryState))) throw new Error(`private artifact temporary replaced before rename: ${temporary}`);
|
|
249
|
+
syscall('artifact-rename', () => fs.renameSync(temporary, name)); published = true;
|
|
250
|
+
} catch (error) { primary = error; }
|
|
251
|
+
if (fd !== undefined && !closed) { try { closeDescriptor(fd, 'artifact-temp-close', () => { closed = true; }); } catch (closeError) { if (primary) combineFailure(primary, closeError, 'private artifact replacement failed'); throw closeError; } }
|
|
252
|
+
if (primary) { if (!published) { try { cleanupOwnedFile(temporary, created); } catch (cleanupError) { combineFailure(primary, cleanupError, 'private artifact replacement cleanup failed'); } } throw primary; }
|
|
253
|
+
}); }
|
|
254
|
+
export function unlinkPrivateFile(filePath: string): void { pinnedParent(filePath, name => { const stat = fs.lstatSync(name); if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`refusing to unlink non-regular private artifact: ${name}`); hook('final-file', 'beforeUnlink', path.resolve(process.cwd(), name), name); syscall('artifact-cleanup-unlink', () => fs.unlinkSync(name)); }); }
|
|
255
|
+
export function writeJsonPrivate(filePath: string, value: unknown): void { writePrivateFile(filePath, JSON.stringify(value, null, 2)); }
|
|
256
|
+
export function writeNdjsonPrivate(filePath: string, records: unknown[]): void { writePrivateFile(filePath, records.map(record => JSON.stringify(record)).join('\n') + (records.length ? '\n' : '')); }
|
|
257
|
+
export function appendNdjsonPrivate(filePath: string, record: unknown): void { const line = `${JSON.stringify(record)}\n`; try { appendPrivateFile(filePath, line); } catch (error) { if (!isMissing(error)) throw error; try { createPrivateFile(filePath, line); } catch (created) { if (errno(created) === 'EEXIST') appendPrivateFile(filePath, line); else throw created; } } }
|
|
258
|
+
export function writeBinaryPrivate(filePath: string, data: Buffer): void { writePrivateFile(filePath, data); }
|
|
259
|
+
function removePinnedEntry(name: string): void { const absolute = path.resolve(process.cwd(), name); const stat = fs.lstatSync(name); hook('recursive-removal', 'afterChildLstat', absolute, name); if (stat.isSymbolicLink() || stat.isFile()) { hook('recursive-removal', 'beforeChildUnlink', absolute, name); syscall('artifact-cleanup-unlink', () => fs.unlinkSync(name)); return; } if (!stat.isDirectory()) throw new Error(`refusing to remove unusual artifact entry: ${name}`); const parent = identity(fs.statSync('.')); const expected = identity(stat); hook('recursive-removal', 'beforeChildChdir', absolute, name); process.chdir(name); hook('recursive-removal', 'afterChildChdirBeforeIdentityCheck', absolute, name); if (!sameIdentity(expected, identity(fs.statSync('.')))) throw new Error(`artifact directory changed while removing: ${name}`); for (const child of fs.readdirSync('.')) removePinnedEntry(child); process.chdir('..'); if (!sameIdentity(parent, identity(fs.statSync('.')))) throw new Error('artifact parent changed while removing');
|
|
260
|
+
let removalName = name; let displaced = false;
|
|
261
|
+
try { displaced = !sameIdentity(expected, identity(fs.lstatSync(name))); } catch (error) { if (!isMissing(error)) throw error; displaced = true; }
|
|
262
|
+
if (displaced) {
|
|
263
|
+
removalName = fs.readdirSync('.').find(candidate => { try { return sameIdentity(expected, identity(fs.lstatSync(candidate))); } catch { return false; } }) ?? '';
|
|
264
|
+
if (!removalName) throw new Error(`artifact directory changed while removing: ${name}`);
|
|
157
265
|
}
|
|
158
|
-
|
|
159
|
-
fs.
|
|
266
|
+
hook('recursive-removal', 'beforeDirectoryRmdir', path.resolve(process.cwd(), removalName), removalName);
|
|
267
|
+
const finalEntry = fs.lstatSync(removalName);
|
|
268
|
+
if (!sameIdentity(expected, identity(finalEntry)) || !finalEntry.isDirectory()) throw new Error(`artifact directory changed while removing: ${name}`);
|
|
269
|
+
syscall('artifact-cleanup-unlink', () => fs.rmdirSync(removalName));
|
|
270
|
+
if (displaced) throw new Error(`artifact directory changed while removing: ${name}`);
|
|
160
271
|
}
|
|
272
|
+
export function removeArtifactTree(targetPath: string): void { try { pinnedParent(targetPath, removePinnedEntry, false); } catch (error) { if (!isMissing(error)) throw error; } }
|
|
161
273
|
|
|
162
|
-
|
|
163
|
-
export
|
|
164
|
-
|
|
274
|
+
export type PidBirth = { provider: 'linux-proc-v1'; bootId: string; startTicks: string } | { provider: 'darwin-kern-proc-v1'; startSec: string; startUsec: number };
|
|
275
|
+
export type PidBirthRead = { status: 'found'; identity: PidBirth } | { status: 'absent' } | { status: 'unknown'; reason: string };
|
|
276
|
+
export interface PidBirthProvider { read(pid: number): PidBirthRead; }
|
|
277
|
+
function unknown(reason: string): PidBirthRead { return { status: 'unknown', reason }; }
|
|
278
|
+
const BOOT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
279
|
+
/** True when `value` carries exactly `keys` — the strict-record shape gate shared with the log tailer. */
|
|
280
|
+
export function exactKeys(value: Record<string, unknown>, keys: string[]): boolean { const actual = Object.keys(value).sort(); return actual.length === keys.length && actual.every((key, index) => key === keys.slice().sort()[index]); }
|
|
281
|
+
/** Strictly parses a persisted PID-birth identity; anything malformed is undefined. */
|
|
282
|
+
export function parseBirth(value: unknown): PidBirth | undefined {
|
|
283
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
284
|
+
const b = value as Record<string, unknown>;
|
|
285
|
+
if (b.provider === 'linux-proc-v1' && exactKeys(b, ['provider', 'bootId', 'startTicks']) && typeof b.bootId === 'string' && BOOT_ID.test(b.bootId) && typeof b.startTicks === 'string' && /^[1-9][0-9]*$/.test(b.startTicks)) return { provider: 'linux-proc-v1', bootId: b.bootId, startTicks: b.startTicks };
|
|
286
|
+
if (b.provider === 'darwin-kern-proc-v1' && exactKeys(b, ['provider', 'startSec', 'startUsec']) && typeof b.startSec === 'string' && /^[1-9][0-9]*$/.test(b.startSec) && typeof b.startUsec === 'number' && Number.isInteger(b.startUsec) && b.startUsec >= 0 && b.startUsec < 1_000_000) return { provider: 'darwin-kern-proc-v1', startSec: b.startSec, startUsec: b.startUsec };
|
|
287
|
+
return undefined;
|
|
165
288
|
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
writePrivateFile(filePath, body);
|
|
289
|
+
export const MAX_LOG_LABEL_BYTES = 64;
|
|
290
|
+
/** Returns a diagnostic for an invalid log destination filename component, otherwise null. */
|
|
291
|
+
export function rejectLogLabel(label: string): string | null {
|
|
292
|
+
if (label.length === 0) return 'empty';
|
|
293
|
+
if (label === '.' || label === '..') return `\`${label}\` is not a filename`;
|
|
294
|
+
if (label.includes('/') || label.includes('\\')) return 'contains a path separator';
|
|
295
|
+
if (label.includes('\0')) return 'contains a NUL byte';
|
|
296
|
+
if (Buffer.byteLength(label, 'utf-8') > MAX_LOG_LABEL_BYTES) return `exceeds ${MAX_LOG_LABEL_BYTES} bytes`;
|
|
297
|
+
return null;
|
|
176
298
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*/
|
|
185
|
-
export function
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
299
|
+
export const LOG_TAILER_SOCKET_TOKEN = /^[0-9a-f]{16}$/;
|
|
300
|
+
export const LOG_TAILER_NONCE = /^[0-9a-f]{48}$/;
|
|
301
|
+
/** Private control-socket directory shared by every session log tailer. */
|
|
302
|
+
export const LOG_TAILER_SOCKET_DIR = path.join(CAPTURE_ROOT, 'sock');
|
|
303
|
+
const DECIMAL_IDENTITY = /^(?:0|[1-9][0-9]*)$/;
|
|
304
|
+
/** An identity-bearing registered log tailer — exactly the persisted `logPids` record shape. */
|
|
305
|
+
export interface RegisteredLogTailer { pid: number; name: string; sourcePath: string; birth: PidBirth; socketPath: string; socketDev: string; socketIno: string; nonce: string; }
|
|
306
|
+
/** Strictly parses a persisted log tailer registration; anything weaker is undefined. */
|
|
307
|
+
export function parseRegisteredLogTailer(value: unknown): RegisteredLogTailer | undefined {
|
|
308
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
309
|
+
const record = value as Record<string, unknown>;
|
|
310
|
+
const birth = parseBirth(record.birth);
|
|
311
|
+
const valid = birth !== undefined
|
|
312
|
+
&& exactKeys(record, ['pid', 'name', 'sourcePath', 'birth', 'socketPath', 'socketDev', 'socketIno', 'nonce'])
|
|
313
|
+
&& Number.isSafeInteger(record.pid) && (record.pid as number) > 0
|
|
314
|
+
&& typeof record.name === 'string' && rejectLogLabel(record.name) === null
|
|
315
|
+
&& typeof record.sourcePath === 'string' && path.isAbsolute(record.sourcePath)
|
|
316
|
+
&& typeof record.socketPath === 'string'
|
|
317
|
+
&& path.dirname(record.socketPath) === LOG_TAILER_SOCKET_DIR
|
|
318
|
+
&& LOG_TAILER_SOCKET_TOKEN.test(path.basename(record.socketPath, '.sock'))
|
|
319
|
+
&& path.extname(record.socketPath) === '.sock'
|
|
320
|
+
&& typeof record.socketDev === 'string' && DECIMAL_IDENTITY.test(record.socketDev)
|
|
321
|
+
&& typeof record.socketIno === 'string' && DECIMAL_IDENTITY.test(record.socketIno)
|
|
322
|
+
&& typeof record.nonce === 'string' && LOG_TAILER_NONCE.test(record.nonce);
|
|
323
|
+
// `valid` already implies a parsed birth, but the explicit disjunct is what
|
|
324
|
+
// narrows `birth` for the return type — do not "simplify" it away.
|
|
325
|
+
if (!valid || birth === undefined) return undefined;
|
|
326
|
+
return { pid: record.pid as number, name: record.name as string, sourcePath: record.sourcePath as string, birth, socketPath: record.socketPath as string, socketDev: record.socketDev as string, socketIno: record.socketIno as string, nonce: record.nonce as string };
|
|
203
327
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
328
|
+
export interface DarwinKernProcValidationContext { arch: string; selfPid: number; selfStartSeconds: number; nowSeconds: number; }
|
|
329
|
+
export function parseDarwinKernProc(snapshot: Buffer, pid: number, context: DarwinKernProcValidationContext = { arch: process.arch, selfPid: process.pid, selfStartSeconds: Math.floor(Date.now() / 1000 - process.uptime()), nowSeconds: Math.floor(Date.now() / 1000) }): PidBirthRead {
|
|
330
|
+
if (!['arm64', 'x64'].includes(context.arch) || !Number.isSafeInteger(pid) || pid <= 0 || !snapshot.length || snapshot.length % 648) return unknown('unexpected kern.proc layout');
|
|
331
|
+
const seen = new Set<number>(); let found: PidBirth | undefined; let selfStart: bigint | undefined; const future = BigInt(context.nowSeconds + 60);
|
|
332
|
+
for (let offset = 0; offset < snapshot.length; offset += 648) {
|
|
333
|
+
const recordPid = snapshot.readInt32LE(offset + 40); const sec = snapshot.readBigInt64LE(offset); const usec = snapshot.readBigInt64LE(offset + 8);
|
|
334
|
+
// kern.proc can include the kernel PID 0 record; it has no user-process
|
|
335
|
+
// birth timeval and is not a liveness candidate. Every real process record
|
|
336
|
+
// is globally validated before this snapshot can answer for any target.
|
|
337
|
+
if (recordPid < 0 || recordPid > 10_000_000 || seen.has(recordPid) || (recordPid !== 0 && (sec <= 0n || sec > future || usec < 0n || usec >= 1_000_000n))) return unknown('invalid kern.proc snapshot');
|
|
338
|
+
seen.add(recordPid); if (recordPid === context.selfPid) selfStart = sec;
|
|
339
|
+
if (recordPid === pid) found = { provider: 'darwin-kern-proc-v1', startSec: sec.toString(), startUsec: Number(usec) };
|
|
340
|
+
}
|
|
341
|
+
const expectedSelf = BigInt(Math.floor(context.selfStartSeconds));
|
|
342
|
+
if (!seen.has(1) || selfStart === undefined || abs(selfStart - expectedSelf) > 60n) return unknown('kern.proc snapshot missing or invalid self identity');
|
|
343
|
+
return found ? { status: 'found', identity: found } : { status: 'absent' };
|
|
212
344
|
}
|
|
345
|
+
export function parseLinuxProcStat(stat: string, expectedPid: number, bootId: string): PidBirthRead { if (!BOOT_ID.test(bootId.trim()) || !stat.startsWith(`${expectedPid} (`)) return unknown('malformed proc identity'); const close = stat.lastIndexOf(') '); const fields = close < 0 ? [] : stat.slice(close + 2).trim().split(/\s+/); const startTicks = fields[19]; if (!/^[1-9][0-9]*$/.test(startTicks ?? '')) return unknown('malformed proc identity'); return { status: 'found', identity: { provider: 'linux-proc-v1', bootId: bootId.trim(), startTicks } }; }
|
|
346
|
+
type ExecFileSync = typeof execFileSync;
|
|
347
|
+
let execFileSyncForProvider: ExecFileSync = execFileSync;
|
|
348
|
+
type LinuxProviderRead = (file: '/proc/sys/kernel/random/boot_id' | `/proc/${number}/stat`) => string;
|
|
349
|
+
let linuxProviderRead: LinuxProviderRead = file => fs.readFileSync(file, 'utf8');
|
|
350
|
+
/** Test-only provider seams are limited to process-identity inputs. */
|
|
351
|
+
export function __setArtifactTestExecFileSync(next?: ExecFileSync): void { execFileSyncForProvider = next ?? execFileSync; }
|
|
352
|
+
export function __setArtifactTestLinuxProviderRead(next?: LinuxProviderRead): void { linuxProviderRead = next ?? (file => fs.readFileSync(file, 'utf8')); }
|
|
353
|
+
export const processPidBirthProvider: PidBirthProvider = { read(pid) { try { if (!Number.isSafeInteger(pid) || pid <= 0) return unknown('invalid pid'); if (process.platform === 'linux') { let boot: string; try { boot = linuxProviderRead('/proc/sys/kernel/random/boot_id'); } catch (error) { return unknown(String(error)); } try { return parseLinuxProcStat(linuxProviderRead(`/proc/${pid}/stat`), pid, boot); } catch (error) { return isMissing(error) ? { status: 'absent' } : unknown(String(error)); } } if (process.platform === 'darwin') return parseDarwinKernProc(Buffer.from(execFileSyncForProvider('/usr/sbin/sysctl', ['-b', 'kern.proc'], { encoding: null, maxBuffer: 64 * 1024 * 1024, timeout: 1_000 }) as Buffer), pid); return unknown(`unsupported platform ${process.platform}`); } catch (error) { return unknown(String(error)); } } };
|
|
354
|
+
function abs(value: bigint): bigint { return value < 0n ? -value : value; }
|
|
213
355
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*/
|
|
226
|
-
export function
|
|
227
|
-
|
|
228
|
-
|
|
356
|
+
interface LockOwner { version: 1; token: string; pid: number; birth: PidBirth; leaseDeadlineNs: string; }
|
|
357
|
+
export interface PrivateLockHandle { readonly token: string; readonly ownerBirth: PidBirth; release(): void; }
|
|
358
|
+
export interface PrivateLockOptions { acquireTimeoutMs: number; leaseMs: number; pidBirthProvider?: PidBirthProvider; nowNs?: () => bigint; sleep?: (ms: number) => Promise<void>; token?: () => string; afterOwnerValidated?: () => void; afterOwnerRemoved?: () => void; beforeCanonicalRmdir?: () => void; beforePublishRename?: () => void; }
|
|
359
|
+
/** Field-exact birth-identity equality — the gate before any signal is authorized. */
|
|
360
|
+
export function sameBirth(a: PidBirth, b: PidBirth): boolean { return a.provider === b.provider && (a.provider === 'linux-proc-v1' ? a.bootId === (b as Extract<PidBirth, { provider: 'linux-proc-v1' }>).bootId && a.startTicks === (b as Extract<PidBirth, { provider: 'linux-proc-v1' }>).startTicks : a.startSec === (b as Extract<PidBirth, { provider: 'darwin-kern-proc-v1' }>).startSec && a.startUsec === (b as Extract<PidBirth, { provider: 'darwin-kern-proc-v1' }>).startUsec); }
|
|
361
|
+
function sameOwner(a: LockOwner, b: LockOwner): boolean { return a.token === b.token && a.pid === b.pid && a.leaseDeadlineNs === b.leaseDeadlineNs && sameBirth(a.birth, b.birth); }
|
|
362
|
+
function parseOwner(value: Buffer): LockOwner | undefined { try { const o = JSON.parse(value.toString('utf8')) as Record<string, unknown>; const birth = parseBirth(o?.birth); if (!o || Array.isArray(o) || !exactKeys(o, ['version', 'token', 'pid', 'birth', 'leaseDeadlineNs']) || o.version !== 1 || typeof o.token !== 'string' || !/^[0-9a-f]{32,}$/.test(o.token) || !Number.isSafeInteger(o.pid) || (o.pid as number) <= 0 || typeof o.leaseDeadlineNs !== 'string' || !/^(0|[1-9][0-9]*)$/.test(o.leaseDeadlineNs) || !birth) return undefined; BigInt(o.leaseDeadlineNs); return { version: 1, token: o.token, pid: o.pid as number, birth, leaseDeadlineNs: o.leaseDeadlineNs }; } catch { return undefined; } }
|
|
363
|
+
type OwnerState = { kind: 'missing' } | { kind: 'valid'; owner: LockOwner } | { kind: 'malformed' };
|
|
364
|
+
function ownerStateHere(): OwnerState { let fd: number; try { fd = openRegular('owner', fs.constants.O_RDONLY, false, undefined, 'lock-owner-close', 'lock'); } catch (error) { return isMissing(error) ? { kind: 'missing' } : { kind: 'malformed' }; } let state: OwnerState; try { const parsed = parseOwner(readFd(fd)); state = parsed ? { kind: 'valid', owner: parsed } : { kind: 'malformed' }; } catch { state = { kind: 'malformed' }; } try { closeDescriptor(fd, 'lock-owner-close'); } catch { return { kind: 'malformed' }; } return state; }
|
|
365
|
+
function validTiming(value: number, positive: boolean): boolean { return Number.isFinite(value) && value <= MAX_TIMING_MS && (positive ? value > 0 : value >= 0); }
|
|
366
|
+
const sleepDefault = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms));
|
|
367
|
+
/** Acquires a fully-published private lock directory; it never falls back to unlocked operation. */
|
|
368
|
+
export async function acquirePrivateLock(lockPath: string, options: PrivateLockOptions): Promise<PrivateLockHandle> {
|
|
369
|
+
if (!validTiming(options.acquireTimeoutMs, false) || !validTiming(options.leaseMs, true)) throw new Error('invalid private lock timing');
|
|
370
|
+
const canonical = assertUnderCaptureRoot(lockPath); const provider = options.pidBirthProvider ?? processPidBirthProvider; const self = provider.read(process.pid); if (self.status !== 'found') throw new Error(`cannot identify lock owner birth: ${self.status === 'unknown' ? self.reason : 'process absent'}`);
|
|
371
|
+
const now = options.nowNs ?? process.hrtime.bigint; const enforceTimeout = options.acquireTimeoutMs > 0; const deadline = now() + BigInt(Math.floor(options.acquireTimeoutMs * 1e6)); const sleep = options.sleep ?? sleepDefault; const random = options.token ?? lockToken;
|
|
372
|
+
const publish = (): { owner: LockOwner; generation: Identity } | undefined => pinnedParent(canonical, name => {
|
|
373
|
+
hook('lock', 'beforePublishAttempt', canonical, name);
|
|
374
|
+
const token = random(); if (!/^[0-9a-f]{32,}$/.test(token)) throw new Error('lock token source returned unsafe token');
|
|
375
|
+
const stage = `.${name}.${process.pid}.${token}.pending`; const owner: LockOwner = { version: 1, token, pid: process.pid, birth: self.identity, leaseDeadlineNs: (now() + BigInt(Math.floor(options.leaseMs * 1e6))).toString() };
|
|
376
|
+
let inside = false; let stageIdentity: Identity | undefined; let ownerIdentity: Identity | undefined; let primary: unknown;
|
|
377
|
+
try {
|
|
378
|
+
fs.mkdirSync(stage, { mode: DIR_MODE }); stageIdentity = identity(fs.lstatSync(stage));
|
|
379
|
+
const stageParent = identity(fs.statSync('.'));
|
|
380
|
+
hook('lock', 'afterStageMkdirPinned', path.resolve(process.cwd(), stage), stage);
|
|
381
|
+
hook('lock', 'beforeStageChdir', path.resolve(process.cwd(), stage), stage);
|
|
382
|
+
process.chdir(stage); inside = true;
|
|
383
|
+
hook('lock', 'afterStageChdirBeforeIdentityCheck', path.resolve(process.cwd()), stage);
|
|
384
|
+
if (!sameIdentity(stageIdentity, identity(fs.statSync('.'))) || !sameIdentity(stageParent, identity(fs.statSync('..')))) throw new Error('lock stage changed while pinning');
|
|
385
|
+
privateDirHere();
|
|
386
|
+
const fd = openRegular('owner', fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, true, opened => { ownerIdentity = identity(fs.fstatSync(opened)); }, 'lock-owner-close', 'lock'); let ownerPrimary: unknown;
|
|
387
|
+
try { writeAll(fd, JSON.stringify(owner), 'lock-owner-write'); syscall('lock-owner-fsync', () => fs.fsyncSync(fd)); hook('lock', 'afterOwnerWrite', path.resolve(process.cwd(), 'owner'), 'owner'); } catch (error) { ownerPrimary = error; }
|
|
388
|
+
try { closeDescriptor(fd, 'lock-owner-close'); } catch (closeError) { if (ownerPrimary) combineFailure(ownerPrimary, closeError, 'lock owner publication failed'); throw closeError; }
|
|
389
|
+
if (ownerPrimary) throw ownerPrimary;
|
|
390
|
+
const dfd = fs.openSync('.', fs.constants.O_RDONLY | fs.constants.O_DIRECTORY); try { syscall('lock-stage-dir-fsync', () => fs.fsyncSync(dfd)); } finally { fs.closeSync(dfd); }
|
|
391
|
+
process.chdir('..'); inside = false;
|
|
392
|
+
if (!sameIdentity(stageParent, identity(fs.statSync('.')))) throw new Error('lock stage parent changed before publication');
|
|
393
|
+
if (enforceTimeout && options.acquireTimeoutMs > 0 && now() >= deadline) throw new Error(`private lock acquisition timed out: ${canonical}`);
|
|
394
|
+
hook('lock', 'beforePublishRename', canonical, name); options.beforePublishRename?.();
|
|
395
|
+
const currentOwner = fs.lstatSync(path.join(stage, 'owner'));
|
|
396
|
+
if (!ownerIdentity || !currentOwner.isFile() || !sameIdentity(ownerIdentity, identity(currentOwner))) throw new Error('lock owner changed while publishing');
|
|
397
|
+
try {
|
|
398
|
+
syscall('lock-publish-rename', () => fs.renameSync(stage, name));
|
|
399
|
+
const generation = identity(fs.lstatSync(name));
|
|
400
|
+
if (enforceTimeout && options.acquireTimeoutMs > 0 && now() >= deadline) {
|
|
401
|
+
// The rename published this exact stage; retire only that retained generation.
|
|
402
|
+
if (sameIdentity(generation, identity(fs.lstatSync(name)))) { process.chdir(name); try { if (sameIdentity(generation, identity(fs.statSync('.')))) { try { fs.unlinkSync('owner'); } catch (error) { if (!isMissing(error)) throw error; } } } finally { process.chdir('..'); } try { if (sameIdentity(generation, identity(fs.lstatSync(name)))) fs.rmdirSync(name); } catch (error) { if (!isMissing(error) && errno(error) !== 'ENOTEMPTY') throw error; } }
|
|
403
|
+
throw new Error(`private lock acquisition timed out: ${canonical}`);
|
|
404
|
+
}
|
|
405
|
+
return { owner, generation };
|
|
406
|
+
} catch (error) { if (errno(error) === 'EEXIST' || errno(error) === 'ENOTEMPTY' || errno(error) === 'ENOTDIR') return undefined; throw error; }
|
|
407
|
+
} catch (error) { primary = error; throw error; } finally {
|
|
408
|
+
if (inside) process.chdir('..');
|
|
409
|
+
// Only remove the exact staging generation this attempt created. A substituted
|
|
410
|
+
// pathname is untrusted and deliberately left for its owner.
|
|
411
|
+
try {
|
|
412
|
+
const current = identity(fs.lstatSync(stage));
|
|
413
|
+
if (stageIdentity && sameIdentity(current, stageIdentity)) {
|
|
414
|
+
hook('lock', 'beforeStageCleanup', path.resolve(process.cwd(), stage), stage);
|
|
415
|
+
const rechecked = identity(fs.lstatSync(stage));
|
|
416
|
+
if (sameIdentity(rechecked, stageIdentity)) {
|
|
417
|
+
process.chdir(stage);
|
|
418
|
+
if (sameIdentity(stageIdentity, identity(fs.statSync('.')))) {
|
|
419
|
+
try { const child = fs.lstatSync('owner'); if (child.isFile()) { for (let attempt = 0; attempt < 2; attempt++) { try { syscall('artifact-cleanup-unlink', () => fs.unlinkSync('owner')); break; } catch (error) { if (isMissing(error) || attempt) throw error; } } } } catch (error) { if (!isMissing(error)) throw error; }
|
|
420
|
+
process.chdir('..');
|
|
421
|
+
const finalStage = fs.lstatSync(stage);
|
|
422
|
+
if (sameIdentity(stageIdentity, identity(finalStage)) && finalStage.isDirectory()) syscall('lock-canonical-rmdir', () => fs.rmdirSync(stage));
|
|
423
|
+
} else process.chdir('..');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} catch (error) { if (!isMissing(error) && errno(error) !== 'ENOTEMPTY') { if (primary) combineFailure(primary, error, 'lock stage cleanup failed'); throw error; } }
|
|
427
|
+
}
|
|
428
|
+
}, true, 'lock');
|
|
429
|
+
const retire = (): boolean => pinnedParent(canonical, name => {
|
|
430
|
+
try {
|
|
431
|
+
let stat: fs.Stats; try { stat = fs.lstatSync(name); } catch (error) { return isMissing(error); }
|
|
432
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('malformed private lock publication');
|
|
433
|
+
const parent = identity(fs.statSync('.')); const generation = identity(stat); try { process.chdir(name); } catch (error) { if (isMissing(error)) return true; throw error; }
|
|
434
|
+
if (!sameIdentity(generation, identity(fs.statSync('.')))) { process.chdir('..'); return true; }
|
|
435
|
+
const state = ownerStateHere(); process.chdir('..'); if (!sameIdentity(parent, identity(fs.statSync('.')))) return true;
|
|
436
|
+
if (state.kind === 'malformed') throw new Error('malformed private lock publication');
|
|
437
|
+
if (state.kind === 'missing') { hook('lock', 'beforeCanonicalRmdir', canonical, name); options.beforeCanonicalRmdir?.(); try { const final = fs.lstatSync(name); if (!sameIdentity(generation, identity(final)) || !final.isDirectory()) return true; syscall('lock-canonical-rmdir', () => fs.rmdirSync(name)); } catch (error) { if (!isMissing(error) && errno(error) !== 'ENOTEMPTY') throw error; } return true; }
|
|
438
|
+
const owner = state.owner; if (now() < BigInt(owner.leaseDeadlineNs)) return false;
|
|
439
|
+
const observed = provider.read(owner.pid); if (observed.status === 'unknown' || (observed.status === 'found' && sameBirth(observed.identity, owner.birth))) return false;
|
|
440
|
+
try { process.chdir(name); } catch (error) { if (isMissing(error)) return true; throw error; }
|
|
441
|
+
if (!sameIdentity(generation, identity(fs.statSync('.')))) { process.chdir('..'); return true; }
|
|
442
|
+
const current = ownerStateHere(); if (current.kind !== 'valid' || !sameOwner(current.owner, owner)) { process.chdir('..'); return true; }
|
|
443
|
+
hook('lock', 'afterCanonicalOwnerValidation', path.resolve(process.cwd(), 'owner'), 'owner'); options.afterOwnerValidated?.(); try { syscall('artifact-cleanup-unlink', () => fs.unlinkSync('owner')); } catch (error) { if (ownerStateHere().kind !== 'missing') throw error; } hook('lock', 'afterOwnerRemoval', path.resolve(process.cwd(), 'owner'), 'owner'); options.afterOwnerRemoved?.(); process.chdir('..'); hook('lock', 'beforeCanonicalRmdir', canonical, name); options.beforeCanonicalRmdir?.();
|
|
444
|
+
try { const final = fs.lstatSync(name); if (!sameIdentity(generation, identity(final)) || !final.isDirectory()) return true; syscall('lock-canonical-rmdir', () => fs.rmdirSync(name)); } catch (error) { if (!isMissing(error) && errno(error) !== 'ENOTEMPTY') throw error; } return true;
|
|
445
|
+
} catch (error) { if (isMissing(error)) return true; throw error; }
|
|
446
|
+
}, true, 'lock');
|
|
447
|
+
let immediate = true;
|
|
448
|
+
for (;;) {
|
|
449
|
+
if (!immediate && now() >= deadline) throw new Error(`private lock acquisition timed out: ${canonical}`);
|
|
450
|
+
immediate = false;
|
|
451
|
+
const published = publish();
|
|
452
|
+
if (published) {
|
|
453
|
+
const owner = published.owner;
|
|
454
|
+
let released = false; let ownerRemoved = false; const acquiredGeneration = published.generation;
|
|
455
|
+
return { token: owner.token, ownerBirth: self.identity, release() {
|
|
456
|
+
if (released) return;
|
|
457
|
+
pinnedParent(canonical, name => {
|
|
458
|
+
let stat: fs.Stats;
|
|
459
|
+
try { stat = fs.lstatSync(name); } catch (error) { if (isMissing(error)) { released = true; return; } throw error; }
|
|
460
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) return;
|
|
461
|
+
const generation = identity(stat);
|
|
462
|
+
if (!sameIdentity(acquiredGeneration, generation)) return;
|
|
463
|
+
const parent = identity(fs.statSync('.'));
|
|
464
|
+
if (!ownerRemoved) {
|
|
465
|
+
try { process.chdir(name); } catch (error) { if (isMissing(error)) { released = true; return; } throw error; }
|
|
466
|
+
if (!sameIdentity(generation, identity(fs.statSync('.')))) { process.chdir('..'); return; }
|
|
467
|
+
const current = ownerStateHere();
|
|
468
|
+
if (current.kind !== 'valid' || !sameOwner(current.owner, owner)) { process.chdir('..'); return; }
|
|
469
|
+
hook('lock', 'afterCanonicalOwnerValidation', path.resolve(process.cwd(), 'owner'), 'owner'); options.afterOwnerValidated?.(); try { syscall('artifact-cleanup-unlink', () => fs.unlinkSync('owner')); ownerRemoved = true; } catch (error) { if (ownerStateHere().kind === 'missing') ownerRemoved = true; throw error; } hook('lock', 'afterOwnerRemoval', path.resolve(process.cwd(), 'owner'), 'owner'); options.afterOwnerRemoved?.(); process.chdir('..');
|
|
470
|
+
}
|
|
471
|
+
if (!sameIdentity(parent, identity(fs.statSync('.')))) throw new Error('lock parent changed before release');
|
|
472
|
+
hook('lock', 'beforeCanonicalRmdir', canonical, name); options.beforeCanonicalRmdir?.();
|
|
473
|
+
try { const final = fs.lstatSync(name); if (!sameIdentity(acquiredGeneration, identity(final)) || !final.isDirectory()) return; syscall('lock-canonical-rmdir', () => fs.rmdirSync(name)); released = true; } catch (error) { if (isMissing(error)) { released = true; return; } if (errno(error) !== 'ENOTEMPTY') throw error; }
|
|
474
|
+
}, true, 'lock');
|
|
475
|
+
} };
|
|
476
|
+
}
|
|
477
|
+
retire();
|
|
478
|
+
if (now() >= deadline) throw new Error(`private lock acquisition timed out: ${canonical}`);
|
|
479
|
+
await sleep(Math.max(0, Math.min(10, Number((deadline - now()) / 1_000_000n))));
|
|
480
|
+
}
|
|
229
481
|
}
|