@orkestrel/process 0.0.4 → 0.0.5
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/core/index.cjs +1 -0
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +39 -17
- package/dist/src/core/index.d.ts +39 -17
- package/dist/src/core/index.js +1 -0
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +108 -21
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +22 -10
- package/dist/src/server/index.d.ts +22 -10
- package/dist/src/server/index.js +109 -22
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -9
package/dist/src/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { statSync } from "node:fs";
|
|
|
4
4
|
import { join, posix, win32 } from "node:path";
|
|
5
5
|
import { platform } from "node:process";
|
|
6
6
|
import { boundsOf, holds, isNonEmptyString, isNonNegativeInteger, isString } from "@orkestrel/contract";
|
|
7
|
-
import { PROCESS_BACKLOG, PROCESS_CONFIRMATION, PROCESS_EVIDENCE, PROCESS_GRACE, PROCESS_OUTPUT, PROCESS_PATHEXT, PROCESS_TIMER, createDuplicateError, createExecuteError, createInvalidError, createProtocolError } from "../core/index.js";
|
|
7
|
+
import { PROCESS_BACKLOG, PROCESS_CONFIRMATION, PROCESS_EVIDENCE, PROCESS_GRACE, PROCESS_OUTPUT, PROCESS_PATHEXT, PROCESS_TIMER, ProcessError, createDuplicateError, createExecuteError, createInvalidError, createProtocolError } from "../core/index.js";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { StringDecoder } from "node:string_decoder";
|
|
10
10
|
import { Emitter } from "@orkestrel/emitter";
|
|
@@ -683,8 +683,11 @@ function buildExecuteResult(input) {
|
|
|
683
683
|
* overshoot it by the line that crossed it plus the rest of its delivered chunk. Termination never
|
|
684
684
|
* reapplies backpressure, and retained lines are capped at twice `backlog`; later lines are dropped
|
|
685
685
|
* and `truncated` reports the omission. Standard error is decoded and forwarded live as the `stderr`
|
|
686
|
-
* event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries
|
|
687
|
-
* child `error` cause on a spawn fault
|
|
686
|
+
* event while a byte-bounded raw tail is retained as `evidence`. The typed `emitter` also carries a
|
|
687
|
+
* child `error` cause on a spawn fault, a `protocol` {@link ProcessError} on a host-reported stdin
|
|
688
|
+
* fault after the constructor input phase, and the terminal `exit`, alongside the `exit` promise.
|
|
689
|
+
* A fault arising from the constructor-supplied `input` write or its closing `end` stays quiet
|
|
690
|
+
* because the package initiated that sequence. `pid`,
|
|
688
691
|
* `code`, and `signal` read the spawned child directly, so the native exit is readable before the
|
|
689
692
|
* `exit` promise settles on stdio close. `stop` ends the whole tree and reports whether the native
|
|
690
693
|
* exit was observed; `destroy` stops the child, destroys stdin so every pending `send` settles, and
|
|
@@ -710,6 +713,7 @@ var Process = class {
|
|
|
710
713
|
#grace;
|
|
711
714
|
#evidence;
|
|
712
715
|
#backlog;
|
|
716
|
+
#delivery;
|
|
713
717
|
#decoder = new StringDecoder("utf8");
|
|
714
718
|
#exit = Promise.withResolvers();
|
|
715
719
|
#lines;
|
|
@@ -717,6 +721,7 @@ var Process = class {
|
|
|
717
721
|
#waiters = [];
|
|
718
722
|
#signal;
|
|
719
723
|
#abort;
|
|
724
|
+
#writes = /* @__PURE__ */ new Map();
|
|
720
725
|
#tail = Buffer.alloc(0);
|
|
721
726
|
#head = 0;
|
|
722
727
|
#pending = 0;
|
|
@@ -726,6 +731,8 @@ var Process = class {
|
|
|
726
731
|
#terminating = false;
|
|
727
732
|
#closed = false;
|
|
728
733
|
#ended = false;
|
|
734
|
+
#input = 0;
|
|
735
|
+
#failure;
|
|
729
736
|
#stopping;
|
|
730
737
|
#ending;
|
|
731
738
|
/**
|
|
@@ -740,6 +747,7 @@ var Process = class {
|
|
|
740
747
|
const grace = options.grace;
|
|
741
748
|
const evidence = options.evidence;
|
|
742
749
|
const backlog = options.backlog;
|
|
750
|
+
const delivery = options.delivery;
|
|
743
751
|
const writable = options.writable;
|
|
744
752
|
const signal = options.signal;
|
|
745
753
|
const on = options.on;
|
|
@@ -749,6 +757,7 @@ var Process = class {
|
|
|
749
757
|
validateCommand(command);
|
|
750
758
|
validateWorkspace(workspace);
|
|
751
759
|
validateTimer(grace, "option 'grace'");
|
|
760
|
+
validateTimer(delivery, "option 'delivery'");
|
|
752
761
|
validateBytes(evidence, "option 'evidence'", 0);
|
|
753
762
|
validateBytes(backlog, "option 'backlog'", 1);
|
|
754
763
|
this.#emitter = new Emitter({
|
|
@@ -758,6 +767,7 @@ var Process = class {
|
|
|
758
767
|
this.#grace = grace ?? PROCESS_GRACE;
|
|
759
768
|
this.#evidence = evidence ?? PROCESS_EVIDENCE;
|
|
760
769
|
this.#backlog = backlog ?? PROCESS_BACKLOG;
|
|
770
|
+
this.#delivery = delivery ?? 0;
|
|
761
771
|
this.#signal = signal;
|
|
762
772
|
this.#lines = Object.freeze({ [Symbol.asyncIterator]: this.#iterate.bind(this) });
|
|
763
773
|
const childEnvironment = mergeEnvironment(command.isolated === true, command.environment);
|
|
@@ -785,10 +795,16 @@ var Process = class {
|
|
|
785
795
|
this.#reader.once("close", this.#finish.bind(this));
|
|
786
796
|
this.#child.once("error", (cause) => this.#emitter.emit("error", cause));
|
|
787
797
|
this.#child.once("close", this.#close.bind(this));
|
|
788
|
-
this.#child.stdin.on("error", () =>
|
|
798
|
+
this.#child.stdin.on("error", (cause) => this.#failInputStream(cause));
|
|
789
799
|
this.#child.stderr.on("data", this.#retain.bind(this));
|
|
790
|
-
if (input !== void 0)
|
|
791
|
-
|
|
800
|
+
if (input !== void 0) {
|
|
801
|
+
this.#input += 1;
|
|
802
|
+
this.#child.stdin.write(input, this.#completeInput.bind(this));
|
|
803
|
+
}
|
|
804
|
+
if (writable !== true) {
|
|
805
|
+
this.#input += 1;
|
|
806
|
+
this.#child.stdin.end(this.#completeInput.bind(this));
|
|
807
|
+
}
|
|
792
808
|
if (signal !== void 0) {
|
|
793
809
|
this.#abort = this.#terminate.bind(this);
|
|
794
810
|
signal.addEventListener("abort", this.#abort, { once: true });
|
|
@@ -831,24 +847,34 @@ var Process = class {
|
|
|
831
847
|
* Write one line to the open standard-input channel.
|
|
832
848
|
*
|
|
833
849
|
* @remarks
|
|
834
|
-
* Never rejects.
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
850
|
+
* Never rejects. `true` means the host accepted the bytes without reporting a fault; it does not
|
|
851
|
+
* prove that the child read them. An ordinary write settles when the kernel accepts it. Only a
|
|
852
|
+
* full pipe can hold the write unconfirmed. The `delivery` option can bound that wait, and every
|
|
853
|
+
* terminal teardown path settles pending writes. On Windows 11 with Node v24.18.1, measured on
|
|
854
|
+
* 2026-08-21, a child that closes its own file descriptor 0 can leave the parent pipe writable:
|
|
855
|
+
* the write can settle `true` without a callback error or a stream error while the child remains
|
|
856
|
+
* alive. After `stop` or `destroy` begins, a later call settles `false`. Version 0.0.4 could settle
|
|
857
|
+
* that call `true` before teardown destroyed the pipe; returning `false` avoids claiming delivery
|
|
858
|
+
* for bytes the package is about to discard.
|
|
838
859
|
*
|
|
839
860
|
* @param text - The line text without its trailing newline
|
|
840
|
-
* @returns True when the
|
|
861
|
+
* @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`
|
|
841
862
|
*/
|
|
842
863
|
send(text) {
|
|
843
864
|
const stdin = this.#child.stdin;
|
|
844
|
-
if (this.#closed || !stdin.writable) return Promise.resolve(false);
|
|
865
|
+
if (this.#closed || this.#terminating || this.#failure !== void 0 || !stdin.writable) return Promise.resolve(false);
|
|
845
866
|
const settled = Promise.withResolvers();
|
|
867
|
+
this.#writes.set(settled, void 0);
|
|
846
868
|
try {
|
|
847
|
-
stdin.write(`${text}\n`, (
|
|
848
|
-
settled.resolve(error === void 0 || error === null);
|
|
849
|
-
});
|
|
869
|
+
stdin.write(`${text}\n`, this.#confirmWrite.bind(this, settled));
|
|
850
870
|
} catch {
|
|
851
|
-
|
|
871
|
+
this.#settleWrite(settled, false);
|
|
872
|
+
return settled.promise;
|
|
873
|
+
}
|
|
874
|
+
if (this.#delivery > 0 && this.#writes.has(settled)) {
|
|
875
|
+
const timer = setTimeout(() => this.#settleWrite(settled, false), this.#delivery);
|
|
876
|
+
timer.unref();
|
|
877
|
+
this.#writes.set(settled, timer);
|
|
852
878
|
}
|
|
853
879
|
return settled.promise;
|
|
854
880
|
}
|
|
@@ -979,6 +1005,42 @@ var Process = class {
|
|
|
979
1005
|
if (this.#signal === void 0 || this.#abort === void 0) return;
|
|
980
1006
|
this.#signal.removeEventListener("abort", this.#abort);
|
|
981
1007
|
}
|
|
1008
|
+
#confirmWrite(settled, error) {
|
|
1009
|
+
if (error === void 0 || error === null) {
|
|
1010
|
+
this.#settleWrite(settled, true);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
this.#failInputCallback(error);
|
|
1014
|
+
}
|
|
1015
|
+
#completeInput() {
|
|
1016
|
+
this.#input -= 1;
|
|
1017
|
+
}
|
|
1018
|
+
#settleWrite(settled, accepted) {
|
|
1019
|
+
if (!this.#writes.has(settled)) return;
|
|
1020
|
+
const timer = this.#writes.get(settled);
|
|
1021
|
+
this.#writes.delete(settled);
|
|
1022
|
+
clearTimeout(timer);
|
|
1023
|
+
settled.resolve(accepted);
|
|
1024
|
+
}
|
|
1025
|
+
#settleWrites() {
|
|
1026
|
+
for (const settled of this.#writes.keys()) this.#settleWrite(settled, false);
|
|
1027
|
+
}
|
|
1028
|
+
#failInputStream(cause) {
|
|
1029
|
+
if (this.#child.stdin.writableEnded || this.#input > 0) {
|
|
1030
|
+
this.#settleWrites();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
this.#failInputCallback(cause);
|
|
1034
|
+
}
|
|
1035
|
+
#failInputCallback(cause) {
|
|
1036
|
+
if (this.#failure !== void 0 || this.#terminating) return;
|
|
1037
|
+
this.#failure = cause;
|
|
1038
|
+
this.#settleWrites();
|
|
1039
|
+
this.#emitter.emit("error", new ProcessError("The standard-input channel failed", {
|
|
1040
|
+
code: "protocol",
|
|
1041
|
+
cause
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
982
1044
|
#capBacklog() {
|
|
983
1045
|
const limit = this.#backlog * 2;
|
|
984
1046
|
while (this.#pending > limit && this.#queue.length > this.#head) {
|
|
@@ -993,6 +1055,7 @@ var Process = class {
|
|
|
993
1055
|
this.#capBacklog();
|
|
994
1056
|
this.#relieve();
|
|
995
1057
|
const confirmed = await stopChild(this.#child, this.#grace, PROCESS_CONFIRMATION);
|
|
1058
|
+
this.#settleWrites();
|
|
996
1059
|
this.#child.stdin.destroy();
|
|
997
1060
|
return confirmed;
|
|
998
1061
|
}
|
|
@@ -1257,9 +1320,13 @@ var Retention = class {
|
|
|
1257
1320
|
* rather than on process exit, so a descendant that inherited the child's stdio holds the run open
|
|
1258
1321
|
* after the child itself has gone. Give such a run a `timeout`. The child's `environment` merges over
|
|
1259
1322
|
* the parent unless the command is `isolated`, then `options.environment` on top, and `options.input`
|
|
1260
|
-
* overrides `command.input`.
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1323
|
+
* overrides `command.input`. A host fault while writing that input terminates the run by design and
|
|
1324
|
+
* marks its result failed; a strict rejection carries the host fault as its cause. This differs
|
|
1325
|
+
* from `Process` constructor input, whose package-initiated input phase stays quiet. Unless `strict`
|
|
1326
|
+
* is `false`, a failed run rejects with a {@link ProcessError} carrying the {@link ExecuteResult};
|
|
1327
|
+
* {@link createExecuteError} constructs every rejection outside the input-fault door. An invalid
|
|
1328
|
+
* option or command string rejects before the child is spawned, because an async function cannot
|
|
1329
|
+
* throw synchronously.
|
|
1263
1330
|
*
|
|
1264
1331
|
* @param command - The executable, arguments, and optional environment and input
|
|
1265
1332
|
* @param options - Working directory, timeout, grace, signal, capture limit, and failure delivery
|
|
@@ -1303,6 +1370,7 @@ async function execute(command, options) {
|
|
|
1303
1370
|
const terminate = new AbortController();
|
|
1304
1371
|
const cleanup = new AbortController();
|
|
1305
1372
|
const finish = new AbortController();
|
|
1373
|
+
const inputFailure = new AbortController();
|
|
1306
1374
|
const outChunks = [];
|
|
1307
1375
|
const errChunks = [];
|
|
1308
1376
|
const outRetention = new Retention();
|
|
@@ -1350,7 +1418,12 @@ async function execute(command, options) {
|
|
|
1350
1418
|
confirmTimer = setTimeout(() => finish.abort(), PROCESS_CONFIRMATION);
|
|
1351
1419
|
});
|
|
1352
1420
|
}, { once: true });
|
|
1353
|
-
|
|
1421
|
+
inputFailure.signal.addEventListener("abort", () => {
|
|
1422
|
+
if (finish.signal.aborted) return;
|
|
1423
|
+
cause = inputFailure.signal.reason;
|
|
1424
|
+
terminate.abort();
|
|
1425
|
+
}, { once: true });
|
|
1426
|
+
child.stdin.on("error", (error) => inputFailure.abort(error));
|
|
1354
1427
|
child.stdout.on("data", (chunk) => {
|
|
1355
1428
|
const retained = outRetention.retain(chunk, limit);
|
|
1356
1429
|
if (retained !== void 0) outChunks.push(retained);
|
|
@@ -1368,7 +1441,9 @@ async function execute(command, options) {
|
|
|
1368
1441
|
finish.abort();
|
|
1369
1442
|
});
|
|
1370
1443
|
child.once("close", () => finish.abort());
|
|
1371
|
-
if (text !== void 0) child.stdin.write(text)
|
|
1444
|
+
if (text !== void 0) child.stdin.write(text, (error) => {
|
|
1445
|
+
if (error !== void 0 && error !== null) inputFailure.abort(error);
|
|
1446
|
+
});
|
|
1372
1447
|
child.stdin.end();
|
|
1373
1448
|
if (timeout > 0) timeoutTimer = setTimeout(() => {
|
|
1374
1449
|
expired = true;
|
|
@@ -1388,7 +1463,19 @@ async function execute(command, options) {
|
|
|
1388
1463
|
}
|
|
1389
1464
|
}
|
|
1390
1465
|
const result = await settled.promise;
|
|
1391
|
-
if (result.failed && strict)
|
|
1466
|
+
if (result.failed && strict) {
|
|
1467
|
+
if (cause !== void 0 && cause === inputFailure.signal.reason) throw new ProcessError(`Command '${result.command}' failed while writing standard input`, {
|
|
1468
|
+
code: "input",
|
|
1469
|
+
context: {
|
|
1470
|
+
command: result.command,
|
|
1471
|
+
code: result.code,
|
|
1472
|
+
signal: result.signal
|
|
1473
|
+
},
|
|
1474
|
+
cause: inputFailure.signal.reason,
|
|
1475
|
+
result
|
|
1476
|
+
});
|
|
1477
|
+
throw createExecuteError(result, cause);
|
|
1478
|
+
}
|
|
1392
1479
|
return result;
|
|
1393
1480
|
}
|
|
1394
1481
|
//#endregion
|