@primafuture/systemd-tasks 1.0.0 → 1.1.0

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 CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 — 2026-09-17 (local candidate)
4
+
5
+ - Add the `@primafuture/systemd-tasks/observed` entrypoint with immutable
6
+ JSON-safe intents, finite manager-owned stdin, live binary stdout/stderr, and
7
+ separate payload/guardian lifecycle evidence.
8
+ - Create payload, retention, and `Type=notify` guardian services in one atomic
9
+ transaction. Both process services require successful retention initialization;
10
+ payload stop propagates to a drain-aware guardian even when exec was cancelled.
11
+ Fail closed when the guardian dies, while allowing it to drain output after
12
+ payload exit, stop, timeout, client detach, or client death.
13
+ - Add a versioned bounded guardian protocol, sequential asynchronous callback
14
+ backpressure, explicit local detach, and stable `OBSERVATION_FAILED` errors.
15
+ Reconnect, replay, and stream adoption remain deliberately unsupported.
16
+ - Generalize the existing D-Bus adapter to an ordered auxiliary-unit topology and
17
+ share transport ownership, mutation serialization, notification routing,
18
+ cleanup, diagnostics, and `TaskError` identity across root/observed APIs and
19
+ mixed ESM/CommonJS loading.
20
+ - Upgrade the external native transport to the exact published
21
+ `@primafuture/socket-fdx@1.1.0` release and use its owned `socketPair()` and
22
+ `adoptSocket()` primitives.
23
+ - Extend deterministic, real-systemd, installed ESM/CommonJS, and TypeScript
24
+ consumer tests, including the real 512 KiB stdin boundary, partial socket
25
+ allocation rollback, pre-readiness detach, cross-client pre-exec stop, final
26
+ output under slow-callback backpressure during explicit stop, 32 MiB per-channel draining, and guardian
27
+ fail-closed behavior. Detach also settles independently of an already-pending
28
+ application callback. No npm publish was performed.
29
+
3
30
  ## 1.0.0 — 2026-09-07
4
31
 
5
32
  - Add explicit preparation, single-attempt submission, lookup, inspection, waiting,
package/README.md CHANGED
@@ -4,7 +4,7 @@ Run one-shot Linux processes owned by the **systemd user manager**, with literal
4
4
  arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable
5
5
  results after the Node client exits or crashes.
6
6
 
7
- Version 1.0.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
7
+ Version 1.1.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
8
8
  and cgroup v2. The verified environment
9
9
  is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64.
10
10
  Other versions satisfying those minimums are not independently verified.
@@ -15,11 +15,12 @@ Other versions satisfying those minimums are not independently verified.
15
15
  npm install @primafuture/systemd-tasks
