@orkestrel/process 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/server/index.cjs +1140 -1140
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +36 -51
- package/dist/src/server/index.d.ts +36 -51
- package/dist/src/server/index.js +1140 -1140
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -42,9 +42,12 @@ function trimTail(bytes, limit) {
|
|
|
42
42
|
* Trims a buffer to at most `limit` leading bytes without splitting a UTF-8 sequence.
|
|
43
43
|
*
|
|
44
44
|
* @remarks
|
|
45
|
-
* Keeps the head — the captured start of a one-shot run's output.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* Keeps the head — the captured start of a one-shot run's output. The end retreats only when a
|
|
46
|
+
* sequence genuinely spans the cut: the byte at `limit` carries the continuation bit pattern, and a
|
|
47
|
+
* lead byte within the preceding three bytes declares a length that reaches it. The retained bytes
|
|
48
|
+
* then end on a code-point boundary and decode without a replacement character. A byte at `limit`
|
|
49
|
+
* that carries the continuation pattern without such a lead byte is invalid UTF-8 rather than a
|
|
50
|
+
* split sequence, so the full `limit` survives instead of a valid byte being dropped for it.
|
|
48
51
|
*
|
|
49
52
|
* @param bytes - The accumulated bytes
|
|
50
53
|
* @param limit - The maximum retained byte count
|
|
@@ -58,13 +61,50 @@ function trimTail(bytes, limit) {
|
|
|
58
61
|
function trimHead(bytes, limit) {
|
|
59
62
|
const buffer = node_buffer.Buffer.isBuffer(bytes) ? bytes : node_buffer.Buffer.from(bytes);
|
|
60
63
|
if (buffer.byteLength <= limit) return buffer;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const excluded = buffer[limit];
|
|
65
|
+
if (excluded === void 0 || (excluded & 192) !== 128) return node_buffer.Buffer.from(buffer.subarray(0, limit));
|
|
66
|
+
const floor = Math.max(0, limit - 3);
|
|
67
|
+
let start = limit - 1;
|
|
68
|
+
while (start >= floor) {
|
|
69
|
+
const lead = buffer[start];
|
|
70
|
+
if (lead !== void 0 && (lead & 192) !== 128) {
|
|
71
|
+
let span = 1;
|
|
72
|
+
if ((lead & 224) === 192) span = 2;
|
|
73
|
+
else if ((lead & 240) === 224) span = 3;
|
|
74
|
+
else if ((lead & 248) === 240) span = 4;
|
|
75
|
+
if (start + span > limit) return node_buffer.Buffer.from(buffer.subarray(0, start));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
start -= 1;
|
|
66
79
|
}
|
|
67
|
-
return node_buffer.Buffer.from(buffer.subarray(0,
|
|
80
|
+
return node_buffer.Buffer.from(buffer.subarray(0, limit));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Bounds one delivered stream chunk to the bytes a capture still has room for.
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* A chunk that is not a buffer contributes nothing and reports `undefined` rather than throwing,
|
|
87
|
+
* because a stream `data` listener receives an `unknown` payload. The cut is byte-exact and might
|
|
88
|
+
* land inside a multibyte sequence, because {@link buildExecuteResult} performs the single
|
|
89
|
+
* code-point-boundary trim over the whole capture rather than over each chunk. Give the capture one
|
|
90
|
+
* byte more room than its limit, so that final trim can read the first excluded byte and retreat off
|
|
91
|
+
* a split sequence. A chunk that fits its room is returned as the delivered buffer itself rather
|
|
92
|
+
* than a copy, because a capture appending it copies again and never writes through it; a chunk that
|
|
93
|
+
* overruns its room returns a copy of the bytes that fit. Treat the return as read-only either way.
|
|
94
|
+
*
|
|
95
|
+
* @param chunk - The delivered chunk, ignored when it is not a buffer
|
|
96
|
+
* @param room - The bytes the capture still accepts
|
|
97
|
+
* @returns The bytes to retain, or `undefined` when the chunk contributes none
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* captureChunk(Buffer.from('hello'), 3) // <Buffer 68 65 6c>
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
function captureChunk(chunk, room) {
|
|
105
|
+
if (!node_buffer.Buffer.isBuffer(chunk) || room <= 0) return void 0;
|
|
106
|
+
if (chunk.byteLength <= room) return chunk;
|
|
107
|
+
return node_buffer.Buffer.from(chunk.subarray(0, room));
|
|
68
108
|
}
|
|
69
109
|
/**
|
|
70
110
|
* Takes one owned frozen snapshot of a caller's command.
|
|
@@ -692,538 +732,536 @@ function buildExecuteResult(input) {
|
|
|
692
732
|
truncated: input.truncated
|
|
693
733
|
});
|
|
694
734
|
}
|
|
695
|
-
//#endregion
|
|
696
|
-
//#region src/server/Supervisor.ts
|
|
697
735
|
/**
|
|
698
|
-
*
|
|
736
|
+
* Runs one command to completion, buffering its output, and settles with the outcome.
|
|
699
737
|
*
|
|
700
738
|
* @remarks
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
739
|
+
* The executable is resolved through {@link buildSpawn}, so no run uses a shell. On a POSIX host the
|
|
740
|
+
* child is detached, which is what lets its whole process group be terminated. Standard output and
|
|
741
|
+
* error are each byte-bounded by `limit` (default {@link PROCESS_OUTPUT}) and `truncated` reports
|
|
742
|
+
* whether either stream exceeded it. Each capture keeps one byte beyond `limit`, so the single
|
|
743
|
+
* code-point-boundary trim in {@link buildExecuteResult} reads the first excluded byte and never
|
|
744
|
+
* delivers a split multibyte sequence. A positive `timeout` and an aborting `signal` both terminate the
|
|
745
|
+
* child through the same bounded stop, and only the earliest is recorded: a timeout reports
|
|
746
|
+
* `expired` and an abort reports `aborted`, never both. After termination the outcome is awaited for
|
|
747
|
+
* a bounded window, so a descendant holding the child's stdio cannot keep the run pending. That bound
|
|
748
|
+
* covers a terminated run alone: a run with no `timeout` and no `signal` settles on stdio completion
|
|
749
|
+
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
750
|
+
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
751
|
+
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
752
|
+
* overrides `command.input`. A host fault while writing that input terminates the run by design and
|
|
753
|
+
* marks its result failed; a strict rejection carries the host fault as its cause. This differs
|
|
754
|
+
* from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
|
|
755
|
+
* is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
|
|
756
|
+
* {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
|
|
757
|
+
* option or command string rejects before the child is spawned, because an async function cannot
|
|
758
|
+
* throw synchronously.
|
|
707
759
|
*
|
|
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.
|
|
760
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
761
|
+
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
762
|
+
* @returns The settled run outcome
|
|
763
|
+
* @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
764
|
*
|
|
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.
|
|
765
|
+
* @example
|
|
766
|
+
* ```ts
|
|
767
|
+
* const result = await execute({ file: 'git', arguments: ['status'] }, { workspace: process.cwd() })
|
|
768
|
+
* ```
|
|
725
769
|
*/
|
|
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
|
-
"pipe",
|
|
806
|
-
"pipe",
|
|
807
|
-
"pipe"
|
|
808
|
-
],
|
|
809
|
-
windowsHide: true,
|
|
810
|
-
windowsVerbatimArguments: plan.verbatim
|
|
770
|
+
async function execute(command, options) {
|
|
771
|
+
const snapshot = snapshotCommand(command);
|
|
772
|
+
const optionEnvironment = options?.environment;
|
|
773
|
+
const optionWorkspace = options?.workspace;
|
|
774
|
+
const optionInput = options?.input;
|
|
775
|
+
const optionTimeout = options?.timeout;
|
|
776
|
+
const optionGrace = options?.grace;
|
|
777
|
+
const optionSignal = options?.signal;
|
|
778
|
+
const optionStrict = options?.strict;
|
|
779
|
+
const optionLimit = options?.limit;
|
|
780
|
+
validateCommand(snapshot);
|
|
781
|
+
validateEnvironment(optionEnvironment);
|
|
782
|
+
validateWorkspace(optionWorkspace);
|
|
783
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
784
|
+
validateTimer(optionGrace, "option 'grace'");
|
|
785
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
786
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
787
|
+
const grace = optionGrace ?? _src_core.PROCESS_GRACE;
|
|
788
|
+
const timeout = optionTimeout ?? 0;
|
|
789
|
+
const strict = optionStrict ?? true;
|
|
790
|
+
const text = optionInput ?? snapshot.input;
|
|
791
|
+
const line = formatCommand(snapshot);
|
|
792
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
793
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
794
|
+
const plan = buildSpawn(snapshot, {
|
|
795
|
+
workspace,
|
|
796
|
+
environment
|
|
797
|
+
});
|
|
798
|
+
const settled = Promise.withResolvers();
|
|
799
|
+
const terminate = new AbortController();
|
|
800
|
+
const cleanup = new AbortController();
|
|
801
|
+
const finish = new AbortController();
|
|
802
|
+
const inputFailure = new AbortController();
|
|
803
|
+
const outChunks = [];
|
|
804
|
+
const errChunks = [];
|
|
805
|
+
let spawned = false;
|
|
806
|
+
let expired = false;
|
|
807
|
+
let aborted = false;
|
|
808
|
+
let outRetained = 0;
|
|
809
|
+
let errRetained = 0;
|
|
810
|
+
let cause;
|
|
811
|
+
let timeoutTimer;
|
|
812
|
+
let confirmTimer;
|
|
813
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
814
|
+
cwd: workspace,
|
|
815
|
+
detached: process.platform !== "win32",
|
|
816
|
+
env: environment,
|
|
817
|
+
stdio: [
|
|
818
|
+
"pipe",
|
|
819
|
+
"pipe",
|
|
820
|
+
"pipe"
|
|
821
|
+
],
|
|
822
|
+
windowsHide: true,
|
|
823
|
+
windowsVerbatimArguments: plan.verbatim
|
|
824
|
+
});
|
|
825
|
+
finish.signal.addEventListener("abort", () => {
|
|
826
|
+
clearTimeout(timeoutTimer);
|
|
827
|
+
clearTimeout(confirmTimer);
|
|
828
|
+
cleanup.abort();
|
|
829
|
+
settled.resolve(buildExecuteResult({
|
|
830
|
+
command: line,
|
|
831
|
+
stdout: node_buffer.Buffer.concat(outChunks),
|
|
832
|
+
stderr: node_buffer.Buffer.concat(errChunks),
|
|
833
|
+
code: child.exitCode,
|
|
834
|
+
signal: child.signalCode,
|
|
835
|
+
expired,
|
|
836
|
+
aborted,
|
|
837
|
+
truncated: outRetained > limit || errRetained > limit,
|
|
838
|
+
limit,
|
|
839
|
+
...cause === void 0 ? {} : { cause }
|
|
840
|
+
}));
|
|
841
|
+
}, { once: true });
|
|
842
|
+
terminate.signal.addEventListener("abort", () => {
|
|
843
|
+
if (finish.signal.aborted) return;
|
|
844
|
+
clearTimeout(timeoutTimer);
|
|
845
|
+
cleanup.abort();
|
|
846
|
+
stopChild(child, grace, _src_core.PROCESS_CONFIRMATION).then(() => {
|
|
847
|
+
if (finish.signal.aborted) return;
|
|
848
|
+
confirmTimer = setTimeout(() => finish.abort(), _src_core.PROCESS_CONFIRMATION);
|
|
811
849
|
});
|
|
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
|
-
/** True after termination began, including after the terminal moment. */
|
|
856
|
-
get stopping() {
|
|
857
|
-
return this.#stopping;
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
|
-
* The child's own ending, awaited without the terminal moment's drain window.
|
|
861
|
-
*
|
|
862
|
-
* @remarks
|
|
863
|
-
* Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
|
|
864
|
-
* the host records them. It settles at the native exit, and at the terminal moment for a child
|
|
865
|
-
* whose spawn produced no native exit at all, so it can never outlive the supervision.
|
|
866
|
-
*/
|
|
867
|
-
get ending() {
|
|
868
|
-
return this.#ending.promise;
|
|
869
|
-
}
|
|
870
|
-
/** The terminal child state, observed once after stream close or the drain cutoff. */
|
|
871
|
-
get exit() {
|
|
872
|
-
return this.#exit.promise;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* Writes raw bytes to the open standard-input channel.
|
|
876
|
-
*
|
|
877
|
-
* @remarks
|
|
878
|
-
* Never rejects, and adds no framing: a face composes whatever terminator its contract promises
|
|
879
|
-
* before it calls this. `true` means the host accepted the bytes without reporting a fault; it
|
|
880
|
-
* does not prove that the child read them. An ordinary write settles when the kernel accepts it.
|
|
881
|
-
* Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
|
|
882
|
-
* every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
|
|
883
|
-
* call settles `false`, because teardown cannot confirm delivery for bytes it is about to
|
|
884
|
-
* discard.
|
|
885
|
-
*
|
|
886
|
-
* @param bytes - The payload to write, already framed by the caller
|
|
887
|
-
* @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`
|
|
888
|
-
*/
|
|
889
|
-
deliver(bytes) {
|
|
890
|
-
const stdin = this.#child.stdin;
|
|
891
|
-
if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
|
|
892
|
-
const settled = Promise.withResolvers();
|
|
893
|
-
this.#writes.set(settled, void 0);
|
|
894
|
-
try {
|
|
895
|
-
stdin.write(bytes, this.#confirmWrite.bind(this, settled));
|
|
896
|
-
} catch {
|
|
897
|
-
this.#settleWrite(settled, false);
|
|
898
|
-
return settled.promise;
|
|
899
|
-
}
|
|
900
|
-
if (this.#delivery > 0 && this.#writes.has(settled)) {
|
|
901
|
-
const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
|
|
902
|
-
timer.unref();
|
|
903
|
-
this.#writes.set(settled, timer);
|
|
904
|
-
}
|
|
905
|
-
return settled.promise;
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* Closes the standard-input channel while leaving the child running.
|
|
909
|
-
*
|
|
910
|
-
* @remarks
|
|
911
|
-
* Never rejects, and terminates nothing: no signal is sent, `stopping` stays false, no drain
|
|
912
|
-
* window is armed, and the terminal moment does not arrive. The child observes end of input and
|
|
913
|
-
* decides for itself what to do about it, which is what separates this from `stop`.
|
|
914
|
-
*
|
|
915
|
-
* Every call shares one barrier, and it resolves after the host flushes the writes it had already
|
|
916
|
-
* accepted, or reports that it cannot. A `deliver` after this call resolves `false`, because the
|
|
917
|
-
* host stops reporting the channel writable the moment it is ended. That same flag keeps the
|
|
918
|
-
* ended channel quiet for its remaining life, so a later host fault on it settles pending writes
|
|
919
|
-
* and reaches no face. After `stop` or `destroy` has begun, or after the terminal moment, the
|
|
920
|
-
* call resolves and changes nothing, because the channel is already being discarded.
|
|
921
|
-
*
|
|
922
|
-
* @returns The stable barrier shared by every call
|
|
923
|
-
*/
|
|
924
|
-
end() {
|
|
925
|
-
if (this.#closure !== void 0) return this.#closure;
|
|
926
|
-
this.#closure = this.#endInput();
|
|
927
|
-
return this.#closure;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Terminates the child process tree and reaches the terminal observation moment.
|
|
931
|
-
*
|
|
932
|
-
* @remarks
|
|
933
|
-
* Never rejects, and every call shares one termination. After the termination sequence returns,
|
|
934
|
-
* confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
|
|
935
|
-
* and settles `exit` when they remain open. A cutoff reached before the native exit reports the
|
|
936
|
-
* `code` and `signal` the host had recorded by then, which is `null` for a child still running.
|
|
937
|
-
*
|
|
938
|
-
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
939
|
-
*/
|
|
940
|
-
stop() {
|
|
941
|
-
if (this.#termination !== void 0) return this.#termination;
|
|
942
|
-
this.#termination = this.#kill();
|
|
943
|
-
return this.#termination;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Stops the child and releases the composing face after the terminal state freezes.
|
|
947
|
-
*
|
|
948
|
-
* @remarks
|
|
949
|
-
* Always resolves, including when termination was never confirmed. Every call shares one barrier.
|
|
950
|
-
*
|
|
951
|
-
* @returns The stable barrier shared by every call
|
|
952
|
-
*/
|
|
953
|
-
destroy() {
|
|
954
|
-
if (this.#destruction !== void 0) return this.#destruction;
|
|
955
|
-
this.#destruction = this.#conclude();
|
|
956
|
-
return this.#destruction;
|
|
957
|
-
}
|
|
958
|
-
#retain(chunk) {
|
|
959
|
-
if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
|
|
960
|
-
this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
|
|
961
|
-
const text = this.#decoder.write(chunk);
|
|
962
|
-
if (text.length > 0) this.#chunk(text);
|
|
963
|
-
}
|
|
964
|
-
#complete() {
|
|
965
|
-
if (this.#settled) return;
|
|
966
|
-
this.#settle(true);
|
|
967
|
-
}
|
|
968
|
-
#expire() {
|
|
969
|
-
this.#ending.resolve();
|
|
970
|
-
if (this.#settled) return;
|
|
971
|
-
this.#wait();
|
|
972
|
-
}
|
|
973
|
-
#wait() {
|
|
974
|
-
this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
|
|
975
|
-
if (!this.#settled) this.#settle(false);
|
|
976
|
-
});
|
|
977
|
-
return this.#waiting;
|
|
978
|
-
}
|
|
979
|
-
#settle(drained) {
|
|
980
|
-
const suffix = this.#decoder.end();
|
|
981
|
-
if (suffix.length > 0) this.#chunk(suffix);
|
|
982
|
-
this.#settled = true;
|
|
983
|
-
this.#removeAbortListener();
|
|
984
|
-
this.#close();
|
|
985
|
-
const exit = Object.freeze({
|
|
986
|
-
code: this.code,
|
|
987
|
-
signal: this.signal,
|
|
988
|
-
drained
|
|
850
|
+
}, { once: true });
|
|
851
|
+
inputFailure.signal.addEventListener("abort", () => {
|
|
852
|
+
if (finish.signal.aborted) return;
|
|
853
|
+
cause = inputFailure.signal.reason;
|
|
854
|
+
terminate.abort();
|
|
855
|
+
}, { once: true });
|
|
856
|
+
child.stdin.on("error", (error) => inputFailure.abort(error));
|
|
857
|
+
child.stdout.on("data", (chunk) => {
|
|
858
|
+
const captured = captureChunk(chunk, limit + 1 - outRetained);
|
|
859
|
+
if (captured === void 0) return;
|
|
860
|
+
outRetained += captured.byteLength;
|
|
861
|
+
outChunks.push(captured);
|
|
862
|
+
});
|
|
863
|
+
child.stderr.on("data", (chunk) => {
|
|
864
|
+
const captured = captureChunk(chunk, limit + 1 - errRetained);
|
|
865
|
+
if (captured === void 0) return;
|
|
866
|
+
errRetained += captured.byteLength;
|
|
867
|
+
errChunks.push(captured);
|
|
868
|
+
});
|
|
869
|
+
child.once("spawn", () => {
|
|
870
|
+
spawned = true;
|
|
871
|
+
});
|
|
872
|
+
child.once("error", (error) => {
|
|
873
|
+
if (spawned) return;
|
|
874
|
+
cause = error;
|
|
875
|
+
finish.abort();
|
|
876
|
+
});
|
|
877
|
+
child.once("close", () => finish.abort());
|
|
878
|
+
if (text !== void 0) child.stdin.write(text, (error) => {
|
|
879
|
+
if (error !== void 0 && error !== null) inputFailure.abort(error);
|
|
880
|
+
});
|
|
881
|
+
child.stdin.end();
|
|
882
|
+
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
883
|
+
expired = true;
|
|
884
|
+
terminate.abort();
|
|
885
|
+
}, timeout);
|
|
886
|
+
if (optionSignal !== void 0) {
|
|
887
|
+
optionSignal.addEventListener("abort", () => {
|
|
888
|
+
aborted = true;
|
|
889
|
+
terminate.abort();
|
|
890
|
+
}, {
|
|
891
|
+
once: true,
|
|
892
|
+
signal: cleanup.signal
|
|
989
893
|
});
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
this.#child.stdout.destroy();
|
|
994
|
-
this.#child.stderr.destroy();
|
|
995
|
-
}
|
|
996
|
-
#terminate() {
|
|
997
|
-
this.stop();
|
|
998
|
-
}
|
|
999
|
-
#removeAbortListener() {
|
|
1000
|
-
if (this.#signal === void 0 || this.#abort === void 0) return;
|
|
1001
|
-
this.#signal.removeEventListener("abort", this.#abort);
|
|
1002
|
-
}
|
|
1003
|
-
#confirmWrite(settled, error) {
|
|
1004
|
-
if (error === void 0 || error === null) {
|
|
1005
|
-
this.#settleWrite(settled, true);
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
this.#failInputCallback(error);
|
|
1009
|
-
}
|
|
1010
|
-
#completeInput() {
|
|
1011
|
-
this.#input -= 1;
|
|
1012
|
-
}
|
|
1013
|
-
#endInput() {
|
|
1014
|
-
const stdin = this.#child.stdin;
|
|
1015
|
-
if (this.#stopping || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
|
|
1016
|
-
const ended = Promise.withResolvers();
|
|
1017
|
-
stdin.end(() => ended.resolve());
|
|
1018
|
-
return ended.promise;
|
|
1019
|
-
}
|
|
1020
|
-
#settleWrite(settled, accepted) {
|
|
1021
|
-
if (!this.#writes.has(settled)) return;
|
|
1022
|
-
const timer = this.#writes.get(settled);
|
|
1023
|
-
this.#writes.delete(settled);
|
|
1024
|
-
clearTimeout(timer);
|
|
1025
|
-
settled.resolve(accepted);
|
|
1026
|
-
}
|
|
1027
|
-
#settleWrites() {
|
|
1028
|
-
for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
|
|
1029
|
-
}
|
|
1030
|
-
#failInputStream(cause) {
|
|
1031
|
-
if (this.#input > 0) {
|
|
1032
|
-
this.#settleWrites();
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
this.#failInputCallback(cause);
|
|
1036
|
-
}
|
|
1037
|
-
#failInputCallback(cause) {
|
|
1038
|
-
if (this.#child.stdin.writableEnded) {
|
|
1039
|
-
this.#settleWrites();
|
|
1040
|
-
return;
|
|
894
|
+
if (optionSignal.aborted) {
|
|
895
|
+
aborted = true;
|
|
896
|
+
terminate.abort();
|
|
1041
897
|
}
|
|
1042
|
-
if (this.#failure !== void 0 || this.#stopping) return;
|
|
1043
|
-
this.#failure = cause;
|
|
1044
|
-
this.#settleWrites();
|
|
1045
|
-
this.#fault(new _src_core.ProcessError("The standard-input channel failed", {
|
|
1046
|
-
code: "protocol",
|
|
1047
|
-
cause
|
|
1048
|
-
}));
|
|
1049
898
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
899
|
+
const result = await settled.promise;
|
|
900
|
+
if (result.failed && strict) {
|
|
901
|
+
if (cause !== void 0 && cause === inputFailure.signal.reason) throw new _src_core.ProcessError(`Command '${result.command}' failed while writing standard input`, {
|
|
902
|
+
code: "input",
|
|
903
|
+
context: {
|
|
904
|
+
command: result.command,
|
|
905
|
+
code: result.code,
|
|
906
|
+
signal: result.signal
|
|
907
|
+
},
|
|
908
|
+
cause: inputFailure.signal.reason,
|
|
909
|
+
result
|
|
910
|
+
});
|
|
911
|
+
throw (0, _src_core.createExecuteError)(result, cause);
|
|
1063
912
|
}
|
|
1064
|
-
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Runs one command to completion synchronously, buffering its output, and returns the outcome.
|
|
917
|
+
*
|
|
918
|
+
* @remarks
|
|
919
|
+
* The synchronous counterpart of {@link execute}, spawned through the same resolver and never through a
|
|
920
|
+
* shell. The host offers no cooperative termination window and no in-flight cancellation, so this
|
|
921
|
+
* contract carries neither. A positive `timeout` ends only the root process and can leave
|
|
922
|
+
* descendants running; use {@link execute} or {@link Process} when timeout must terminate the tree. A
|
|
923
|
+
* timeout and an output overflow both end the root with `SIGKILL`: an overflow reports `truncated`
|
|
924
|
+
* and `failed` together and trims the partial output to `limit`, where {@link execute} keeps reading and
|
|
925
|
+
* reports `truncated` without failing. The environment and input follow the same merge as
|
|
926
|
+
* {@link execute}. Unless `strict` is `false`, a failed run throws a {@link createExecuteError}
|
|
927
|
+
* carrying the {@link ExecuteResult}.
|
|
928
|
+
*
|
|
929
|
+
* @param command - The executable, arguments, and optional environment and input
|
|
930
|
+
* @param options - Working directory, timeout, capture limit, and failure delivery
|
|
931
|
+
* @returns The run outcome
|
|
932
|
+
* @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`
|
|
933
|
+
*
|
|
934
|
+
* @example
|
|
935
|
+
* ```ts
|
|
936
|
+
* const result = executeSync({ file: 'git', arguments: ['--version'] }, { strict: false })
|
|
937
|
+
* ```
|
|
938
|
+
*/
|
|
939
|
+
function executeSync(command, options) {
|
|
940
|
+
const snapshot = snapshotCommand(command);
|
|
941
|
+
const optionEnvironment = options?.environment;
|
|
942
|
+
const optionWorkspace = options?.workspace;
|
|
943
|
+
const optionInput = options?.input;
|
|
944
|
+
const optionTimeout = options?.timeout;
|
|
945
|
+
const optionStrict = options?.strict;
|
|
946
|
+
const optionLimit = options?.limit;
|
|
947
|
+
validateCommand(snapshot);
|
|
948
|
+
validateEnvironment(optionEnvironment);
|
|
949
|
+
validateWorkspace(optionWorkspace);
|
|
950
|
+
validateTimer(optionTimeout, "option 'timeout'");
|
|
951
|
+
validateBytes(optionLimit, "option 'limit'", 0);
|
|
952
|
+
const limit = optionLimit ?? _src_core.PROCESS_OUTPUT;
|
|
953
|
+
const timeout = optionTimeout ?? 0;
|
|
954
|
+
const strict = optionStrict ?? true;
|
|
955
|
+
const text = optionInput ?? snapshot.input;
|
|
956
|
+
const line = formatCommand(snapshot);
|
|
957
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
958
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment, optionEnvironment);
|
|
959
|
+
const plan = buildSpawn(snapshot, {
|
|
960
|
+
workspace,
|
|
961
|
+
environment
|
|
962
|
+
});
|
|
963
|
+
const outcome = (0, node_child_process.spawnSync)(plan.file, [...plan.arguments], {
|
|
964
|
+
cwd: workspace,
|
|
965
|
+
env: environment,
|
|
966
|
+
encoding: "buffer",
|
|
967
|
+
maxBuffer: limit,
|
|
968
|
+
killSignal: "SIGKILL",
|
|
969
|
+
windowsHide: true,
|
|
970
|
+
windowsVerbatimArguments: plan.verbatim,
|
|
971
|
+
...text !== void 0 ? { input: node_buffer.Buffer.from(text) } : {},
|
|
972
|
+
...timeout > 0 ? { timeout } : {}
|
|
973
|
+
});
|
|
974
|
+
const error = outcome.error;
|
|
975
|
+
const fault = error !== void 0 && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : "";
|
|
976
|
+
const result = buildExecuteResult({
|
|
977
|
+
command: line,
|
|
978
|
+
stdout: node_buffer.Buffer.isBuffer(outcome.stdout) ? outcome.stdout : node_buffer.Buffer.alloc(0),
|
|
979
|
+
stderr: node_buffer.Buffer.isBuffer(outcome.stderr) ? outcome.stderr : node_buffer.Buffer.alloc(0),
|
|
980
|
+
code: outcome.status,
|
|
981
|
+
signal: outcome.signal,
|
|
982
|
+
expired: fault === "ETIMEDOUT",
|
|
983
|
+
aborted: false,
|
|
984
|
+
truncated: fault === "ENOBUFS",
|
|
985
|
+
limit,
|
|
986
|
+
...error === void 0 ? {} : { cause: error }
|
|
987
|
+
});
|
|
988
|
+
if (result.failed && strict) throw (0, _src_core.createExecuteError)(result, error);
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Spawns one command as a detached process and returns without waiting for it.
|
|
993
|
+
*
|
|
994
|
+
* @remarks
|
|
995
|
+
* The child owns no stdio and is unreferenced, so it outlives this process and nothing here observes
|
|
996
|
+
* its outcome. The error listener is attached before the child is unreferenced, so a post-spawn host
|
|
997
|
+
* fault is swallowed rather than crashing the caller; an invalid command is refused before anything
|
|
998
|
+
* is spawned.
|
|
999
|
+
*
|
|
1000
|
+
* @param command - The executable, arguments, and optional environment
|
|
1001
|
+
* @param options - The working directory the detached child starts in
|
|
1002
|
+
* @returns Nothing
|
|
1003
|
+
* @throws A {@link ProcessError} coded `invalid` when the working directory or a command string is malformed
|
|
1004
|
+
*
|
|
1005
|
+
* @example
|
|
1006
|
+
* ```ts
|
|
1007
|
+
* detach({ file: 'node', arguments: ['daemon.js'] }, { workspace: process.cwd() })
|
|
1008
|
+
* ```
|
|
1009
|
+
*/
|
|
1010
|
+
function detach(command, options) {
|
|
1011
|
+
const snapshot = snapshotCommand(command);
|
|
1012
|
+
const optionWorkspace = options?.workspace;
|
|
1013
|
+
validateCommand(snapshot);
|
|
1014
|
+
validateWorkspace(optionWorkspace);
|
|
1015
|
+
const workspace = optionWorkspace ?? process.cwd();
|
|
1016
|
+
const environment = mergeEnvironment(snapshot.isolated === true, snapshot.environment);
|
|
1017
|
+
const plan = buildSpawn(snapshot, {
|
|
1018
|
+
workspace,
|
|
1019
|
+
environment
|
|
1020
|
+
});
|
|
1021
|
+
const child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1022
|
+
cwd: workspace,
|
|
1023
|
+
detached: true,
|
|
1024
|
+
env: environment,
|
|
1025
|
+
stdio: "ignore",
|
|
1026
|
+
windowsHide: true,
|
|
1027
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1028
|
+
});
|
|
1029
|
+
child.once("error", () => void 0);
|
|
1030
|
+
child.unref();
|
|
1031
|
+
}
|
|
1065
1032
|
//#endregion
|
|
1066
|
-
//#region src/server/
|
|
1033
|
+
//#region src/server/Supervisor.ts
|
|
1067
1034
|
/**
|
|
1068
|
-
* Supervises one child
|
|
1035
|
+
* Supervises one child process and reports each lifecycle moment to the face composing it.
|
|
1069
1036
|
*
|
|
1070
1037
|
* @remarks
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
*
|
|
1077
|
-
* the push channel never disappears while a pull channel can still change or wait indefinitely.
|
|
1078
|
-
*
|
|
1079
|
-
* Standard output is framed through `readline` into a single-consumer stream. Lines already framed
|
|
1080
|
-
* and queued remain available through the terminal moment, while bytes arriving after a requested
|
|
1081
|
-
* termination may be cut off. Standard error is forwarded through the live `stderr` event and
|
|
1082
|
-
* retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
|
|
1083
|
-
* observation surfaces. `destroy` performs that stop and then destroys the emitter after the
|
|
1084
|
-
* frozen terminal state exists.
|
|
1038
|
+
* This is the engine every supervised-child face shares. It owns the eager spawn, the byte-bounded
|
|
1039
|
+
* stderr tail and its decoded chunks, the standard-input channel and its bounded confirmation, the
|
|
1040
|
+
* termination sequence, and the progression from the child's native exit through the bounded `drain`
|
|
1041
|
+
* window to one terminal moment. It owns no output framing and no observation surface: a face
|
|
1042
|
+
* attaches its own consumer to `stdout`, frames bytes however its contract promises, and owns the
|
|
1043
|
+
* emitter and event map its consumers subscribe to.
|
|
1085
1044
|
*
|
|
1086
|
-
*
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1045
|
+
* The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read the
|
|
1046
|
+
* host child directly, so they expose native exit as soon as the host records it, and `ending`
|
|
1047
|
+
* settles at that same moment. `settled`, `exit`, and `evidence` reach one terminal moment after the
|
|
1048
|
+
* read channels close or the bounded `drain` window cuts them off. That window is armed by the
|
|
1049
|
+
* child's native exit and by a termination this package initiated, so every way a child can end
|
|
1050
|
+
* reaches the moment. The terminal routine hands the face its `close` and `terminal` moments before
|
|
1051
|
+
* it destroys stdout and stderr, so a face's own pull surfaces are final while its read ends still
|
|
1052
|
+
* exist.
|
|
1089
1053
|
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1054
|
+
* The face is a set of callbacks rather than an emitter, because each face publishes a different
|
|
1055
|
+
* event map over the same moments. `chunk` and `fault` carry the live stderr text and the child or
|
|
1056
|
+
* channel faults, `relieve` reports that a termination began so the face can release whatever
|
|
1057
|
+
* backpressure it holds, `close` ends the face's read pipeline at the terminal moment, `terminal`
|
|
1058
|
+
* carries the frozen {@link ProcessExit}, and `teardown` releases the face's own surface after the
|
|
1059
|
+
* termination completes. `relieve` is optional because a face that never pauses the child's output
|
|
1060
|
+
* holds no backpressure to release, and a face with nothing to do there would otherwise declare an
|
|
1061
|
+
* empty method to say so.
|
|
1097
1062
|
*/
|
|
1098
|
-
var
|
|
1099
|
-
#
|
|
1100
|
-
#
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
#
|
|
1104
|
-
#
|
|
1105
|
-
#
|
|
1106
|
-
#
|
|
1107
|
-
#
|
|
1108
|
-
#
|
|
1109
|
-
#
|
|
1110
|
-
#
|
|
1111
|
-
#
|
|
1063
|
+
var Supervisor = class {
|
|
1064
|
+
#chunk;
|
|
1065
|
+
#fault;
|
|
1066
|
+
#relieve;
|
|
1067
|
+
#close;
|
|
1068
|
+
#terminal;
|
|
1069
|
+
#teardown;
|
|
1070
|
+
#child;
|
|
1071
|
+
#grace;
|
|
1072
|
+
#drain;
|
|
1073
|
+
#evidence;
|
|
1074
|
+
#delivery;
|
|
1075
|
+
#decoder = new node_string_decoder.StringDecoder("utf8");
|
|
1076
|
+
#exit = Promise.withResolvers();
|
|
1077
|
+
#ending = Promise.withResolvers();
|
|
1078
|
+
#signal;
|
|
1079
|
+
#abort;
|
|
1080
|
+
#writes = /* @__PURE__ */ new Map();
|
|
1081
|
+
#tail = node_buffer.Buffer.alloc(0);
|
|
1082
|
+
#stopping = false;
|
|
1083
|
+
#settled = false;
|
|
1084
|
+
#input = 0;
|
|
1085
|
+
#failure;
|
|
1086
|
+
#waiting;
|
|
1087
|
+
#closure;
|
|
1088
|
+
#termination;
|
|
1089
|
+
#destruction;
|
|
1112
1090
|
/**
|
|
1113
|
-
* Spawn one child process and begin
|
|
1091
|
+
* Spawn one child process and begin standard-error capture.
|
|
1114
1092
|
*
|
|
1115
|
-
* @
|
|
1093
|
+
* @remarks
|
|
1094
|
+
* The face's callbacks are captured before anything is read or spawned, so the first moment the
|
|
1095
|
+
* child can produce already has somewhere to go. `options` supplies the command, workspace,
|
|
1096
|
+
* termination, capture, and standard-input settings; a face's own settings, such as an output
|
|
1097
|
+
* retention bound, are read and validated by that face before it constructs this engine, so an
|
|
1098
|
+
* invalid one refuses construction while nothing has been spawned.
|
|
1099
|
+
*
|
|
1100
|
+
* @param options - Command, workspace, termination, capture, and stdin settings
|
|
1101
|
+
* @param face - The composing face's callbacks for each lifecycle moment
|
|
1116
1102
|
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1117
1103
|
*/
|
|
1118
|
-
constructor(options) {
|
|
1104
|
+
constructor(options, face) {
|
|
1105
|
+
this.#chunk = face.chunk;
|
|
1106
|
+
this.#fault = face.fault;
|
|
1107
|
+
this.#relieve = face.relieve;
|
|
1108
|
+
this.#close = face.close;
|
|
1109
|
+
this.#terminal = face.terminal;
|
|
1110
|
+
this.#teardown = face.teardown;
|
|
1119
1111
|
const source = options.command;
|
|
1120
1112
|
const workspace = options.workspace;
|
|
1121
1113
|
const grace = options.grace;
|
|
1122
1114
|
const drain = options.drain;
|
|
1123
1115
|
const evidence = options.evidence;
|
|
1124
|
-
const backlog = options.backlog;
|
|
1125
1116
|
const delivery = options.delivery;
|
|
1126
1117
|
const writable = options.writable;
|
|
1127
1118
|
const signal = options.signal;
|
|
1128
|
-
const on = options.on;
|
|
1129
|
-
const error = options.error;
|
|
1130
1119
|
const command = snapshotCommand(source);
|
|
1120
|
+
const input = command.input;
|
|
1131
1121
|
validateCommand(command);
|
|
1132
1122
|
validateWorkspace(workspace);
|
|
1133
1123
|
validateTimer(grace, "option 'grace'");
|
|
1134
1124
|
validateTimer(drain, "option 'drain'");
|
|
1135
1125
|
validateTimer(delivery, "option 'delivery'");
|
|
1136
1126
|
validateBytes(evidence, "option 'evidence'", 0);
|
|
1137
|
-
|
|
1138
|
-
this.#
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
this.#engine = new Supervisor({
|
|
1145
|
-
command,
|
|
1127
|
+
this.#grace = grace ?? _src_core.PROCESS_GRACE;
|
|
1128
|
+
this.#drain = drain ?? _src_core.PROCESS_DRAIN;
|
|
1129
|
+
this.#evidence = evidence ?? _src_core.PROCESS_EVIDENCE;
|
|
1130
|
+
this.#delivery = delivery ?? 0;
|
|
1131
|
+
this.#signal = signal;
|
|
1132
|
+
const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
|
|
1133
|
+
const plan = buildSpawn(command, {
|
|
1146
1134
|
workspace,
|
|
1147
|
-
|
|
1148
|
-
...drain === void 0 ? {} : { drain },
|
|
1149
|
-
...evidence === void 0 ? {} : { evidence },
|
|
1150
|
-
...delivery === void 0 ? {} : { delivery },
|
|
1151
|
-
...writable === void 0 ? {} : { writable },
|
|
1152
|
-
...signal === void 0 ? {} : { signal }
|
|
1153
|
-
}, {
|
|
1154
|
-
chunk: this.#reportStderr.bind(this),
|
|
1155
|
-
fault: this.#reportFault.bind(this),
|
|
1156
|
-
relieve: this.#releaseBackpressure.bind(this),
|
|
1157
|
-
close: this.#closeReader.bind(this),
|
|
1158
|
-
terminal: this.#reportExit.bind(this),
|
|
1159
|
-
teardown: this.#emitter.destroy.bind(this.#emitter)
|
|
1135
|
+
environment: childEnvironment
|
|
1160
1136
|
});
|
|
1161
|
-
this.#
|
|
1162
|
-
|
|
1163
|
-
|
|
1137
|
+
this.#child = (0, node_child_process.spawn)(plan.file, [...plan.arguments], {
|
|
1138
|
+
cwd: workspace,
|
|
1139
|
+
detached: process.platform !== "win32",
|
|
1140
|
+
env: childEnvironment,
|
|
1141
|
+
stdio: [
|
|
1142
|
+
"pipe",
|
|
1143
|
+
"pipe",
|
|
1144
|
+
"pipe"
|
|
1145
|
+
],
|
|
1146
|
+
windowsHide: true,
|
|
1147
|
+
windowsVerbatimArguments: plan.verbatim
|
|
1164
1148
|
});
|
|
1165
|
-
this.#
|
|
1166
|
-
this.#
|
|
1149
|
+
this.#child.once("error", this.#fault);
|
|
1150
|
+
this.#child.once("exit", this.#expire.bind(this));
|
|
1151
|
+
this.#child.once("close", this.#complete.bind(this));
|
|
1152
|
+
this.#child.stdin.on("error", this.#failInputStream.bind(this));
|
|
1153
|
+
this.#child.stderr.on("data", this.#retain.bind(this));
|
|
1154
|
+
if (input !== void 0) {
|
|
1155
|
+
this.#input += 1;
|
|
1156
|
+
this.#child.stdin.write(input, this.#completeInput.bind(this));
|
|
1157
|
+
}
|
|
1158
|
+
if (writable !== true) {
|
|
1159
|
+
this.#input += 1;
|
|
1160
|
+
this.#child.stdin.end(this.#completeInput.bind(this));
|
|
1161
|
+
}
|
|
1162
|
+
if (signal !== void 0) {
|
|
1163
|
+
this.#abort = this.#terminate.bind(this);
|
|
1164
|
+
signal.addEventListener("abort", this.#abort, { once: true });
|
|
1165
|
+
if (signal.aborted) this.stop();
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
/** The child's standard-output stream, for the composing face to attach its own consumer to. */
|
|
1169
|
+
get stdout() {
|
|
1170
|
+
return this.#child.stdout;
|
|
1167
1171
|
}
|
|
1168
1172
|
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1169
1173
|
get pid() {
|
|
1170
|
-
return this.#
|
|
1174
|
+
return this.#child.pid;
|
|
1171
1175
|
}
|
|
1172
1176
|
/** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1173
1177
|
get code() {
|
|
1174
|
-
return this.#
|
|
1178
|
+
return this.#child.exitCode;
|
|
1175
1179
|
}
|
|
1176
1180
|
/** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1177
1181
|
get signal() {
|
|
1178
|
-
return this.#
|
|
1179
|
-
}
|
|
1180
|
-
/** The typed lifecycle observation surface. */
|
|
1181
|
-
get emitter() {
|
|
1182
|
-
return this.#emitter;
|
|
1183
|
-
}
|
|
1184
|
-
/** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
|
|
1185
|
-
get lines() {
|
|
1186
|
-
return this.#lines;
|
|
1182
|
+
return this.#child.signalCode;
|
|
1187
1183
|
}
|
|
1188
1184
|
/** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1189
1185
|
get evidence() {
|
|
1190
|
-
return this.#
|
|
1191
|
-
}
|
|
1192
|
-
/** True when the `lines` stream omitted output after a retention bound was reached. */
|
|
1193
|
-
get truncated() {
|
|
1194
|
-
return this.#truncated;
|
|
1186
|
+
return this.#tail.toString("utf8");
|
|
1195
1187
|
}
|
|
1196
1188
|
/** True after the terminal moment arrived and `exit` settled. */
|
|
1197
1189
|
get settled() {
|
|
1198
|
-
return this.#
|
|
1190
|
+
return this.#settled;
|
|
1199
1191
|
}
|
|
1200
1192
|
/** True after termination began, including after the terminal moment. */
|
|
1201
1193
|
get stopping() {
|
|
1202
|
-
return this.#
|
|
1194
|
+
return this.#stopping;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* The child's own ending, awaited without the terminal moment's drain window.
|
|
1198
|
+
*
|
|
1199
|
+
* @remarks
|
|
1200
|
+
* Never rejects, and resolves no value: `code` and `signal` carry the terminal facts as soon as
|
|
1201
|
+
* the host records them. It settles at the native exit, and at the terminal moment for a child
|
|
1202
|
+
* whose spawn produced no native exit at all, so it can never outlive the supervision.
|
|
1203
|
+
*/
|
|
1204
|
+
get ending() {
|
|
1205
|
+
return this.#ending.promise;
|
|
1203
1206
|
}
|
|
1204
1207
|
/** The terminal child state, observed once after stream close or the drain cutoff. */
|
|
1205
1208
|
get exit() {
|
|
1206
|
-
return this.#
|
|
1209
|
+
return this.#exit.promise;
|
|
1207
1210
|
}
|
|
1208
1211
|
/**
|
|
1209
|
-
*
|
|
1212
|
+
* Writes raw bytes to the open standard-input channel.
|
|
1213
|
+
*
|
|
1214
|
+
* @remarks
|
|
1215
|
+
* Never rejects, and adds no framing: a face composes whatever terminator its contract promises
|
|
1216
|
+
* before it calls this. `true` means the host accepted the bytes without reporting a fault; it
|
|
1217
|
+
* does not prove that the child read them. An ordinary write settles when the kernel accepts it.
|
|
1218
|
+
* Only a full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and
|
|
1219
|
+
* every terminal teardown path settles pending writes. After `stop` or `destroy` begins, a later
|
|
1220
|
+
* call settles `false`, because teardown cannot confirm delivery for bytes it is about to
|
|
1221
|
+
* discard.
|
|
1222
|
+
*
|
|
1223
|
+
* @param bytes - The payload to write, already framed by the caller
|
|
1224
|
+
* @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`
|
|
1225
|
+
*/
|
|
1226
|
+
deliver(bytes) {
|
|
1227
|
+
const stdin = this.#child.stdin;
|
|
1228
|
+
if (this.#settled || this.#stopping || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
|
|
1229
|
+
const settled = Promise.withResolvers();
|
|
1230
|
+
this.#writes.set(settled, void 0);
|
|
1231
|
+
try {
|
|
1232
|
+
stdin.write(bytes, this.#confirmWrite.bind(this, settled));
|
|
1233
|
+
} catch {
|
|
1234
|
+
this.#settleWrite(settled, false);
|
|
1235
|
+
return settled.promise;
|
|
1236
|
+
}
|
|
1237
|
+
if (this.#delivery > 0 && this.#writes.has(settled)) {
|
|
1238
|
+
const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
|
|
1239
|
+
timer.unref();
|
|
1240
|
+
this.#writes.set(settled, timer);
|
|
1241
|
+
}
|
|
1242
|
+
return settled.promise;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Closes the standard-input channel while leaving the child running.
|
|
1246
|
+
*
|
|
1247
|
+
* @remarks
|
|
1248
|
+
* Never rejects, and terminates nothing: no signal is sent, `stopping` stays false, no drain
|
|
1249
|
+
* window is armed, and the terminal moment does not arrive. The child observes end of input and
|
|
1250
|
+
* decides for itself what to do about it, which is what separates this from `stop`.
|
|
1210
1251
|
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
* the write can settle `true` without a callback error or a stream error while the child remains
|
|
1218
|
-
* alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
|
|
1219
|
-
* that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
|
|
1220
|
-
* for bytes the package is about to discard.
|
|
1252
|
+
* Every call shares one barrier, and it resolves after the host flushes the writes it had already
|
|
1253
|
+
* accepted, or reports that it cannot. A `deliver` after this call resolves `false`, because the
|
|
1254
|
+
* host stops reporting the channel writable the moment it is ended. That same flag keeps the
|
|
1255
|
+
* ended channel quiet for its remaining life, so a later host fault on it settles pending writes
|
|
1256
|
+
* and reaches no face. After `stop` or `destroy` has begun, or after the terminal moment, the
|
|
1257
|
+
* call resolves and changes nothing, because the channel is already being discarded.
|
|
1221
1258
|
*
|
|
1222
|
-
* @
|
|
1223
|
-
* @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`
|
|
1259
|
+
* @returns The stable barrier shared by every call
|
|
1224
1260
|
*/
|
|
1225
|
-
|
|
1226
|
-
|
|
1261
|
+
end() {
|
|
1262
|
+
if (this.#closure !== void 0) return this.#closure;
|
|
1263
|
+
this.#closure = this.#endInput();
|
|
1264
|
+
return this.#closure;
|
|
1227
1265
|
}
|
|
1228
1266
|
/**
|
|
1229
1267
|
* Terminates the child process tree and reaches the terminal observation moment.
|
|
@@ -1237,10 +1275,12 @@ var Process = class {
|
|
|
1237
1275
|
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
1238
1276
|
*/
|
|
1239
1277
|
stop() {
|
|
1240
|
-
return this.#
|
|
1278
|
+
if (this.#termination !== void 0) return this.#termination;
|
|
1279
|
+
this.#termination = this.#kill();
|
|
1280
|
+
return this.#termination;
|
|
1241
1281
|
}
|
|
1242
1282
|
/**
|
|
1243
|
-
* Stops the child and
|
|
1283
|
+
* Stops the child and releases the composing face after the terminal state freezes.
|
|
1244
1284
|
*
|
|
1245
1285
|
* @remarks
|
|
1246
1286
|
* Always resolves, including when termination was never confirmed. Every call shares one barrier.
|
|
@@ -1248,351 +1288,219 @@ var Process = class {
|
|
|
1248
1288
|
* @returns The stable barrier shared by every call
|
|
1249
1289
|
*/
|
|
1250
1290
|
destroy() {
|
|
1251
|
-
return this.#
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
this.#requested = true;
|
|
1255
|
-
return Object.freeze({ next: this.#next.bind(this) });
|
|
1256
|
-
}
|
|
1257
|
-
#next() {
|
|
1258
|
-
const line = this.#take();
|
|
1259
|
-
if (line !== void 0) return Promise.resolve(Object.freeze({
|
|
1260
|
-
done: false,
|
|
1261
|
-
value: line
|
|
1262
|
-
}));
|
|
1263
|
-
if (this.#ended) return Promise.resolve(Object.freeze({
|
|
1264
|
-
done: true,
|
|
1265
|
-
value: void 0
|
|
1266
|
-
}));
|
|
1267
|
-
const waiter = Promise.withResolvers();
|
|
1268
|
-
this.#waiters.push(waiter);
|
|
1269
|
-
return waiter.promise;
|
|
1270
|
-
}
|
|
1271
|
-
#take() {
|
|
1272
|
-
if (this.#head >= this.#queue.length) return void 0;
|
|
1273
|
-
const line = this.#queue[this.#head];
|
|
1274
|
-
this.#queue[this.#head] = "";
|
|
1275
|
-
this.#head += 1;
|
|
1276
|
-
if (this.#head >= this.#queue.length) {
|
|
1277
|
-
this.#queue.length = 0;
|
|
1278
|
-
this.#head = 0;
|
|
1279
|
-
}
|
|
1280
|
-
if (line === void 0) return void 0;
|
|
1281
|
-
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
1282
|
-
this.#relieve();
|
|
1283
|
-
return line;
|
|
1284
|
-
}
|
|
1285
|
-
#push(line) {
|
|
1286
|
-
if (this.#ended) return;
|
|
1287
|
-
const waiter = this.#waiters.shift();
|
|
1288
|
-
if (waiter !== void 0) {
|
|
1289
|
-
waiter.resolve(Object.freeze({
|
|
1290
|
-
done: false,
|
|
1291
|
-
value: line
|
|
1292
|
-
}));
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
const bytes = node_buffer.Buffer.byteLength(line) + 1;
|
|
1296
|
-
const stopping = this.#engine.stopping;
|
|
1297
|
-
const limit = stopping ? this.#backlog * 2 : this.#backlog;
|
|
1298
|
-
if ((!this.#requested || stopping) && this.#pending + bytes > limit) {
|
|
1299
|
-
this.#truncated = true;
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
this.#queue.push(line);
|
|
1303
|
-
this.#pending += bytes;
|
|
1304
|
-
this.#restrain();
|
|
1305
|
-
}
|
|
1306
|
-
#restrain() {
|
|
1307
|
-
if (this.#paused || this.#engine.stopping || !this.#requested) return;
|
|
1308
|
-
if (this.#pending < this.#backlog) return;
|
|
1309
|
-
this.#paused = true;
|
|
1310
|
-
this.#reader.pause();
|
|
1311
|
-
}
|
|
1312
|
-
#relieve() {
|
|
1313
|
-
if (!this.#paused || this.#ended) return;
|
|
1314
|
-
if (!this.#engine.stopping && this.#pending > this.#backlog / 2) return;
|
|
1315
|
-
this.#paused = false;
|
|
1316
|
-
this.#reader.resume();
|
|
1317
|
-
}
|
|
1318
|
-
#finish() {
|
|
1319
|
-
if (this.#ended) return;
|
|
1320
|
-
this.#ended = true;
|
|
1321
|
-
this.#paused = false;
|
|
1322
|
-
for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
|
|
1323
|
-
done: true,
|
|
1324
|
-
value: void 0
|
|
1325
|
-
}));
|
|
1326
|
-
this.#waiters.length = 0;
|
|
1327
|
-
}
|
|
1328
|
-
#reportStderr(chunk) {
|
|
1329
|
-
this.#emitter.emit("stderr", chunk);
|
|
1330
|
-
}
|
|
1331
|
-
#reportFault(cause) {
|
|
1332
|
-
this.#emitter.emit("error", cause);
|
|
1333
|
-
}
|
|
1334
|
-
#reportExit(exit) {
|
|
1335
|
-
this.#emitter.emit("exit", exit);
|
|
1336
|
-
}
|
|
1337
|
-
#closeReader() {
|
|
1338
|
-
this.#reader.close();
|
|
1339
|
-
}
|
|
1340
|
-
#releaseBackpressure() {
|
|
1341
|
-
this.#capBacklog();
|
|
1342
|
-
this.#relieve();
|
|
1343
|
-
}
|
|
1344
|
-
#capBacklog() {
|
|
1345
|
-
const limit = this.#backlog * 2;
|
|
1346
|
-
while (this.#pending > limit && this.#queue.length > this.#head) {
|
|
1347
|
-
const line = this.#queue.pop();
|
|
1348
|
-
if (line === void 0) break;
|
|
1349
|
-
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
1350
|
-
this.#truncated = true;
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
//#endregion
|
|
1355
|
-
//#region src/server/ProcessManager.ts
|
|
1356
|
-
/**
|
|
1357
|
-
* A keyed registry of live supervised child processes.
|
|
1358
|
-
*
|
|
1359
|
-
* @remarks
|
|
1360
|
-
* A child launched under an id joins the registry and emits `launch`; when it settles it removes
|
|
1361
|
-
* itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
|
|
1362
|
-
* before the child is constructed and released when construction throws, so a refused launch never
|
|
1363
|
-
* strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
|
|
1364
|
-
* it lands one microtask after the child's public `exit` event: a listener on that event still sees
|
|
1365
|
-
* the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
|
|
1366
|
-
* and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
|
|
1367
|
-
* getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
|
|
1368
|
-
* `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
|
|
1369
|
-
* of a child a lost race refused, which destroys each child's observation emitter and leaves every
|
|
1370
|
-
* subscription on it silently inert, then destroys the registry emitter last.
|
|
1371
|
-
*
|
|
1372
|
-
* @example
|
|
1373
|
-
* ```ts
|
|
1374
|
-
* import { ProcessManager } from '@orkestrel/process/server'
|
|
1375
|
-
*
|
|
1376
|
-
* const manager = new ProcessManager()
|
|
1377
|
-
* const child = manager.launch('build', {
|
|
1378
|
-
* command: { file: 'node', arguments: ['build.js'] },
|
|
1379
|
-
* workspace: process.cwd(),
|
|
1380
|
-
* })
|
|
1381
|
-
* await child.exit
|
|
1382
|
-
* await manager.destroy()
|
|
1383
|
-
* ```
|
|
1384
|
-
*/
|
|
1385
|
-
var ProcessManager = class {
|
|
1386
|
-
#emitter;
|
|
1387
|
-
#children = /* @__PURE__ */ new Map();
|
|
1388
|
-
#ids = /* @__PURE__ */ new Set();
|
|
1389
|
-
#orphans = /* @__PURE__ */ new Set();
|
|
1390
|
-
#destroying = false;
|
|
1391
|
-
#ending;
|
|
1392
|
-
/**
|
|
1393
|
-
* Construct a process registry.
|
|
1394
|
-
*
|
|
1395
|
-
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
1396
|
-
*/
|
|
1397
|
-
constructor(options) {
|
|
1398
|
-
const on = options?.on;
|
|
1399
|
-
const error = options?.error;
|
|
1400
|
-
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1401
|
-
...on === void 0 ? {} : { on },
|
|
1402
|
-
...error === void 0 ? {} : { error }
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
/** The typed fleet-level observation surface. */
|
|
1406
|
-
get emitter() {
|
|
1407
|
-
return this.#emitter;
|
|
1291
|
+
if (this.#destruction !== void 0) return this.#destruction;
|
|
1292
|
+
this.#destruction = this.#conclude();
|
|
1293
|
+
return this.#destruction;
|
|
1408
1294
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1295
|
+
#retain(chunk) {
|
|
1296
|
+
if (!node_buffer.Buffer.isBuffer(chunk) || this.#settled) return;
|
|
1297
|
+
this.#tail = trimTail(node_buffer.Buffer.concat([this.#tail, chunk]), this.#evidence);
|
|
1298
|
+
const text = this.#decoder.write(chunk);
|
|
1299
|
+
if (text.length > 0) this.#chunk(text);
|
|
1412
1300
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
* @param id - The registry key
|
|
1417
|
-
* @returns The child, or `undefined`
|
|
1418
|
-
*/
|
|
1419
|
-
process(id) {
|
|
1420
|
-
return this.#children.get(id);
|
|
1301
|
+
#complete() {
|
|
1302
|
+
if (this.#settled) return;
|
|
1303
|
+
this.#settle(true);
|
|
1421
1304
|
}
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
*/
|
|
1427
|
-
processes() {
|
|
1428
|
-
return [...this.#children.values()];
|
|
1305
|
+
#expire() {
|
|
1306
|
+
this.#ending.resolve();
|
|
1307
|
+
if (this.#settled) return;
|
|
1308
|
+
this.#wait();
|
|
1429
1309
|
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
* nothing has started and a throw from one strands no process. A getter that begins `destroy`
|
|
1436
|
-
* without throwing is the one remaining race: the child is already spawned, so the launch is
|
|
1437
|
-
* refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
|
|
1438
|
-
* teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
|
|
1439
|
-
* synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
|
|
1440
|
-
* terminal moment before the barrier resolves.
|
|
1441
|
-
*
|
|
1442
|
-
* @param id - The registry key, unique among live children
|
|
1443
|
-
* @param options - The child construction options
|
|
1444
|
-
* @returns The launched child
|
|
1445
|
-
* @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
|
|
1446
|
-
*/
|
|
1447
|
-
launch(id, options) {
|
|
1448
|
-
if (this.#destroying) throw (0, _src_core.createProtocolError)(id);
|
|
1449
|
-
if (this.#ids.has(id)) throw (0, _src_core.createDuplicateError)(id);
|
|
1450
|
-
this.#ids.add(id);
|
|
1451
|
-
const child = this.#construct(id, options);
|
|
1452
|
-
if (this.#destroying) {
|
|
1453
|
-
this.#ids.delete(id);
|
|
1454
|
-
this.#orphans.add(child);
|
|
1455
|
-
child.destroy();
|
|
1456
|
-
throw (0, _src_core.createProtocolError)(id);
|
|
1457
|
-
}
|
|
1458
|
-
this.#children.set(id, child);
|
|
1459
|
-
child.exit.then((exit) => this.#evict(id, child, exit));
|
|
1460
|
-
this.#emitter.emit("launch", id);
|
|
1461
|
-
return child;
|
|
1310
|
+
#wait() {
|
|
1311
|
+
this.#waiting ??= waitForClose(this.#child, this.#drain).then(() => {
|
|
1312
|
+
if (!this.#settled) this.#settle(false);
|
|
1313
|
+
});
|
|
1314
|
+
return this.#waiting;
|
|
1462
1315
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1316
|
+
#settle(drained) {
|
|
1317
|
+
const suffix = this.#decoder.end();
|
|
1318
|
+
if (suffix.length > 0) this.#chunk(suffix);
|
|
1319
|
+
this.#settled = true;
|
|
1320
|
+
this.#removeAbortListener();
|
|
1321
|
+
this.#close();
|
|
1322
|
+
const exit = Object.freeze({
|
|
1323
|
+
code: this.code,
|
|
1324
|
+
signal: this.signal,
|
|
1325
|
+
drained
|
|
1326
|
+
});
|
|
1327
|
+
this.#exit.resolve(exit);
|
|
1328
|
+
this.#ending.resolve();
|
|
1329
|
+
this.#terminal(exit);
|
|
1330
|
+
this.#child.stdout.destroy();
|
|
1331
|
+
this.#child.stderr.destroy();
|
|
1467
1332
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
*
|
|
1471
|
-
* @remarks
|
|
1472
|
-
* Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
|
|
1473
|
-
* destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
|
|
1474
|
-
* subscription on it goes silently inert.
|
|
1475
|
-
*
|
|
1476
|
-
* @returns The stable barrier shared by every call
|
|
1477
|
-
*/
|
|
1478
|
-
destroy() {
|
|
1479
|
-
if (this.#ending !== void 0) return this.#ending;
|
|
1480
|
-
this.#destroying = true;
|
|
1481
|
-
this.#ending = this.#teardown();
|
|
1482
|
-
return this.#ending;
|
|
1333
|
+
#terminate() {
|
|
1334
|
+
this.stop();
|
|
1483
1335
|
}
|
|
1484
|
-
#
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1336
|
+
#removeAbortListener() {
|
|
1337
|
+
if (this.#signal === void 0 || this.#abort === void 0) return;
|
|
1338
|
+
this.#signal.removeEventListener("abort", this.#abort);
|
|
1339
|
+
}
|
|
1340
|
+
#confirmWrite(settled, error) {
|
|
1341
|
+
if (error === void 0 || error === null) {
|
|
1342
|
+
this.#settleWrite(settled, true);
|
|
1343
|
+
return;
|
|
1490
1344
|
}
|
|
1345
|
+
this.#failInputCallback(error);
|
|
1491
1346
|
}
|
|
1492
|
-
#
|
|
1493
|
-
|
|
1494
|
-
this.#children.delete(id);
|
|
1495
|
-
this.#ids.delete(id);
|
|
1496
|
-
this.#emitter.emit("exit", id, exit);
|
|
1347
|
+
#completeInput() {
|
|
1348
|
+
this.#input -= 1;
|
|
1497
1349
|
}
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1350
|
+
#endInput() {
|
|
1351
|
+
const stdin = this.#child.stdin;
|
|
1352
|
+
if (this.#stopping || this.#settled || stdin.writableEnded || stdin.destroyed) return Promise.resolve();
|
|
1353
|
+
const ended = Promise.withResolvers();
|
|
1354
|
+
stdin.end(() => ended.resolve());
|
|
1355
|
+
return ended.promise;
|
|
1502
1356
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1357
|
+
#settleWrite(settled, accepted) {
|
|
1358
|
+
if (!this.#writes.has(settled)) return;
|
|
1359
|
+
const timer = this.#writes.get(settled);
|
|
1360
|
+
this.#writes.delete(settled);
|
|
1361
|
+
clearTimeout(timer);
|
|
1362
|
+
settled.resolve(accepted);
|
|
1505
1363
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1364
|
+
#settleWrites() {
|
|
1365
|
+
for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
|
|
1508
1366
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
this.#orphans.clear();
|
|
1514
|
-
await Promise.allSettled(orphans.map((child) => child.destroy()));
|
|
1367
|
+
#failInputStream(cause) {
|
|
1368
|
+
if (this.#input > 0) {
|
|
1369
|
+
this.#settleWrites();
|
|
1370
|
+
return;
|
|
1515
1371
|
}
|
|
1516
|
-
this.#
|
|
1517
|
-
|
|
1518
|
-
|
|
1372
|
+
this.#failInputCallback(cause);
|
|
1373
|
+
}
|
|
1374
|
+
#failInputCallback(cause) {
|
|
1375
|
+
if (this.#child.stdin.writableEnded) {
|
|
1376
|
+
this.#settleWrites();
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (this.#failure !== void 0 || this.#stopping) return;
|
|
1380
|
+
this.#failure = cause;
|
|
1381
|
+
this.#settleWrites();
|
|
1382
|
+
this.#fault(new _src_core.ProcessError("The standard-input channel failed", {
|
|
1383
|
+
code: "protocol",
|
|
1384
|
+
cause
|
|
1385
|
+
}));
|
|
1386
|
+
}
|
|
1387
|
+
async #kill() {
|
|
1388
|
+
this.#stopping = true;
|
|
1389
|
+
this.#relieve?.();
|
|
1390
|
+
const confirmed = await stopChild(this.#child, this.#grace, _src_core.PROCESS_CONFIRMATION);
|
|
1391
|
+
this.#settleWrites();
|
|
1392
|
+
this.#child.stdin.destroy();
|
|
1393
|
+
if (!this.#settled) await this.#wait();
|
|
1394
|
+
if (!this.#settled) this.#settle(false);
|
|
1395
|
+
return confirmed;
|
|
1396
|
+
}
|
|
1397
|
+
async #conclude() {
|
|
1398
|
+
await this.stop();
|
|
1399
|
+
this.#teardown();
|
|
1519
1400
|
}
|
|
1520
1401
|
};
|
|
1521
1402
|
//#endregion
|
|
1522
|
-
//#region src/server/
|
|
1403
|
+
//#region src/server/Process.ts
|
|
1523
1404
|
/**
|
|
1524
|
-
* Supervises one child
|
|
1405
|
+
* Supervises one child while keeping every observation channel aligned at termination.
|
|
1525
1406
|
*
|
|
1526
1407
|
* @remarks
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
* `
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
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.
|
|
1408
|
+
* The child's ending and the supervision's ending are distinct. `pid`, `code`, and `signal` read
|
|
1409
|
+
* the host child directly, so they expose native exit as soon as the host records it. `settled`,
|
|
1410
|
+
* `exit`, `evidence`, and `lines` reach one terminal moment after the read channels close or the
|
|
1411
|
+
* bounded `drain` window cuts them off. That window is armed by the child's native exit and by a
|
|
1412
|
+
* termination this package initiated, so every way a child can end reaches the moment. The terminal
|
|
1413
|
+
* routine freezes `evidence` and ends `lines` before it destroys stdout, stderr, or the emitter, so
|
|
1414
|
+
* the push channel never disappears while a pull channel can still change or wait indefinitely.
|
|
1537
1415
|
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
*
|
|
1542
|
-
* the
|
|
1416
|
+
* Standard output is framed through `readline` into a single-consumer stream. Lines already framed
|
|
1417
|
+
* and queued remain available through the terminal moment, while bytes arriving after a requested
|
|
1418
|
+
* termination may be cut off. Standard error is forwarded through the live `stderr` event and
|
|
1419
|
+
* retained as a byte-bounded raw tail until that same moment. `stop` ends the child tree and the
|
|
1420
|
+
* observation surfaces. `destroy` performs that stop and then destroys the emitter after the
|
|
1421
|
+
* frozen terminal state exists.
|
|
1543
1422
|
*
|
|
1544
1423
|
* @example
|
|
1545
1424
|
* ```ts
|
|
1546
|
-
* import {
|
|
1425
|
+
* import { Process } from '@orkestrel/process/server'
|
|
1547
1426
|
*
|
|
1548
|
-
* const
|
|
1549
|
-
* command: { file: 'node', arguments: ['
|
|
1427
|
+
* const child = new Process({
|
|
1428
|
+
* command: { file: 'node', arguments: ['--version'] },
|
|
1550
1429
|
* workspace: process.cwd(),
|
|
1551
1430
|
* })
|
|
1431
|
+
* const exit = await child.exit
|
|
1432
|
+
* await child.destroy()
|
|
1552
1433
|
* ```
|
|
1553
1434
|
*/
|
|
1554
|
-
var
|
|
1435
|
+
var Process = class {
|
|
1555
1436
|
#emitter;
|
|
1556
1437
|
#engine;
|
|
1557
|
-
#
|
|
1438
|
+
#reader;
|
|
1439
|
+
#backlog;
|
|
1440
|
+
#lines;
|
|
1441
|
+
#queue = [];
|
|
1442
|
+
#waiters = [];
|
|
1443
|
+
#head = 0;
|
|
1444
|
+
#pending = 0;
|
|
1445
|
+
#requested = false;
|
|
1446
|
+
#paused = false;
|
|
1447
|
+
#truncated = false;
|
|
1448
|
+
#ended = false;
|
|
1558
1449
|
/**
|
|
1559
|
-
* Spawn one child process and begin
|
|
1450
|
+
* Spawn one child process and begin stream capture.
|
|
1560
1451
|
*
|
|
1561
|
-
* @param options - Command, workspace, termination,
|
|
1452
|
+
* @param options - Command, workspace, termination, capture, stdin, and observation settings
|
|
1562
1453
|
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1563
1454
|
*/
|
|
1564
1455
|
constructor(options) {
|
|
1565
|
-
const
|
|
1456
|
+
const source = options.command;
|
|
1566
1457
|
const workspace = options.workspace;
|
|
1567
1458
|
const grace = options.grace;
|
|
1568
1459
|
const drain = options.drain;
|
|
1569
1460
|
const evidence = options.evidence;
|
|
1461
|
+
const backlog = options.backlog;
|
|
1570
1462
|
const delivery = options.delivery;
|
|
1463
|
+
const writable = options.writable;
|
|
1571
1464
|
const signal = options.signal;
|
|
1572
1465
|
const on = options.on;
|
|
1573
1466
|
const error = options.error;
|
|
1467
|
+
const command = snapshotCommand(source);
|
|
1468
|
+
validateCommand(command);
|
|
1469
|
+
validateWorkspace(workspace);
|
|
1470
|
+
validateTimer(grace, "option 'grace'");
|
|
1471
|
+
validateTimer(drain, "option 'drain'");
|
|
1472
|
+
validateTimer(delivery, "option 'delivery'");
|
|
1473
|
+
validateBytes(evidence, "option 'evidence'", 0);
|
|
1474
|
+
validateBytes(backlog, "option 'backlog'", 1);
|
|
1574
1475
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1575
1476
|
...on === void 0 ? {} : { on },
|
|
1576
1477
|
...error === void 0 ? {} : { error }
|
|
1577
1478
|
});
|
|
1578
|
-
this.#
|
|
1479
|
+
this.#backlog = backlog ?? _src_core.PROCESS_BACKLOG;
|
|
1480
|
+
this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
|
|
1579
1481
|
this.#engine = new Supervisor({
|
|
1580
1482
|
command,
|
|
1581
1483
|
workspace,
|
|
1582
|
-
writable: true,
|
|
1583
1484
|
...grace === void 0 ? {} : { grace },
|
|
1584
1485
|
...drain === void 0 ? {} : { drain },
|
|
1585
1486
|
...evidence === void 0 ? {} : { evidence },
|
|
1586
1487
|
...delivery === void 0 ? {} : { delivery },
|
|
1488
|
+
...writable === void 0 ? {} : { writable },
|
|
1587
1489
|
...signal === void 0 ? {} : { signal }
|
|
1588
1490
|
}, {
|
|
1589
1491
|
chunk: this.#reportStderr.bind(this),
|
|
1590
1492
|
fault: this.#reportFault.bind(this),
|
|
1591
|
-
|
|
1493
|
+
relieve: this.#releaseBackpressure.bind(this),
|
|
1494
|
+
close: this.#closeReader.bind(this),
|
|
1592
1495
|
terminal: this.#reportExit.bind(this),
|
|
1593
1496
|
teardown: this.#emitter.destroy.bind(this.#emitter)
|
|
1594
1497
|
});
|
|
1595
|
-
this.#
|
|
1498
|
+
this.#reader = (0, node_readline.createInterface)({
|
|
1499
|
+
input: this.#engine.stdout,
|
|
1500
|
+
crlfDelay: Infinity
|
|
1501
|
+
});
|
|
1502
|
+
this.#reader.on("line", this.#push.bind(this));
|
|
1503
|
+
this.#reader.once("close", this.#finish.bind(this));
|
|
1596
1504
|
}
|
|
1597
1505
|
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1598
1506
|
get pid() {
|
|
@@ -1610,59 +1518,58 @@ var Session = class {
|
|
|
1610
1518
|
get emitter() {
|
|
1611
1519
|
return this.#emitter;
|
|
1612
1520
|
}
|
|
1521
|
+
/** The captured stdout lines, in arrival order, ending after queued lines at the terminal moment. */
|
|
1522
|
+
get lines() {
|
|
1523
|
+
return this.#lines;
|
|
1524
|
+
}
|
|
1613
1525
|
/** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1614
1526
|
get evidence() {
|
|
1615
1527
|
return this.#engine.evidence;
|
|
1616
1528
|
}
|
|
1529
|
+
/** True when the `lines` stream omitted output after a retention bound was reached. */
|
|
1530
|
+
get truncated() {
|
|
1531
|
+
return this.#truncated;
|
|
1532
|
+
}
|
|
1617
1533
|
/** True after the terminal moment arrived and `exit` settled. */
|
|
1618
1534
|
get settled() {
|
|
1619
1535
|
return this.#engine.settled;
|
|
1620
1536
|
}
|
|
1621
|
-
/** True after termination began, including after the terminal moment.
|
|
1537
|
+
/** True after termination began, including after the terminal moment. */
|
|
1622
1538
|
get stopping() {
|
|
1623
1539
|
return this.#engine.stopping;
|
|
1624
1540
|
}
|
|
1625
|
-
/** The child's own ending, awaited without the terminal moment's drain window. */
|
|
1626
|
-
get ending() {
|
|
1627
|
-
return this.#engine.ending;
|
|
1628
|
-
}
|
|
1629
1541
|
/** The terminal child state, observed once after stream close or the drain cutoff. */
|
|
1630
1542
|
get exit() {
|
|
1631
1543
|
return this.#engine.exit;
|
|
1632
1544
|
}
|
|
1633
1545
|
/**
|
|
1634
|
-
*
|
|
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.
|
|
1546
|
+
* Write one line to the open standard-input channel.
|
|
1650
1547
|
*
|
|
1651
1548
|
* @remarks
|
|
1652
|
-
* Never rejects
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1549
|
+
* Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
|
|
1550
|
+
* prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
|
|
1551
|
+
* full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
|
|
1552
|
+
* terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
|
|
1553
|
+
* 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
|
|
1554
|
+
* the write can settle `true` without a callback error or a stream error while the child remains
|
|
1555
|
+
* alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
|
|
1556
|
+
* that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
|
|
1557
|
+
* for bytes the package is about to discard.
|
|
1655
1558
|
*
|
|
1656
|
-
* @
|
|
1559
|
+
* @param text - The line text without its trailing newline
|
|
1560
|
+
* @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`
|
|
1657
1561
|
*/
|
|
1658
|
-
|
|
1659
|
-
return this.#engine.
|
|
1562
|
+
send(text) {
|
|
1563
|
+
return this.#engine.deliver(node_buffer.Buffer.from(`${text}\n`, "utf8"));
|
|
1660
1564
|
}
|
|
1661
1565
|
/**
|
|
1662
1566
|
* Terminates the child process tree and reaches the terminal observation moment.
|
|
1663
1567
|
*
|
|
1664
1568
|
* @remarks
|
|
1665
|
-
* Never rejects, and every call shares one termination
|
|
1569
|
+
* Never rejects, and every call shares one termination. After the termination sequence returns,
|
|
1570
|
+
* confirmed or not, it waits at most `drain` for the read channels to close, then cuts them off
|
|
1571
|
+
* and settles `exit` when they remain open. A cutoff reached before the native exit reports the
|
|
1572
|
+
* `code` and `signal` the host had recorded by then, which is `null` for a child still running.
|
|
1666
1573
|
*
|
|
1667
1574
|
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
1668
1575
|
*/
|
|
@@ -1680,9 +1587,80 @@ var Session = class {
|
|
|
1680
1587
|
destroy() {
|
|
1681
1588
|
return this.#engine.destroy();
|
|
1682
1589
|
}
|
|
1683
|
-
#
|
|
1684
|
-
|
|
1685
|
-
|
|
1590
|
+
#iterate() {
|
|
1591
|
+
this.#requested = true;
|
|
1592
|
+
return Object.freeze({ next: this.#next.bind(this) });
|
|
1593
|
+
}
|
|
1594
|
+
#next() {
|
|
1595
|
+
const line = this.#take();
|
|
1596
|
+
if (line !== void 0) return Promise.resolve(Object.freeze({
|
|
1597
|
+
done: false,
|
|
1598
|
+
value: line
|
|
1599
|
+
}));
|
|
1600
|
+
if (this.#ended) return Promise.resolve(Object.freeze({
|
|
1601
|
+
done: true,
|
|
1602
|
+
value: void 0
|
|
1603
|
+
}));
|
|
1604
|
+
const waiter = Promise.withResolvers();
|
|
1605
|
+
this.#waiters.push(waiter);
|
|
1606
|
+
return waiter.promise;
|
|
1607
|
+
}
|
|
1608
|
+
#take() {
|
|
1609
|
+
if (this.#head >= this.#queue.length) return void 0;
|
|
1610
|
+
const line = this.#queue[this.#head];
|
|
1611
|
+
this.#queue[this.#head] = "";
|
|
1612
|
+
this.#head += 1;
|
|
1613
|
+
if (this.#head >= this.#queue.length) {
|
|
1614
|
+
this.#queue.length = 0;
|
|
1615
|
+
this.#head = 0;
|
|
1616
|
+
}
|
|
1617
|
+
if (line === void 0) return void 0;
|
|
1618
|
+
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
1619
|
+
this.#relieve();
|
|
1620
|
+
return line;
|
|
1621
|
+
}
|
|
1622
|
+
#push(line) {
|
|
1623
|
+
if (this.#ended) return;
|
|
1624
|
+
const waiter = this.#waiters.shift();
|
|
1625
|
+
if (waiter !== void 0) {
|
|
1626
|
+
waiter.resolve(Object.freeze({
|
|
1627
|
+
done: false,
|
|
1628
|
+
value: line
|
|
1629
|
+
}));
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
const bytes = node_buffer.Buffer.byteLength(line) + 1;
|
|
1633
|
+
const stopping = this.#engine.stopping;
|
|
1634
|
+
const limit = stopping ? this.#backlog * 2 : this.#backlog;
|
|
1635
|
+
if ((!this.#requested || stopping) && this.#pending + bytes > limit) {
|
|
1636
|
+
this.#truncated = true;
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
this.#queue.push(line);
|
|
1640
|
+
this.#pending += bytes;
|
|
1641
|
+
this.#restrain();
|
|
1642
|
+
}
|
|
1643
|
+
#restrain() {
|
|
1644
|
+
if (this.#paused || this.#engine.stopping || !this.#requested) return;
|
|
1645
|
+
if (this.#pending < this.#backlog) return;
|
|
1646
|
+
this.#paused = true;
|
|
1647
|
+
this.#reader.pause();
|
|
1648
|
+
}
|
|
1649
|
+
#relieve() {
|
|
1650
|
+
if (!this.#paused || this.#ended) return;
|
|
1651
|
+
if (!this.#engine.stopping && this.#pending > this.#backlog / 2) return;
|
|
1652
|
+
this.#paused = false;
|
|
1653
|
+
this.#reader.resume();
|
|
1654
|
+
}
|
|
1655
|
+
#finish() {
|
|
1656
|
+
if (this.#ended) return;
|
|
1657
|
+
this.#ended = true;
|
|
1658
|
+
this.#paused = false;
|
|
1659
|
+
for (const waiter of this.#waiters) waiter.resolve(Object.freeze({
|
|
1660
|
+
done: true,
|
|
1661
|
+
value: void 0
|
|
1662
|
+
}));
|
|
1663
|
+
this.#waiters.length = 0;
|
|
1686
1664
|
}
|
|
1687
1665
|
#reportStderr(chunk) {
|
|
1688
1666
|
this.#emitter.emit("stderr", chunk);
|
|
@@ -1693,414 +1671,436 @@ var Session = class {
|
|
|
1693
1671
|
#reportExit(exit) {
|
|
1694
1672
|
this.#emitter.emit("exit", exit);
|
|
1695
1673
|
}
|
|
1696
|
-
#
|
|
1697
|
-
this.#
|
|
1674
|
+
#closeReader() {
|
|
1675
|
+
this.#reader.close();
|
|
1676
|
+
}
|
|
1677
|
+
#releaseBackpressure() {
|
|
1678
|
+
this.#capBacklog();
|
|
1679
|
+
this.#relieve();
|
|
1680
|
+
}
|
|
1681
|
+
#capBacklog() {
|
|
1682
|
+
const limit = this.#backlog * 2;
|
|
1683
|
+
while (this.#pending > limit && this.#queue.length > this.#head) {
|
|
1684
|
+
const line = this.#queue.pop();
|
|
1685
|
+
if (line === void 0) break;
|
|
1686
|
+
this.#pending -= node_buffer.Buffer.byteLength(line) + 1;
|
|
1687
|
+
this.#truncated = true;
|
|
1688
|
+
}
|
|
1698
1689
|
}
|
|
1699
1690
|
};
|
|
1700
1691
|
//#endregion
|
|
1701
|
-
//#region src/server/
|
|
1702
|
-
/**
|
|
1703
|
-
* Creates one supervised child process.
|
|
1704
|
-
*
|
|
1705
|
-
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
1706
|
-
* @returns The launched {@link ProcessInterface}
|
|
1707
|
-
*
|
|
1708
|
-
* @example
|
|
1709
|
-
* ```ts
|
|
1710
|
-
* import { createProcess } from '@orkestrel/process/server'
|
|
1711
|
-
*
|
|
1712
|
-
* const child = createProcess({
|
|
1713
|
-
* command: { file: 'node', arguments: ['--version'] },
|
|
1714
|
-
* workspace: process.cwd(),
|
|
1715
|
-
* grace: 5000,
|
|
1716
|
-
* })
|
|
1717
|
-
* ```
|
|
1718
|
-
*/
|
|
1719
|
-
function createProcess(options) {
|
|
1720
|
-
return new Process(options);
|
|
1721
|
-
}
|
|
1692
|
+
//#region src/server/ProcessManager.ts
|
|
1722
1693
|
/**
|
|
1723
|
-
*
|
|
1694
|
+
* A keyed registry of live supervised child processes.
|
|
1724
1695
|
*
|
|
1725
|
-
* @
|
|
1726
|
-
*
|
|
1696
|
+
* @remarks
|
|
1697
|
+
* A child launched under an id joins the registry and emits `launch`; when it settles it removes
|
|
1698
|
+
* itself and emits `exit`, so `count` and `processes` reflect only live children. The id is reserved
|
|
1699
|
+
* before the child is constructed and released when construction throws, so a refused launch never
|
|
1700
|
+
* strands its key. Eviction follows the child's own `exit` promise, which no listener can forge, so
|
|
1701
|
+
* it lands one microtask after the child's public `exit` event: a listener on that event still sees
|
|
1702
|
+
* the child registered. `launch` throws a {@link createDuplicateError} when the id is already live
|
|
1703
|
+
* and a {@link createProtocolError} after `destroy` has begun, including when the caller's own option
|
|
1704
|
+
* getter begins that teardown mid-construction — a spawn fault surfaces through the returned child's
|
|
1705
|
+
* `exit`, never from `launch`. `destroy` awaits every child's own teardown, including the teardown
|
|
1706
|
+
* of a child a lost race refused, which destroys each child's observation emitter and leaves every
|
|
1707
|
+
* subscription on it silently inert, then destroys the registry emitter last.
|
|
1727
1708
|
*
|
|
1728
1709
|
* @example
|
|
1729
1710
|
* ```ts
|
|
1730
|
-
* import {
|
|
1711
|
+
* import { ProcessManager } from '@orkestrel/process/server'
|
|
1731
1712
|
*
|
|
1732
|
-
* const
|
|
1733
|
-
*
|
|
1713
|
+
* const manager = new ProcessManager()
|
|
1714
|
+
* const child = manager.launch('build', {
|
|
1715
|
+
* command: { file: 'node', arguments: ['build.js'] },
|
|
1734
1716
|
* workspace: process.cwd(),
|
|
1735
|
-
* grace: 5000,
|
|
1736
1717
|
* })
|
|
1718
|
+
* await child.exit
|
|
1719
|
+
* await manager.destroy()
|
|
1737
1720
|
* ```
|
|
1738
1721
|
*/
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
*
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1722
|
+
var ProcessManager = class {
|
|
1723
|
+
#emitter;
|
|
1724
|
+
#children = /* @__PURE__ */ new Map();
|
|
1725
|
+
#ids = /* @__PURE__ */ new Set();
|
|
1726
|
+
#orphans = /* @__PURE__ */ new Set();
|
|
1727
|
+
#destroying = false;
|
|
1728
|
+
#ending;
|
|
1729
|
+
/**
|
|
1730
|
+
* Construct a process registry.
|
|
1731
|
+
*
|
|
1732
|
+
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
1733
|
+
*/
|
|
1734
|
+
constructor(options) {
|
|
1735
|
+
const on = options?.on;
|
|
1736
|
+
const error = options?.error;
|
|
1737
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1738
|
+
...on === void 0 ? {} : { on },
|
|
1739
|
+
...error === void 0 ? {} : { error }
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
/** The typed fleet-level observation surface. */
|
|
1743
|
+
get emitter() {
|
|
1744
|
+
return this.#emitter;
|
|
1745
|
+
}
|
|
1746
|
+
/** The number of live children. */
|
|
1747
|
+
get count() {
|
|
1748
|
+
return this.#children.size;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* The live child under `id`, or `undefined` when none is.
|
|
1752
|
+
*
|
|
1753
|
+
* @param id - The registry key
|
|
1754
|
+
* @returns The child, or `undefined`
|
|
1755
|
+
*/
|
|
1756
|
+
process(id) {
|
|
1757
|
+
return this.#children.get(id);
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* A snapshot of every live child.
|
|
1761
|
+
*
|
|
1762
|
+
* @returns The live children in launch order
|
|
1763
|
+
*/
|
|
1764
|
+
processes() {
|
|
1765
|
+
return [...this.#children.values()];
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Spawn and register one child under `id`.
|
|
1769
|
+
*
|
|
1770
|
+
* @remarks
|
|
1771
|
+
* {@link Process} reads every option before it spawns, so a caller's own option getter runs while
|
|
1772
|
+
* nothing has started and a throw from one strands no process. A getter that begins `destroy`
|
|
1773
|
+
* without throwing is the one remaining race: the child is already spawned, so the launch is
|
|
1774
|
+
* refused with a {@link createProtocolError} and that child is destroyed rather than adopted, its
|
|
1775
|
+
* teardown bounded by `grace` plus the confirmation window. The `protocol` refusal throws
|
|
1776
|
+
* synchronously, and the `destroy` barrier covers that teardown, so the refused child reaches its
|
|
1777
|
+
* terminal moment before the barrier resolves.
|
|
1778
|
+
*
|
|
1779
|
+
* @param id - The registry key, unique among live children
|
|
1780
|
+
* @param options - The child construction options
|
|
1781
|
+
* @returns The launched child
|
|
1782
|
+
* @throws A {@link ProcessError} coded `duplicate` when `id` is already live, `protocol` when the registry is being destroyed, or `invalid` when an option or command string is malformed
|
|
1783
|
+
*/
|
|
1784
|
+
launch(id, options) {
|
|
1785
|
+
if (this.#destroying) throw (0, _src_core.createProtocolError)(id);
|
|
1786
|
+
if (this.#ids.has(id)) throw (0, _src_core.createDuplicateError)(id);
|
|
1787
|
+
this.#ids.add(id);
|
|
1788
|
+
const child = this.#construct(id, options);
|
|
1789
|
+
if (this.#destroying) {
|
|
1790
|
+
this.#ids.delete(id);
|
|
1791
|
+
this.#orphans.add(child);
|
|
1792
|
+
child.destroy();
|
|
1793
|
+
throw (0, _src_core.createProtocolError)(id);
|
|
1794
|
+
}
|
|
1795
|
+
this.#children.set(id, child);
|
|
1796
|
+
child.exit.then((exit) => this.#evict(id, child, exit));
|
|
1797
|
+
this.#emitter.emit("launch", id);
|
|
1798
|
+
return child;
|
|
1776
1799
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
return this.#
|
|
1800
|
+
stop(target) {
|
|
1801
|
+
if (target === void 0) return this.#stopAll();
|
|
1802
|
+
if (typeof target === "string") return this.#stopOne(target);
|
|
1803
|
+
return this.#stopMany(target);
|
|
1780
1804
|
}
|
|
1781
1805
|
/**
|
|
1782
|
-
*
|
|
1806
|
+
* Stop every child, then destroy the registry emitter last.
|
|
1807
|
+
*
|
|
1808
|
+
* @remarks
|
|
1809
|
+
* Always resolves, and refuses a later `launch` with a {@link createProtocolError}. Each child is
|
|
1810
|
+
* destroyed rather than merely stopped, so its own observation emitter is destroyed too and every
|
|
1811
|
+
* subscription on it goes silently inert.
|
|
1783
1812
|
*
|
|
1784
|
-
* @
|
|
1785
|
-
* @param limit - The maximum retained byte count
|
|
1786
|
-
* @returns The retained slice, or `undefined` when the chunk contributes no retained bytes
|
|
1813
|
+
* @returns The stable barrier shared by every call
|
|
1787
1814
|
*/
|
|
1788
|
-
|
|
1789
|
-
if (
|
|
1790
|
-
this.#
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1815
|
+
destroy() {
|
|
1816
|
+
if (this.#ending !== void 0) return this.#ending;
|
|
1817
|
+
this.#destroying = true;
|
|
1818
|
+
this.#ending = this.#teardown();
|
|
1819
|
+
return this.#ending;
|
|
1820
|
+
}
|
|
1821
|
+
#construct(id, options) {
|
|
1822
|
+
try {
|
|
1823
|
+
return new Process(options);
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
this.#ids.delete(id);
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
#evict(id, child, exit) {
|
|
1830
|
+
if (this.#children.get(id) !== child) return;
|
|
1831
|
+
this.#children.delete(id);
|
|
1832
|
+
this.#ids.delete(id);
|
|
1833
|
+
this.#emitter.emit("exit", id, exit);
|
|
1834
|
+
}
|
|
1835
|
+
async #stopOne(id) {
|
|
1836
|
+
const child = this.#children.get(id);
|
|
1837
|
+
if (child === void 0) return false;
|
|
1838
|
+
return child.stop();
|
|
1839
|
+
}
|
|
1840
|
+
async #stopMany(ids) {
|
|
1841
|
+
return (await Promise.all(ids.map((id) => this.#stopOne(id)))).every((ok) => ok);
|
|
1842
|
+
}
|
|
1843
|
+
async #stopAll() {
|
|
1844
|
+
await Promise.allSettled([...this.#children.values()].map((child) => child.stop()));
|
|
1845
|
+
}
|
|
1846
|
+
async #teardown() {
|
|
1847
|
+
await Promise.allSettled([...this.#children.values()].map((child) => child.destroy()));
|
|
1848
|
+
while (this.#orphans.size > 0) {
|
|
1849
|
+
const orphans = [...this.#orphans];
|
|
1850
|
+
this.#orphans.clear();
|
|
1851
|
+
await Promise.allSettled(orphans.map((child) => child.destroy()));
|
|
1852
|
+
}
|
|
1853
|
+
this.#children.clear();
|
|
1854
|
+
this.#ids.clear();
|
|
1855
|
+
this.#emitter.destroy();
|
|
1796
1856
|
}
|
|
1797
1857
|
};
|
|
1798
1858
|
//#endregion
|
|
1799
|
-
//#region src/server/
|
|
1859
|
+
//#region src/server/Session.ts
|
|
1800
1860
|
/**
|
|
1801
|
-
*
|
|
1861
|
+
* Supervises one child and publishes its standard output as raw bytes.
|
|
1802
1862
|
*
|
|
1803
1863
|
* @remarks
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1806
|
-
*
|
|
1807
|
-
*
|
|
1808
|
-
*
|
|
1809
|
-
* `
|
|
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.
|
|
1864
|
+
* This is the transport face of the supervision engine `Process` also composes. It frames nothing,
|
|
1865
|
+
* retains nothing, and decodes nothing on the output side: every chunk the host delivers becomes one
|
|
1866
|
+
* `stdout` event carrying that chunk's bytes, in arrival order, so a consumer speaking a
|
|
1867
|
+
* length-prefixed or delimiter-framed protocol reads exactly what the child wrote and frames it
|
|
1868
|
+
* itself. Standard error is decoded and forwarded as `stderr`, with the same byte-bounded `evidence`
|
|
1869
|
+
* tail a `Process` retains, because a diagnostic stream is read rather than parsed.
|
|
1822
1870
|
*
|
|
1823
|
-
*
|
|
1824
|
-
*
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1871
|
+
* Each emitted chunk is this session's own copy, and a plain `Uint8Array` rather than the host buffer
|
|
1872
|
+
* the read produced, so a consumer keeps it, mutates it, concatenates it, and reads its whole backing
|
|
1873
|
+
* buffer without reaching memory the host still manages and without depending on a host type.
|
|
1874
|
+
*
|
|
1875
|
+
* The standard-input channel is open from the spawn, and `end` closes it without terminating
|
|
1876
|
+
* anything. That is the member a cooperative protocol reaches for: the child observes end of input
|
|
1877
|
+
* and exits on its own, which `ending` reports. `stop` and `destroy` remain the terminations, and
|
|
1878
|
+
* `exit` remains the terminal moment — the native exit plus at most `drain` while a descendant holds
|
|
1879
|
+
* the inherited read ends.
|
|
1827
1880
|
*
|
|
1828
1881
|
* @example
|
|
1829
1882
|
* ```ts
|
|
1830
|
-
*
|
|
1883
|
+
* import { Session } from '@orkestrel/process/server'
|
|
1884
|
+
*
|
|
1885
|
+
* const session = new Session({
|
|
1886
|
+
* command: { file: 'node', arguments: ['-e', 'process.stdin.pipe(process.stdout)'] },
|
|
1887
|
+
* workspace: process.cwd(),
|
|
1888
|
+
* })
|
|
1831
1889
|
* ```
|
|
1832
1890
|
*/
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
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);
|
|
1912
|
-
});
|
|
1913
|
-
}, { once: true });
|
|
1914
|
-
inputFailure.signal.addEventListener("abort", () => {
|
|
1915
|
-
if (finish.signal.aborted) return;
|
|
1916
|
-
cause = inputFailure.signal.reason;
|
|
1917
|
-
terminate.abort();
|
|
1918
|
-
}, { once: true });
|
|
1919
|
-
child.stdin.on("error", (error) => inputFailure.abort(error));
|
|
1920
|
-
child.stdout.on("data", (chunk) => {
|
|
1921
|
-
const retained = outRetention.retain(chunk, limit);
|
|
1922
|
-
if (retained !== void 0) outChunks.push(retained);
|
|
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();
|
|
1949
|
-
}, {
|
|
1950
|
-
once: true,
|
|
1951
|
-
signal: cleanup.signal
|
|
1891
|
+
var Session = class {
|
|
1892
|
+
#emitter;
|
|
1893
|
+
#engine;
|
|
1894
|
+
#output;
|
|
1895
|
+
/**
|
|
1896
|
+
* Spawn one child process and begin byte capture.
|
|
1897
|
+
*
|
|
1898
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
1899
|
+
* @throws A {@link ProcessError} coded `invalid` when an option or command string is malformed
|
|
1900
|
+
*/
|
|
1901
|
+
constructor(options) {
|
|
1902
|
+
const command = options.command;
|
|
1903
|
+
const workspace = options.workspace;
|
|
1904
|
+
const grace = options.grace;
|
|
1905
|
+
const drain = options.drain;
|
|
1906
|
+
const evidence = options.evidence;
|
|
1907
|
+
const delivery = options.delivery;
|
|
1908
|
+
const signal = options.signal;
|
|
1909
|
+
const on = options.on;
|
|
1910
|
+
const error = options.error;
|
|
1911
|
+
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
1912
|
+
...on === void 0 ? {} : { on },
|
|
1913
|
+
...error === void 0 ? {} : { error }
|
|
1952
1914
|
});
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1915
|
+
this.#output = this.#publish.bind(this);
|
|
1916
|
+
this.#engine = new Supervisor({
|
|
1917
|
+
command,
|
|
1918
|
+
workspace,
|
|
1919
|
+
writable: true,
|
|
1920
|
+
...grace === void 0 ? {} : { grace },
|
|
1921
|
+
...drain === void 0 ? {} : { drain },
|
|
1922
|
+
...evidence === void 0 ? {} : { evidence },
|
|
1923
|
+
...delivery === void 0 ? {} : { delivery },
|
|
1924
|
+
...signal === void 0 ? {} : { signal }
|
|
1925
|
+
}, {
|
|
1926
|
+
chunk: this.#reportStderr.bind(this),
|
|
1927
|
+
fault: this.#reportFault.bind(this),
|
|
1928
|
+
close: this.#closeOutput.bind(this),
|
|
1929
|
+
terminal: this.#reportExit.bind(this),
|
|
1930
|
+
teardown: this.#emitter.destroy.bind(this.#emitter)
|
|
1969
1931
|
});
|
|
1970
|
-
|
|
1932
|
+
this.#engine.stdout.on("data", this.#output);
|
|
1971
1933
|
}
|
|
1972
|
-
|
|
1973
|
-
|
|
1934
|
+
/** The host process id, fixed when construction returns, or `undefined` when the spawn produced none. */
|
|
1935
|
+
get pid() {
|
|
1936
|
+
return this.#engine.pid;
|
|
1937
|
+
}
|
|
1938
|
+
/** The exit code the host recorded, or `null` while the child has not exited and when a signal ended it. */
|
|
1939
|
+
get code() {
|
|
1940
|
+
return this.#engine.code;
|
|
1941
|
+
}
|
|
1942
|
+
/** The terminating signal name the host recorded, or `null` while the child has not exited and when it exited on its own. */
|
|
1943
|
+
get signal() {
|
|
1944
|
+
return this.#engine.signal;
|
|
1945
|
+
}
|
|
1946
|
+
/** The typed lifecycle observation surface. */
|
|
1947
|
+
get emitter() {
|
|
1948
|
+
return this.#emitter;
|
|
1949
|
+
}
|
|
1950
|
+
/** The decoded byte-bounded stderr tail, frozen at the terminal moment. */
|
|
1951
|
+
get evidence() {
|
|
1952
|
+
return this.#engine.evidence;
|
|
1953
|
+
}
|
|
1954
|
+
/** True after the terminal moment arrived and `exit` settled. */
|
|
1955
|
+
get settled() {
|
|
1956
|
+
return this.#engine.settled;
|
|
1957
|
+
}
|
|
1958
|
+
/** True after termination began, including after the terminal moment. `end` never turns it true. */
|
|
1959
|
+
get stopping() {
|
|
1960
|
+
return this.#engine.stopping;
|
|
1961
|
+
}
|
|
1962
|
+
/** The child's own ending, awaited without the terminal moment's drain window. */
|
|
1963
|
+
get ending() {
|
|
1964
|
+
return this.#engine.ending;
|
|
1965
|
+
}
|
|
1966
|
+
/** The terminal child state, observed once after stream close or the drain cutoff. */
|
|
1967
|
+
get exit() {
|
|
1968
|
+
return this.#engine.exit;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1971
|
+
* Writes raw bytes to the open standard-input channel.
|
|
1972
|
+
*
|
|
1973
|
+
* @remarks
|
|
1974
|
+
* Never rejects, and appends no terminator: the child receives exactly these bytes. `true` means
|
|
1975
|
+
* the host accepted them without reporting a fault; it does not prove that the child read them.
|
|
1976
|
+
* The channel can queue the payload, so treat the array as owned by the channel until the
|
|
1977
|
+
* returned promise settles.
|
|
1978
|
+
*
|
|
1979
|
+
* @param bytes - The payload to write, already framed by the caller
|
|
1980
|
+
* @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`
|
|
1981
|
+
*/
|
|
1982
|
+
write(bytes) {
|
|
1983
|
+
return this.#engine.deliver(bytes);
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Closes the standard-input channel and leaves the child running.
|
|
1987
|
+
*
|
|
1988
|
+
* @remarks
|
|
1989
|
+
* Never rejects and terminates nothing: `stopping` stays false and the terminal moment does not
|
|
1990
|
+
* arrive. Every call shares one barrier, which resolves after the host flushes the writes it had
|
|
1991
|
+
* already accepted. A later `write` resolves `false`.
|
|
1992
|
+
*
|
|
1993
|
+
* @returns The stable barrier shared by every call
|
|
1994
|
+
*/
|
|
1995
|
+
end() {
|
|
1996
|
+
return this.#engine.end();
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Terminates the child process tree and reaches the terminal observation moment.
|
|
2000
|
+
*
|
|
2001
|
+
* @remarks
|
|
2002
|
+
* Never rejects, and every call shares one termination bounded by `grace` and `drain`.
|
|
2003
|
+
*
|
|
2004
|
+
* @returns True when the child's native exit was observed; false when the confirmation deadline elapsed without it
|
|
2005
|
+
*/
|
|
2006
|
+
stop() {
|
|
2007
|
+
return this.#engine.stop();
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Stops the child and destroys the observation emitter after the terminal state freezes.
|
|
2011
|
+
*
|
|
2012
|
+
* @remarks
|
|
2013
|
+
* Always resolves, including when termination was never confirmed. Every call shares one barrier.
|
|
2014
|
+
*
|
|
2015
|
+
* @returns The stable barrier shared by every call
|
|
2016
|
+
*/
|
|
2017
|
+
destroy() {
|
|
2018
|
+
return this.#engine.destroy();
|
|
2019
|
+
}
|
|
2020
|
+
#publish(chunk) {
|
|
2021
|
+
if (!node_buffer.Buffer.isBuffer(chunk) || this.#engine.settled) return;
|
|
2022
|
+
this.#emitter.emit("stdout", new Uint8Array(chunk));
|
|
2023
|
+
}
|
|
2024
|
+
#reportStderr(chunk) {
|
|
2025
|
+
this.#emitter.emit("stderr", chunk);
|
|
2026
|
+
}
|
|
2027
|
+
#reportFault(cause) {
|
|
2028
|
+
this.#emitter.emit("error", cause);
|
|
2029
|
+
}
|
|
2030
|
+
#reportExit(exit) {
|
|
2031
|
+
this.#emitter.emit("exit", exit);
|
|
2032
|
+
}
|
|
2033
|
+
#closeOutput() {
|
|
2034
|
+
this.#engine.stdout.off("data", this.#output);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
1974
2037
|
//#endregion
|
|
1975
|
-
//#region src/server/
|
|
2038
|
+
//#region src/server/factories.ts
|
|
1976
2039
|
/**
|
|
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}.
|
|
2040
|
+
* Creates one supervised child process.
|
|
1989
2041
|
*
|
|
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`
|
|
2042
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
2043
|
+
* @returns The launched {@link ProcessInterface}
|
|
1994
2044
|
*
|
|
1995
2045
|
* @example
|
|
1996
2046
|
* ```ts
|
|
1997
|
-
*
|
|
2047
|
+
* import { createProcess } from '@orkestrel/process/server'
|
|
2048
|
+
*
|
|
2049
|
+
* const child = createProcess({
|
|
2050
|
+
* command: { file: 'node', arguments: ['--version'] },
|
|
2051
|
+
* workspace: process.cwd(),
|
|
2052
|
+
* grace: 5000,
|
|
2053
|
+
* })
|
|
1998
2054
|
* ```
|
|
1999
2055
|
*/
|
|
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;
|
|
2056
|
+
function createProcess(options) {
|
|
2057
|
+
return new Process(options);
|
|
2051
2058
|
}
|
|
2052
|
-
//#endregion
|
|
2053
|
-
//#region src/server/execution/detach.ts
|
|
2054
2059
|
/**
|
|
2055
|
-
*
|
|
2060
|
+
* Creates one raw byte session over a supervised child process.
|
|
2056
2061
|
*
|
|
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.
|
|
2062
|
+
* @param options - Command, workspace, termination, evidence, stdin, and observation settings
|
|
2063
|
+
* @returns The launched {@link SessionInterface}
|
|
2062
2064
|
*
|
|
2063
|
-
* @
|
|
2064
|
-
*
|
|
2065
|
-
* @
|
|
2066
|
-
*
|
|
2065
|
+
* @example
|
|
2066
|
+
* ```ts
|
|
2067
|
+
* import { createSession } from '@orkestrel/process/server'
|
|
2068
|
+
*
|
|
2069
|
+
* const session = createSession({
|
|
2070
|
+
* command: { file: 'node', arguments: ['server.js', '--stdio'] },
|
|
2071
|
+
* workspace: process.cwd(),
|
|
2072
|
+
* grace: 5000,
|
|
2073
|
+
* })
|
|
2074
|
+
* ```
|
|
2075
|
+
*/
|
|
2076
|
+
function createSession(options) {
|
|
2077
|
+
return new Session(options);
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Creates one keyed registry of supervised child processes.
|
|
2081
|
+
*
|
|
2082
|
+
* @param options - Initial fleet-level observation hooks and listener-error handling
|
|
2083
|
+
* @returns The {@link ProcessManagerInterface} registry
|
|
2067
2084
|
*
|
|
2068
2085
|
* @example
|
|
2069
2086
|
* ```ts
|
|
2070
|
-
*
|
|
2087
|
+
* import { createProcessManager } from '@orkestrel/process/server'
|
|
2088
|
+
*
|
|
2089
|
+
* const manager = createProcessManager()
|
|
2071
2090
|
* ```
|
|
2072
2091
|
*/
|
|
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();
|
|
2092
|
+
function createProcessManager(options) {
|
|
2093
|
+
return new ProcessManager(options);
|
|
2094
2094
|
}
|
|
2095
2095
|
//#endregion
|
|
2096
2096
|
exports.Process = Process;
|
|
2097
2097
|
exports.ProcessManager = ProcessManager;
|
|
2098
|
-
exports.Retention = Retention;
|
|
2099
2098
|
exports.Session = Session;
|
|
2100
2099
|
exports.buildExecutableCandidates = buildExecutableCandidates;
|
|
2101
2100
|
exports.buildExecuteResult = buildExecuteResult;
|
|
2102
2101
|
exports.buildPlatformSpawn = buildPlatformSpawn;
|
|
2103
2102
|
exports.buildSpawn = buildSpawn;
|
|
2103
|
+
exports.captureChunk = captureChunk;
|
|
2104
2104
|
exports.createProcess = createProcess;
|
|
2105
2105
|
exports.createProcessManager = createProcessManager;
|
|
2106
2106
|
exports.createSession = createSession;
|