@smthrs/kernel 0.0.0-stage → 1.0.0-rc.3
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 +206 -0
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/cjs/CapabilitySet.d.ts +6 -0
- package/dist/cjs/CapabilitySet.d.ts.map +1 -0
- package/dist/cjs/CapabilitySet.js +19 -0
- package/dist/cjs/CapabilitySet.js.map +7 -0
- package/dist/cjs/ChildProcessEnvironment.d.ts +60 -0
- package/dist/cjs/ChildProcessEnvironment.d.ts.map +1 -0
- package/dist/cjs/ChildProcessEnvironment.js +61 -0
- package/dist/cjs/ChildProcessEnvironment.js.map +7 -0
- package/dist/cjs/ChildProcessSpawner.d.ts +100 -0
- package/dist/cjs/ChildProcessSpawner.d.ts.map +1 -0
- package/dist/cjs/ChildProcessSpawner.js +291 -0
- package/dist/cjs/ChildProcessSpawner.js.map +7 -0
- package/dist/cjs/CommandLine.d.ts +173 -0
- package/dist/cjs/CommandLine.d.ts.map +1 -0
- package/dist/cjs/CommandLine.js +54 -0
- package/dist/cjs/CommandLine.js.map +7 -0
- package/dist/cjs/ContainedSpawner.d.ts +135 -0
- package/dist/cjs/ContainedSpawner.d.ts.map +1 -0
- package/dist/cjs/ContainedSpawner.js +141 -0
- package/dist/cjs/ContainedSpawner.js.map +7 -0
- package/dist/cjs/FileSystem.d.ts +452 -0
- package/dist/cjs/FileSystem.d.ts.map +1 -0
- package/dist/cjs/FileSystem.js +890 -0
- package/dist/cjs/FileSystem.js.map +7 -0
- package/dist/cjs/FileSystemBatch.d.ts +94 -0
- package/dist/cjs/FileSystemBatch.d.ts.map +1 -0
- package/dist/cjs/FileSystemBatch.js +31 -0
- package/dist/cjs/FileSystemBatch.js.map +7 -0
- package/dist/cjs/GrantEvent.d.ts +240 -0
- package/dist/cjs/GrantEvent.d.ts.map +1 -0
- package/dist/cjs/GrantEvent.js +115 -0
- package/dist/cjs/GrantEvent.js.map +7 -0
- package/dist/cjs/GrantStore.d.ts +270 -0
- package/dist/cjs/GrantStore.d.ts.map +1 -0
- package/dist/cjs/GrantStore.js +857 -0
- package/dist/cjs/GrantStore.js.map +7 -0
- package/dist/cjs/HostError.d.ts +36 -0
- package/dist/cjs/HostError.d.ts.map +1 -0
- package/dist/cjs/HostError.js +17 -0
- package/dist/cjs/HostError.js.map +7 -0
- package/dist/cjs/HostServices.d.ts +101 -0
- package/dist/cjs/HostServices.d.ts.map +1 -0
- package/dist/cjs/HostServices.js +66 -0
- package/dist/cjs/HostServices.js.map +7 -0
- package/dist/cjs/HttpClient.d.ts +177 -0
- package/dist/cjs/HttpClient.d.ts.map +1 -0
- package/dist/cjs/HttpClient.js +217 -0
- package/dist/cjs/HttpClient.js.map +7 -0
- package/dist/cjs/Jj.d.ts +67 -0
- package/dist/cjs/Jj.d.ts.map +1 -0
- package/dist/cjs/Jj.js +118 -0
- package/dist/cjs/Jj.js.map +7 -0
- package/dist/cjs/JournalGrantStore.d.ts +65 -0
- package/dist/cjs/JournalGrantStore.d.ts.map +1 -0
- package/dist/cjs/JournalGrantStore.js +354 -0
- package/dist/cjs/JournalGrantStore.js.map +7 -0
- package/dist/cjs/Path.d.ts +37 -0
- package/dist/cjs/Path.d.ts.map +1 -0
- package/dist/cjs/Path.js +46 -0
- package/dist/cjs/Path.js.map +7 -0
- package/dist/cjs/ProcessConfinement.d.ts +55 -0
- package/dist/cjs/ProcessConfinement.d.ts.map +1 -0
- package/dist/cjs/ProcessConfinement.js +94 -0
- package/dist/cjs/ProcessConfinement.js.map +7 -0
- package/dist/cjs/ProcessLedger.d.ts +260 -0
- package/dist/cjs/ProcessLedger.d.ts.map +1 -0
- package/dist/cjs/ProcessLedger.js +190 -0
- package/dist/cjs/ProcessLedger.js.map +7 -0
- package/dist/cjs/Rooted.d.ts +53 -0
- package/dist/cjs/Rooted.d.ts.map +1 -0
- package/dist/cjs/Rooted.js +101 -0
- package/dist/cjs/Rooted.js.map +7 -0
- package/dist/cjs/Unreachable.d.ts +27 -0
- package/dist/cjs/Unreachable.d.ts.map +1 -0
- package/dist/cjs/Unreachable.js +43 -0
- package/dist/cjs/Unreachable.js.map +7 -0
- package/dist/cjs/Workspace.d.ts +62 -0
- package/dist/cjs/Workspace.d.ts.map +1 -0
- package/dist/cjs/Workspace.js +35 -0
- package/dist/cjs/Workspace.js.map +7 -0
- package/dist/cjs/index.d.ts +161 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +73 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/Containment.d.ts +24 -0
- package/dist/cjs/internal/Containment.d.ts.map +1 -0
- package/dist/cjs/internal/Containment.js +30 -0
- package/dist/cjs/internal/Containment.js.map +7 -0
- package/dist/cjs/internal/Pipeline.d.ts +18 -0
- package/dist/cjs/internal/Pipeline.d.ts.map +1 -0
- package/dist/cjs/internal/Pipeline.js +90 -0
- package/dist/cjs/internal/Pipeline.js.map +7 -0
- package/dist/cjs/internal/makeCapability.d.ts +16 -0
- package/dist/cjs/internal/makeCapability.d.ts.map +1 -0
- package/dist/cjs/internal/makeCapability.js +44 -0
- package/dist/cjs/internal/makeCapability.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/test/FileSystemBatchContract.d.ts +15 -0
- package/dist/cjs/test/FileSystemBatchContract.d.ts.map +1 -0
- package/dist/cjs/test/FileSystemBatchContract.js +76 -0
- package/dist/cjs/test/FileSystemBatchContract.js.map +7 -0
- package/dist/cjs/test/HostContract.d.ts +225 -0
- package/dist/cjs/test/HostContract.d.ts.map +1 -0
- package/dist/cjs/test/HostContract.js +489 -0
- package/dist/cjs/test/HostContract.js.map +7 -0
- package/dist/cjs/test/TestGrantStore.d.ts +31 -0
- package/dist/cjs/test/TestGrantStore.d.ts.map +1 -0
- package/dist/cjs/test/TestGrantStore.js +65 -0
- package/dist/cjs/test/TestGrantStore.js.map +7 -0
- package/dist/esm/CapabilitySet.d.ts +6 -0
- package/dist/esm/CapabilitySet.d.ts.map +1 -0
- package/dist/esm/CapabilitySet.js +6 -0
- package/dist/esm/CapabilitySet.js.map +1 -0
- package/dist/esm/ChildProcessEnvironment.d.ts +60 -0
- package/dist/esm/ChildProcessEnvironment.d.ts.map +1 -0
- package/dist/esm/ChildProcessEnvironment.js +91 -0
- package/dist/esm/ChildProcessEnvironment.js.map +1 -0
- package/dist/esm/ChildProcessSpawner.d.ts +100 -0
- package/dist/esm/ChildProcessSpawner.d.ts.map +1 -0
- package/dist/esm/ChildProcessSpawner.js +342 -0
- package/dist/esm/ChildProcessSpawner.js.map +1 -0
- package/dist/esm/CommandLine.d.ts +173 -0
- package/dist/esm/CommandLine.d.ts.map +1 -0
- package/dist/esm/CommandLine.js +195 -0
- package/dist/esm/CommandLine.js.map +1 -0
- package/dist/esm/ContainedSpawner.d.ts +135 -0
- package/dist/esm/ContainedSpawner.d.ts.map +1 -0
- package/dist/esm/ContainedSpawner.js +180 -0
- package/dist/esm/ContainedSpawner.js.map +1 -0
- package/dist/esm/FileSystem.d.ts +452 -0
- package/dist/esm/FileSystem.d.ts.map +1 -0
- package/dist/esm/FileSystem.js +989 -0
- package/dist/esm/FileSystem.js.map +1 -0
- package/dist/esm/FileSystemBatch.d.ts +94 -0
- package/dist/esm/FileSystemBatch.d.ts.map +1 -0
- package/dist/esm/FileSystemBatch.js +26 -0
- package/dist/esm/FileSystemBatch.js.map +1 -0
- package/dist/esm/GrantEvent.d.ts +240 -0
- package/dist/esm/GrantEvent.d.ts.map +1 -0
- package/dist/esm/GrantEvent.js +155 -0
- package/dist/esm/GrantEvent.js.map +1 -0
- package/dist/esm/GrantStore.d.ts +270 -0
- package/dist/esm/GrantStore.d.ts.map +1 -0
- package/dist/esm/GrantStore.js +951 -0
- package/dist/esm/GrantStore.js.map +1 -0
- package/dist/esm/HostError.d.ts +36 -0
- package/dist/esm/HostError.d.ts.map +1 -0
- package/dist/esm/HostError.js +20 -0
- package/dist/esm/HostError.js.map +1 -0
- package/dist/esm/HostServices.d.ts +101 -0
- package/dist/esm/HostServices.d.ts.map +1 -0
- package/dist/esm/HostServices.js +76 -0
- package/dist/esm/HostServices.js.map +1 -0
- package/dist/esm/HttpClient.d.ts +177 -0
- package/dist/esm/HttpClient.d.ts.map +1 -0
- package/dist/esm/HttpClient.js +299 -0
- package/dist/esm/HttpClient.js.map +1 -0
- package/dist/esm/Jj.d.ts +67 -0
- package/dist/esm/Jj.d.ts.map +1 -0
- package/dist/esm/Jj.js +111 -0
- package/dist/esm/Jj.js.map +1 -0
- package/dist/esm/JournalGrantStore.d.ts +65 -0
- package/dist/esm/JournalGrantStore.d.ts.map +1 -0
- package/dist/esm/JournalGrantStore.js +350 -0
- package/dist/esm/JournalGrantStore.js.map +1 -0
- package/dist/esm/Path.d.ts +37 -0
- package/dist/esm/Path.d.ts.map +1 -0
- package/dist/esm/Path.js +34 -0
- package/dist/esm/Path.js.map +1 -0
- package/dist/esm/ProcessConfinement.d.ts +55 -0
- package/dist/esm/ProcessConfinement.d.ts.map +1 -0
- package/dist/esm/ProcessConfinement.js +106 -0
- package/dist/esm/ProcessConfinement.js.map +1 -0
- package/dist/esm/ProcessLedger.d.ts +260 -0
- package/dist/esm/ProcessLedger.d.ts.map +1 -0
- package/dist/esm/ProcessLedger.js +283 -0
- package/dist/esm/ProcessLedger.js.map +1 -0
- package/dist/esm/Rooted.d.ts +53 -0
- package/dist/esm/Rooted.d.ts.map +1 -0
- package/dist/esm/Rooted.js +104 -0
- package/dist/esm/Rooted.js.map +1 -0
- package/dist/esm/Unreachable.d.ts +27 -0
- package/dist/esm/Unreachable.d.ts.map +1 -0
- package/dist/esm/Unreachable.js +26 -0
- package/dist/esm/Unreachable.js.map +1 -0
- package/dist/esm/Workspace.d.ts +62 -0
- package/dist/esm/Workspace.d.ts.map +1 -0
- package/dist/esm/Workspace.js +51 -0
- package/dist/esm/Workspace.js.map +1 -0
- package/dist/esm/index.d.ts +161 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +151 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/Containment.d.ts +24 -0
- package/dist/esm/internal/Containment.d.ts.map +1 -0
- package/dist/esm/internal/Containment.js +27 -0
- package/dist/esm/internal/Containment.js.map +1 -0
- package/dist/esm/internal/Pipeline.d.ts +18 -0
- package/dist/esm/internal/Pipeline.d.ts.map +1 -0
- package/dist/esm/internal/Pipeline.js +68 -0
- package/dist/esm/internal/Pipeline.js.map +1 -0
- package/dist/esm/internal/makeCapability.d.ts +16 -0
- package/dist/esm/internal/makeCapability.d.ts.map +1 -0
- package/dist/esm/internal/makeCapability.js +22 -0
- package/dist/esm/internal/makeCapability.js.map +1 -0
- package/dist/esm/test/FileSystemBatchContract.d.ts +15 -0
- package/dist/esm/test/FileSystemBatchContract.d.ts.map +1 -0
- package/dist/esm/test/FileSystemBatchContract.js +57 -0
- package/dist/esm/test/FileSystemBatchContract.js.map +1 -0
- package/dist/esm/test/HostContract.d.ts +225 -0
- package/dist/esm/test/HostContract.d.ts.map +1 -0
- package/dist/esm/test/HostContract.js +460 -0
- package/dist/esm/test/HostContract.js.map +1 -0
- package/dist/esm/test/TestGrantStore.d.ts +31 -0
- package/dist/esm/test/TestGrantStore.d.ts.map +1 -0
- package/dist/esm/test/TestGrantStore.js +57 -0
- package/dist/esm/test/TestGrantStore.js.map +1 -0
- package/docs/README.md +162 -0
- package/docs/api.md +1532 -0
- package/docs/concepts/decoration-in-place.md +117 -0
- package/docs/concepts/filesystem-batches.md +58 -0
- package/docs/concepts/filesystem-confinement.md +168 -0
- package/docs/concepts/grant-decisions.md +181 -0
- package/docs/concepts/process-containment.md +155 -0
- package/docs/contract.md +14 -0
- package/docs/guides/adapt-a-new-host-platform.md +148 -0
- package/docs/guides/answer-permission-requests.md +151 -0
- package/docs/guides/authorize-network-and-model-calls.md +118 -0
- package/docs/guides/contain-spawned-processes.md +118 -0
- package/docs/guides/guard-a-host-bundle.md +109 -0
- package/docs/guides/persist-grants-across-restarts.md +135 -0
- package/docs/guides/write-a-capability-policy.md +159 -0
- package/docs/installation.md +115 -0
- package/docs/quickstart.md +182 -0
- package/docs/testing.md +108 -0
- package/docs/troubleshooting.md +281 -0
- package/package.json +344 -3
- package/src/CapabilitySet.ts +6 -0
- package/src/ChildProcessEnvironment.ts +98 -0
- package/src/ChildProcessSpawner.ts +414 -0
- package/src/CommandLine.ts +250 -0
- package/src/ContainedSpawner.ts +284 -0
- package/src/FileSystem.ts +1655 -0
- package/src/FileSystemBatch.ts +90 -0
- package/src/GrantEvent.ts +174 -0
- package/src/GrantStore.ts +1345 -0
- package/src/HostError.ts +37 -0
- package/src/HostServices.ts +133 -0
- package/src/HttpClient.ts +406 -0
- package/src/Jj.ts +159 -0
- package/src/JournalGrantStore.ts +461 -0
- package/src/Path.ts +47 -0
- package/src/ProcessConfinement.ts +156 -0
- package/src/ProcessLedger.ts +423 -0
- package/src/Rooted.ts +128 -0
- package/src/Unreachable.ts +30 -0
- package/src/Workspace.ts +65 -0
- package/src/index.ts +181 -0
- package/src/internal/Containment.ts +37 -0
- package/src/internal/Pipeline.ts +85 -0
- package/src/internal/makeCapability.ts +26 -0
- package/src/test/FileSystemBatchContract.ts +59 -0
- package/src/test/HostContract.ts +785 -0
- package/src/test/TestGrantStore.ts +73 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 1.0.0-rc.0
|
|
3
|
+
*
|
|
4
|
+
* Rendering an `effect/unstable/process` `Command` back to a shell command
|
|
5
|
+
* line.
|
|
6
|
+
*
|
|
7
|
+
* Two callers need the same string and must agree on it exactly:
|
|
8
|
+
*
|
|
9
|
+
* 1. `@smthrs/kernel/ChildProcessSpawner` uses it as the `proc:spawn`
|
|
10
|
+
* capability resource, so a grant reads the way an operator wrote it; and
|
|
11
|
+
* 2. `@smthrs/platform-browser/BrowserChildProcessSpawner` uses it as the line
|
|
12
|
+
* it hands to the in-browser bash interpreter, which has no `argv` to spawn
|
|
13
|
+
* with.
|
|
14
|
+
*
|
|
15
|
+
* A single renderer keeps a granted capability and the command a browser
|
|
16
|
+
* actually runs from drifting apart.
|
|
17
|
+
*
|
|
18
|
+
* The module is pure string handling — no host access — so it stays on the
|
|
19
|
+
* browser-safe side of the package.
|
|
20
|
+
*/
|
|
21
|
+
import { isInheritedName } from "./ChildProcessEnvironment.js";
|
|
22
|
+
/** Tokens made only of these characters need no quoting in a POSIX shell. */
|
|
23
|
+
const SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
24
|
+
/**
|
|
25
|
+
* Quotes one token for a POSIX shell, leaving obviously safe tokens alone.
|
|
26
|
+
*
|
|
27
|
+
* @category rendering
|
|
28
|
+
* @since 1.0.0-rc.0
|
|
29
|
+
*/
|
|
30
|
+
export const quote = (token) => token !== "" && SAFE.test(token) ? token : `'${token.replaceAll("'", `'\\''`)}'`;
|
|
31
|
+
/**
|
|
32
|
+
* Renders a `Command` as a single shell command line.
|
|
33
|
+
*
|
|
34
|
+
* A standard command with `shell: true` renders its tokens verbatim, matching
|
|
35
|
+
* the line Node and the browser interpreter hand to the default shell. A
|
|
36
|
+
* custom shell renders as an explicit `<shell> -c <line>` invocation so the
|
|
37
|
+
* selected executable is part of the permission resource. Without `shell`,
|
|
38
|
+
* every token is POSIX-quoted to preserve literal argv semantics. These
|
|
39
|
+
* distinctions are security-sensitive: the rendered value is also the
|
|
40
|
+
* `proc:spawn` capability resource, so it must describe what will execute.
|
|
41
|
+
* The rendering is POSIX-only by contract: on Windows, Node invokes the shell
|
|
42
|
+
* with `/d /s /c` rather than `-c`, so the rendered line describes the POSIX
|
|
43
|
+
* invocation, not a cmd.exe one.
|
|
44
|
+
*
|
|
45
|
+
* The `proc:spawn` capability resource is {@link resource}, which starts from
|
|
46
|
+
* this line and adds what the line alone cannot show.
|
|
47
|
+
*
|
|
48
|
+
* A `PipedCommand` renders with `|` between its sides. That is a faithful
|
|
49
|
+
* rendering of what the pipeline does, and it is the only form an interpreter
|
|
50
|
+
* that takes a command line rather than an `argv` can be given. `from`/`to`
|
|
51
|
+
* pipe options are *not* expressible this way; rendering ignores them, so a
|
|
52
|
+
* pipeline that redirects `stderr` renders like one that pipes `stdout`.
|
|
53
|
+
* Capability checks therefore see the commands, never the plumbing.
|
|
54
|
+
*
|
|
55
|
+
* @category rendering
|
|
56
|
+
* @since 1.0.0-rc.0
|
|
57
|
+
*/
|
|
58
|
+
export const render = (command) => command._tag === "StandardCommand"
|
|
59
|
+
? command.options.shell === undefined || command.options.shell === false
|
|
60
|
+
? [command.command, ...command.args].map(quote).join(" ")
|
|
61
|
+
: command.options.shell === true
|
|
62
|
+
? [command.command, ...command.args].join(" ")
|
|
63
|
+
: `${quote(command.options.shell)} -c ${quote([command.command, ...command.args].join(" "))}`
|
|
64
|
+
: `${render(command.left)} | ${render(command.right)}`;
|
|
65
|
+
/**
|
|
66
|
+
* Shell syntax that chains, substitutes, groups, or redirects: everything that
|
|
67
|
+
* lets one shell line run or write more than a single simple command.
|
|
68
|
+
*/
|
|
69
|
+
const shellControl = /[;&|`$<>()\n\r]/;
|
|
70
|
+
/**
|
|
71
|
+
* Redirections that neither run nor write anything a grant should see: an fd
|
|
72
|
+
* duplication such as `2>&1` and a discard to `/dev/null`. They are removed
|
|
73
|
+
* before {@link shellControl} is tested, so `git status 2>&1` keeps its
|
|
74
|
+
* verbatim resource. Only a space or tab may sit between `>` and `/dev/null`:
|
|
75
|
+
* `>` followed by a line break is not a discard, so it stays control syntax.
|
|
76
|
+
*/
|
|
77
|
+
const harmlessRedirect = /(^|\s)(?:\d*>&\d+|\d*>[ \t]*\/dev\/null)(?=\s|$)/g;
|
|
78
|
+
/**
|
|
79
|
+
* Names of the environment variables a stage sets that the child would not
|
|
80
|
+
* otherwise inherit: every name outside the bootstrap set, and a bootstrap
|
|
81
|
+
* name whose value differs from the ambient one.
|
|
82
|
+
*/
|
|
83
|
+
const overriddenNames = (command, ambient) => Object.entries(command.options.env ?? {})
|
|
84
|
+
.filter(([name, value]) => value !== undefined && (!isInheritedName(name) || ambient[name] !== value))
|
|
85
|
+
.map(([name]) => name)
|
|
86
|
+
.sort();
|
|
87
|
+
/**
|
|
88
|
+
* The stages of a command in execution order: the command itself, or every
|
|
89
|
+
* `StandardCommand` of a pipeline from left to right. The spawner checks each
|
|
90
|
+
* stage as its own `proc:spawn` capability.
|
|
91
|
+
*
|
|
92
|
+
* @category rendering
|
|
93
|
+
* @since 1.0.0-rc.1
|
|
94
|
+
*/
|
|
95
|
+
export const stages = (command) => command._tag === "StandardCommand" ? [command] : [...stages(command.left), ...stages(command.right)];
|
|
96
|
+
/**
|
|
97
|
+
* The `proc:spawn` capability resource for one stage: {@link render}, with
|
|
98
|
+
* three additions that keep a prefix grant from authorizing code the stage
|
|
99
|
+
* never named.
|
|
100
|
+
*
|
|
101
|
+
* - A `shell: true` stage whose line holds shell control syntax (`;`, `&`,
|
|
102
|
+
* `|`, `` ` ``, `$`, `<`, `>`, `(`, `)`, or a line break) is marked as the
|
|
103
|
+
* explicit `sh -c '<line>'` it is. A grant such as `git status *` then
|
|
104
|
+
* cannot match `git status; curl x | sh`, while a simple shell line such as
|
|
105
|
+
* `git status --short` keeps its verbatim resource. An fd duplication such
|
|
106
|
+
* as `2>&1` and a discard to `/dev/null` do not count as control syntax.
|
|
107
|
+
* - A stage that sets an environment variable the child would not otherwise
|
|
108
|
+
* inherit is prefixed with `env <NAME>… -- `, sorted names only. That is
|
|
109
|
+
* every name outside the bootstrap set `ChildProcessEnvironment` inherits
|
|
110
|
+
* (`PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`, `SHELL`, `LC_*`), and a
|
|
111
|
+
* bootstrap name whose value differs from `context.ambient`. A grant for
|
|
112
|
+
* `git status` covers neither `env GIT_SSH_COMMAND -- git status` nor
|
|
113
|
+
* `env PATH -- git status`. Values never enter the resource because they
|
|
114
|
+
* often carry credentials.
|
|
115
|
+
* - A stage whose `cwd` `context.contains` rejects is prefixed with
|
|
116
|
+
* `cwd <path> -- `, so a grant does not follow the command into a
|
|
117
|
+
* directory whose configuration the flow does not govern. When
|
|
118
|
+
* `context.resolvedCwd` is given, that real path is judged and named
|
|
119
|
+
* instead of the lexical `cwd`.
|
|
120
|
+
*
|
|
121
|
+
* A pipeline has no single resource: the spawner checks every one of its
|
|
122
|
+
* {@link stages}, so `git status | sh` needs a grant for `sh` too.
|
|
123
|
+
*
|
|
124
|
+
* The `env` and `cwd` prefixes share their spelling with a literal command:
|
|
125
|
+
* `make("env", ["PATH", "--", "git", "status"])` renders as
|
|
126
|
+
* `env PATH -- git status`, the resource of `git status` with `PATH`
|
|
127
|
+
* overridden. The alias grants nothing extra. The literal command runs a
|
|
128
|
+
* program named `PATH` (or `cwd`) from the ambient `PATH`, not `git`. A grant
|
|
129
|
+
* with `*` inside the prefix, such as `env * -- git status`, also matches
|
|
130
|
+
* `env -C /x -- git status`, but it already admits any variable name,
|
|
131
|
+
* including `GIT_SSH_COMMAND` and `LD_PRELOAD`, which run arbitrary code.
|
|
132
|
+
* Grant a prefixed resource exactly as the request shows it.
|
|
133
|
+
*
|
|
134
|
+
* @category rendering
|
|
135
|
+
* @since 1.0.0-rc.1
|
|
136
|
+
*/
|
|
137
|
+
export const resource = (command, context = {}) => {
|
|
138
|
+
const line = render(command);
|
|
139
|
+
const shown = command.options.shell === true && shellControl.test(line.replace(harmlessRedirect, "$1"))
|
|
140
|
+
? `sh -c ${quote(line)}`
|
|
141
|
+
: line;
|
|
142
|
+
const names = overriddenNames(command, context.ambient ?? {});
|
|
143
|
+
const withEnv = names.length === 0 ? shown : `env ${names.map(quote).join(" ")} -- ${shown}`;
|
|
144
|
+
const directory = command.options.cwd === undefined ? undefined : context.resolvedCwd ?? command.options.cwd;
|
|
145
|
+
return directory === undefined || context.contains?.(directory) === true
|
|
146
|
+
? withEnv
|
|
147
|
+
: `cwd ${quote(directory)} -- ${withEnv}`;
|
|
148
|
+
};
|
|
149
|
+
/** The leading token of a shell line, which is the program the line runs. */
|
|
150
|
+
const program = (line) => {
|
|
151
|
+
const trimmed = line.trimStart();
|
|
152
|
+
const end = trimmed.search(/\s/);
|
|
153
|
+
return end === -1 ? trimmed : trimmed.slice(0, end);
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The executable a command runs, without its arguments.
|
|
157
|
+
*
|
|
158
|
+
* A durable record of a spawned process names its program with this rather
|
|
159
|
+
* than with {@link render}: arguments carry credentials (`curl -u user:pass`,
|
|
160
|
+
* `mysql -phunter2`), and a journal that keeps them keeps them permanently.
|
|
161
|
+
* Nothing that reads such a record needs more than the program name; a reaper
|
|
162
|
+
* matches processes by pid and process group.
|
|
163
|
+
*
|
|
164
|
+
* Without `shell`, `command` is the executable itself, spaces and all. With a
|
|
165
|
+
* shell, the line is what runs, so the program is its first token: both
|
|
166
|
+
* `make("mysql -phunter2", [], { shell: true })` and
|
|
167
|
+
* `make("mysql", ["-phunter2"])` yield `mysql`. A pipeline names one
|
|
168
|
+
* executable per stage, joined the way {@link render} joins the stages.
|
|
169
|
+
*
|
|
170
|
+
* @category rendering
|
|
171
|
+
* @since 1.0.0-rc.0
|
|
172
|
+
*/
|
|
173
|
+
export const executable = (command) => command._tag === "StandardCommand"
|
|
174
|
+
? command.options.shell === undefined || command.options.shell === false
|
|
175
|
+
? command.command
|
|
176
|
+
: program(command.command)
|
|
177
|
+
: `${executable(command.left)} | ${executable(command.right)}`;
|
|
178
|
+
/**
|
|
179
|
+
* The working directory a command runs in, taking the leftmost stage of a
|
|
180
|
+
* pipeline, which is the stage `setCwd` and the spawners agree to treat as the
|
|
181
|
+
* pipeline's own directory.
|
|
182
|
+
*
|
|
183
|
+
* @category rendering
|
|
184
|
+
* @since 1.0.0-rc.0
|
|
185
|
+
*/
|
|
186
|
+
export const cwd = (command) => command._tag === "StandardCommand" ? command.options.cwd : cwd(command.left);
|
|
187
|
+
/**
|
|
188
|
+
* The environment overrides a command runs with, taking the leftmost stage of
|
|
189
|
+
* a pipeline for the same reason {@link cwd} does.
|
|
190
|
+
*
|
|
191
|
+
* @category rendering
|
|
192
|
+
* @since 1.0.0-rc.0
|
|
193
|
+
*/
|
|
194
|
+
export const env = (command) => command._tag === "StandardCommand" ? command.options.env : env(command.left);
|
|
195
|
+
//# sourceMappingURL=CommandLine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandLine.js","sourceRoot":"","sources":["../../src/CommandLine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @since 1.0.0-rc.0\n *\n * Rendering an `effect/unstable/process` `Command` back to a shell command\n * line.\n *\n * Two callers need the same string and must agree on it exactly:\n *\n * 1. `@smthrs/kernel/ChildProcessSpawner` uses it as the `proc:spawn`\n * capability resource, so a grant reads the way an operator wrote it; and\n * 2. `@smthrs/platform-browser/BrowserChildProcessSpawner` uses it as the line\n * it hands to the in-browser bash interpreter, which has no `argv` to spawn\n * with.\n *\n * A single renderer keeps a granted capability and the command a browser\n * actually runs from drifting apart.\n *\n * The module is pure string handling — no host access — so it stays on the\n * browser-safe side of the package.\n */\n\nimport type * as ChildProcess from \"effect/unstable/process/ChildProcess\"\nimport { isInheritedName } from \"./ChildProcessEnvironment.ts\"\n\n/** Tokens made only of these characters need no quoting in a POSIX shell. */\nconst SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/\n\n/**\n * Quotes one token for a POSIX shell, leaving obviously safe tokens alone.\n *\n * @category rendering\n * @since 1.0.0-rc.0\n */\nexport const quote = (token: string): string =>\n token !== \"\" && SAFE.test(token) ? token : `'${token.replaceAll(\"'\", `'\\\\''`)}'`\n\n/**\n * Renders a `Command` as a single shell command line.\n *\n * A standard command with `shell: true` renders its tokens verbatim, matching\n * the line Node and the browser interpreter hand to the default shell. A\n * custom shell renders as an explicit `<shell> -c <line>` invocation so the\n * selected executable is part of the permission resource. Without `shell`,\n * every token is POSIX-quoted to preserve literal argv semantics. These\n * distinctions are security-sensitive: the rendered value is also the\n * `proc:spawn` capability resource, so it must describe what will execute.\n * The rendering is POSIX-only by contract: on Windows, Node invokes the shell\n * with `/d /s /c` rather than `-c`, so the rendered line describes the POSIX\n * invocation, not a cmd.exe one.\n *\n * The `proc:spawn` capability resource is {@link resource}, which starts from\n * this line and adds what the line alone cannot show.\n *\n * A `PipedCommand` renders with `|` between its sides. That is a faithful\n * rendering of what the pipeline does, and it is the only form an interpreter\n * that takes a command line rather than an `argv` can be given. `from`/`to`\n * pipe options are *not* expressible this way; rendering ignores them, so a\n * pipeline that redirects `stderr` renders like one that pipes `stdout`.\n * Capability checks therefore see the commands, never the plumbing.\n *\n * @category rendering\n * @since 1.0.0-rc.0\n */\nexport const render = (command: ChildProcess.Command): string =>\n command._tag === \"StandardCommand\"\n ? command.options.shell === undefined || command.options.shell === false\n ? [command.command, ...command.args].map(quote).join(\" \")\n : command.options.shell === true\n ? [command.command, ...command.args].join(\" \")\n : `${quote(command.options.shell)} -c ${quote([command.command, ...command.args].join(\" \"))}`\n : `${render(command.left)} | ${render(command.right)}`\n\n/**\n * Shell syntax that chains, substitutes, groups, or redirects: everything that\n * lets one shell line run or write more than a single simple command.\n */\nconst shellControl = /[;&|`$<>()\\n\\r]/\n\n/**\n * Redirections that neither run nor write anything a grant should see: an fd\n * duplication such as `2>&1` and a discard to `/dev/null`. They are removed\n * before {@link shellControl} is tested, so `git status 2>&1` keeps its\n * verbatim resource. Only a space or tab may sit between `>` and `/dev/null`:\n * `>` followed by a line break is not a discard, so it stays control syntax.\n */\nconst harmlessRedirect = /(^|\\s)(?:\\d*>&\\d+|\\d*>[ \\t]*\\/dev\\/null)(?=\\s|$)/g\n\n/**\n * What {@link resource} compares a stage against, supplied by the spawner that\n * checks it.\n *\n * @category models\n * @since 1.0.0-rc.1\n */\nexport interface ResourceContext {\n /**\n * The environment the spawning process itself holds. A bootstrap name\n * (`PATH`, `HOME`, ...) whose declared value equals the ambient value is the\n * inherited default and stays out of the resource; one set to anything else\n * is named. Omitted, every declared bootstrap name is named.\n */\n readonly ambient?: Readonly<Record<string, string | undefined>> | undefined\n /**\n * Whether a stage's working directory is inside the directory the flow\n * already governs. A `cwd` it rejects is named in the resource. Omitted,\n * every `cwd` a stage sets is named.\n */\n readonly contains?: ((directory: string) => boolean) | undefined\n /**\n * The stage `cwd` with every symlink resolved, when the host could resolve\n * it. It replaces the lexical `cwd` both for `contains` and in the\n * `cwd <path> -- ` prefix, so a symlink inside the governed directory that\n * leads out of it is judged, and shown, by where the command really runs.\n */\n readonly resolvedCwd?: string | undefined\n}\n\n/**\n * Names of the environment variables a stage sets that the child would not\n * otherwise inherit: every name outside the bootstrap set, and a bootstrap\n * name whose value differs from the ambient one.\n */\nconst overriddenNames = (\n command: ChildProcess.StandardCommand,\n ambient: Readonly<Record<string, string | undefined>>\n): ReadonlyArray<string> =>\n Object.entries(command.options.env ?? {})\n .filter(([name, value]) => value !== undefined && (!isInheritedName(name) || ambient[name] !== value))\n .map(([name]) => name)\n .sort()\n\n/**\n * The stages of a command in execution order: the command itself, or every\n * `StandardCommand` of a pipeline from left to right. The spawner checks each\n * stage as its own `proc:spawn` capability.\n *\n * @category rendering\n * @since 1.0.0-rc.1\n */\nexport const stages = (command: ChildProcess.Command): ReadonlyArray<ChildProcess.StandardCommand> =>\n command._tag === \"StandardCommand\" ? [command] : [...stages(command.left), ...stages(command.right)]\n\n/**\n * The `proc:spawn` capability resource for one stage: {@link render}, with\n * three additions that keep a prefix grant from authorizing code the stage\n * never named.\n *\n * - A `shell: true` stage whose line holds shell control syntax (`;`, `&`,\n * `|`, `` ` ``, `$`, `<`, `>`, `(`, `)`, or a line break) is marked as the\n * explicit `sh -c '<line>'` it is. A grant such as `git status *` then\n * cannot match `git status; curl x | sh`, while a simple shell line such as\n * `git status --short` keeps its verbatim resource. An fd duplication such\n * as `2>&1` and a discard to `/dev/null` do not count as control syntax.\n * - A stage that sets an environment variable the child would not otherwise\n * inherit is prefixed with `env <NAME>… -- `, sorted names only. That is\n * every name outside the bootstrap set `ChildProcessEnvironment` inherits\n * (`PATH`, `HOME`, `USER`, `LANG`, `TERM`, `TMPDIR`, `SHELL`, `LC_*`), and a\n * bootstrap name whose value differs from `context.ambient`. A grant for\n * `git status` covers neither `env GIT_SSH_COMMAND -- git status` nor\n * `env PATH -- git status`. Values never enter the resource because they\n * often carry credentials.\n * - A stage whose `cwd` `context.contains` rejects is prefixed with\n * `cwd <path> -- `, so a grant does not follow the command into a\n * directory whose configuration the flow does not govern. When\n * `context.resolvedCwd` is given, that real path is judged and named\n * instead of the lexical `cwd`.\n *\n * A pipeline has no single resource: the spawner checks every one of its\n * {@link stages}, so `git status | sh` needs a grant for `sh` too.\n *\n * The `env` and `cwd` prefixes share their spelling with a literal command:\n * `make(\"env\", [\"PATH\", \"--\", \"git\", \"status\"])` renders as\n * `env PATH -- git status`, the resource of `git status` with `PATH`\n * overridden. The alias grants nothing extra. The literal command runs a\n * program named `PATH` (or `cwd`) from the ambient `PATH`, not `git`. A grant\n * with `*` inside the prefix, such as `env * -- git status`, also matches\n * `env -C /x -- git status`, but it already admits any variable name,\n * including `GIT_SSH_COMMAND` and `LD_PRELOAD`, which run arbitrary code.\n * Grant a prefixed resource exactly as the request shows it.\n *\n * @category rendering\n * @since 1.0.0-rc.1\n */\nexport const resource = (command: ChildProcess.StandardCommand, context: ResourceContext = {}): string => {\n const line = render(command)\n const shown = command.options.shell === true && shellControl.test(line.replace(harmlessRedirect, \"$1\"))\n ? `sh -c ${quote(line)}`\n : line\n const names = overriddenNames(command, context.ambient ?? {})\n const withEnv = names.length === 0 ? shown : `env ${names.map(quote).join(\" \")} -- ${shown}`\n const directory = command.options.cwd === undefined ? undefined : context.resolvedCwd ?? command.options.cwd\n return directory === undefined || context.contains?.(directory) === true\n ? withEnv\n : `cwd ${quote(directory)} -- ${withEnv}`\n}\n\n/** The leading token of a shell line, which is the program the line runs. */\nconst program = (line: string): string => {\n const trimmed = line.trimStart()\n const end = trimmed.search(/\\s/)\n return end === -1 ? trimmed : trimmed.slice(0, end)\n}\n\n/**\n * The executable a command runs, without its arguments.\n *\n * A durable record of a spawned process names its program with this rather\n * than with {@link render}: arguments carry credentials (`curl -u user:pass`,\n * `mysql -phunter2`), and a journal that keeps them keeps them permanently.\n * Nothing that reads such a record needs more than the program name; a reaper\n * matches processes by pid and process group.\n *\n * Without `shell`, `command` is the executable itself, spaces and all. With a\n * shell, the line is what runs, so the program is its first token: both\n * `make(\"mysql -phunter2\", [], { shell: true })` and\n * `make(\"mysql\", [\"-phunter2\"])` yield `mysql`. A pipeline names one\n * executable per stage, joined the way {@link render} joins the stages.\n *\n * @category rendering\n * @since 1.0.0-rc.0\n */\nexport const executable = (command: ChildProcess.Command): string =>\n command._tag === \"StandardCommand\"\n ? command.options.shell === undefined || command.options.shell === false\n ? command.command\n : program(command.command)\n : `${executable(command.left)} | ${executable(command.right)}`\n\n/**\n * The working directory a command runs in, taking the leftmost stage of a\n * pipeline, which is the stage `setCwd` and the spawners agree to treat as the\n * pipeline's own directory.\n *\n * @category rendering\n * @since 1.0.0-rc.0\n */\nexport const cwd = (command: ChildProcess.Command): string | undefined =>\n command._tag === \"StandardCommand\" ? command.options.cwd : cwd(command.left)\n\n/**\n * The environment overrides a command runs with, taking the leftmost stage of\n * a pipeline for the same reason {@link cwd} does.\n *\n * @category rendering\n * @since 1.0.0-rc.0\n */\nexport const env = (\n command: ChildProcess.Command\n): Record<string, string | undefined> | undefined =>\n command._tag === \"StandardCommand\" ? command.options.env : env(command.left)\n"]}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process containment: prepared ownership, bounded cleanup and durable records.
|
|
3
|
+
*
|
|
4
|
+
* A host supplies a {@link Lifecycle} that owns launch and cleanup independently
|
|
5
|
+
* of the command's exit status. The lifecycle first prepares a process identity;
|
|
6
|
+
* this decorator commits that identity to {@link ProcessLedger.ProcessLedger}
|
|
7
|
+
* before activating the caller's command. Failed or interrupted startup closes
|
|
8
|
+
* its child scope immediately, including any earlier legs of a partial pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Each command receives default termination and escalation options. The host
|
|
11
|
+
* lifecycle validates that policy and verifies cleanup before the decorator
|
|
12
|
+
* retires its record. Unverifiable cleanup retains the record for recovery;
|
|
13
|
+
* a host that dies also leaves its committed records discoverable by the next
|
|
14
|
+
* incarnation's reaper (`ProcessReaper` in `@smthrs/platform-node`).
|
|
15
|
+
*
|
|
16
|
+
* Every pipeline leg is prepared and recorded separately, including legs whose
|
|
17
|
+
* pid the aggregate handle does not expose. The module remains platform-neutral.
|
|
18
|
+
* A layer without a lifecycle provides only deadline defaults and bookkeeping;
|
|
19
|
+
* it does not declare the verified-cleanup contract read by {@link isContained}.
|
|
20
|
+
*
|
|
21
|
+
* Governing design:
|
|
22
|
+
* `docs/specs/Concepts/Host Adapters.md`.
|
|
23
|
+
*
|
|
24
|
+
* @since 1.0.0-rc.0
|
|
25
|
+
*/
|
|
26
|
+
import * as Effect from "effect/Effect";
|
|
27
|
+
import * as Layer from "effect/Layer";
|
|
28
|
+
import * as PlatformError from "effect/PlatformError";
|
|
29
|
+
import * as Scope from "effect/Scope";
|
|
30
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
31
|
+
import { type ChildProcessHandle, ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
32
|
+
import * as ProcessLedger from "./ProcessLedger.ts";
|
|
33
|
+
/**
|
|
34
|
+
* How long a contained child may take to honour `SIGTERM`.
|
|
35
|
+
*
|
|
36
|
+
* @category models
|
|
37
|
+
* @since 1.0.0-rc.0
|
|
38
|
+
*/
|
|
39
|
+
export declare const defaultGraceMs = 2000;
|
|
40
|
+
/**
|
|
41
|
+
* Containment configuration.
|
|
42
|
+
*
|
|
43
|
+
* @category models
|
|
44
|
+
* @since 1.0.0-rc.0
|
|
45
|
+
*/
|
|
46
|
+
export interface Options {
|
|
47
|
+
/**
|
|
48
|
+
* Milliseconds between the `SIGTERM` that asks a child to stop and the
|
|
49
|
+
* `SIGKILL` that makes it. Default {@link defaultGraceMs}.
|
|
50
|
+
*/
|
|
51
|
+
readonly graceMs?: number | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The platform the host spawner runs on, as `process.platform` spells it.
|
|
54
|
+
* Windows records no POSIX process group, including for detached children.
|
|
55
|
+
* On POSIX, detachment determines whether the child leads its own group.
|
|
56
|
+
* Default `"linux"`; host bundles pass their actual platform.
|
|
57
|
+
*/
|
|
58
|
+
readonly platform?: string | undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A platform's preparation, activation, and cleanup of a contained process.
|
|
62
|
+
*
|
|
63
|
+
* Preparation receives the underlying spawner and must own cleanup before it
|
|
64
|
+
* can fail or be interrupted. It returns the process identity to record and an
|
|
65
|
+
* idempotent `activate` effect. The decorator runs activation only after that
|
|
66
|
+
* identity commits to the ledger. A platform can therefore prepare an owner
|
|
67
|
+
* without executing the caller's command before it is durably discoverable.
|
|
68
|
+
*
|
|
69
|
+
* Failed startup closes its own scope immediately. A successful startup keeps
|
|
70
|
+
* that scope attached to the caller's scope. `settled` must stay false on failed
|
|
71
|
+
* or unverifiable cleanup; only verified cleanup permits ledger retirement.
|
|
72
|
+
* The wrapped handle applies the same policy to an explicit `kill`.
|
|
73
|
+
*
|
|
74
|
+
* @category models
|
|
75
|
+
* @since 1.0.0
|
|
76
|
+
*/
|
|
77
|
+
export type Lifecycle = (command: ChildProcess.StandardCommand, spawn: (command: ChildProcess.StandardCommand) => Effect.Effect<ChildProcessHandle, PlatformError.PlatformError, Scope.Scope>) => Effect.Effect<{
|
|
78
|
+
readonly handle: ChildProcessHandle;
|
|
79
|
+
readonly activate: Effect.Effect<void, PlatformError.PlatformError, Scope.Scope>;
|
|
80
|
+
readonly settled: Effect.Effect<boolean>;
|
|
81
|
+
}, PlatformError.PlatformError, Scope.Scope>;
|
|
82
|
+
/**
|
|
83
|
+
* Whether a spawner declares the platform lifecycle required for containment.
|
|
84
|
+
*
|
|
85
|
+
* Only a layer with a {@link Lifecycle} declares this contract. A kill deadline
|
|
86
|
+
* alone does not verify cleanup. The permission decorator preserves the
|
|
87
|
+
* declaration when it wraps a contained service. The marker interoperates
|
|
88
|
+
* across the package's ESM and CommonJS projections.
|
|
89
|
+
*
|
|
90
|
+
* This is a trusted composition check: a caller-supplied lifecycle remains
|
|
91
|
+
* responsible for implementing its contract.
|
|
92
|
+
*
|
|
93
|
+
* @category predicates
|
|
94
|
+
* @since 1.0.0
|
|
95
|
+
*/
|
|
96
|
+
export declare const isContained: (spawner: ChildProcessSpawner["Service"]) => boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Supplies a command's default termination signal and escalation deadline.
|
|
99
|
+
*
|
|
100
|
+
* Both legs of a pipeline get the same policy — a pipeline is one action's
|
|
101
|
+
* process tree and a surviving leg is as much of a leak as a surviving
|
|
102
|
+
* child. Explicit `killSignal` and `forceKillAfter` values are preserved for
|
|
103
|
+
* the host lifecycle to validate.
|
|
104
|
+
*
|
|
105
|
+
* @category constructors
|
|
106
|
+
* @since 1.0.0-rc.0
|
|
107
|
+
*/
|
|
108
|
+
export declare const withContainment: (command: ChildProcess.Command, options?: Options) => ChildProcess.Command;
|
|
109
|
+
/**
|
|
110
|
+
* The process group a started child leads, or `null` when it shares the
|
|
111
|
+
* host's or the platform has no POSIX process groups.
|
|
112
|
+
*
|
|
113
|
+
* A detached POSIX child leads a group whose id is its own pid. A child in
|
|
114
|
+
* the host's group must not record that group as its own cleanup target.
|
|
115
|
+
* Windows detachment creates an independent console, not a POSIX group;
|
|
116
|
+
* Windows records therefore always carry `null`.
|
|
117
|
+
*
|
|
118
|
+
* @category constructors
|
|
119
|
+
* @since 1.0.0-rc.0
|
|
120
|
+
*/
|
|
121
|
+
export declare const groupOf: (command: ChildProcess.Command, pid: number, platform?: string) => number | null;
|
|
122
|
+
/**
|
|
123
|
+
* Decorates the process spawner in place with containment and bookkeeping.
|
|
124
|
+
*
|
|
125
|
+
* Compose it over a host spawner layer with `Layer.provide`. It provides the
|
|
126
|
+
* tag it also requires. Compose the permission decorator over this layer to
|
|
127
|
+
* authorize the caller's whole pipeline before its per-process expansion. A
|
|
128
|
+
* lifecycle may use different commands during preparation; a decorator below
|
|
129
|
+
* containment sees those platform commands instead of the caller's command.
|
|
130
|
+
*
|
|
131
|
+
* @category layers
|
|
132
|
+
* @since 1.0.0-rc.0
|
|
133
|
+
*/
|
|
134
|
+
export declare const layer: (options?: Options, lifecycle?: Lifecycle) => Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger.ProcessLedger>;
|
|
135
|
+
//# sourceMappingURL=ContainedSpawner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContainedSpawner.d.ts","sourceRoot":"","sources":["../../src/ContainedSpawner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,aAAa,MAAM,sBAAsB,CAAA;AAErD,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AACrC,OAAO,KAAK,YAAY,MAAM,sCAAsC,CAAA;AACpE,OAAO,EACL,KAAK,kBAAkB,EACvB,mBAAmB,EAEpB,MAAM,6CAA6C,CAAA;AAIpD,OAAO,KAAK,aAAa,MAAM,oBAAoB,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,OAAO,CAAA;AAElC;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CACvC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,GAAG,CACtB,OAAO,EAAE,YAAY,CAAC,eAAe,EACrC,KAAK,EAAE,CACL,OAAO,EAAE,YAAY,CAAC,eAAe,KAClC,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,aAAa,CAAC,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,KAC7E,MAAM,CAAC,MAAM,CAChB;IACE,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAA;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;IAChF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACzC,EACD,aAAa,CAAC,aAAa,EAC3B,KAAK,CAAC,KAAK,CACZ,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,mBAAmB,CAAC,SAAS,CAAC,KAAG,OAA2C,CAAA;AAEjH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,YAAY,CAAC,OAAO,EAC7B,UAAU,OAAO,KAChB,YAAY,CAAC,OAcf,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,SAAS,YAAY,CAAC,OAAO,EAAE,KAAK,MAAM,EAAE,WAAW,MAAM,KAAG,MAAM,GAAG,IAIhG,CAAA;AAKD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,KAAK,GAChB,UAAU,OAAO,EACjB,YAAY,SAAS,KACpB,KAAK,CAAC,KAAK,CAAC,mBAAmB,EAAE,KAAK,EAAE,mBAAmB,GAAG,aAAa,CAAC,aAAa,CAmEzF,CAAA"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process containment: prepared ownership, bounded cleanup and durable records.
|
|
3
|
+
*
|
|
4
|
+
* A host supplies a {@link Lifecycle} that owns launch and cleanup independently
|
|
5
|
+
* of the command's exit status. The lifecycle first prepares a process identity;
|
|
6
|
+
* this decorator commits that identity to {@link ProcessLedger.ProcessLedger}
|
|
7
|
+
* before activating the caller's command. Failed or interrupted startup closes
|
|
8
|
+
* its child scope immediately, including any earlier legs of a partial pipeline.
|
|
9
|
+
*
|
|
10
|
+
* Each command receives default termination and escalation options. The host
|
|
11
|
+
* lifecycle validates that policy and verifies cleanup before the decorator
|
|
12
|
+
* retires its record. Unverifiable cleanup retains the record for recovery;
|
|
13
|
+
* a host that dies also leaves its committed records discoverable by the next
|
|
14
|
+
* incarnation's reaper (`ProcessReaper` in `@smthrs/platform-node`).
|
|
15
|
+
*
|
|
16
|
+
* Every pipeline leg is prepared and recorded separately, including legs whose
|
|
17
|
+
* pid the aggregate handle does not expose. The module remains platform-neutral.
|
|
18
|
+
* A layer without a lifecycle provides only deadline defaults and bookkeeping;
|
|
19
|
+
* it does not declare the verified-cleanup contract read by {@link isContained}.
|
|
20
|
+
*
|
|
21
|
+
* Governing design:
|
|
22
|
+
* `docs/specs/Concepts/Host Adapters.md`.
|
|
23
|
+
*
|
|
24
|
+
* @since 1.0.0-rc.0
|
|
25
|
+
*/
|
|
26
|
+
import * as Effect from "effect/Effect";
|
|
27
|
+
import * as Exit from "effect/Exit";
|
|
28
|
+
import * as Layer from "effect/Layer";
|
|
29
|
+
import * as PlatformError from "effect/PlatformError";
|
|
30
|
+
import * as Schedule from "effect/Schedule";
|
|
31
|
+
import * as Scope from "effect/Scope";
|
|
32
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess";
|
|
33
|
+
import { ChildProcessSpawner, make as makeSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
34
|
+
import * as CommandLine from "./CommandLine.js";
|
|
35
|
+
import * as Containment from "./internal/Containment.js";
|
|
36
|
+
import * as Pipeline from "./internal/Pipeline.js";
|
|
37
|
+
import * as ProcessLedger from "./ProcessLedger.js";
|
|
38
|
+
/**
|
|
39
|
+
* How long a contained child may take to honour `SIGTERM`.
|
|
40
|
+
*
|
|
41
|
+
* @category models
|
|
42
|
+
* @since 1.0.0-rc.0
|
|
43
|
+
*/
|
|
44
|
+
export const defaultGraceMs = 2000;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a spawner declares the platform lifecycle required for containment.
|
|
47
|
+
*
|
|
48
|
+
* Only a layer with a {@link Lifecycle} declares this contract. A kill deadline
|
|
49
|
+
* alone does not verify cleanup. The permission decorator preserves the
|
|
50
|
+
* declaration when it wraps a contained service. The marker interoperates
|
|
51
|
+
* across the package's ESM and CommonJS projections.
|
|
52
|
+
*
|
|
53
|
+
* This is a trusted composition check: a caller-supplied lifecycle remains
|
|
54
|
+
* responsible for implementing its contract.
|
|
55
|
+
*
|
|
56
|
+
* @category predicates
|
|
57
|
+
* @since 1.0.0
|
|
58
|
+
*/
|
|
59
|
+
export const isContained = (spawner) => Containment.isContained(spawner);
|
|
60
|
+
/**
|
|
61
|
+
* Supplies a command's default termination signal and escalation deadline.
|
|
62
|
+
*
|
|
63
|
+
* Both legs of a pipeline get the same policy — a pipeline is one action's
|
|
64
|
+
* process tree and a surviving leg is as much of a leak as a surviving
|
|
65
|
+
* child. Explicit `killSignal` and `forceKillAfter` values are preserved for
|
|
66
|
+
* the host lifecycle to validate.
|
|
67
|
+
*
|
|
68
|
+
* @category constructors
|
|
69
|
+
* @since 1.0.0-rc.0
|
|
70
|
+
*/
|
|
71
|
+
export const withContainment = (command, options) => {
|
|
72
|
+
const graceMs = options?.graceMs ?? defaultGraceMs;
|
|
73
|
+
if (command._tag === "PipedCommand") {
|
|
74
|
+
return ChildProcess.pipeTo(withContainment(command.left, options), withContainment(command.right, options), command.options);
|
|
75
|
+
}
|
|
76
|
+
return ChildProcess.make(command.command, [...command.args], {
|
|
77
|
+
...command.options,
|
|
78
|
+
killSignal: command.options.killSignal ?? "SIGTERM",
|
|
79
|
+
forceKillAfter: command.options.forceKillAfter ?? graceMs
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* The process group a started child leads, or `null` when it shares the
|
|
84
|
+
* host's or the platform has no POSIX process groups.
|
|
85
|
+
*
|
|
86
|
+
* A detached POSIX child leads a group whose id is its own pid. A child in
|
|
87
|
+
* the host's group must not record that group as its own cleanup target.
|
|
88
|
+
* Windows detachment creates an independent console, not a POSIX group;
|
|
89
|
+
* Windows records therefore always carry `null`.
|
|
90
|
+
*
|
|
91
|
+
* @category constructors
|
|
92
|
+
* @since 1.0.0-rc.0
|
|
93
|
+
*/
|
|
94
|
+
export const groupOf = (command, pid, platform) => {
|
|
95
|
+
if (platform === "win32")
|
|
96
|
+
return null;
|
|
97
|
+
const options = command._tag === "PipedCommand" ? rightmost(command).options : command.options;
|
|
98
|
+
return (options.detached ?? true) ? pid : null;
|
|
99
|
+
};
|
|
100
|
+
const rightmost = (command) => command._tag === "StandardCommand" ? command : rightmost(command.right);
|
|
101
|
+
/**
|
|
102
|
+
* Decorates the process spawner in place with containment and bookkeeping.
|
|
103
|
+
*
|
|
104
|
+
* Compose it over a host spawner layer with `Layer.provide`. It provides the
|
|
105
|
+
* tag it also requires. Compose the permission decorator over this layer to
|
|
106
|
+
* authorize the caller's whole pipeline before its per-process expansion. A
|
|
107
|
+
* lifecycle may use different commands during preparation; a decorator below
|
|
108
|
+
* containment sees those platform commands instead of the caller's command.
|
|
109
|
+
*
|
|
110
|
+
* @category layers
|
|
111
|
+
* @since 1.0.0-rc.0
|
|
112
|
+
*/
|
|
113
|
+
export const layer = (options, lifecycle) => Layer.effect(ChildProcessSpawner, Effect.gen(function* () {
|
|
114
|
+
const spawner = yield* ChildProcessSpawner;
|
|
115
|
+
const ledger = yield* ProcessLedger.ProcessLedger;
|
|
116
|
+
const service = makeSpawner((command) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
117
|
+
// A failed startup must clean up even when its caller catches the
|
|
118
|
+
// error and keeps the outer scope open. One child scope contains
|
|
119
|
+
// the entire pipeline, so failure of a later leg also closes all
|
|
120
|
+
// earlier legs. Successful spawns retain it until their caller closes.
|
|
121
|
+
const processScope = yield* Scope.fork(yield* Scope.Scope);
|
|
122
|
+
return yield* restore(Pipeline.spawn(command, Effect.fn("ContainedSpawner.spawn")(function* (command) {
|
|
123
|
+
// The release finalizer is registered BEFORE the spawn, so scope
|
|
124
|
+
// closure runs it AFTER the spawn's own kill finalizer: finalizers
|
|
125
|
+
// run last-registered-first, and a ledger that announced an exit
|
|
126
|
+
// while the process was still being signalled would be telling the
|
|
127
|
+
// next incarnation to stop looking for something still alive. The
|
|
128
|
+
// slot is empty until the record exists, which is also what makes a
|
|
129
|
+
// spawn that never got recorded a no-op to release.
|
|
130
|
+
const slot = {
|
|
131
|
+
settled: Effect.succeed(true)
|
|
132
|
+
};
|
|
133
|
+
yield* Effect.addFinalizer(() => Effect.gen(function* () {
|
|
134
|
+
if (slot.current === undefined)
|
|
135
|
+
return;
|
|
136
|
+
if (yield* slot.settled)
|
|
137
|
+
return yield* releaseQuietly(ledger, slot.current);
|
|
138
|
+
yield* Effect.logWarning(`process cleanup could not verify ${slot.current.pid}; retaining its ledger record`);
|
|
139
|
+
}));
|
|
140
|
+
const contained = withContainment(command, options);
|
|
141
|
+
const watched = lifecycle === undefined ? undefined : yield* lifecycle(contained, spawner.spawn);
|
|
142
|
+
const handle = watched?.handle ?? (yield* spawner.spawn(contained));
|
|
143
|
+
slot.settled = watched?.settled ?? slot.settled;
|
|
144
|
+
const pid = handle.pid;
|
|
145
|
+
slot.current = yield* ledger.record({
|
|
146
|
+
pid,
|
|
147
|
+
pgid: groupOf(command, pid, options?.platform),
|
|
148
|
+
commandDigest: CommandLine.executable(command)
|
|
149
|
+
}).pipe(
|
|
150
|
+
// A spawn whose record did not commit is precisely the child this
|
|
151
|
+
// module exists to make discoverable, so it is not started: the
|
|
152
|
+
// process is signalled and the caller is told, rather than left
|
|
153
|
+
// running with nothing durable naming it.
|
|
154
|
+
Effect.tapCause(() => Effect.ignore(handle.kill())), Effect.mapError(unrecorded));
|
|
155
|
+
if (watched !== undefined)
|
|
156
|
+
yield* watched.activate;
|
|
157
|
+
return handle;
|
|
158
|
+
})).pipe(Scope.provide(processScope))).pipe(Effect.onError((cause) => Scope.close(processScope, Exit.failCause(cause))));
|
|
159
|
+
})));
|
|
160
|
+
return lifecycle === undefined ? service : Containment.mark(service);
|
|
161
|
+
}));
|
|
162
|
+
/** The spawn failure a caller sees when the ledger could not record a child. */
|
|
163
|
+
const unrecorded = (cause) => PlatformError.systemError({
|
|
164
|
+
_tag: "Unknown",
|
|
165
|
+
module: "ChildProcess",
|
|
166
|
+
method: "spawn",
|
|
167
|
+
description: "the process ledger could not record this spawn durably",
|
|
168
|
+
cause
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* Retires a record on scope close, retrying the durable write.
|
|
172
|
+
*
|
|
173
|
+
* A finalizer has nowhere to report a failure, so the retry is the report: an
|
|
174
|
+
* exit that never commits leaves the record inherited, and the next
|
|
175
|
+
* incarnation's reaper finds a pid that is already gone and retires it then.
|
|
176
|
+
* That is the safe direction, and the reason this is the ONE place a ledger
|
|
177
|
+
* failure is logged rather than propagated.
|
|
178
|
+
*/
|
|
179
|
+
const releaseQuietly = (ledger, record) => ledger.release(record).pipe(Effect.retry(Schedule.recurs(2)), Effect.catch((error) => Effect.logWarning(`process ledger could not retire ${record.pid}; leaving it for the reaper`, error)));
|
|
180
|
+
//# sourceMappingURL=ContainedSpawner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContainedSpawner.js","sourceRoot":"","sources":["../../src/ContainedSpawner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Process containment: prepared ownership, bounded cleanup and durable records.\n *\n * A host supplies a {@link Lifecycle} that owns launch and cleanup independently\n * of the command's exit status. The lifecycle first prepares a process identity;\n * this decorator commits that identity to {@link ProcessLedger.ProcessLedger}\n * before activating the caller's command. Failed or interrupted startup closes\n * its child scope immediately, including any earlier legs of a partial pipeline.\n *\n * Each command receives default termination and escalation options. The host\n * lifecycle validates that policy and verifies cleanup before the decorator\n * retires its record. Unverifiable cleanup retains the record for recovery;\n * a host that dies also leaves its committed records discoverable by the next\n * incarnation's reaper (`ProcessReaper` in `@smthrs/platform-node`).\n *\n * Every pipeline leg is prepared and recorded separately, including legs whose\n * pid the aggregate handle does not expose. The module remains platform-neutral.\n * A layer without a lifecycle provides only deadline defaults and bookkeeping;\n * it does not declare the verified-cleanup contract read by {@link isContained}.\n *\n * Governing design:\n * `docs/specs/Concepts/Host Adapters.md`.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport * as Exit from \"effect/Exit\"\nimport * as Layer from \"effect/Layer\"\nimport * as PlatformError from \"effect/PlatformError\"\nimport * as Schedule from \"effect/Schedule\"\nimport * as Scope from \"effect/Scope\"\nimport * as ChildProcess from \"effect/unstable/process/ChildProcess\"\nimport {\n type ChildProcessHandle,\n ChildProcessSpawner,\n make as makeSpawner\n} from \"effect/unstable/process/ChildProcessSpawner\"\nimport * as CommandLine from \"./CommandLine.ts\"\nimport * as Containment from \"./internal/Containment.ts\"\nimport * as Pipeline from \"./internal/Pipeline.ts\"\nimport * as ProcessLedger from \"./ProcessLedger.ts\"\n\n/**\n * How long a contained child may take to honour `SIGTERM`.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport const defaultGraceMs = 2000\n\n/**\n * Containment configuration.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Options {\n /**\n * Milliseconds between the `SIGTERM` that asks a child to stop and the\n * `SIGKILL` that makes it. Default {@link defaultGraceMs}.\n */\n readonly graceMs?: number | undefined\n /**\n * The platform the host spawner runs on, as `process.platform` spells it.\n * Windows records no POSIX process group, including for detached children.\n * On POSIX, detachment determines whether the child leads its own group.\n * Default `\"linux\"`; host bundles pass their actual platform.\n */\n readonly platform?: string | undefined\n}\n\n/**\n * A platform's preparation, activation, and cleanup of a contained process.\n *\n * Preparation receives the underlying spawner and must own cleanup before it\n * can fail or be interrupted. It returns the process identity to record and an\n * idempotent `activate` effect. The decorator runs activation only after that\n * identity commits to the ledger. A platform can therefore prepare an owner\n * without executing the caller's command before it is durably discoverable.\n *\n * Failed startup closes its own scope immediately. A successful startup keeps\n * that scope attached to the caller's scope. `settled` must stay false on failed\n * or unverifiable cleanup; only verified cleanup permits ledger retirement.\n * The wrapped handle applies the same policy to an explicit `kill`.\n *\n * @category models\n * @since 1.0.0\n */\nexport type Lifecycle = (\n command: ChildProcess.StandardCommand,\n spawn: (\n command: ChildProcess.StandardCommand\n ) => Effect.Effect<ChildProcessHandle, PlatformError.PlatformError, Scope.Scope>\n) => Effect.Effect<\n {\n readonly handle: ChildProcessHandle\n readonly activate: Effect.Effect<void, PlatformError.PlatformError, Scope.Scope>\n readonly settled: Effect.Effect<boolean>\n },\n PlatformError.PlatformError,\n Scope.Scope\n>\n\n/**\n * Whether a spawner declares the platform lifecycle required for containment.\n *\n * Only a layer with a {@link Lifecycle} declares this contract. A kill deadline\n * alone does not verify cleanup. The permission decorator preserves the\n * declaration when it wraps a contained service. The marker interoperates\n * across the package's ESM and CommonJS projections.\n *\n * This is a trusted composition check: a caller-supplied lifecycle remains\n * responsible for implementing its contract.\n *\n * @category predicates\n * @since 1.0.0\n */\nexport const isContained = (spawner: ChildProcessSpawner[\"Service\"]): boolean => Containment.isContained(spawner)\n\n/**\n * Supplies a command's default termination signal and escalation deadline.\n *\n * Both legs of a pipeline get the same policy — a pipeline is one action's\n * process tree and a surviving leg is as much of a leak as a surviving\n * child. Explicit `killSignal` and `forceKillAfter` values are preserved for\n * the host lifecycle to validate.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const withContainment = (\n command: ChildProcess.Command,\n options?: Options\n): ChildProcess.Command => {\n const graceMs = options?.graceMs ?? defaultGraceMs\n if (command._tag === \"PipedCommand\") {\n return ChildProcess.pipeTo(\n withContainment(command.left, options),\n withContainment(command.right, options),\n command.options\n )\n }\n return ChildProcess.make(command.command, [...command.args], {\n ...command.options,\n killSignal: command.options.killSignal ?? \"SIGTERM\",\n forceKillAfter: command.options.forceKillAfter ?? graceMs\n })\n}\n\n/**\n * The process group a started child leads, or `null` when it shares the\n * host's or the platform has no POSIX process groups.\n *\n * A detached POSIX child leads a group whose id is its own pid. A child in\n * the host's group must not record that group as its own cleanup target.\n * Windows detachment creates an independent console, not a POSIX group;\n * Windows records therefore always carry `null`.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const groupOf = (command: ChildProcess.Command, pid: number, platform?: string): number | null => {\n if (platform === \"win32\") return null\n const options = command._tag === \"PipedCommand\" ? rightmost(command).options : command.options\n return (options.detached ?? true) ? pid : null\n}\n\nconst rightmost = (command: ChildProcess.Command): ChildProcess.StandardCommand =>\n command._tag === \"StandardCommand\" ? command : rightmost(command.right)\n\n/**\n * Decorates the process spawner in place with containment and bookkeeping.\n *\n * Compose it over a host spawner layer with `Layer.provide`. It provides the\n * tag it also requires. Compose the permission decorator over this layer to\n * authorize the caller's whole pipeline before its per-process expansion. A\n * lifecycle may use different commands during preparation; a decorator below\n * containment sees those platform commands instead of the caller's command.\n *\n * @category layers\n * @since 1.0.0-rc.0\n */\nexport const layer = (\n options?: Options,\n lifecycle?: Lifecycle\n): Layer.Layer<ChildProcessSpawner, never, ChildProcessSpawner | ProcessLedger.ProcessLedger> =>\n Layer.effect(\n ChildProcessSpawner,\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner\n const ledger = yield* ProcessLedger.ProcessLedger\n const service = makeSpawner(\n (command) =>\n Effect.uninterruptibleMask((restore) =>\n Effect.gen(function*() {\n // A failed startup must clean up even when its caller catches the\n // error and keeps the outer scope open. One child scope contains\n // the entire pipeline, so failure of a later leg also closes all\n // earlier legs. Successful spawns retain it until their caller closes.\n const processScope = yield* Scope.fork(yield* Scope.Scope)\n return yield* restore(\n Pipeline.spawn(\n command,\n Effect.fn(\"ContainedSpawner.spawn\")(function*(command) {\n // The release finalizer is registered BEFORE the spawn, so scope\n // closure runs it AFTER the spawn's own kill finalizer: finalizers\n // run last-registered-first, and a ledger that announced an exit\n // while the process was still being signalled would be telling the\n // next incarnation to stop looking for something still alive. The\n // slot is empty until the record exists, which is also what makes a\n // spawn that never got recorded a no-op to release.\n const slot: { current?: ProcessLedger.ProcessRecord; settled: Effect.Effect<boolean> } = {\n settled: Effect.succeed(true)\n }\n yield* Effect.addFinalizer(() =>\n Effect.gen(function*() {\n if (slot.current === undefined) return\n if (yield* slot.settled) return yield* releaseQuietly(ledger, slot.current)\n yield* Effect.logWarning(\n `process cleanup could not verify ${slot.current.pid}; retaining its ledger record`\n )\n })\n )\n const contained = withContainment(command, options) as ChildProcess.StandardCommand\n const watched = lifecycle === undefined ? undefined : yield* lifecycle(contained, spawner.spawn)\n const handle = watched?.handle ?? (yield* spawner.spawn(contained))\n slot.settled = watched?.settled ?? slot.settled\n const pid = handle.pid as number\n slot.current = yield* ledger.record({\n pid,\n pgid: groupOf(command, pid, options?.platform),\n commandDigest: CommandLine.executable(command)\n }).pipe(\n // A spawn whose record did not commit is precisely the child this\n // module exists to make discoverable, so it is not started: the\n // process is signalled and the caller is told, rather than left\n // running with nothing durable naming it.\n Effect.tapCause(() => Effect.ignore(handle.kill())),\n Effect.mapError(unrecorded)\n )\n if (watched !== undefined) yield* watched.activate\n return handle\n })\n ).pipe(Scope.provide(processScope))\n ).pipe(\n Effect.onError((cause) => Scope.close(processScope, Exit.failCause(cause)))\n )\n })\n )\n )\n return lifecycle === undefined ? service : Containment.mark(service)\n })\n )\n\n/** The spawn failure a caller sees when the ledger could not record a child. */\nconst unrecorded = (cause: unknown): PlatformError.PlatformError =>\n PlatformError.systemError({\n _tag: \"Unknown\",\n module: \"ChildProcess\",\n method: \"spawn\",\n description: \"the process ledger could not record this spawn durably\",\n cause\n })\n\n/**\n * Retires a record on scope close, retrying the durable write.\n *\n * A finalizer has nowhere to report a failure, so the retry is the report: an\n * exit that never commits leaves the record inherited, and the next\n * incarnation's reaper finds a pid that is already gone and retires it then.\n * That is the safe direction, and the reason this is the ONE place a ledger\n * failure is logged rather than propagated.\n */\nconst releaseQuietly = (\n ledger: ProcessLedger.Service,\n record: ProcessLedger.ProcessRecord\n): Effect.Effect<void> =>\n ledger.release(record).pipe(\n Effect.retry(Schedule.recurs(2)),\n Effect.catch((error) =>\n Effect.logWarning(`process ledger could not retire ${record.pid}; leaving it for the reaper`, error)\n )\n )\n"]}
|