16
16
  ```
17
17
 
18
- The pinned native dependency `@primafuture/socket-fdx@1.0.0` is installed under
19
- the npm alias `usocket` and supplies its compatible `USocket` adapter. It ships a
20
- prebuilt native binary: ordinary installation needs no compiler, Python, or install
21
- scripts. Consumers do not need `overrides`. The dependency is pinned to its stable release;
22
- musl Linux and other architectures are outside its current support. The JavaScript D-Bus stack
18
+ The native dependency `@primafuture/socket-fdx@1.1.0` is installed under the npm
19
+ alias `usocket` and supplies both the D-Bus adapter and explicitly owned Unix
20
+ socket-pair primitives. It is pinned to the exact published release and ships a
21
+ prebuilt native binary, so ordinary installation needs no compiler, Python, install
22
+ scripts, or consumer override. Musl Linux and other architectures remain unsupported.
23
+ The JavaScript D-Bus stack
23
24
  (`dbus-next@0.10.2` and its dependencies) is bundled; licenses are recorded in
24
25
  [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
25
26
 
@@ -29,6 +30,9 @@ that the retention executable resolves to a regular executable file before openi
29
30
  the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager,
30
31
  enable linger, request elevated privileges, or fall back to child_process.
31
32
  Importing either the ESM or CommonJS entrypoint does not connect or start a process.
33
+ Observed tasks additionally require `/usr/bin/systemd-notify`, `/usr/bin/systemctl`,
34
+ and the packaged guardian bundle; `connectObserved()` verifies them before opening
35
+ the bus.
32
36
 
33
37
  ## Basic execution
34
38
 
@@ -70,6 +74,79 @@ environment that are absent from this map, using a snapshot immediately before
70
74
  submission. Systemd may add execution metadata such as `INVOCATION_ID`. Concurrent
71
75
  external changes to manager configuration are outside this guarantee.
72
76
 
77
+ ## Observed execution
78
+
79
+ The separate `@primafuture/systemd-tasks/observed` entrypoint adds live binary
80
+ stdout/stderr delivery without wrapping the payload process:
81
+
82
+ ```ts
83
+ import * as observed from '@primafuture/systemd-tasks/observed';
84
+
85
+ const intent = observed.prepareObserved({
86
+ executable: '/usr/bin/sort',
87
+ argv: [],
88
+ cwd: '/tmp',
89
+ env: {},
90
+ runtimeLimitMs: 5000,
91
+ stopGraceMs: 500,
92
+ stdin: 'second\nfirst\n',
93
+ });
94
+
95
+ const client = await observed.connectObserved();
96
+ try {
97
+ const started = await client.startObserved(intent, {
98
+ async onOutput(event) {
99
+ // Awaiting here applies bounded backpressure to both output channels.
100
+ await consume(event.stream, event.data);
101
+ },
102
+ });
103
+ await started.observation.completion;
104
+ const completed = await client.wait(intent, { timeoutMs: 10000 });
105
+ await client.release(completed.reference);
106
+ } finally {
107
+ await client.disconnect();
108
+ }
109
+ ```
110
+
111
+ `stdin` is mandatory. A string is snapshotted as UTF-8, a `Uint8Array` is copied,
112
+ an empty value remains an explicit empty data input, and `null` selects `/dev/null`.
113
+ The fixed limit is 512 KiB, chosen to remain below systemd's manager-state line
114
+ serialization boundary after base64 expansion. The canonical base64 snapshot is
115
+ part of the serialized intent and its fingerprint. Systemd owns `StandardInputData` after submission and
116
+ may retain those bytes in manager state until release, so treat the intent and
117
+ manager access as sensitive when stdin contains secrets.
118
+
119
+ Each observed task consists of payload, retention, and `Type=notify` guardian
120
+ services created by one `StartTransientUnit` transaction. The payload is ordered
121
+ after and `BindsTo` the guardian; both process services require successful
122
+ retention initialization. `startObserved()` returns only after manager
123
+ acceptance and guardian readiness, with a fixed five-second readiness budget. A
124
+ failure after manager acceptance reports `mutationOutcome: 'accepted'`; it is not
125
+ permission to retry.
126
+
127
+ Output callbacks are sequential and their returned promises provide real bounded
128
+ backpressure. Callback failure rejects `observation.completion` with
129
+ `OBSERVATION_FAILED` and detaches locally; it does not stop the payload. `detach()`
130
+ and `disconnect()` likewise close only the local observer. The guardian continues
131
+ to drain and discard both channels so the payload does not receive `EPIPE` merely
132
+ because the client left. There is deliberately no reconnect, replay, or adoption
133
+ of another client's live stream. Use ordinary durable file I/O instead when output
134
+ must survive observation loss.
135
+
136
+ An already-invoked callback is application code and cannot be cancelled. Explicit
137
+ detach closes the socket and settles `completion` as `detached` without waiting for
138
+ that callback promise; no later callback is invoked, and a later settlement of the
139
+ detached callback cannot change the observation outcome or block `disconnect()`.
140
+
141
+ The guardian emits a private versioned binary protocol with separate stdout and
142
+ stderr frames. Payload bytes and callback causes are never included in diagnostics.
143
+ `wait()` reports payload and guardian results independently and reaches `completed`
144
+ only after both services are terminal with confirmed empty cgroups. `stop()` targets
145
+ the payload; systemd propagates that stop to the guardian in reverse dependency
146
+ order, so it drains bytes already written before completing. This also closes a
147
+ guardian whose payload start was cancelled before exec. `release()` refuses until
148
+ both process services are clean, then removes retention.
149
+
73
150
  ## Lifecycle and recovery
74
151
 
75
152
  | Operation | Contract |
@@ -206,7 +283,7 @@ service manipulation can invalidate evidence. The library does not enable linger
206
283
  recreate missing tasks, implement a daemon, or promise survival when the manager
207
284
  itself stops. Connection failure is distinct from missing units.
208
285
 
209
- The only advanced properties allowed in 1.0.0 are `CPUWeight` and `IOWeight`, each
286
+ The only advanced properties allowed in 1.x are `CPUWeight` and `IOWeight`, each
210
287
  with signature `'t'` and an integer value from 1 to 10000. Duplicates, unknown
211
288
  properties, and lifecycle overrides are rejected before mutation. Kernel/controller
212
289
  configuration determines the practical scheduling effect of those weights.
@@ -228,7 +305,8 @@ npm run check
228
305
  `check` runs strict typechecking, unit tests, real systemd integration tests, dual
229
306
  ESM/CJS build, and tarball runtime/type smoke tests. `prepublishOnly` invokes the
230
307
  same chain. Integration tests fail explicitly when prerequisites are missing.
231
- Tests create unique identities and clean only their recorded task/retention pairs.
308
+ Tests create unique identities and clean only their recorded task/retention pairs,
309
+ plus the exact guardian identity for observed tests.
232
310
  They do not restart the user manager or change host configuration.
233
311
 
234
312
  Runnable repository examples after `npm run build`:
@@ -25,7 +25,7 @@ This file is generated by `scripts/licenses.ts` from the distributed source map.
25
25
 
26
26
  ## External native dependency
27
27
 
28
- `@primafuture/socket-fdx` 1.0.0 (ISC) is installed separately under the npm alias `usocket`. It ships its native binary, license, and third-party notices, has no runtime dependencies, and is not embedded in this JavaScript bundle. The direct pinned alias applies in consumer installations without root overrides.
28
+ `@primafuture/socket-fdx` 1.1.0 (ISC) is installed separately under the npm alias `usocket`. It ships its native binary, license, and third-party notices, has no runtime dependencies, and is not embedded in this JavaScript bundle. The direct pinned alias applies in consumer installations without root overrides.
29
29
 
30
30
  ## @nornagon/put 0.0.8
31
31
 
@@ -0,0 +1,212 @@
1
+ // src/guardian.ts
2
+ import * as childProcess from "child_process";
3
+ import * as fs from "fs";
4
+ import * as net from "net";
5
+
6
+ // src/observed-framing.ts
7
+ var magic = Buffer.from("SDTO", "ascii");
8
+ var version = 1;
9
+ var headerBytes = 10;
10
+ var maximumPayloadBytes = 64 * 1024;
11
+ var kind = Object.freeze({ ready: 1, stdout: 2, stderr: 3, complete: 4 });
12
+ function encode(frameKind, payload = new Uint8Array()) {
13
+ if (!Object.values(kind).includes(frameKind) || payload.byteLength > maximumPayloadBytes) throw new Error("Invalid observed guardian frame.");
14
+ const frame = Buffer.allocUnsafe(headerBytes + payload.byteLength);
15
+ magic.copy(frame, 0);
16
+ frame[4] = version;
17
+ frame[5] = frameKind;
18
+ frame.writeUInt32BE(payload.byteLength, 6);
19
+ frame.set(payload, headerBytes);
20
+ return frame;
21
+ }
22
+
23
+ // src/guardian.ts
24
+ function fail(message, cause) {
25
+ throw new Error(`Observed task guardian failed: ${message}`, cause === void 0 ? void 0 : { cause });
26
+ }
27
+ function validateDescriptors() {
28
+ if (process.env.LISTEN_PID !== String(process.pid)) fail("LISTEN_PID does not identify this process.");
29
+ if (process.env.LISTEN_FDS !== "3") fail("LISTEN_FDS is not exactly three.");
30
+ if (process.env.LISTEN_FDNAMES !== "task-stdout:task-stderr:observer") fail("LISTEN_FDNAMES has an unexpected order.");
31
+ for (const descriptor of [3, 4, 5]) if (!fs.fstatSync(descriptor).isSocket()) fail(`descriptor ${descriptor} is not a socket.`);
32
+ delete process.env.LISTEN_PID;
33
+ delete process.env.LISTEN_FDS;
34
+ delete process.env.LISTEN_FDNAMES;
35
+ }
36
+ function notifyReady() {
37
+ const result = childProcess.spawnSync("/usr/bin/systemd-notify", ["--ready", "--pid=parent"], {
38
+ env: process.env,
39
+ stdio: ["ignore", "pipe", "pipe"],
40
+ encoding: "utf8",
41
+ timeout: 5e3
42
+ });
43
+ if (result.error) fail("systemd-notify could not execute.", result.error);
44
+ if (result.status !== 0) fail(`systemd-notify exited with status ${String(result.status)}.`);
45
+ }
46
+ function payloadWasInvoked(unitName) {
47
+ if (process.getuid === void 0) return null;
48
+ const runtimeDirectory = `/run/user/${String(process.getuid())}`;
49
+ const result = childProcess.spawnSync("/usr/bin/systemctl", ["--user", "show", unitName, "--property=InvocationID", "--value"], {
50
+ env: { XDG_RUNTIME_DIR: runtimeDirectory, DBUS_SESSION_BUS_ADDRESS: `unix:path=${runtimeDirectory}/bus` },
51
+ stdio: ["ignore", "pipe", "pipe"],
52
+ encoding: "utf8",
53
+ timeout: 5e3,
54
+ maxBuffer: 4096
55
+ });
56
+ if (result.error || result.status !== 0) return null;
57
+ const invocationId = result.stdout.trim();
58
+ if (invocationId === "") return false;
59
+ return /^[0-9a-f]{32}$/.test(invocationId) ? true : null;
60
+ }
61
+ async function writeReady(observer) {
62
+ return await new Promise(
63
+ /** Chyba client-facing socketu znamená detach, nikoliv selhání guardian infrastruktury. */
64
+ (resolve) => {
65
+ try {
66
+ observer.write(encode(kind.ready), (error) => {
67
+ resolve(error === void 0 || error === null);
68
+ });
69
+ } catch {
70
+ resolve(false);
71
+ }
72
+ }
73
+ );
74
+ }
75
+ async function run() {
76
+ validateDescriptors();
77
+ const payloadUnitName = process.argv[2];
78
+ if (!payloadUnitName || !/^pf-systemd-tasks-observed-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\.service$/.test(payloadUnitName)) fail("payload unit name is invalid.");
79
+ const stdout = new net.Socket({ fd: 3, readable: true, writable: false });
80
+ const stderr = new net.Socket({ fd: 4, readable: true, writable: false });
81
+ const observer = new net.Socket({ fd: 5, readable: true, writable: true, allowHalfOpen: true });
82
+ const sources = [
83
+ { stream: stdout, kind: kind.stdout, ended: false, pending: null },
84
+ { stream: stderr, kind: kind.stderr, ended: false, pending: null }
85
+ ];
86
+ let attached = true;
87
+ let blocked = false;
88
+ let pumping = false;
89
+ let finishing = false;
90
+ let nextSourceIndex = 0;
91
+ let readyFrameSettled = false;
92
+ function detach() {
93
+ if (!attached) return;
94
+ attached = false;
95
+ blocked = false;
96
+ observer.destroy();
97
+ for (const source of sources) {
98
+ source.pending = null;
99
+ source.stream.off("readable", pump);
100
+ source.stream.resume();
101
+ }
102
+ }
103
+ function finishIfComplete() {
104
+ if (!readyFrameSettled || finishing || !sources.every(
105
+ /** Oba payload descriptory musí potvrdit EOF. */
106
+ (source) => source.ended
107
+ )) return;
108
+ finishing = true;
109
+ if (!attached) process.exit(0);
110
+ observer.end(
111
+ encode(kind.complete),
112
+ /** Lokální shutdown callback potvrzuje odeslaný FIN bez čekání na vzdálený close. */
113
+ () => process.exit(0)
114
+ );
115
+ }
116
+ function pump() {
117
+ if (pumping || blocked || !readyFrameSettled) return;
118
+ pumping = true;
119
+ try {
120
+ let idleSources = 0;
121
+ while (!blocked && idleSources < sources.length) {
122
+ const source = sources[nextSourceIndex];
123
+ nextSourceIndex = (nextSourceIndex + 1) % sources.length;
124
+ const available = source.pending ?? source.stream.read();
125
+ if (available === null) {
126
+ idleSources++;
127
+ continue;
128
+ }
129
+ idleSources = 0;
130
+ const chunk = available.subarray(0, maximumPayloadBytes);
131
+ source.pending = available.byteLength > chunk.byteLength ? available.subarray(chunk.byteLength) : null;
132
+ if (attached) {
133
+ const writable = observer.write(encode(source.kind, chunk));
134
+ if (attached && !writable) blocked = true;
135
+ }
136
+ }
137
+ } finally {
138
+ pumping = false;
139
+ }
140
+ finishIfComplete();
141
+ }
142
+ function sourceFailure(error) {
143
+ stdout.destroy();
144
+ stderr.destroy();
145
+ observer.destroy();
146
+ process.nextTick(
147
+ /** Throw mimo EventEmitter callback zachová nenulový process exit. */
148
+ () => {
149
+ throw error;
150
+ }
151
+ );
152
+ }
153
+ for (const source of sources) {
154
+ source.stream.on("readable", pump);
155
+ source.stream.once(
156
+ "end",
157
+ /** Drain zbytku před terminal EOF. */
158
+ () => {
159
+ source.ended = true;
160
+ pump();
161
+ finishIfComplete();
162
+ }
163
+ );
164
+ source.stream.once("error", sourceFailure);
165
+ }
166
+ observer.on(
167
+ "drain",
168
+ /** Pokračuje až po skutečném poklesu Node write fronty. */
169
+ () => {
170
+ blocked = false;
171
+ pump();
172
+ }
173
+ );
174
+ observer.on(
175
+ "data",
176
+ /** Protokol je jednosměrný; neočekávaný klientský payload znamená detach. */
177
+ detach
178
+ );
179
+ observer.once("end", detach);
180
+ observer.once("close", detach);
181
+ observer.once("error", detach);
182
+ process.on(
183
+ "SIGTERM",
184
+ /** Jen pre-exec cancellation postrádá skutečné EOF, protože manager dál vlastní write endpointy. */
185
+ () => {
186
+ if (payloadWasInvoked(payloadUnitName) !== false) {
187
+ pump();
188
+ return;
189
+ }
190
+ for (const source of sources) {
191
+ source.ended = true;
192
+ source.pending = null;
193
+ source.stream.destroy();
194
+ }
195
+ finishIfComplete();
196
+ }
197
+ );
198
+ notifyReady();
199
+ if (!await writeReady(observer)) detach();
200
+ readyFrameSettled = true;
201
+ if (attached) observer.resume();
202
+ pump();
203
+ }
204
+ void run().catch(
205
+ /** Top-level startup failure musí zachovat nenulový guardian exit. */
206
+ (error) => {
207
+ process.nextTick(() => {
208
+ throw error;
209
+ });
210
+ }
211
+ );
212
+ //# sourceMappingURL=guardian.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/guardian.ts","../src/observed-framing.ts"],"sourcesContent":["import * as childProcess from 'node:child_process';\nimport * as fs from 'node:fs';\nimport * as net from 'node:net';\nimport * as framing from './observed-framing';\n\n/** Jeden guardian input drží svůj framing kind a případný zbytek velkého stream readu. */\ninterface Source {\n\t/** Raw task stream vlastněný tímto guardian procesem. */\n\treadonly stream: net.Socket;\n\t/** Output kind zachovávající oddělení stdout a stderr. */\n\treadonly kind: typeof framing.kind.stdout | typeof framing.kind.stderr;\n\t/** EOF je autoritativní až po vyčerpání kernel bufferu. */\n\tended: boolean;\n\t/** Zbytek většího readu čekající na další bounded frame. */\n\tpending: Buffer | null;\n}\n\n/** Ukončí guardian fail-closed bez zápisu citlivých output dat na jeho stdio. */\nfunction fail(message: string, cause?: unknown): never {\n\tthrow new Error(`Observed task guardian failed: ${message}`, cause === undefined ? undefined : { cause });\n}\n\n/** Ověří přesný systemd fd-store kontrakt dřív, než guardian oznámí readiness. */\nfunction validateDescriptors(): void {\n\tif (process.env.LISTEN_PID !== String(process.pid)) fail('LISTEN_PID does not identify this process.');\n\tif (process.env.LISTEN_FDS !== '3') fail('LISTEN_FDS is not exactly three.');\n\tif (process.env.LISTEN_FDNAMES !== 'task-stdout:task-stderr:observer') fail('LISTEN_FDNAMES has an unexpected order.');\n\tfor (const descriptor of [3, 4, 5]) if (!fs.fstatSync(descriptor).isSocket()) fail(`descriptor ${descriptor} is not a socket.`);\n\tdelete process.env.LISTEN_PID;\n\tdelete process.env.LISTEN_FDS;\n\tdelete process.env.LISTEN_FDNAMES;\n}\n\n/** Potvrdí systemd readiness synchronně, aby ready frame nikdy nepředběhl selhání notify. */\nfunction notifyReady(): void {\n\tconst result = childProcess.spawnSync('/usr/bin/systemd-notify', ['--ready', '--pid=parent'], {\n\t\tenv: process.env,\n\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\tencoding: 'utf8',\n\t\ttimeout: 5000,\n\t});\n\tif (result.error) fail('systemd-notify could not execute.', result.error);\n\tif (result.status !== 0) fail(`systemd-notify exited with status ${String(result.status)}.`);\n}\n\n/** Přečte payload InvocationID až po ordered stopu; prázdná hodnota spolehlivě značí zrušení před exec. */\nfunction payloadWasInvoked(unitName: string): boolean | null {\n\tif (process.getuid === undefined) return null;\n\tconst runtimeDirectory = `/run/user/${String(process.getuid())}`;\n\tconst result = childProcess.spawnSync('/usr/bin/systemctl', ['--user', 'show', unitName, '--property=InvocationID', '--value'], {\n\t\tenv: { XDG_RUNTIME_DIR: runtimeDirectory, DBUS_SESSION_BUS_ADDRESS: `unix:path=${runtimeDirectory}/bus` },\n\t\tstdio: ['ignore', 'pipe', 'pipe'],\n\t\tencoding: 'utf8',\n\t\ttimeout: 5000,\n\t\tmaxBuffer: 4096,\n\t});\n\tif (result.error || result.status !== 0) return null;\n\tconst invocationId = result.stdout.trim();\n\tif (invocationId === '') return false;\n\treturn /^[0-9a-f]{32}$/.test(invocationId) ? true : null;\n}\n\n/** Rozliší lokální acceptance ready frame od observeru odpojeného během startupu. */\nasync function writeReady(observer: net.Socket): Promise<boolean> {\n\treturn await new Promise<boolean>(/** Chyba client-facing socketu znamená detach, nikoliv selhání guardian infrastruktury. */ resolve => {\n\t\ttry { observer.write(framing.encode(framing.kind.ready), error => { resolve(error === undefined || error === null); }); }\n\t\tcatch { resolve(false); }\n\t});\n}\n\n/** Drží output pipes průchozí po detach a končí až po EOF obou payload kanálů. */\nasync function run(): Promise<void> {\n\tvalidateDescriptors();\n\tconst payloadUnitName = process.argv[2];\n\tif (!payloadUnitName || !/^pf-systemd-tasks-observed-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\\.service$/.test(payloadUnitName)) fail('payload unit name is invalid.');\n\tconst stdout = new net.Socket({ fd: 3, readable: true, writable: false });\n\tconst stderr = new net.Socket({ fd: 4, readable: true, writable: false });\n\tconst observer = new net.Socket({ fd: 5, readable: true, writable: true, allowHalfOpen: true });\n\tconst sources: Source[] = [\n\t\t{ stream: stdout, kind: framing.kind.stdout, ended: false, pending: null },\n\t\t{ stream: stderr, kind: framing.kind.stderr, ended: false, pending: null },\n\t];\n\tlet attached = true;\n\tlet blocked = false;\n\tlet pumping = false;\n\tlet finishing = false;\n\tlet nextSourceIndex = 0;\n\tlet readyFrameSettled = false;\n\n\t/** Přepne nevratně do drain/discard a uvolní output backpressure payloadu. */\n\tfunction detach(): void {\n\t\tif (!attached) return;\n\t\tattached = false;\n\t\tblocked = false;\n\t\tobserver.destroy();\n\t\tfor (const source of sources) {\n\t\t\tsource.pending = null;\n\t\t\tsource.stream.off('readable', pump);\n\t\t\tsource.stream.resume();\n\t\t}\n\t}\n\n\t/** Po obou EOF odešle jediný complete frame, nebo po detach prostě dovolí exit. */\n\tfunction finishIfComplete(): void {\n\t\tif (!readyFrameSettled || finishing || !sources.every(/** Oba payload descriptory musí potvrdit EOF. */ source => source.ended)) return;\n\t\tfinishing = true;\n\t\tif (!attached) process.exit(0);\n\t\tobserver.end(framing.encode(framing.kind.complete), /** Lokální shutdown callback potvrzuje odeslaný FIN bez čekání na vzdálený close. */ () => process.exit(0));\n\t}\n\n\t/** Fair round-robin čte nejvýše bounded framy a respektuje skutečné EOF i observer write backpressure. */\n\tfunction pump(): void {\n\t\tif (pumping || blocked || !readyFrameSettled) return;\n\t\tpumping = true;\n\t\ttry {\n\t\t\tlet idleSources = 0;\n\t\t\twhile (!blocked && idleSources < sources.length) {\n\t\t\t\tconst source = sources[nextSourceIndex]!;\n\t\t\t\tnextSourceIndex = (nextSourceIndex + 1) % sources.length;\n\t\t\t\tconst available = source.pending ?? source.stream.read();\n\t\t\t\tif (available === null) { idleSources++; continue; }\n\t\t\t\tidleSources = 0;\n\t\t\t\tconst chunk = available.subarray(0, framing.maximumPayloadBytes);\n\t\t\t\tsource.pending = available.byteLength > chunk.byteLength ? available.subarray(chunk.byteLength) : null;\n\t\t\t\tif (attached) {\n\t\t\t\t\tconst writable = observer.write(framing.encode(source.kind, chunk));\n\t\t\t\t\tif (attached && !writable) blocked = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} finally { pumping = false; }\n\t\tfinishIfComplete();\n\t}\n\n\t/** Zdrojová chyba musí shodit guardian a přes BindsTo fail-closed zastavit payload. */\n\tfunction sourceFailure(error: Error): void {\n\t\tstdout.destroy();\n\t\tstderr.destroy();\n\t\tobserver.destroy();\n\t\tprocess.nextTick(/** Throw mimo EventEmitter callback zachová nenulový process exit. */ () => { throw error; });\n\t}\n\n\tfor (const source of sources) {\n\t\tsource.stream.on('readable', pump);\n\t\tsource.stream.once('end', /** Drain zbytku před terminal EOF. */ () => { source.ended = true; pump(); finishIfComplete(); });\n\t\tsource.stream.once('error', sourceFailure);\n\t}\n\tobserver.on('drain', /** Pokračuje až po skutečném poklesu Node write fronty. */ () => { blocked = false; pump(); });\n\tobserver.on('data', /** Protokol je jednosměrný; neočekávaný klientský payload znamená detach. */ detach);\n\tobserver.once('end', detach);\n\tobserver.once('close', detach);\n\tobserver.once('error', detach);\n\tprocess.on('SIGTERM', /** Jen pre-exec cancellation postrádá skutečné EOF, protože manager dál vlastní write endpointy. */ () => {\n\t\tif (payloadWasInvoked(payloadUnitName) !== false) { pump(); return; }\n\t\tfor (const source of sources) {\n\t\t\tsource.ended = true;\n\t\t\tsource.pending = null;\n\t\t\tsource.stream.destroy();\n\t\t}\n\t\tfinishIfComplete();\n\t});\n\n\tnotifyReady();\n\tif (!await writeReady(observer)) detach();\n\treadyFrameSettled = true;\n\tif (attached) observer.resume();\n\tpump();\n}\n\nvoid run().catch(/** Top-level startup failure musí zachovat nenulový guardian exit. */ error => { process.nextTick(() => { throw error; }); });\n","/** Čtyřbytový magic odděluje guardian stream od náhodného nebo staršího protokolu. */\nexport const magic = Buffer.from('SDTO', 'ascii');\n/** První veřejně implementovaná verze interního guardian protokolu. */\nexport const version = 1;\n/** Fixní hlavička obsahuje magic, verzi, kind a uint32be délku. */\nexport const headerBytes = 10;\n/** Jeden payload frame je bounded nezávisle na Node stream high-water marku. */\nexport const maximumPayloadBytes = 64 * 1024;\n\n/** Wire hodnoty jsou stabilní a nesmějí záviset na pořadí TypeScript enumu. */\nexport const kind = Object.freeze({ ready: 1, stdout: 2, stderr: 3, complete: 4 } as const);\n\n/** Zakóduje jeden již bounded frame pro guardian write frontu. */\nexport function encode(frameKind: number, payload: Uint8Array = new Uint8Array()): Buffer {\n\tif (!Object.values(kind).includes(frameKind as 1 | 2 | 3 | 4) || payload.byteLength > maximumPayloadBytes) throw new Error('Invalid observed guardian frame.');\n\tconst frame = Buffer.allocUnsafe(headerBytes + payload.byteLength);\n\tmagic.copy(frame, 0);\n\tframe[4] = version;\n\tframe[5] = frameKind;\n\tframe.writeUInt32BE(payload.byteLength, 6);\n\tframe.set(payload, headerBytes);\n\treturn frame;\n}\n"],"mappings":"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","names":[]}