@orkestrel/process 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/dist/src/core/index.cjs +24 -13
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +155 -141
- package/dist/src/core/index.d.ts +155 -141
- package/dist/src/core/index.js +24 -13
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +833 -788
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +328 -128
- package/dist/src/server/index.d.ts +328 -128
- package/dist/src/server/index.js +832 -788
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -11
|
@@ -9,6 +9,42 @@ let _src_core = require("../core/index.cjs");
|
|
|
9
9
|
let node_readline = require("node:readline");
|
|
10
10
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
11
11
|
let node_string_decoder = require("node:string_decoder");
|
|
12
|
+
//#region src/server/cloners.ts
|
|
13
|
+
/**
|
|
14
|
+
* Takes one owned frozen snapshot of a caller's command.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Every public entry point snapshots before it validates, so the object validated is the object
|
|
18
|
+
* spawned. Each property is read exactly once, because reading one runs the caller's own getter: a
|
|
19
|
+
* command whose `file` changes between reads would otherwise validate one executable and spawn
|
|
20
|
+
* another. The argument vector and the environment record are copied and frozen, so a caller
|
|
21
|
+
* mutating either after the call cannot reach the spawn. An absent optional stays absent rather than
|
|
22
|
+
* becoming an explicit `undefined`.
|
|
23
|
+
*
|
|
24
|
+
* @param command - The caller's command, whose properties may be getters
|
|
25
|
+
* @returns A frozen command carrying the values read at this instant
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* snapshotCommand({ file: 'git', arguments: ['status'] }) // { file: 'git', arguments: ['status'] }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function snapshotCommand(command) {
|
|
33
|
+
const file = command.file;
|
|
34
|
+
const argumentsList = Object.freeze([...command.arguments]);
|
|
35
|
+
const sourceEnvironment = command.environment;
|
|
36
|
+
const environment = sourceEnvironment === void 0 ? void 0 : Object.freeze({ ...sourceEnvironment });
|
|
37
|
+
const input = command.input;
|
|
38
|
+
const isolated = command.isolated;
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
file,
|
|
41
|
+
arguments: argumentsList,
|
|
42
|
+
...environment === void 0 ? {} : { environment },
|
|
43
|
+
...input === void 0 ? {} : { input },
|
|
44
|
+
...isolated === void 0 ? {} : { isolated }
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
12
48
|
//#region src/server/helpers.ts
|
|
13
49
|
/**
|
|
14
50
|
* Trims a buffer to at most `limit` trailing bytes without splitting a UTF-8 sequence.
|
|
@@ -42,9 +78,12 @@ function trimTail(bytes, limit) {
|
|
|
42
78
|
* Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
|
|
43
79
|
*
|
|
44
80
|
* @remarks
|
|
45
|
-
* Keeps the head — the captured start of a one-shot run's output.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
81
|
+
* Keeps the head — the captured start of a one-shot run's output. The end retreats only when a
|
|
82
|
+
* sequence genuinely spans the cut: the byte at `limit` carries the continuation bit pattern, and a
|
|
83
|
+
* lead byte within the preceding three bytes declares a length that reaches it. The retained bytes
|
|
84
|
+
* then end on a code-point boundary and decode without a replacement character. A byte at `limit`
|
|
85
|
+
* that carries the continuation pattern without such a lead byte is invalid UTF-8 rather than a
|
|
86
|
+
* split sequence, so the full `limit` survives instead of a valid byte being dropped for it.
|
|
48
87
|
*
|
|
49
88
|
* @param bytes - The accumulated bytes
|
|
50
89
|
* @param limit - The maximum retained byte count
|
|
@@ -58,47 +97,50 @@ function trimTail(bytes, limit) {
|
|
|
58
97
|
function trimHead(bytes, limit) {
|
|
59
98
|
const buffer = node_buffer.Buffer.isBuffer(bytes) ? bytes : node_buffer.Buffer.from(bytes);
|
|
60
99
|
if (buffer.byteLength <= limit) return buffer;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
const excluded = buffer[limit];
|
|
101
|
+
if (excluded === void 0 || (excluded & 192) !== 128) return node_buffer.Buffer.from(buffer.subarray(0, limit));
|
|
102
|
+
const floor = Math.max(0, limit - 3);
|
|
103
|
+
let start = limit - 1;
|
|
104
|
+
while (start >= floor) {
|
|
105
|
+
const lead = buffer[start];
|
|
106
|
+
if (lead !== void 0 && (lead & 192) !== 128) {
|
|
107
|
+
let span = 1;
|
|
108
|
+
if ((lead & 224) === 192) span = 2;
|
|
109
|
+
else if ((lead & 240) === 224) span = 3;
|
|
110
|
+
else if ((lead & 248) === 240) span = 4;
|
|
111
|
+
if (start + span > limit) return node_buffer.Buffer.from(buffer.subarray(0, start));
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
start -= 1;
|
|
66
115
|
}
|
|
67
|
-
return node_buffer.Buffer.from(buffer.subarray(0,
|
|
116
|
+
return node_buffer.Buffer.from(buffer.subarray(0, limit));
|
|
68
117
|
}
|
|
69
118
|
/**
|
|
70
|
-
*
|
|
119
|
+
* Bounds one delivered stream chunk to the bytes a capture still has room for.
|
|
71
120
|
*
|
|
72
121
|
* @remarks
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
122
|
+
* A chunk that is not a buffer contributes nothing and reports `undefined` rather than throwing,
|
|
123
|
+
* because a stream `data` listener receives an `unknown` payload. The cut is byte-exact and might
|
|
124
|
+
* land inside a multibyte sequence, because {@link buildExecuteResult} performs the single
|
|
125
|
+
* code-point-boundary trim over the whole capture rather than over each chunk. Give the capture one
|
|
126
|
+
* byte more room than its limit, so that final trim can read the first excluded byte and retreat off
|
|
127
|
+
* a split sequence. A chunk that fits its room is returned as the delivered buffer itself rather
|
|
128
|
+
* than a copy, because a capture appending it copies again and never writes through it; a chunk that
|
|
129
|
+
* overruns its room returns a copy of the bytes that fit. Treat the return as read-only either way.
|
|
130
|
+
*
|
|
131
|
+
* @param chunk - The delivered chunk, ignored when it is not a buffer
|
|
132
|
+
* @param room - The bytes the capture still accepts
|
|
133
|
+
* @returns The bytes to retain, or `undefined` when the chunk contributes none
|
|
82
134
|
*
|
|
83
135
|
* @example
|
|
84
136
|
* ```ts
|
|
85
|
-
*
|
|
137
|
+
* captureChunk(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
|
|
86
138
|
* ```
|
|
87
139
|
*/
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const environment = sourceEnvironment === void 0 ? void 0 : Object.freeze({ ...sourceEnvironment });
|
|
93
|
-
const input = command.input;
|
|
94
|
-
const isolated = command.isolated;
|
|
95
|
-
return Object.freeze({
|
|
96
|
-
file,
|
|
97
|
-
arguments: argumentsList,
|
|
98
|
-
...environment === void 0 ? {} : { environment },
|
|
99
|
-
...input === void 0 ? {} : { input },
|
|
100
|
-
...isolated === void 0 ? {} : { isolated }
|
|
101
|
-
});
|
|
140
|
+
function captureChunk(chunk, room) {
|
|
141
|
+
if (!node_buffer.Buffer.isBuffer(chunk) || room <= 0) return void 0;
|
|
142
|
+
if (chunk.byteLength <= room) return chunk;
|
|
143
|
+
return node_buffer.Buffer.from(chunk.subarray(0, room));
|
|
102
144
|
}
|
|
103
145
|
/**
|
|
104
146
|
* Renders one command into its diagnostic command line.
|
|
@@ -217,7 +259,7 @@ function mergeEnvironment(isolated, base, override) {
|
|
|
217
259
|
* command. An unreadable or malformed path reports `false` rather than throwing.
|
|
218
260
|
*
|
|
219
261
|
* @param target - The absolute or relative path to inspect
|
|
220
|
-
* @returns True
|
|
262
|
+
* @returns True if the path resolves to a regular file; false otherwise
|
|
221
263
|
*
|
|
222
264
|
* @example
|
|
223
265
|
* ```ts
|
|
@@ -490,7 +532,7 @@ function validateWorkspace(workspace) {
|
|
|
490
532
|
* initiated against a pid the host may have reused.
|
|
491
533
|
*
|
|
492
534
|
* @param child - The child boundary
|
|
493
|
-
* @returns True
|
|
535
|
+
* @returns True if the process has exited; false otherwise
|
|
494
536
|
*
|
|
495
537
|
* @example
|
|
496
538
|
* ```ts
|
|
@@ -546,7 +588,7 @@ function killProcess(child, signal) {
|
|
|
546
588
|
*
|
|
547
589
|
* @param pid - The root process id of the tree to end
|
|
548
590
|
* @param timeout - The milliseconds the utility is given before it is killed
|
|
549
|
-
* @returns True
|
|
591
|
+
* @returns True if the utility reported success; false otherwise (it failed, was unavailable, or was cut off)
|
|
550
592
|
*
|
|
551
593
|
* @example
|
|
552
594
|
* ```ts
|
|
@@ -597,7 +639,7 @@ function waitForExit(child, timeout) {
|
|
|
597
639
|
*
|
|
598
640
|
* @param child - The child boundary
|
|
599
641
|
* @param timeout - The milliseconds to wait before giving up
|
|
600
|
-
* @returns True
|
|
642
|
+
* @returns True if the host close arrived inside the deadline; false otherwise
|
|
601
643
|
*
|
|
602
644
|
* @example
|
|
603
645
|
* ```ts
|
|
@@ -632,7 +674,7 @@ function waitForClose(child, timeout) {
|
|
|
632
674
|
* @param child - The child boundary
|
|
633
675
|
* @param grace - The cooperative POSIX window in milliseconds between `SIGTERM` and `SIGKILL`
|
|
634
676
|
* @param confirm - The milliseconds the native exit is awaited after the final kill
|
|
635
|
-
* @returns True
|
|
677
|
+
* @returns True if the native exit was observed; false otherwise (`confirm` elapsed without it)
|
|
636
678
|
*
|
|
637
679
|
* @example
|
|
638
680
|
* ```ts
|
|
@@ -692,191 +734,507 @@ function buildExecuteResult(input) {
|
|
|
692
734
|
truncated: input.truncated
|
|
693
735
|
});
|
|
694
736
|
}
|
|
695
|
-
//#endregion
|
|
696
|
-
//#region src/server/Supervisor.ts
|
|
697
737
|
/**
|
|
698
|
-
*
|
|
738
|
+
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
699
739
|
*
|
|
700
740
|
* @remarks
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
741
|
+
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
742
|
+
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
743
|
+
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
744
|
+
* whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
|
|
745
|
+
* code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
|
|
746
|
+
* delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
|
|
747
|
+
* child through the same bounded stop, and only the earliest is recorded: a timeout reports
|
|
748
|
+
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
749
|
+
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
750
|
+
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
751
|
+
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
752
|
+
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
753
|
+
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
754
|
+
* overrides `command.input`. A host fault while writing that input terminates the run by design and
|
|
755
|
+
* marks its result failed; a strict rejection carries the host fault as its cause. This differs
|
|
756
|
+
* from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
|
|
757
|
+
* is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
|
|
758
|
+
* {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
|
|
759
|
+
* option or command string rejects before the child is spawned, because an async function cannot
|
|
760
|
+
* throw synchronously.
|
|
707
761
|
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
* child's native exit and by a termination this package initiated, so every way a child can end
|
|
713
|
-
* reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
|
|
714
|
-
* it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
|
|
715
|
-
* exist.
|
|
762
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
763
|
+
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
764
|
+
* @returns The settled run outcome
|
|
765
|
+
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
716
766
|
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
* carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
|
|
722
|
-
* termination completes. `relieve` is optional because a face that never pauses the child's output
|
|
723
|
-
* holds no backpressure to release, and a face with nothing to do there would otherwise declare an
|
|
724
|
-
* empty method to say so.
|
|
767
|
+
* @example
|
|
768
|
+
* ```ts
|
|
769
|
+
* const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
770
|
+
* ```
|
|
725
771
|
*/
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
772
|
+
async function execute(command, options) {
|
|
773
|
+
const snapshot = snapshotCommand(command);
|
|
774
|
+
const optionEnvironment = options?.environment;
|
|
775
|
+
const optionWorkspace = options?.workspace;
|
|
776
|
+
const optionInput = options?.input;
|
|
777
|
+
const optionTimeout = options?.timeout;
|
|
778
|
+
const optionGrace = options?.grace;
|
|
779
|
+
const optionSignal = options?.signal;
|
|
780
|
+
const optionStrict = options?.strict;
|
|
781
|
+
const optionLimit = options?.limit;
|
|
782
|
+
validateCommand(snapshot);
|
|
783
|
+
validateEnvironment(optionEnvironment);
|
|
784
|
+
validateWorkspace(optionWorkspace);
|
|
785
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
786
|
+
validateTimer(optionGrace, "option 'grace'");
|
|
787
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
788
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
789
|
+
const grace = optionGrace ?? _src_core.PROCESS_GRACE;
|
|
790
|
+
const timeout = optionTimeout ?? 0;
|
|
791
|
+
const strict = optionStrict ?? true;
|
|
792
|
+
const text = optionInput ?? snapshot.input;
|
|
793
|
+
const line = formatCommand(snapshot);
|
|
794
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
795
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
796
|
+
const plan = buildSpawn(snapshot, {
|
|
797
|
+
workspace,
|
|
798
|
+
environment
|
|
799
|
+
});
|
|
800
|
+
const settled = Promise.withResolvers();
|
|
801
|
+
const terminate = new AbortController();
|
|
802
|
+
const cleanup = new AbortController();
|
|
803
|
+
const finish = new AbortController();
|
|
804
|
+
const inputFailure = new AbortController();
|
|
805
|
+
const outChunks = [];
|
|
806
|
+
const errChunks = [];
|
|
807
|
+
let spawned = false;
|
|
808
|
+
let expired = false;
|
|
809
|
+
let aborted = false;
|
|
810
|
+
let outRetained = 0;
|
|
811
|
+
let errRetained = 0;
|
|
812
|
+
let cause;
|
|
813
|
+
let timeoutTimer;
|
|
814
|
+
let confirmTimer;
|
|
815
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
816
|
+
cwd: workspace,
|
|
817
|
+
detached: process.platform !== "win32",
|
|
818
|
+
env: environment,
|
|
819
|
+
stdio: [
|
|
820
|
+
"pipe",
|
|
821
|
+
"pipe",
|
|
822
|
+
"pipe"
|
|
823
|
+
],
|
|
824
|
+
windowsHide: true,
|
|
825
|
+
windowsVerbatimArguments: plan.verbatim
|
|
826
|
+
});
|
|
827
|
+
finish.signal.addEventListener("abort", () => {
|
|
828
|
+
clearTimeout(timeoutTimer);
|
|
829
|
+
clearTimeout(confirmTimer);
|
|
830
|
+
cleanup.abort();
|
|
831
|
+
settled.resolve(buildExecuteResult({
|
|
832
|
+
command: line,
|
|
833
|
+
stdout: node_buffer.Buffer.concat(outChunks),
|
|
834
|
+
stderr: node_buffer.Buffer.concat(errChunks),
|
|
835
|
+
code: child.exitCode,
|
|
836
|
+
signal: child.signalCode,
|
|
837
|
+
expired,
|
|
838
|
+
aborted,
|
|
839
|
+
truncated: outRetained > limit || errRetained > limit,
|
|
840
|
+
limit,
|
|
841
|
+
...cause === void 0 ? {} : { cause }
|
|
842
|
+
}));
|
|
843
|
+
}, { once: true });
|
|
844
|
+
terminate.signal.addEventListener("abort", () => {
|
|
845
|
+
if (finish.signal.aborted) return;
|
|
846
|
+
clearTimeout(timeoutTimer);
|
|
847
|
+
cleanup.abort();
|
|
848
|
+
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
849
|
+
if (finish.signal.aborted) return;
|
|
850
|
+
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
851
|
+
});
|
|
852
|
+
}, { once: true });
|
|
853
|
+
inputFailure.signal.addEventListener("abort", () => {
|
|
854
|
+
if (finish.signal.aborted) return;
|
|
855
|
+
cause = inputFailure.signal.reason;
|
|
856
|
+
terminate.abort();
|
|
857
|
+
}, { once: true });
|
|
858
|
+
child.stdin.on("error", (error) => inputFailure.abort(error));
|
|
859
|
+
child.stdout.on("data", (chunk) => {
|
|
860
|
+
const captured = captureChunk(chunk, limit + 1 - outRetained);
|
|
861
|
+
if (captured === void 0) return;
|
|
862
|
+
outRetained += captured.byteLength;
|
|
863
|
+
outChunks.push(captured);
|
|
864
|
+
});
|
|
865
|
+
child.stderr.on("data", (chunk) => {
|
|
866
|
+
const captured = captureChunk(chunk, limit + 1 - errRetained);
|
|
867
|
+
if (captured === void 0) return;
|
|
868
|
+
errRetained += captured.byteLength;
|
|
869
|
+
errChunks.push(captured);
|
|
870
|
+
});
|
|
871
|
+
child.once("spawn", () => {
|
|
872
|
+
spawned = true;
|
|
873
|
+
});
|
|
874
|
+
child.once("error", (error) => {
|
|
875
|
+
if (spawned) return;
|
|
876
|
+
cause = error;
|
|
877
|
+
finish.abort();
|
|
878
|
+
});
|
|
879
|
+
child.once("close", () => finish.abort());
|
|
880
|
+
if (text !== void 0) child.stdin.write(text, (error) => {
|
|
881
|
+
if (error !== void 0 && error !== null) inputFailure.abort(error);
|
|
882
|
+
});
|
|
883
|
+
child.stdin.end();
|
|
884
|
+
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
885
|
+
expired = true;
|
|
886
|
+
terminate.abort();
|
|
887
|
+
}, timeout);
|
|
888
|
+
if (optionSignal !== void 0) {
|
|
889
|
+
optionSignal.addEventListener("abort", () => {
|
|
890
|
+
aborted = true;
|
|
891
|
+
terminate.abort();
|
|
892
|
+
}, {
|
|
893
|
+
once: true,
|
|
894
|
+
signal: cleanup.signal
|
|
895
|
+
});
|
|
896
|
+
if (optionSignal.aborted) {
|
|
897
|
+
aborted = true;
|
|
898
|
+
terminate.abort();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const result = await settled.promise;
|
|
902
|
+
if (result.failed && strict) {
|
|
903
|
+
if (cause !== void 0 && cause === inputFailure.signal.reason) throw new _src_core.ProcessError(`Command '${result.command}' failed while writing standard input`, {
|
|
904
|
+
code: "input",
|
|
905
|
+
context: {
|
|
906
|
+
command: result.command,
|
|
907
|
+
code: result.code,
|
|
908
|
+
signal: result.signal
|
|
909
|
+
},
|
|
910
|
+
cause: inputFailure.signal.reason,
|
|
911
|
+
result
|
|
912
|
+
});
|
|
913
|
+
throw (0, _src_core.createExecuteError)(result, cause);
|
|
914
|
+
}
|
|
915
|
+
return result;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Runs one command to completion synchronously, buffering its output, and returns the outcome.
|
|
919
|
+
*
|
|
920
|
+
* @remarks
|
|
921
|
+
* The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
|
|
922
|
+
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
923
|
+
* contract carries neither. A positive `timeout` ends only the root process and can leave
|
|
924
|
+
* descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
|
|
925
|
+
* timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
|
|
926
|
+
* and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
|
|
927
|
+
* reports `truncated` without failing. The environment and input follow the same merge as
|
|
928
|
+
* {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
|
|
929
|
+
* carrying the {@link ExecuteResult}.
|
|
930
|
+
*
|
|
931
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
932
|
+
* @param options - Working directory, timeout, capture limit, and failure delivery
|
|
933
|
+
* @returns The run outcome
|
|
934
|
+
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
935
|
+
*
|
|
936
|
+
* @example
|
|
937
|
+
* ```ts
|
|
938
|
+
* const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
|
|
939
|
+
* ```
|
|
940
|
+
*/
|
|
941
|
+
function executeSync(command, options) {
|
|
942
|
+
const snapshot = snapshotCommand(command);
|
|
943
|
+
const optionEnvironment = options?.environment;
|
|
944
|
+
const optionWorkspace = options?.workspace;
|
|
945
|
+
const optionInput = options?.input;
|
|
946
|
+
const optionTimeout = options?.timeout;
|
|
947
|
+
const optionStrict = options?.strict;
|
|
948
|
+
const optionLimit = options?.limit;
|
|
949
|
+
validateCommand(snapshot);
|
|
950
|
+
validateEnvironment(optionEnvironment);
|
|
951
|
+
validateWorkspace(optionWorkspace);
|
|
952
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
953
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
954
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
955
|
+
const timeout = optionTimeout ?? 0;
|
|
956
|
+
const strict = optionStrict ?? true;
|
|
957
|
+
const text = optionInput ?? snapshot.input;
|
|
958
|
+
const line = formatCommand(snapshot);
|
|
959
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
960
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
961
|
+
const plan = buildSpawn(snapshot, {
|
|
962
|
+
workspace,
|
|
963
|
+
environment
|
|
964
|
+
});
|
|
965
|
+
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
966
|
+
cwd: workspace,
|
|
967
|
+
env: environment,
|
|
968
|
+
encoding: "buffer",
|
|
969
|
+
maxBuffer: limit,
|
|
970
|
+
killSignal: "SIGKILL",
|
|
971
|
+
windowsHide: true,
|
|
972
|
+
windowsVerbatimArguments: plan.verbatim,
|
|
973
|
+
...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
|
|
974
|
+
...timeout > 0 ? { timeout } : {}
|
|
975
|
+
});
|
|
976
|
+
const error = outcome.error;
|
|
977
|
+
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
978
|
+
const result = buildExecuteResult({
|
|
979
|
+
command: line,
|
|
980
|
+
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
981
|
+
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
982
|
+
code: outcome.status,
|
|
983
|
+
signal: outcome.signal,
|
|
984
|
+
expired: fault === "ETIMEDOUT",
|
|
985
|
+
aborted: false,
|
|
986
|
+
truncated: fault === "ENOBUFS",
|
|
987
|
+
limit,
|
|
988
|
+
...error === void 0 ? {} : { cause: error }
|
|
989
|
+
});
|
|
990
|
+
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Spawns one command as a detached process and returns without waiting for it.
|
|
995
|
+
*
|
|
996
|
+
* @remarks
|
|
997
|
+
* The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
|
|
998
|
+
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
999
|
+
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
1000
|
+
* is spawned.
|
|
1001
|
+
*
|
|
1002
|
+
* @param command - The executable, arguments, and optional environment
|
|
1003
|
+
* @param options - The working directory the detached child starts in
|
|
1004
|
+
* @returns Nothing
|
|
1005
|
+
* @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
|
|
1006
|
+
*
|
|
1007
|
+
* @example
|
|
1008
|
+
* ```ts
|
|
1009
|
+
* detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
|
|
1010
|
+
* ```
|
|
1011
|
+
*/
|
|
1012
|
+
function detach(command, options) {
|
|
1013
|
+
const snapshot = snapshotCommand(command);
|
|
1014
|
+
const optionWorkspace = options?.workspace;
|
|
1015
|
+
validateCommand(snapshot);
|
|
1016
|
+
validateWorkspace(optionWorkspace);
|
|
1017
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1018
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
|
|
1019
|
+
const plan = buildSpawn(snapshot, {
|
|
1020
|
+
workspace,
|
|
1021
|
+
environment
|
|
1022
|
+
});
|
|
1023
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1024
|
+
cwd: workspace,
|
|
1025
|
+
detached: true,
|
|
1026
|
+
env: environment,
|
|
1027
|
+
stdio: "ignore",
|
|
1028
|
+
windowsHide: true,
|
|
1029
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1030
|
+
});
|
|
1031
|
+
child.once("error", () => void 0);
|
|
1032
|
+
child.unref();
|
|
1033
|
+
}
|
|
1034
|
+
//#endregion
|
|
1035
|
+
//#region src/server/processes/Supervisor.ts
|
|
1036
|
+
/**
|
|
1037
|
+
* Supervises one child process and reports each lifecycle moment to the face composing it.
|
|
1038
|
+
*
|
|
1039
|
+
* @remarks
|
|
1040
|
+
* This is the engine every supervised-child face shares. It owns the eager spawn, the byte-bounded
|
|
1041
|
+
* stderr tail and its decoded chunks, the standard-input channel and its bounded confirmation, the
|
|
1042
|
+
* termination sequence, and the progression from the child's native exit through the bounded `drain`
|
|
1043
|
+
* window to one terminal moment. It owns no output framing and no observation surface: a face
|
|
1044
|
+
* attaches its own consumer to `stdout`, frames bytes however its contract promises, and owns the
|
|
1045
|
+
* emitter and event map its consumers subscribe to.
|
|
1046
|
+
*
|
|
1047
|
+
* The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
|
|
1048
|
+
* host child directly, so they expose native exit as soon as the host records it, and `ending`
|
|
1049
|
+
* settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
|
|
1050
|
+
* read channels close or the bounded `drain` window cuts them off. That window is armed by the
|
|
1051
|
+
* child's native exit and by a termination this package initiated, so every way a child can end
|
|
1052
|
+
* reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
|
|
1053
|
+
* it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
|
|
1054
|
+
* exist.
|
|
1055
|
+
*
|
|
1056
|
+
* The face is a set of callbacks rather than an emitter, because each face publishes a different
|
|
1057
|
+
* event map over the same moments. `chunk` and `fault` carry the live stderr text and the child or
|
|
1058
|
+
* channel faults, `relieve` reports that a termination began so the face can release whatever
|
|
1059
|
+
* backpressure it holds, `close` ends the face's read pipeline at the terminal moment, `terminal`
|
|
1060
|
+
* carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
|
|
1061
|
+
* termination completes. `relieve` is optional because a face that never pauses the child's output
|
|
1062
|
+
* holds no backpressure to release, and a face with nothing to do there would otherwise declare an
|
|
1063
|
+
* empty method to say so.
|
|
1064
|
+
*
|
|
1065
|
+
* @example
|
|
1066
|
+
* ```ts
|
|
1067
|
+
* import { Supervisor } from '@orkestrel/process/server'
|
|
1068
|
+
*
|
|
1069
|
+
* const engine = new Supervisor(
|
|
1070
|
+
* { command: { file: 'node', arguments: ['--version'] }, workspace: process.cwd() },
|
|
1071
|
+
* {
|
|
1072
|
+
* chunk: () => undefined,
|
|
1073
|
+
* fault: () => undefined,
|
|
1074
|
+
* close: () => undefined,
|
|
1075
|
+
* terminal: () => undefined,
|
|
1076
|
+
* teardown: () => undefined,
|
|
1077
|
+
* },
|
|
1078
|
+
* )
|
|
1079
|
+
* const exit = await engine.exit
|
|
1080
|
+
* exit.code // 0
|
|
1081
|
+
* await engine.destroy()
|
|
1082
|
+
* ```
|
|
1083
|
+
*/
|
|
1084
|
+
var Supervisor = class {
|
|
1085
|
+
#chunk;
|
|
1086
|
+
#fault;
|
|
1087
|
+
#relieve;
|
|
1088
|
+
#close;
|
|
1089
|
+
#terminal;
|
|
1090
|
+
#teardown;
|
|
1091
|
+
#child;
|
|
1092
|
+
#grace;
|
|
1093
|
+
#drain;
|
|
1094
|
+
#evidence;
|
|
1095
|
+
#delivery;
|
|
1096
|
+
#decoder = new node_string_decoder.StringDecoder("utf8");
|
|
1097
|
+
#exit = Promise.withResolvers();
|
|
1098
|
+
#ending = Promise.withResolvers();
|
|
1099
|
+
#signal;
|
|
1100
|
+
#abort;
|
|
1101
|
+
#writes = /* @__PURE__ */ new Map();
|
|
1102
|
+
#tail = node_buffer.Buffer.alloc(0);
|
|
1103
|
+
#stopping = false;
|
|
1104
|
+
#settled = false;
|
|
1105
|
+
#input = 0;
|
|
1106
|
+
#failure;
|
|
1107
|
+
#waiting;
|
|
1108
|
+
#closure;
|
|
1109
|
+
#termination;
|
|
1110
|
+
#destruction;
|
|
1111
|
+
/**
|
|
1112
|
+
* Spawns one child process and begins standard-error capture.
|
|
1113
|
+
*
|
|
1114
|
+
* @remarks
|
|
1115
|
+
* The face's callbacks are captured before anything is read or spawned, so the first moment the
|
|
1116
|
+
* child can produce already has somewhere to go. `options` supplies the command, workspace,
|
|
1117
|
+
* termination, capture, and standard-input settings; a face's own settings, such as an output
|
|
1118
|
+
* retention bound, are read and validated by that face before it constructs this engine, so an
|
|
1119
|
+
* invalid one refuses construction while nothing has been spawned.
|
|
1120
|
+
*
|
|
1121
|
+
* @param options - Command, workspace, termination, capture, and stdin settings
|
|
1122
|
+
* @param face - The composing face's callbacks for each lifecycle moment
|
|
1123
|
+
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1124
|
+
*/
|
|
1125
|
+
constructor(options, face) {
|
|
1126
|
+
this.#chunk = face.chunk;
|
|
1127
|
+
this.#fault = face.fault;
|
|
1128
|
+
this.#relieve = face.relieve;
|
|
1129
|
+
this.#close = face.close;
|
|
1130
|
+
this.#terminal = face.terminal;
|
|
1131
|
+
this.#teardown = face.teardown;
|
|
1132
|
+
const source = options.command;
|
|
1133
|
+
const workspace = options.workspace;
|
|
1134
|
+
const grace = options.grace;
|
|
1135
|
+
const drain = options.drain;
|
|
1136
|
+
const evidence = options.evidence;
|
|
1137
|
+
const delivery = options.delivery;
|
|
1138
|
+
const writable = options.writable;
|
|
1139
|
+
const signal = options.signal;
|
|
1140
|
+
const command = snapshotCommand(source);
|
|
1141
|
+
const input = command.input;
|
|
1142
|
+
validateCommand(command);
|
|
1143
|
+
validateWorkspace(workspace);
|
|
1144
|
+
validateTimer(grace, "option 'grace'");
|
|
1145
|
+
validateTimer(drain, "option 'drain'");
|
|
1146
|
+
validateTimer(delivery, "option 'delivery'");
|
|
1147
|
+
validateBytes(evidence, "option 'evidence'", 0);
|
|
1148
|
+
this.#grace = grace ?? _src_core.PROCESS_GRACE;
|
|
1149
|
+
this.#drain = drain ?? _src_core.PROCESS_DRAIN;
|
|
1150
|
+
this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
|
|
1151
|
+
this.#delivery = delivery ?? 0;
|
|
1152
|
+
this.#signal = signal;
|
|
1153
|
+
const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
|
|
1154
|
+
const plan = buildSpawn(command, {
|
|
1155
|
+
workspace,
|
|
1156
|
+
environment: childEnvironment
|
|
1157
|
+
});
|
|
1158
|
+
this.#child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1159
|
+
cwd: workspace,
|
|
1160
|
+
detached: process.platform !== "win32",
|
|
1161
|
+
env: childEnvironment,
|
|
1162
|
+
stdio: [
|
|
1163
|
+
"pipe",
|
|
1164
|
+
"pipe",
|
|
1165
|
+
"pipe"
|
|
1166
|
+
],
|
|
1167
|
+
windowsHide: true,
|
|
1168
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1169
|
+
});
|
|
1170
|
+
this.#child.once("error", this.#fault);
|
|
1171
|
+
this.#child.once("exit", this.#expire.bind(this));
|
|
1172
|
+
this.#child.once("close", this.#complete.bind(this));
|
|
1173
|
+
this.#child.stdin.on("error", this.#failInputStream.bind(this));
|
|
1174
|
+
this.#child.stderr.on("data", this.#retain.bind(this));
|
|
1175
|
+
if (input !== void 0) {
|
|
1176
|
+
this.#input += 1;
|
|
1177
|
+
this.#child.stdin.write(input, this.#completeInput.bind(this));
|
|
1178
|
+
}
|
|
1179
|
+
if (writable !== true) {
|
|
1180
|
+
this.#input += 1;
|
|
1181
|
+
this.#child.stdin.end(this.#completeInput.bind(this));
|
|
1182
|
+
}
|
|
1183
|
+
if (signal !== void 0) {
|
|
1184
|
+
this.#abort = this.#terminate.bind(this);
|
|
1185
|
+
signal.addEventListener("abort", this.#abort, { once: true });
|
|
1186
|
+
if (signal.aborted) this.stop();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** Holds the child's standard-output stream, for the composing face to attach its own consumer to. */
|
|
1190
|
+
get stdout() {
|
|
1191
|
+
return this.#child.stdout;
|
|
1192
|
+
}
|
|
1193
|
+
/** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1194
|
+
get pid() {
|
|
1195
|
+
return this.#child.pid;
|
|
1196
|
+
}
|
|
1197
|
+
/** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1198
|
+
get code() {
|
|
1199
|
+
return this.#child.exitCode;
|
|
1200
|
+
}
|
|
1201
|
+
/** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1202
|
+
get signal() {
|
|
1203
|
+
return this.#child.signalCode;
|
|
1204
|
+
}
|
|
1205
|
+
/** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1206
|
+
get evidence() {
|
|
1207
|
+
return this.#tail.toString("utf8");
|
|
1208
|
+
}
|
|
1209
|
+
/** Reports whether the terminal moment arrived and `exit` settled. */
|
|
1210
|
+
get settled() {
|
|
1211
|
+
return this.#settled;
|
|
1212
|
+
}
|
|
1213
|
+
/** Reports whether termination began, including after the terminal moment. */
|
|
1214
|
+
get stopping() {
|
|
1215
|
+
return this.#stopping;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Settles at the child's own ending, awaited without the terminal moment's drain window.
|
|
1219
|
+
*
|
|
1220
|
+
* @remarks
|
|
1221
|
+
* Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
|
|
1222
|
+
* the host records them. It settles at the native exit, and at the terminal moment for a child
|
|
1223
|
+
* whose spawn produced no native exit at all, so it can never outlive the supervision.
|
|
1224
|
+
*/
|
|
1225
|
+
get ending() {
|
|
1226
|
+
return this.#ending.promise;
|
|
1227
|
+
}
|
|
1228
|
+
/** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
|
|
1229
|
+
get exit() {
|
|
1230
|
+
return this.#exit.promise;
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Writes raw bytes to the open standard-input channel.
|
|
1234
|
+
*
|
|
1235
|
+
* @remarks
|
|
1236
|
+
* Never rejects, and adds no framing: a face composes whatever terminator its contract promises
|
|
1237
|
+
* before it calls this. `true` means the host accepted the bytes without reporting a fault; it
|
|
880
1238
|
* does not prove that the child read them. An ordinary write settles when the kernel accepts it.
|
|
881
1239
|
* Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
|
|
882
1240
|
* every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
|
|
@@ -884,7 +1242,30 @@ var Supervisor = class {
|
|
|
884
1242
|
* discard.
|
|
885
1243
|
*
|
|
886
1244
|
* @param bytes - The payload to write, already framed by the caller
|
|
887
|
-
* @returns True
|
|
1245
|
+
* @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
|
|
1246
|
+
*
|
|
1247
|
+
* @example
|
|
1248
|
+
* ```ts
|
|
1249
|
+
* import { Supervisor } from '@orkestrel/process/server'
|
|
1250
|
+
*
|
|
1251
|
+
* const engine = new Supervisor(
|
|
1252
|
+
* {
|
|
1253
|
+
* command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
|
|
1254
|
+
* workspace: process.cwd(),
|
|
1255
|
+
* writable: true,
|
|
1256
|
+
* },
|
|
1257
|
+
* {
|
|
1258
|
+
* chunk: () => undefined,
|
|
1259
|
+
* fault: () => undefined,
|
|
1260
|
+
* close: () => undefined,
|
|
1261
|
+
* terminal: () => undefined,
|
|
1262
|
+
* teardown: () => undefined,
|
|
1263
|
+
* },
|
|
1264
|
+
* )
|
|
1265
|
+
* const accepted = await engine.deliver(new TextEncoder().encode('ping\n'))
|
|
1266
|
+
* accepted // true
|
|
1267
|
+
* await engine.destroy()
|
|
1268
|
+
* ```
|
|
888
1269
|
*/
|
|
889
1270
|
deliver(bytes) {
|
|
890
1271
|
const stdin = this.#child.stdin;
|
|
@@ -935,7 +1316,7 @@ var Supervisor = class {
|
|
|
935
1316
|
* and settles `exit` when they remain open. A cutoff reached before the native exit reports the
|
|
936
1317
|
* `code` and `signal` the host had recorded by then, which is `null` for a child still running.
|
|
937
1318
|
*
|
|
938
|
-
* @returns True
|
|
1319
|
+
* @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
|
|
939
1320
|
*/
|
|
940
1321
|
stop() {
|
|
941
1322
|
if (this.#termination !== void 0) return this.#termination;
|
|
@@ -1063,7 +1444,7 @@ var Supervisor = class {
|
|
|
1063
1444
|
}
|
|
1064
1445
|
};
|
|
1065
1446
|
//#endregion
|
|
1066
|
-
//#region src/server/Process.ts
|
|
1447
|
+
//#region src/server/processes/Process.ts
|
|
1067
1448
|
/**
|
|
1068
1449
|
* Supervises one child while keeping every observation channel aligned at termination.
|
|
1069
1450
|
*
|
|
@@ -1110,7 +1491,7 @@ var Process = class {
|
|
|
1110
1491
|
#truncated = false;
|
|
1111
1492
|
#ended = false;
|
|
1112
1493
|
/**
|
|
1113
|
-
*
|
|
1494
|
+
* Spawns one child process and begins stream capture.
|
|
1114
1495
|
*
|
|
1115
1496
|
* @param options - Command, workspace, termination, capture, stdin, and observation settings
|
|
1116
1497
|
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
@@ -1165,48 +1546,48 @@ var Process = class {
|
|
|
1165
1546
|
this.#reader.on("line", this.#push.bind(this));
|
|
1166
1547
|
this.#reader.once("close", this.#finish.bind(this));
|
|
1167
1548
|
}
|
|
1168
|
-
/**
|
|
1549
|
+
/** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1169
1550
|
get pid() {
|
|
1170
1551
|
return this.#engine.pid;
|
|
1171
1552
|
}
|
|
1172
|
-
/**
|
|
1553
|
+
/** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1173
1554
|
get code() {
|
|
1174
1555
|
return this.#engine.code;
|
|
1175
1556
|
}
|
|
1176
|
-
/**
|
|
1557
|
+
/** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1177
1558
|
get signal() {
|
|
1178
1559
|
return this.#engine.signal;
|
|
1179
1560
|
}
|
|
1180
|
-
/**
|
|
1561
|
+
/** Holds the typed lifecycle observation surface. */
|
|
1181
1562
|
get emitter() {
|
|
1182
1563
|
return this.#emitter;
|
|
1183
1564
|
}
|
|
1184
|
-
/**
|
|
1565
|
+
/** Yields the captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
|
|
1185
1566
|
get lines() {
|
|
1186
1567
|
return this.#lines;
|
|
1187
1568
|
}
|
|
1188
|
-
/**
|
|
1569
|
+
/** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1189
1570
|
get evidence() {
|
|
1190
1571
|
return this.#engine.evidence;
|
|
1191
1572
|
}
|
|
1192
|
-
/**
|
|
1573
|
+
/** Reports whether the `lines` stream omitted output after a retention bound was reached. */
|
|
1193
1574
|
get truncated() {
|
|
1194
1575
|
return this.#truncated;
|
|
1195
1576
|
}
|
|
1196
|
-
/**
|
|
1577
|
+
/** Reports whether the terminal moment arrived and `exit` settled. */
|
|
1197
1578
|
get settled() {
|
|
1198
1579
|
return this.#engine.settled;
|
|
1199
1580
|
}
|
|
1200
|
-
/**
|
|
1581
|
+
/** Reports whether termination began, including after the terminal moment. */
|
|
1201
1582
|
get stopping() {
|
|
1202
1583
|
return this.#engine.stopping;
|
|
1203
1584
|
}
|
|
1204
|
-
/**
|
|
1585
|
+
/** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
|
|
1205
1586
|
get exit() {
|
|
1206
1587
|
return this.#engine.exit;
|
|
1207
1588
|
}
|
|
1208
1589
|
/**
|
|
1209
|
-
*
|
|
1590
|
+
* Writes one line to the open standard-input channel.
|
|
1210
1591
|
*
|
|
1211
1592
|
* @remarks
|
|
1212
1593
|
* Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
|
|
@@ -1220,7 +1601,7 @@ var Process = class {
|
|
|
1220
1601
|
* for bytes the package is about to discard.
|
|
1221
1602
|
*
|
|
1222
1603
|
* @param text - The line text without its trailing newline
|
|
1223
|
-
* @returns True
|
|
1604
|
+
* @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
|
|
1224
1605
|
*/
|
|
1225
1606
|
send(text) {
|
|
1226
1607
|
return this.#engine.deliver(node_buffer.Buffer.from(`${text}\n`, "utf8"));
|
|
@@ -1234,7 +1615,7 @@ var Process = class {
|
|
|
1234
1615
|
* and settles `exit` when they remain open. A cutoff reached before the native exit reports the
|
|
1235
1616
|
* `code` and `signal` the host had recorded by then, which is `null` for a child still running.
|
|
1236
1617
|
*
|
|
1237
|
-
* @returns True
|
|
1618
|
+
* @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
|
|
1238
1619
|
*/
|
|
1239
1620
|
stop() {
|
|
1240
1621
|
return this.#engine.stop();
|
|
@@ -1352,9 +1733,9 @@ var Process = class {
|
|
|
1352
1733
|
}
|
|
1353
1734
|
};
|
|
1354
1735
|
//#endregion
|
|
1355
|
-
//#region src/server/ProcessManager.ts
|
|
1736
|
+
//#region src/server/processes/ProcessManager.ts
|
|
1356
1737
|
/**
|
|
1357
|
-
*
|
|
1738
|
+
* Represents a keyed registry of live supervised child processes.
|
|
1358
1739
|
*
|
|
1359
1740
|
* @remarks
|
|
1360
1741
|
* A child launched under an id joins the registry and emits `launch`; when it settles it removes
|
|
@@ -1390,7 +1771,7 @@ var ProcessManager = class {
|
|
|
1390
1771
|
#destroying = false;
|
|
1391
1772
|
#ending;
|
|
1392
1773
|
/**
|
|
1393
|
-
*
|
|
1774
|
+
* Constructs a process registry.
|
|
1394
1775
|
*
|
|
1395
1776
|
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
1396
1777
|
*/
|
|
@@ -1402,16 +1783,16 @@ var ProcessManager = class {
|
|
|
1402
1783
|
...error === void 0 ? {} : { error }
|
|
1403
1784
|
});
|
|
1404
1785
|
}
|
|
1405
|
-
/**
|
|
1786
|
+
/** Holds the typed fleet-level observation surface. */
|
|
1406
1787
|
get emitter() {
|
|
1407
1788
|
return this.#emitter;
|
|
1408
1789
|
}
|
|
1409
|
-
/**
|
|
1790
|
+
/** Counts the live children. */
|
|
1410
1791
|
get count() {
|
|
1411
1792
|
return this.#children.size;
|
|
1412
1793
|
}
|
|
1413
1794
|
/**
|
|
1414
|
-
*
|
|
1795
|
+
* Returns the live child under `id`, or `undefined` when none is.
|
|
1415
1796
|
*
|
|
1416
1797
|
* @param id - The registry key
|
|
1417
1798
|
* @returns The child, or `undefined`
|
|
@@ -1420,7 +1801,7 @@ var ProcessManager = class {
|
|
|
1420
1801
|
return this.#children.get(id);
|
|
1421
1802
|
}
|
|
1422
1803
|
/**
|
|
1423
|
-
*
|
|
1804
|
+
* Returns a snapshot of every live child.
|
|
1424
1805
|
*
|
|
1425
1806
|
* @returns The live children in launch order
|
|
1426
1807
|
*/
|
|
@@ -1428,7 +1809,7 @@ var ProcessManager = class {
|
|
|
1428
1809
|
return [...this.#children.values()];
|
|
1429
1810
|
}
|
|
1430
1811
|
/**
|
|
1431
|
-
*
|
|
1812
|
+
* Spawns and registers one child under `id`.
|
|
1432
1813
|
*
|
|
1433
1814
|
* @remarks
|
|
1434
1815
|
* {@link Process} reads every option before it spawns, so a caller's own option getter runs while
|
|
@@ -1466,7 +1847,7 @@ var ProcessManager = class {
|
|
|
1466
1847
|
return this.#stopMany(target);
|
|
1467
1848
|
}
|
|
1468
1849
|
/**
|
|
1469
|
-
*
|
|
1850
|
+
* Stops every child, then destroys the registry emitter last.
|
|
1470
1851
|
*
|
|
1471
1852
|
* @remarks
|
|
1472
1853
|
* Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
|
|
@@ -1508,599 +1889,263 @@ var ProcessManager = class {
|
|
|
1508
1889
|
}
|
|
1509
1890
|
async #teardown() {
|
|
1510
1891
|
await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
|
|
1511
|
-
while (this.#orphans.size > 0) {
|
|
1512
|
-
const orphans = [...this.#orphans];
|
|
1513
|
-
this.#orphans.clear();
|
|
1514
|
-
await Promise.allSettled(orphans.map((child) => child.destroy()));
|
|
1515
|
-
}
|
|
1516
|
-
this.#children.clear();
|
|
1517
|
-
this.#ids.clear();
|
|
1518
|
-
this.#emitter.destroy();
|
|
1519
|
-
}
|
|
1520
|
-
};
|
|
1521
|
-
//#endregion
|
|
1522
|
-
//#region src/server/Session.ts
|
|
1523
|
-
/**
|
|
1524
|
-
* Supervises one child and publishes its standard output as raw bytes.
|
|
1525
|
-
*
|
|
1526
|
-
* @remarks
|
|
1527
|
-
* This is the transport face of the supervision engine `Process` also composes. It frames nothing,
|
|
1528
|
-
* retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
|
|
1529
|
-
* `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
|
|
1530
|
-
* length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
|
|
1531
|
-
* itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
|
|
1532
|
-
* tail a `Process` retains, because a diagnostic stream is read rather than parsed.
|
|
1533
|
-
*
|
|
1534
|
-
* Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
|
|
1535
|
-
* the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
|
|
1536
|
-
* buffer without reaching memory the host still manages and without depending on a host type.
|
|
1537
|
-
*
|
|
1538
|
-
* The standard-input channel is open from the spawn, and `end` closes it without terminating
|
|
1539
|
-
* anything. That is the member a cooperative protocol reaches for: the child observes end of input
|
|
1540
|
-
* and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
|
|
1541
|
-
* `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
|
|
1542
|
-
* the inherited read ends.
|
|
1543
|
-
*
|
|
1544
|
-
* @example
|
|
1545
|
-
* ```ts
|
|
1546
|
-
* import { Session } from '@orkestrel/process/server'
|
|
1547
|
-
*
|
|
1548
|
-
* const session = new Session({
|
|
1549
|
-
* command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
|
|
1550
|
-
* workspace: process.cwd(),
|
|
1551
|
-
* })
|
|
1552
|
-
* ```
|
|
1553
|
-
*/
|
|
1554
|
-
var Session = class {
|
|
1555
|
-
#emitter;
|
|
1556
|
-
#engine;
|
|
1557
|
-
#output;
|
|
1558
|
-
/**
|
|
1559
|
-
* Spawn one child process and begin byte capture.
|
|
1560
|
-
*
|
|
1561
|
-
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
1562
|
-
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1563
|
-
*/
|
|
1564
|
-
constructor(options) {
|
|
1565
|
-
const command = options.command;
|
|
1566
|
-
const workspace = options.workspace;
|
|
1567
|
-
const grace = options.grace;
|
|
1568
|
-
const drain = options.drain;
|
|
1569
|
-
const evidence = options.evidence;
|
|
1570
|
-
const delivery = options.delivery;
|
|
1571
|
-
const signal = options.signal;
|
|
1572
|
-
const on = options.on;
|
|
1573
|
-
const error = options.error;
|
|
1574
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1575
|
-
...on === void 0 ? {} : { on },
|
|
1576
|
-
...error === void 0 ? {} : { error }
|
|
1577
|
-
});
|
|
1578
|
-
this.#output = this.#publish.bind(this);
|
|
1579
|
-
this.#engine = new Supervisor({
|
|
1580
|
-
command,
|
|
1581
|
-
workspace,
|
|
1582
|
-
writable: true,
|
|
1583
|
-
...grace === void 0 ? {} : { grace },
|
|
1584
|
-
...drain === void 0 ? {} : { drain },
|
|
1585
|
-
...evidence === void 0 ? {} : { evidence },
|
|
1586
|
-
...delivery === void 0 ? {} : { delivery },
|
|
1587
|
-
...signal === void 0 ? {} : { signal }
|
|
1588
|
-
}, {
|
|
1589
|
-
chunk: this.#reportStderr.bind(this),
|
|
1590
|
-
fault: this.#reportFault.bind(this),
|
|
1591
|
-
close: this.#closeOutput.bind(this),
|
|
1592
|
-
terminal: this.#reportExit.bind(this),
|
|
1593
|
-
teardown: this.#emitter.destroy.bind(this.#emitter)
|
|
1594
|
-
});
|
|
1595
|
-
this.#engine.stdout.on("data", this.#output);
|
|
1596
|
-
}
|
|
1597
|
-
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1598
|
-
get pid() {
|
|
1599
|
-
return this.#engine.pid;
|
|
1600
|
-
}
|
|
1601
|
-
/** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1602
|
-
get code() {
|
|
1603
|
-
return this.#engine.code;
|
|
1604
|
-
}
|
|
1605
|
-
/** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1606
|
-
get signal() {
|
|
1607
|
-
return this.#engine.signal;
|
|
1608
|
-
}
|
|
1609
|
-
/** The typed lifecycle observation surface. */
|
|
1610
|
-
get emitter() {
|
|
1611
|
-
return this.#emitter;
|
|
1612
|
-
}
|
|
1613
|
-
/** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1614
|
-
get evidence() {
|
|
1615
|
-
return this.#engine.evidence;
|
|
1616
|
-
}
|
|
1617
|
-
/** True after the terminal moment arrived and `exit` settled. */
|
|
1618
|
-
get settled() {
|
|
1619
|
-
return this.#engine.settled;
|
|
1620
|
-
}
|
|
1621
|
-
/** True after termination began, including after the terminal moment. `end` never turns it true. */
|
|
1622
|
-
get stopping() {
|
|
1623
|
-
return this.#engine.stopping;
|
|
1624
|
-
}
|
|
1625
|
-
/** The child's own ending, awaited without the terminal moment's drain window. */
|
|
1626
|
-
get ending() {
|
|
1627
|
-
return this.#engine.ending;
|
|
1628
|
-
}
|
|
1629
|
-
/** The terminal child state, observed once after stream close or the drain cutoff. */
|
|
1630
|
-
get exit() {
|
|
1631
|
-
return this.#engine.exit;
|
|
1632
|
-
}
|
|
1633
|
-
/**
|
|
1634
|
-
* Writes raw bytes to the open standard-input channel.
|
|
1635
|
-
*
|
|
1636
|
-
* @remarks
|
|
1637
|
-
* Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
|
|
1638
|
-
* the host accepted them without reporting a fault; it does not prove that the child read them.
|
|
1639
|
-
* The channel can queue the payload, so treat the array as owned by the channel until the
|
|
1640
|
-
* returned promise settles.
|
|
1641
|
-
*
|
|
1642
|
-
* @param bytes - The payload to write, already framed by the caller
|
|
1643
|
-
* @returns True when the host accepted the bytes without reporting a fault; false when the channel was closed, destroyed, ended, failed, or remained unconfirmed through `delivery`
|
|
1644
|
-
*/
|
|
1645
|
-
write(bytes) {
|
|
1646
|
-
return this.#engine.deliver(bytes);
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Closes the standard-input channel and leaves the child running.
|
|
1650
|
-
*
|
|
1651
|
-
* @remarks
|
|
1652
|
-
* Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
|
|
1653
|
-
* arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
|
|
1654
|
-
* already accepted. A later `write` resolves `false`.
|
|
1655
|
-
*
|
|
1656
|
-
* @returns The stable barrier shared by every call
|
|
1657
|
-
*/
|
|
1658
|
-
end() {
|
|
1659
|
-
return this.#engine.end();
|
|
1660
|
-
}
|
|
1661
|
-
/**
|
|
1662
|
-
* Terminates the child process tree and reaches the terminal observation moment.
|
|
1663
|
-
*
|
|
1664
|
-
* @remarks
|
|
1665
|
-
* Never rejects, and every call shares one termination bounded by `grace` and `drain`.
|
|
1666
|
-
*
|
|
1667
|
-
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
1668
|
-
*/
|
|
1669
|
-
stop() {
|
|
1670
|
-
return this.#engine.stop();
|
|
1671
|
-
}
|
|
1672
|
-
/**
|
|
1673
|
-
* Stops the child and destroys the observation emitter after the terminal state freezes.
|
|
1674
|
-
*
|
|
1675
|
-
* @remarks
|
|
1676
|
-
* Always resolves, including when termination was never confirmed. Every call shares one barrier.
|
|
1677
|
-
*
|
|
1678
|
-
* @returns The stable barrier shared by every call
|
|
1679
|
-
*/
|
|
1680
|
-
destroy() {
|
|
1681
|
-
return this.#engine.destroy();
|
|
1682
|
-
}
|
|
1683
|
-
#publish(chunk) {
|
|
1684
|
-
if (!node_buffer.Buffer.isBuffer(chunk) || this.#engine.settled) return;
|
|
1685
|
-
this.#emitter.emit("stdout", new Uint8Array(chunk));
|
|
1686
|
-
}
|
|
1687
|
-
#reportStderr(chunk) {
|
|
1688
|
-
this.#emitter.emit("stderr", chunk);
|
|
1689
|
-
}
|
|
1690
|
-
#reportFault(cause) {
|
|
1691
|
-
this.#emitter.emit("error", cause);
|
|
1692
|
-
}
|
|
1693
|
-
#reportExit(exit) {
|
|
1694
|
-
this.#emitter.emit("exit", exit);
|
|
1695
|
-
}
|
|
1696
|
-
#closeOutput() {
|
|
1697
|
-
this.#engine.stdout.off("data", this.#output);
|
|
1892
|
+
while (this.#orphans.size > 0) {
|
|
1893
|
+
const orphans = [...this.#orphans];
|
|
1894
|
+
this.#orphans.clear();
|
|
1895
|
+
await Promise.allSettled(orphans.map((child) => child.destroy()));
|
|
1896
|
+
}
|
|
1897
|
+
this.#children.clear();
|
|
1898
|
+
this.#ids.clear();
|
|
1899
|
+
this.#emitter.destroy();
|
|
1698
1900
|
}
|
|
1699
1901
|
};
|
|
1700
1902
|
//#endregion
|
|
1701
|
-
//#region src/server/
|
|
1903
|
+
//#region src/server/processes/Session.ts
|
|
1702
1904
|
/**
|
|
1703
|
-
*
|
|
1704
|
-
*
|
|
1705
|
-
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
1706
|
-
* @returns The launched {@link ProcessInterface}
|
|
1905
|
+
* Supervises one child and publishes its standard output as raw bytes.
|
|
1707
1906
|
*
|
|
1708
|
-
* @
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1907
|
+
* @remarks
|
|
1908
|
+
* This is the transport face of the supervision engine `Process` also composes. It frames nothing,
|
|
1909
|
+
* retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
|
|
1910
|
+
* `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
|
|
1911
|
+
* length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
|
|
1912
|
+
* itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
|
|
1913
|
+
* tail a `Process` retains, because a diagnostic stream is read rather than parsed.
|
|
1711
1914
|
*
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
* grace: 5000,
|
|
1716
|
-
* })
|
|
1717
|
-
* ```
|
|
1718
|
-
*/
|
|
1719
|
-
function createProcess(options) {
|
|
1720
|
-
return new Process(options);
|
|
1721
|
-
}
|
|
1722
|
-
/**
|
|
1723
|
-
* Creates one raw byte session over a supervised child process.
|
|
1915
|
+
* Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
|
|
1916
|
+
* the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
|
|
1917
|
+
* buffer without reaching memory the host still manages and without depending on a host type.
|
|
1724
1918
|
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1919
|
+
* The standard-input channel is open from the spawn, and `end` closes it without terminating
|
|
1920
|
+
* anything. That is the member a cooperative protocol reaches for: the child observes end of input
|
|
1921
|
+
* and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
|
|
1922
|
+
* `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
|
|
1923
|
+
* the inherited read ends.
|
|
1727
1924
|
*
|
|
1728
1925
|
* @example
|
|
1729
1926
|
* ```ts
|
|
1730
|
-
* import {
|
|
1927
|
+
* import { Session } from '@orkestrel/process/server'
|
|
1731
1928
|
*
|
|
1732
|
-
* const session =
|
|
1733
|
-
* command: { file: 'node', arguments: ['
|
|
1929
|
+
* const session = new Session({
|
|
1930
|
+
* command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
|
|
1734
1931
|
* workspace: process.cwd(),
|
|
1735
|
-
* grace: 5000,
|
|
1736
1932
|
* })
|
|
1737
1933
|
* ```
|
|
1738
1934
|
*/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
* Creates one keyed registry of supervised child processes.
|
|
1744
|
-
*
|
|
1745
|
-
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
1746
|
-
* @returns The {@link ProcessManagerInterface} registry
|
|
1747
|
-
*
|
|
1748
|
-
* @example
|
|
1749
|
-
* ```ts
|
|
1750
|
-
* import { createProcessManager } from '@orkestrel/process/server'
|
|
1751
|
-
*
|
|
1752
|
-
* const manager = createProcessManager()
|
|
1753
|
-
* ```
|
|
1754
|
-
*/
|
|
1755
|
-
function createProcessManager(options) {
|
|
1756
|
-
return new ProcessManager(options);
|
|
1757
|
-
}
|
|
1758
|
-
//#endregion
|
|
1759
|
-
//#region src/server/Retention.ts
|
|
1760
|
-
/**
|
|
1761
|
-
* Accumulates byte totals while retaining a bounded stream head.
|
|
1762
|
-
*
|
|
1763
|
-
* @example
|
|
1764
|
-
* ```ts
|
|
1765
|
-
* const retention = new Retention()
|
|
1766
|
-
* retention.retain(Buffer.from('hello'), 3)?.toString('utf8') // 'hel'
|
|
1767
|
-
* retention.retained // 3
|
|
1768
|
-
* ```
|
|
1769
|
-
*/
|
|
1770
|
-
var Retention = class {
|
|
1771
|
-
#delivered = 0;
|
|
1772
|
-
#retained = 0;
|
|
1773
|
-
/** The bytes delivered by the stream. */
|
|
1774
|
-
get delivered() {
|
|
1775
|
-
return this.#delivered;
|
|
1776
|
-
}
|
|
1777
|
-
/** The bytes retained from the stream head. */
|
|
1778
|
-
get retained() {
|
|
1779
|
-
return this.#retained;
|
|
1780
|
-
}
|
|
1935
|
+
var Session = class {
|
|
1936
|
+
#emitter;
|
|
1937
|
+
#engine;
|
|
1938
|
+
#output;
|
|
1781
1939
|
/**
|
|
1782
|
-
*
|
|
1940
|
+
* Spawns one child process and begins byte capture.
|
|
1783
1941
|
*
|
|
1784
|
-
* @param
|
|
1785
|
-
* @
|
|
1786
|
-
* @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
|
|
1942
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
1943
|
+
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1787
1944
|
*/
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
const
|
|
1792
|
-
|
|
1793
|
-
const
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
1802
|
-
*
|
|
1803
|
-
* @remarks
|
|
1804
|
-
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
1805
|
-
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
1806
|
-
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
1807
|
-
* whether either stream exceeded it. A positive `timeout` and an aborting `signal` both terminate the
|
|
1808
|
-
* child through the same bounded stop, and only the earliest is recorded: a timeout reports
|
|
1809
|
-
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
1810
|
-
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
1811
|
-
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
1812
|
-
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
1813
|
-
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
1814
|
-
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
1815
|
-
* overrides `command.input`. A host fault while writing that input terminates the run by design and
|
|
1816
|
-
* marks its result failed; a strict rejection carries the host fault as its cause. This differs
|
|
1817
|
-
* from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
|
|
1818
|
-
* is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
|
|
1819
|
-
* {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
|
|
1820
|
-
* option or command string rejects before the child is spawned, because an async function cannot
|
|
1821
|
-
* throw synchronously.
|
|
1822
|
-
*
|
|
1823
|
-
* @param command - The executable, arguments, and optional environment and input
|
|
1824
|
-
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
1825
|
-
* @returns The settled run outcome
|
|
1826
|
-
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
1827
|
-
*
|
|
1828
|
-
* @example
|
|
1829
|
-
* ```ts
|
|
1830
|
-
* const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
1831
|
-
* ```
|
|
1832
|
-
*/
|
|
1833
|
-
async function execute(command, options) {
|
|
1834
|
-
const snapshot = snapshotCommand(command);
|
|
1835
|
-
const optionEnvironment = options?.environment;
|
|
1836
|
-
const optionWorkspace = options?.workspace;
|
|
1837
|
-
const optionInput = options?.input;
|
|
1838
|
-
const optionTimeout = options?.timeout;
|
|
1839
|
-
const optionGrace = options?.grace;
|
|
1840
|
-
const optionSignal = options?.signal;
|
|
1841
|
-
const optionStrict = options?.strict;
|
|
1842
|
-
const optionLimit = options?.limit;
|
|
1843
|
-
validateCommand(snapshot);
|
|
1844
|
-
validateEnvironment(optionEnvironment);
|
|
1845
|
-
validateWorkspace(optionWorkspace);
|
|
1846
|
-
validateTimer(optionTimeout, "option 'timeout'");
|
|
1847
|
-
validateTimer(optionGrace, "option 'grace'");
|
|
1848
|
-
validateBytes(optionLimit, "option 'limit'", 0);
|
|
1849
|
-
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
1850
|
-
const grace = optionGrace ?? _src_core.PROCESS_GRACE;
|
|
1851
|
-
const timeout = optionTimeout ?? 0;
|
|
1852
|
-
const strict = optionStrict ?? true;
|
|
1853
|
-
const text = optionInput ?? snapshot.input;
|
|
1854
|
-
const line = formatCommand(snapshot);
|
|
1855
|
-
const workspace = optionWorkspace ?? process.cwd();
|
|
1856
|
-
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
1857
|
-
const plan = buildSpawn(snapshot, {
|
|
1858
|
-
workspace,
|
|
1859
|
-
environment
|
|
1860
|
-
});
|
|
1861
|
-
const settled = Promise.withResolvers();
|
|
1862
|
-
const terminate = new AbortController();
|
|
1863
|
-
const cleanup = new AbortController();
|
|
1864
|
-
const finish = new AbortController();
|
|
1865
|
-
const inputFailure = new AbortController();
|
|
1866
|
-
const outChunks = [];
|
|
1867
|
-
const errChunks = [];
|
|
1868
|
-
const outRetention = new Retention();
|
|
1869
|
-
const errRetention = new Retention();
|
|
1870
|
-
let spawned = false;
|
|
1871
|
-
let expired = false;
|
|
1872
|
-
let aborted = false;
|
|
1873
|
-
let cause;
|
|
1874
|
-
let timeoutTimer;
|
|
1875
|
-
let confirmTimer;
|
|
1876
|
-
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1877
|
-
cwd: workspace,
|
|
1878
|
-
detached: process.platform !== "win32",
|
|
1879
|
-
env: environment,
|
|
1880
|
-
stdio: [
|
|
1881
|
-
"pipe",
|
|
1882
|
-
"pipe",
|
|
1883
|
-
"pipe"
|
|
1884
|
-
],
|
|
1885
|
-
windowsHide: true,
|
|
1886
|
-
windowsVerbatimArguments: plan.verbatim
|
|
1887
|
-
});
|
|
1888
|
-
finish.signal.addEventListener("abort", () => {
|
|
1889
|
-
clearTimeout(timeoutTimer);
|
|
1890
|
-
clearTimeout(confirmTimer);
|
|
1891
|
-
cleanup.abort();
|
|
1892
|
-
settled.resolve(buildExecuteResult({
|
|
1893
|
-
command: line,
|
|
1894
|
-
stdout: node_buffer.Buffer.concat(outChunks),
|
|
1895
|
-
stderr: node_buffer.Buffer.concat(errChunks),
|
|
1896
|
-
code: child.exitCode,
|
|
1897
|
-
signal: child.signalCode,
|
|
1898
|
-
expired,
|
|
1899
|
-
aborted,
|
|
1900
|
-
truncated: outRetention.delivered > limit || errRetention.delivered > limit,
|
|
1901
|
-
limit,
|
|
1902
|
-
...cause === void 0 ? {} : { cause }
|
|
1903
|
-
}));
|
|
1904
|
-
}, { once: true });
|
|
1905
|
-
terminate.signal.addEventListener("abort", () => {
|
|
1906
|
-
if (finish.signal.aborted) return;
|
|
1907
|
-
clearTimeout(timeoutTimer);
|
|
1908
|
-
cleanup.abort();
|
|
1909
|
-
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
1910
|
-
if (finish.signal.aborted) return;
|
|
1911
|
-
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
1945
|
+
constructor(options) {
|
|
1946
|
+
const command = options.command;
|
|
1947
|
+
const workspace = options.workspace;
|
|
1948
|
+
const grace = options.grace;
|
|
1949
|
+
const drain = options.drain;
|
|
1950
|
+
const evidence = options.evidence;
|
|
1951
|
+
const delivery = options.delivery;
|
|
1952
|
+
const signal = options.signal;
|
|
1953
|
+
const on = options.on;
|
|
1954
|
+
const error = options.error;
|
|
1955
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1956
|
+
...on === void 0 ? {} : { on },
|
|
1957
|
+
...error === void 0 ? {} : { error }
|
|
1912
1958
|
});
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
});
|
|
1924
|
-
child.stderr.on("data", (chunk) => {
|
|
1925
|
-
const retained = errRetention.retain(chunk, limit);
|
|
1926
|
-
if (retained !== void 0) errChunks.push(retained);
|
|
1927
|
-
});
|
|
1928
|
-
child.once("spawn", () => {
|
|
1929
|
-
spawned = true;
|
|
1930
|
-
});
|
|
1931
|
-
child.once("error", (error) => {
|
|
1932
|
-
if (spawned) return;
|
|
1933
|
-
cause = error;
|
|
1934
|
-
finish.abort();
|
|
1935
|
-
});
|
|
1936
|
-
child.once("close", () => finish.abort());
|
|
1937
|
-
if (text !== void 0) child.stdin.write(text, (error) => {
|
|
1938
|
-
if (error !== void 0 && error !== null) inputFailure.abort(error);
|
|
1939
|
-
});
|
|
1940
|
-
child.stdin.end();
|
|
1941
|
-
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
1942
|
-
expired = true;
|
|
1943
|
-
terminate.abort();
|
|
1944
|
-
}, timeout);
|
|
1945
|
-
if (optionSignal !== void 0) {
|
|
1946
|
-
optionSignal.addEventListener("abort", () => {
|
|
1947
|
-
aborted = true;
|
|
1948
|
-
terminate.abort();
|
|
1959
|
+
this.#output = this.#publish.bind(this);
|
|
1960
|
+
this.#engine = new Supervisor({
|
|
1961
|
+
command,
|
|
1962
|
+
workspace,
|
|
1963
|
+
writable: true,
|
|
1964
|
+
...grace === void 0 ? {} : { grace },
|
|
1965
|
+
...drain === void 0 ? {} : { drain },
|
|
1966
|
+
...evidence === void 0 ? {} : { evidence },
|
|
1967
|
+
...delivery === void 0 ? {} : { delivery },
|
|
1968
|
+
...signal === void 0 ? {} : { signal }
|
|
1949
1969
|
}, {
|
|
1950
|
-
|
|
1951
|
-
|
|
1970
|
+
chunk: this.#reportStderr.bind(this),
|
|
1971
|
+
fault: this.#reportFault.bind(this),
|
|
1972
|
+
close: this.#closeOutput.bind(this),
|
|
1973
|
+
terminal: this.#reportExit.bind(this),
|
|
1974
|
+
teardown: this.#emitter.destroy.bind(this.#emitter)
|
|
1952
1975
|
});
|
|
1953
|
-
|
|
1954
|
-
aborted = true;
|
|
1955
|
-
terminate.abort();
|
|
1956
|
-
}
|
|
1976
|
+
this.#engine.stdout.on("data", this.#output);
|
|
1957
1977
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
code: "input",
|
|
1962
|
-
context: {
|
|
1963
|
-
command: result.command,
|
|
1964
|
-
code: result.code,
|
|
1965
|
-
signal: result.signal
|
|
1966
|
-
},
|
|
1967
|
-
cause: inputFailure.signal.reason,
|
|
1968
|
-
result
|
|
1969
|
-
});
|
|
1970
|
-
throw (0, _src_core.createExecuteError)(result, cause);
|
|
1978
|
+
/** Holds the host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1979
|
+
get pid() {
|
|
1980
|
+
return this.#engine.pid;
|
|
1971
1981
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1982
|
+
/** Holds the exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1983
|
+
get code() {
|
|
1984
|
+
return this.#engine.code;
|
|
1985
|
+
}
|
|
1986
|
+
/** Holds the terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1987
|
+
get signal() {
|
|
1988
|
+
return this.#engine.signal;
|
|
1989
|
+
}
|
|
1990
|
+
/** Holds the typed lifecycle observation surface. */
|
|
1991
|
+
get emitter() {
|
|
1992
|
+
return this.#emitter;
|
|
1993
|
+
}
|
|
1994
|
+
/** Holds the decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1995
|
+
get evidence() {
|
|
1996
|
+
return this.#engine.evidence;
|
|
1997
|
+
}
|
|
1998
|
+
/** Reports whether the terminal moment arrived and `exit` settled. */
|
|
1999
|
+
get settled() {
|
|
2000
|
+
return this.#engine.settled;
|
|
2001
|
+
}
|
|
2002
|
+
/** Reports whether termination began, including after the terminal moment. `end` never turns it true. */
|
|
2003
|
+
get stopping() {
|
|
2004
|
+
return this.#engine.stopping;
|
|
2005
|
+
}
|
|
2006
|
+
/** Settles at the child's own ending, awaited without the terminal moment's drain window. */
|
|
2007
|
+
get ending() {
|
|
2008
|
+
return this.#engine.ending;
|
|
2009
|
+
}
|
|
2010
|
+
/** Settles with the terminal child state, observed once after stream close or the drain cutoff. */
|
|
2011
|
+
get exit() {
|
|
2012
|
+
return this.#engine.exit;
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* Writes raw bytes to the open standard-input channel.
|
|
2016
|
+
*
|
|
2017
|
+
* @remarks
|
|
2018
|
+
* Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
|
|
2019
|
+
* the host accepted them without reporting a fault; it does not prove that the child read them.
|
|
2020
|
+
* The channel can queue the payload, so treat the array as owned by the channel until the
|
|
2021
|
+
* returned promise settles.
|
|
2022
|
+
*
|
|
2023
|
+
* @param bytes - The payload to write, already framed by the caller
|
|
2024
|
+
* @returns True if the host accepted the bytes without reporting a fault; false otherwise (the channel was closed, destroyed, ended, failed, or the write remained unconfirmed through `delivery`)
|
|
2025
|
+
*/
|
|
2026
|
+
write(bytes) {
|
|
2027
|
+
return this.#engine.deliver(bytes);
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Closes the standard-input channel and leaves the child running.
|
|
2031
|
+
*
|
|
2032
|
+
* @remarks
|
|
2033
|
+
* Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
|
|
2034
|
+
* arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
|
|
2035
|
+
* already accepted. A later `write` resolves `false`.
|
|
2036
|
+
*
|
|
2037
|
+
* @returns The stable barrier shared by every call
|
|
2038
|
+
*/
|
|
2039
|
+
end() {
|
|
2040
|
+
return this.#engine.end();
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* Terminates the child process tree and reaches the terminal observation moment.
|
|
2044
|
+
*
|
|
2045
|
+
* @remarks
|
|
2046
|
+
* Never rejects, and every call shares one termination bounded by `grace` and `drain`.
|
|
2047
|
+
*
|
|
2048
|
+
* @returns True if the child's native exit was observed; false otherwise (the confirmation deadline elapsed without it)
|
|
2049
|
+
*/
|
|
2050
|
+
stop() {
|
|
2051
|
+
return this.#engine.stop();
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Stops the child and destroys the observation emitter after the terminal state freezes.
|
|
2055
|
+
*
|
|
2056
|
+
* @remarks
|
|
2057
|
+
* Always resolves, including when termination was never confirmed. Every call shares one barrier.
|
|
2058
|
+
*
|
|
2059
|
+
* @returns The stable barrier shared by every call
|
|
2060
|
+
*/
|
|
2061
|
+
destroy() {
|
|
2062
|
+
return this.#engine.destroy();
|
|
2063
|
+
}
|
|
2064
|
+
#publish(chunk) {
|
|
2065
|
+
if (!node_buffer.Buffer.isBuffer(chunk) || this.#engine.settled) return;
|
|
2066
|
+
this.#emitter.emit("stdout", new Uint8Array(chunk));
|
|
2067
|
+
}
|
|
2068
|
+
#reportStderr(chunk) {
|
|
2069
|
+
this.#emitter.emit("stderr", chunk);
|
|
2070
|
+
}
|
|
2071
|
+
#reportFault(cause) {
|
|
2072
|
+
this.#emitter.emit("error", cause);
|
|
2073
|
+
}
|
|
2074
|
+
#reportExit(exit) {
|
|
2075
|
+
this.#emitter.emit("exit", exit);
|
|
2076
|
+
}
|
|
2077
|
+
#closeOutput() {
|
|
2078
|
+
this.#engine.stdout.off("data", this.#output);
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
1974
2081
|
//#endregion
|
|
1975
|
-
//#region src/server/
|
|
2082
|
+
//#region src/server/factories.ts
|
|
1976
2083
|
/**
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
* @remarks
|
|
1980
|
-
* The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
|
|
1981
|
-
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
1982
|
-
* contract carries neither. A positive `timeout` ends only the root process and can leave
|
|
1983
|
-
* descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
|
|
1984
|
-
* timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
|
|
1985
|
-
* and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
|
|
1986
|
-
* reports `truncated` without failing. The environment and input follow the same merge as
|
|
1987
|
-
* {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
|
|
1988
|
-
* carrying the {@link ExecuteResult}.
|
|
2084
|
+
* Creates one supervised child process.
|
|
1989
2085
|
*
|
|
1990
|
-
* @param
|
|
1991
|
-
* @
|
|
1992
|
-
* @returns The run outcome
|
|
1993
|
-
* @throws A {@link ProcessError} coded `invalid` for a malformed option, command string, or batch-bound argument, or one carrying the {@link ExecuteResult} when the run failed and `strict` is not `false`
|
|
2086
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
2087
|
+
* @returns The launched {@link ProcessInterface}
|
|
1994
2088
|
*
|
|
1995
2089
|
* @example
|
|
1996
2090
|
* ```ts
|
|
1997
|
-
*
|
|
2091
|
+
* import { createProcess } from '@orkestrel/process/server'
|
|
2092
|
+
*
|
|
2093
|
+
* const child = createProcess({
|
|
2094
|
+
* command: { file: 'node', arguments: ['--version'] },
|
|
2095
|
+
* workspace: process.cwd(),
|
|
2096
|
+
* grace: 5000,
|
|
2097
|
+
* })
|
|
1998
2098
|
* ```
|
|
1999
2099
|
*/
|
|
2000
|
-
function
|
|
2001
|
-
|
|
2002
|
-
const optionEnvironment = options?.environment;
|
|
2003
|
-
const optionWorkspace = options?.workspace;
|
|
2004
|
-
const optionInput = options?.input;
|
|
2005
|
-
const optionTimeout = options?.timeout;
|
|
2006
|
-
const optionStrict = options?.strict;
|
|
2007
|
-
const optionLimit = options?.limit;
|
|
2008
|
-
validateCommand(snapshot);
|
|
2009
|
-
validateEnvironment(optionEnvironment);
|
|
2010
|
-
validateWorkspace(optionWorkspace);
|
|
2011
|
-
validateTimer(optionTimeout, "option 'timeout'");
|
|
2012
|
-
validateBytes(optionLimit, "option 'limit'", 0);
|
|
2013
|
-
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
2014
|
-
const timeout = optionTimeout ?? 0;
|
|
2015
|
-
const strict = optionStrict ?? true;
|
|
2016
|
-
const text = optionInput ?? snapshot.input;
|
|
2017
|
-
const line = formatCommand(snapshot);
|
|
2018
|
-
const workspace = optionWorkspace ?? process.cwd();
|
|
2019
|
-
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
2020
|
-
const plan = buildSpawn(snapshot, {
|
|
2021
|
-
workspace,
|
|
2022
|
-
environment
|
|
2023
|
-
});
|
|
2024
|
-
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
2025
|
-
cwd: workspace,
|
|
2026
|
-
env: environment,
|
|
2027
|
-
encoding: "buffer",
|
|
2028
|
-
maxBuffer: limit,
|
|
2029
|
-
killSignal: "SIGKILL",
|
|
2030
|
-
windowsHide: true,
|
|
2031
|
-
windowsVerbatimArguments: plan.verbatim,
|
|
2032
|
-
...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
|
|
2033
|
-
...timeout > 0 ? { timeout } : {}
|
|
2034
|
-
});
|
|
2035
|
-
const error = outcome.error;
|
|
2036
|
-
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
2037
|
-
const result = buildExecuteResult({
|
|
2038
|
-
command: line,
|
|
2039
|
-
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
2040
|
-
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
2041
|
-
code: outcome.status,
|
|
2042
|
-
signal: outcome.signal,
|
|
2043
|
-
expired: fault === "ETIMEDOUT",
|
|
2044
|
-
aborted: false,
|
|
2045
|
-
truncated: fault === "ENOBUFS",
|
|
2046
|
-
limit,
|
|
2047
|
-
...error === void 0 ? {} : { cause: error }
|
|
2048
|
-
});
|
|
2049
|
-
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
|
|
2050
|
-
return result;
|
|
2100
|
+
function createProcess(options) {
|
|
2101
|
+
return new Process(options);
|
|
2051
2102
|
}
|
|
2052
|
-
//#endregion
|
|
2053
|
-
//#region src/server/execution/detach.ts
|
|
2054
2103
|
/**
|
|
2055
|
-
*
|
|
2104
|
+
* Creates one raw byte session over a supervised child process.
|
|
2056
2105
|
*
|
|
2057
|
-
* @
|
|
2058
|
-
* The
|
|
2059
|
-
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
2060
|
-
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
2061
|
-
* is spawned.
|
|
2106
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
2107
|
+
* @returns The launched {@link SessionInterface}
|
|
2062
2108
|
*
|
|
2063
|
-
* @
|
|
2064
|
-
*
|
|
2065
|
-
* @
|
|
2066
|
-
*
|
|
2109
|
+
* @example
|
|
2110
|
+
* ```ts
|
|
2111
|
+
* import { createSession } from '@orkestrel/process/server'
|
|
2112
|
+
*
|
|
2113
|
+
* const session = createSession({
|
|
2114
|
+
* command: { file: 'node', arguments: ['server.js', '--stdio'] },
|
|
2115
|
+
* workspace: process.cwd(),
|
|
2116
|
+
* grace: 5000,
|
|
2117
|
+
* })
|
|
2118
|
+
* ```
|
|
2119
|
+
*/
|
|
2120
|
+
function createSession(options) {
|
|
2121
|
+
return new Session(options);
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Creates one keyed registry of supervised child processes.
|
|
2125
|
+
*
|
|
2126
|
+
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
2127
|
+
* @returns The {@link ProcessManagerInterface} registry
|
|
2067
2128
|
*
|
|
2068
2129
|
* @example
|
|
2069
2130
|
* ```ts
|
|
2070
|
-
*
|
|
2131
|
+
* import { createProcessManager } from '@orkestrel/process/server'
|
|
2132
|
+
*
|
|
2133
|
+
* const manager = createProcessManager()
|
|
2071
2134
|
* ```
|
|
2072
2135
|
*/
|
|
2073
|
-
function
|
|
2074
|
-
|
|
2075
|
-
const optionWorkspace = options?.workspace;
|
|
2076
|
-
validateCommand(snapshot);
|
|
2077
|
-
validateWorkspace(optionWorkspace);
|
|
2078
|
-
const workspace = optionWorkspace ?? process.cwd();
|
|
2079
|
-
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
|
|
2080
|
-
const plan = buildSpawn(snapshot, {
|
|
2081
|
-
workspace,
|
|
2082
|
-
environment
|
|
2083
|
-
});
|
|
2084
|
-
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
2085
|
-
cwd: workspace,
|
|
2086
|
-
detached: true,
|
|
2087
|
-
env: environment,
|
|
2088
|
-
stdio: "ignore",
|
|
2089
|
-
windowsHide: true,
|
|
2090
|
-
windowsVerbatimArguments: plan.verbatim
|
|
2091
|
-
});
|
|
2092
|
-
child.once("error", () => void 0);
|
|
2093
|
-
child.unref();
|
|
2136
|
+
function createProcessManager(options) {
|
|
2137
|
+
return new ProcessManager(options);
|
|
2094
2138
|
}
|
|
2095
2139
|
//#endregion
|
|
2096
2140
|
exports.Process = Process;
|
|
2097
2141
|
exports.ProcessManager = ProcessManager;
|
|
2098
|
-
exports.Retention = Retention;
|
|
2099
2142
|
exports.Session = Session;
|
|
2143
|
+
exports.Supervisor = Supervisor;
|
|
2100
2144
|
exports.buildExecutableCandidates = buildExecutableCandidates;
|
|
2101
2145
|
exports.buildExecuteResult = buildExecuteResult;
|
|
2102
2146
|
exports.buildPlatformSpawn = buildPlatformSpawn;
|
|
2103
2147
|
exports.buildSpawn = buildSpawn;
|
|
2148
|
+
exports.captureChunk = captureChunk;
|
|
2104
2149
|
exports.createProcess = createProcess;
|
|
2105
2150
|
exports.createProcessManager = createProcessManager;
|
|
2106
2151
|
exports.createSession = createSession;
|