@deepseek-ai/dsh-subprocess-local 0.1.5-rc.2 → 0.1.6-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +20 -2
- package/README.zh.md +27 -9
- package/lib/index.js +483 -88
- package/lib/output.js +181 -0
- package/lib/{runner-launch-COYGu0Dl.js → runner-launch-DGV26RBf.js} +108 -206
- package/lib/runner.js +21 -10
- package/lib/types/control-spawn.d.ts +19 -0
- package/lib/types/index.d.ts +5 -1
- package/lib/types/linux-execve.d.ts +1 -1
- package/lib/types/linux-scope.d.ts +13 -3
- package/lib/types/managed-owner.d.ts +7 -1
- package/lib/types/output.d.ts +85 -0
- package/lib/types/process-inspector.d.ts +3 -0
- package/lib/types/runner-launch.d.ts +4 -2
- package/lib/types/runner-protocol.d.ts +2 -0
- package/lib/types/shell-activity.d.ts +37 -0
- package/lib/types/spawn-runner.d.ts +1 -1
- package/lib/types/spawn.d.ts +1 -74
- package/lib/types/terminal.d.ts +13 -2
- package/package.json +15 -9
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OutputCollector, prepareManagedProcessBinding } from "./output.js";
|
|
2
|
+
import { accessSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
2
4
|
import { basename, dirname, extname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
|
|
3
6
|
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
4
7
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { randomBytes } from "node:crypto";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
8
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
9
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
9
|
-
import
|
|
10
|
+
import { SUBPROCESS_CONTROL_ENV, SUBPROCESS_CONTROL_FD } from "@deepseek-ai/dsh-subprocess/control";
|
|
10
11
|
import { getSystemErrorMessage, getSystemErrorName, inspect } from "node:util";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
//#region lib/types/managed-owner.js
|
|
@@ -38,6 +39,30 @@ async function waitWithAbort(pending, signal) {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
//#endregion
|
|
42
|
+
//#region lib/types/control-spawn.js
|
|
43
|
+
/** Parent-side setup for one explicitly requested inherited control pipe. */
|
|
44
|
+
/**
|
|
45
|
+
* Read the optional extra pipe from Node's stdio tuple.
|
|
46
|
+
* @param child - child whose requested extra pipe was allocated by Node.
|
|
47
|
+
* @param control - requested transport, or undefined when absent.
|
|
48
|
+
* @returns the parent duplex endpoint, absent when not requested or native startup failed.
|
|
49
|
+
*/
|
|
50
|
+
function controlPipe(child, control) {
|
|
51
|
+
const streams = child.stdio;
|
|
52
|
+
return control === "pipe" ? streams[SUBPROCESS_CONTROL_FD] : void 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Stamp the private marker on a fresh child environment after rejecting a caller override.
|
|
56
|
+
* @param env - newly materialized child environment, owned by the caller.
|
|
57
|
+
* @param control - requested control transport, or undefined when absent.
|
|
58
|
+
* @returns the same environment with the provider-owned launch marker when requested.
|
|
59
|
+
*/
|
|
60
|
+
function controlEnvironment(env, control) {
|
|
61
|
+
for (const [key, value] of Object.entries(env)) if (key.toUpperCase() === SUBPROCESS_CONTROL_ENV && value !== void 0) throw new Error(`${SUBPROCESS_CONTROL_ENV} is reserved for subprocess control-channel setup`);
|
|
62
|
+
if (control === "pipe") Object.assign(env, { [SUBPROCESS_CONTROL_ENV]: "pipe" });
|
|
63
|
+
return env;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
41
66
|
//#region lib/types/windows-inspector.js
|
|
42
67
|
/**
|
|
43
68
|
* Windows process-table operations for terminal readiness, signalling, and
|
|
@@ -49,6 +74,7 @@ async function waitWithAbort(pending, signal) {
|
|
|
49
74
|
* an injectable internals boundary so suites can pin it on any host.
|
|
50
75
|
* @module dsh-subprocess-local/windows-inspector
|
|
51
76
|
*/
|
|
77
|
+
const requireKoffi$1 = createLazyRequire("koffi", import.meta.url);
|
|
52
78
|
/**
|
|
53
79
|
* Walk a process table from one root in children-first order, retaining only
|
|
54
80
|
* members whose start identity is readable (unreadable members are detector
|
|
@@ -150,7 +176,6 @@ function isInvalidHandle(value) {
|
|
|
150
176
|
const asBigInt = value;
|
|
151
177
|
return asBigInt === 0n || asBigInt === 18446744073709551615n || asBigInt === -1n;
|
|
152
178
|
}
|
|
153
|
-
const PVOID = koffi.pointer("void");
|
|
154
179
|
/**
|
|
155
180
|
* Resolve the koffi Win32 struct types once. Registration is lazy and cached
|
|
156
181
|
* because koffi's type registry is global per process: test runners that
|
|
@@ -159,6 +184,8 @@ const PVOID = koffi.pointer("void");
|
|
|
159
184
|
*/
|
|
160
185
|
function win32Structs() {
|
|
161
186
|
if (cachedStructs !== void 0) return cachedStructs;
|
|
187
|
+
const koffi = requireKoffi$1();
|
|
188
|
+
const PVOID = koffi.pointer("void");
|
|
162
189
|
const PROCESSENTRY32W = koffi.struct("PROCESSENTRY32W", {
|
|
163
190
|
dwSize: "uint32",
|
|
164
191
|
cntUsage: "uint32",
|
|
@@ -179,6 +206,7 @@ function win32Structs() {
|
|
|
179
206
|
if (PROCESSENTRY32W.size !== 568) throw new Error(`PROCESSENTRY32W layout mismatch: koffi computed ${PROCESSENTRY32W.size}, Windows headers say 568`);
|
|
180
207
|
/* v8 ignore stop */
|
|
181
208
|
cachedStructs = {
|
|
209
|
+
PVOID,
|
|
182
210
|
PROCESSENTRY32W,
|
|
183
211
|
FILETIME
|
|
184
212
|
};
|
|
@@ -195,7 +223,8 @@ let cachedBindings;
|
|
|
195
223
|
*/
|
|
196
224
|
function win32Bindings() {
|
|
197
225
|
if (cachedBindings !== void 0) return cachedBindings;
|
|
198
|
-
const
|
|
226
|
+
const koffi = requireKoffi$1();
|
|
227
|
+
const { PVOID, PROCESSENTRY32W, FILETIME } = win32Structs();
|
|
199
228
|
const kernel32 = koffi.load("kernel32.dll");
|
|
200
229
|
const bind = (name, result, args) => kernel32.func("__stdcall", name, result, args);
|
|
201
230
|
cachedBindings = {
|
|
@@ -227,10 +256,11 @@ function win32Bindings() {
|
|
|
227
256
|
* @returns the branded allocation pointer.
|
|
228
257
|
*/
|
|
229
258
|
function allocNative(type, count) {
|
|
230
|
-
return
|
|
259
|
+
return requireKoffi$1().alloc(type, count);
|
|
231
260
|
}
|
|
232
261
|
/** Enumerate the current process table through Toolhelp32. */
|
|
233
262
|
function snapshotWindowsProcesses(bindings) {
|
|
263
|
+
const koffi = requireKoffi$1();
|
|
234
264
|
const { PROCESSENTRY32W } = win32Structs();
|
|
235
265
|
const snapshot = bindings.createToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
236
266
|
/* v8 ignore next -- an invalid snapshot for the process flag is not producible through the public API;
|
|
@@ -256,6 +286,7 @@ function snapshotWindowsProcesses(bindings) {
|
|
|
256
286
|
}
|
|
257
287
|
/** Read one process's creation identity and current wait state. */
|
|
258
288
|
function windowsProcessState(bindings, pid) {
|
|
289
|
+
const koffi = requireKoffi$1();
|
|
259
290
|
const { FILETIME } = win32Structs();
|
|
260
291
|
const handle = bindings.openProcess(1052672, 0, pid);
|
|
261
292
|
if (isInvalidHandle(handle)) return void 0;
|
|
@@ -392,7 +423,7 @@ function numericEntries(internals, path) {
|
|
|
392
423
|
try {
|
|
393
424
|
return internals.readDir(path).filter((entry) => /^\d+$/.test(entry)).map(Number);
|
|
394
425
|
} catch (_unreadableProcDirectory) {
|
|
395
|
-
return
|
|
426
|
+
return;
|
|
396
427
|
}
|
|
397
428
|
}
|
|
398
429
|
function readSyscall(internals, pid, tid) {
|
|
@@ -491,9 +522,11 @@ function quiescent(state) {
|
|
|
491
522
|
}
|
|
492
523
|
var PosixProcessSnapshot = class {
|
|
493
524
|
rows;
|
|
525
|
+
complete;
|
|
494
526
|
byPid;
|
|
495
|
-
constructor(rows) {
|
|
527
|
+
constructor(rows, complete) {
|
|
496
528
|
this.rows = rows;
|
|
529
|
+
this.complete = complete;
|
|
497
530
|
this.byPid = new Map(rows.map((row) => [row.pid, row]));
|
|
498
531
|
}
|
|
499
532
|
tree(rootPid) {
|
|
@@ -550,10 +583,10 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
|
|
|
550
583
|
if (shell === void 0) return false;
|
|
551
584
|
const terminalDevice = readLinuxTerminalDevice(this.internals, shellPid, shell.ttyDevice);
|
|
552
585
|
if (terminalDevice === void 0) return false;
|
|
553
|
-
for (const pid of numericEntries(this.internals, "/proc")) {
|
|
586
|
+
for (const pid of numericEntries(this.internals, "/proc") ?? []) {
|
|
554
587
|
const process = readLinuxStat(this.internals, pid);
|
|
555
588
|
if (process?.pgrp !== pgid) continue;
|
|
556
|
-
for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
|
|
589
|
+
for (const tid of numericEntries(this.internals, `/proc/${pid}/task`) ?? []) {
|
|
557
590
|
const syscall = readSyscall(this.internals, pid, tid);
|
|
558
591
|
if (syscall !== void 0 && syscallWaitsOnStdin(this.internals, pid, tid, syscall, tables) && readLinuxTerminalDevice(this.internals, pid, process.ttyDevice, tid) === terminalDevice) return true;
|
|
559
592
|
}
|
|
@@ -565,8 +598,12 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
|
|
|
565
598
|
return stat?.started === identity.started && !quiescent(stat.state);
|
|
566
599
|
}
|
|
567
600
|
snapshot() {
|
|
568
|
-
|
|
601
|
+
const pids = numericEntries(this.internals, "/proc");
|
|
602
|
+
if (pids === void 0) throw new Error("Cannot inspect processes: /proc directory is unreadable");
|
|
603
|
+
let complete = true;
|
|
604
|
+
return new PosixProcessSnapshot(pids.flatMap((pid) => {
|
|
569
605
|
const stat = readLinuxStat(this.internals, pid);
|
|
606
|
+
if (stat === void 0) complete = false;
|
|
570
607
|
return stat === void 0 ? [] : [{
|
|
571
608
|
pid,
|
|
572
609
|
parentPid: stat.parentPid,
|
|
@@ -574,21 +611,28 @@ var LinuxProcessInspector = class extends PosixProcessInspector {
|
|
|
574
611
|
session: stat.session,
|
|
575
612
|
state: stat.state
|
|
576
613
|
}];
|
|
577
|
-
}));
|
|
614
|
+
}), complete);
|
|
578
615
|
}
|
|
579
616
|
};
|
|
580
617
|
function macProcessTable(internals) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
618
|
+
let complete = true;
|
|
619
|
+
return {
|
|
620
|
+
rows: internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
|
|
621
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
|
|
622
|
+
if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) {
|
|
623
|
+
if (line.trim().length > 0) complete = false;
|
|
624
|
+
return [];
|
|
625
|
+
}
|
|
626
|
+
return [{
|
|
627
|
+
pid: Number(match[1]),
|
|
628
|
+
parentPid: Number(match[2]),
|
|
629
|
+
started: match[3],
|
|
630
|
+
session: void 0,
|
|
631
|
+
state: void 0
|
|
632
|
+
}];
|
|
633
|
+
}),
|
|
634
|
+
complete
|
|
635
|
+
};
|
|
592
636
|
}
|
|
593
637
|
var MacProcessInspector = class extends PosixProcessInspector {
|
|
594
638
|
foregroundPgid(shellPid) {
|
|
@@ -608,10 +652,11 @@ var MacProcessInspector = class extends PosixProcessInspector {
|
|
|
608
652
|
return false;
|
|
609
653
|
}
|
|
610
654
|
isAlive(identity) {
|
|
611
|
-
return macProcessTable(this.internals).some((entry) => entry.pid === identity.pid && entry.started === identity.started);
|
|
655
|
+
return macProcessTable(this.internals).rows.some((entry) => entry.pid === identity.pid && entry.started === identity.started);
|
|
612
656
|
}
|
|
613
657
|
snapshot() {
|
|
614
|
-
|
|
658
|
+
const table = macProcessTable(this.internals);
|
|
659
|
+
return new PosixProcessSnapshot(table.rows, table.complete);
|
|
615
660
|
}
|
|
616
661
|
};
|
|
617
662
|
/**
|
|
@@ -669,169 +714,6 @@ function childEnv(extra) {
|
|
|
669
714
|
function sleepTick() {
|
|
670
715
|
return setTimeout$1(15);
|
|
671
716
|
}
|
|
672
|
-
let spillCounter = 0;
|
|
673
|
-
let defaultSpillDir;
|
|
674
|
-
/**
|
|
675
|
-
* The default spill location: a private (0700) per-process directory under
|
|
676
|
-
* the OS tmpdir, created lazily. Predictable world-readable paths would let
|
|
677
|
-
* other local users read command output or pre-create symlinks. At a
|
|
678
|
-
* JavaScript-observable process exit the directory is removed only when it
|
|
679
|
-
* holds no completed spill file (spill files are retained as full-output
|
|
680
|
-
* recovery artifacts until an external cleanup).
|
|
681
|
-
*/
|
|
682
|
-
function privateSpillDir() {
|
|
683
|
-
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
|
|
684
|
-
return defaultSpillDir;
|
|
685
|
-
}
|
|
686
|
-
/* v8 ignore next 4 -- exit listeners run after the coverage dump; removal is verified by the CI /tmp residue measurement. */
|
|
687
|
-
process.once("exit", () => {
|
|
688
|
-
if (defaultSpillDir === void 0) return;
|
|
689
|
-
try {
|
|
690
|
-
rmdirSync(defaultSpillDir);
|
|
691
|
-
} catch {}
|
|
692
|
-
});
|
|
693
|
-
/**
|
|
694
|
-
* Prepare fallible output storage before starting a managed native process.
|
|
695
|
-
* @param internals - optional caller-owned spill directory.
|
|
696
|
-
* @returns binding inputs whose spill directory is ready for use.
|
|
697
|
-
*/
|
|
698
|
-
function prepareManagedProcessBinding(internals = {}) {
|
|
699
|
-
return { spillDir: internals.spillDir ?? privateSpillDir() };
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Collects one stream with a bounded in-memory tail. With a spill cap, on
|
|
703
|
-
* first overflow a spill file is created and every chunk (including those
|
|
704
|
-
* already collected) is appended there while the full stream remains within
|
|
705
|
-
* the cap; without one, only the in-memory tail is ever retained (the
|
|
706
|
-
* diagnostic-tail shape — a language server's stderr).
|
|
707
|
-
*
|
|
708
|
-
* Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
|
|
709
|
-
* end of command output; the spill file covers the head.
|
|
710
|
-
*/
|
|
711
|
-
var OutputCollector = class {
|
|
712
|
-
maxBytes;
|
|
713
|
-
maxSpillBytes;
|
|
714
|
-
label;
|
|
715
|
-
spillDir;
|
|
716
|
-
chunks = [];
|
|
717
|
-
bytes = 0;
|
|
718
|
-
dropped = false;
|
|
719
|
-
spillFd;
|
|
720
|
-
spillFile;
|
|
721
|
-
spillDisabled;
|
|
722
|
-
/** Total bytes ever pushed (not just retained). */
|
|
723
|
-
total = 0;
|
|
724
|
-
constructor(maxBytes, maxSpillBytes, label, spillDir) {
|
|
725
|
-
this.maxBytes = maxBytes;
|
|
726
|
-
this.maxSpillBytes = maxSpillBytes;
|
|
727
|
-
this.label = label;
|
|
728
|
-
this.spillDir = spillDir;
|
|
729
|
-
this.spillDisabled = maxSpillBytes === void 0;
|
|
730
|
-
}
|
|
731
|
-
/**
|
|
732
|
-
* Ingest one stream chunk, counting it toward the whole-stream total. On
|
|
733
|
-
* first overflow of the in-memory cap a spill file is opened (when spilling
|
|
734
|
-
* is enabled) and every chunk (already-collected ones included) is appended
|
|
735
|
-
* there from then on; the in-memory tail then drops whole chunks from its
|
|
736
|
-
* head (or the head of a single over-cap chunk) until it fits the cap again.
|
|
737
|
-
* @param chunk - the raw bytes from one stream 'data' event.
|
|
738
|
-
*/
|
|
739
|
-
push(chunk) {
|
|
740
|
-
this.total += chunk.length;
|
|
741
|
-
const overflows = this.bytes + chunk.length > this.maxBytes;
|
|
742
|
-
if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(chunk);
|
|
743
|
-
this.chunks.push(chunk);
|
|
744
|
-
this.bytes += chunk.length;
|
|
745
|
-
while (this.bytes > this.maxBytes) {
|
|
746
|
-
const head = this.chunks[0];
|
|
747
|
-
const excess = this.bytes - this.maxBytes;
|
|
748
|
-
if (head.length <= excess) {
|
|
749
|
-
this.chunks.shift();
|
|
750
|
-
this.bytes -= head.length;
|
|
751
|
-
} else {
|
|
752
|
-
this.chunks[0] = head.subarray(excess);
|
|
753
|
-
this.bytes -= excess;
|
|
754
|
-
}
|
|
755
|
-
this.dropped = true;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
/** Open the spill file lazily and append `chunk` (and any prior chunks once). */
|
|
759
|
-
spillAll(chunk) {
|
|
760
|
-
if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
|
|
761
|
-
this.discardSpill();
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
if (this.spillFd === void 0) {
|
|
765
|
-
this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
|
|
766
|
-
this.spillFd = openSync(this.spillFile, "wx", 384);
|
|
767
|
-
for (const prior of this.chunks) writeSync(this.spillFd, prior);
|
|
768
|
-
}
|
|
769
|
-
writeSync(this.spillFd, chunk);
|
|
770
|
-
}
|
|
771
|
-
/** Stop spilling and remove the file once it can no longer hold the complete stream. */
|
|
772
|
-
discardSpill() {
|
|
773
|
-
const fd = this.spillFd;
|
|
774
|
-
const file = this.spillFile;
|
|
775
|
-
this.spillFd = void 0;
|
|
776
|
-
this.spillFile = void 0;
|
|
777
|
-
this.spillDisabled = true;
|
|
778
|
-
if (fd !== void 0) try {
|
|
779
|
-
closeSync(fd);
|
|
780
|
-
} catch {
|
|
781
|
-
this.spillFd = fd;
|
|
782
|
-
}
|
|
783
|
-
if (file !== void 0) try {
|
|
784
|
-
unlinkSync(file);
|
|
785
|
-
} catch {}
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Incremental read in whole-stream byte coordinates: returns everything
|
|
789
|
-
* pushed since `fromByte`. When `fromByte` has already slid out of the
|
|
790
|
-
* in-memory tail window, the read is `lossy` — it returns the whole
|
|
791
|
-
* retained tail and the gap is only recoverable from the spill file.
|
|
792
|
-
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
|
|
793
|
-
* @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
|
|
794
|
-
*/
|
|
795
|
-
readFrom(fromByte) {
|
|
796
|
-
const windowStart = this.total - this.bytes;
|
|
797
|
-
const buffer = Buffer.concat(this.chunks);
|
|
798
|
-
const lossy = fromByte < windowStart;
|
|
799
|
-
return {
|
|
800
|
-
text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
|
|
801
|
-
nextOffset: this.total,
|
|
802
|
-
lossy,
|
|
803
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
/**
|
|
807
|
-
* Close the spill file once the stream has ended. A failed close (delayed
|
|
808
|
-
* writeback fault) stops advertising the spill path — the file may be
|
|
809
|
-
* missing its tail — while every in-memory read keeps working. Idempotent;
|
|
810
|
-
* the spawn path seals both collectors at settlement so reads after exit
|
|
811
|
-
* never point at a still-open file.
|
|
812
|
-
*/
|
|
813
|
-
seal() {
|
|
814
|
-
if (this.spillFd === void 0) return;
|
|
815
|
-
try {
|
|
816
|
-
closeSync(this.spillFd);
|
|
817
|
-
} catch {
|
|
818
|
-
this.spillFile = void 0;
|
|
819
|
-
}
|
|
820
|
-
this.spillFd = void 0;
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Seal the spill file and return the final output.
|
|
824
|
-
* @returns the final collected output: tail text, truncation flag, and the spill path when intact.
|
|
825
|
-
*/
|
|
826
|
-
finalize() {
|
|
827
|
-
this.seal();
|
|
828
|
-
return {
|
|
829
|
-
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
830
|
-
truncated: this.dropped,
|
|
831
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
832
|
-
};
|
|
833
|
-
}
|
|
834
|
-
};
|
|
835
717
|
/**
|
|
836
718
|
* Terminate one Windows process tree with `taskkill /T /F`. Contained like
|
|
837
719
|
* POSIX group signalling — delivery races tree exit, so an absent tree, a
|
|
@@ -1113,6 +995,7 @@ function bindManagedProcess(spec, launch, internals = {}) {
|
|
|
1113
995
|
stdin: stdinMode === "pipe" ? stdin ?? void 0 : void 0,
|
|
1114
996
|
stdout: outMode === "pipe" ? stdout ?? void 0 : void 0,
|
|
1115
997
|
stderr: errMode === "pipe" ? stderr ?? void 0 : void 0,
|
|
998
|
+
control: launch.control,
|
|
1116
999
|
/* v8 ignore stop */
|
|
1117
1000
|
collected: {
|
|
1118
1001
|
...stdoutCollector !== void 0 ? { stdout: stdoutCollector } : {},
|
|
@@ -1134,14 +1017,19 @@ function spawnSubprocess(spec, internals = {}) {
|
|
|
1134
1017
|
const binding = prepareManagedProcessBinding(internals);
|
|
1135
1018
|
const platform = internals.platform ?? process.platform;
|
|
1136
1019
|
const [program, ...args] = spec.argv;
|
|
1020
|
+
const stdio = [
|
|
1021
|
+
spec.stdio.stdin === "ignore" ? "ignore" : "pipe",
|
|
1022
|
+
spec.stdio.stdout === "inherit" ? "inherit" : "pipe",
|
|
1023
|
+
spec.stdio.stderr === "inherit" ? "inherit" : "pipe"
|
|
1024
|
+
];
|
|
1025
|
+
if (spec.stdio.control === "pipe") {
|
|
1026
|
+
while (stdio.length < SUBPROCESS_CONTROL_FD) stdio.push("ignore");
|
|
1027
|
+
stdio.push("overlapped");
|
|
1028
|
+
}
|
|
1137
1029
|
const child = (internals.spawn ?? spawn)(program, args, {
|
|
1138
1030
|
cwd: spec.cwd,
|
|
1139
|
-
env: childEnv(spec.env),
|
|
1140
|
-
stdio
|
|
1141
|
-
spec.stdio.stdin === "ignore" ? "ignore" : "pipe",
|
|
1142
|
-
spec.stdio.stdout === "inherit" ? "inherit" : "pipe",
|
|
1143
|
-
spec.stdio.stderr === "inherit" ? "inherit" : "pipe"
|
|
1144
|
-
],
|
|
1031
|
+
env: controlEnvironment(childEnv(spec.env), spec.stdio.control),
|
|
1032
|
+
stdio,
|
|
1145
1033
|
detached: platform !== "win32",
|
|
1146
1034
|
windowsHide: platform === "win32"
|
|
1147
1035
|
});
|
|
@@ -1152,6 +1040,7 @@ function spawnSubprocess(spec, internals = {}) {
|
|
|
1152
1040
|
stdin: child.stdin,
|
|
1153
1041
|
stdout: child.stdout,
|
|
1154
1042
|
stderr: child.stderr,
|
|
1043
|
+
control: controlPipe(child, spec.stdio.control),
|
|
1155
1044
|
direct,
|
|
1156
1045
|
owner
|
|
1157
1046
|
}, binding);
|
|
@@ -1159,6 +1048,7 @@ function spawnSubprocess(spec, internals = {}) {
|
|
|
1159
1048
|
//#endregion
|
|
1160
1049
|
//#region lib/types/linux-execve.js
|
|
1161
1050
|
/** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
|
|
1051
|
+
const requireKoffi = createLazyRequire("koffi", import.meta.url);
|
|
1162
1052
|
const STANDARD_FILE_DESCRIPTORS = [
|
|
1163
1053
|
0,
|
|
1164
1054
|
1,
|
|
@@ -1186,11 +1076,13 @@ function systemError(errno, syscall, path) {
|
|
|
1186
1076
|
*/
|
|
1187
1077
|
function loadLinuxExecve() {
|
|
1188
1078
|
if (cachedExecve !== void 0) return cachedExecve;
|
|
1079
|
+
const koffi = requireKoffi();
|
|
1189
1080
|
const libc = koffi.load(null);
|
|
1190
1081
|
const nativeExecve = libc.func("int execve(const char *pathname, const char **argv, const char **envp)");
|
|
1191
1082
|
const nativeFcntl = libc.func("int fcntl(int fd, int cmd, int arg)");
|
|
1192
|
-
cachedExecve = (file, argv, env) => {
|
|
1193
|
-
|
|
1083
|
+
cachedExecve = (file, argv, env, control) => {
|
|
1084
|
+
const descriptors = control === "pipe" ? [...STANDARD_FILE_DESCRIPTORS, SUBPROCESS_CONTROL_FD] : STANDARD_FILE_DESCRIPTORS;
|
|
1085
|
+
for (const fd of descriptors) {
|
|
1194
1086
|
const flags = nativeFcntl(fd, F_GETFD, 0);
|
|
1195
1087
|
if (flags === -1) throw systemError(koffi.errno(), "fcntl");
|
|
1196
1088
|
if ((flags & FD_CLOEXEC) === 0) continue;
|
|
@@ -1277,10 +1169,11 @@ function consumeLinuxLaunchRequest(requestPath) {
|
|
|
1277
1169
|
const text = readFileSync(requestPath, "utf8");
|
|
1278
1170
|
unlinkSync(requestPath);
|
|
1279
1171
|
const value = JSON.parse(text);
|
|
1280
|
-
if (!isRecord(value) || !hasExactKeys(value, ["cwd", "env"]) || typeof value.cwd !== "string" || !isStringRecord(value.env)) throw new Error("subprocess runner received an invalid Linux launch request");
|
|
1172
|
+
if (!isRecord(value) || !hasExactKeys(value, ["cwd", "env"], ["control"]) || typeof value.cwd !== "string" || !isStringRecord(value.env) || value.control !== void 0 && value.control !== "pipe") throw new Error("subprocess runner received an invalid Linux launch request");
|
|
1281
1173
|
return {
|
|
1282
1174
|
cwd: value.cwd,
|
|
1283
|
-
env: value.env
|
|
1175
|
+
env: value.env,
|
|
1176
|
+
...value.control === "pipe" ? { control: "pipe" } : {}
|
|
1284
1177
|
};
|
|
1285
1178
|
}
|
|
1286
1179
|
/**
|
|
@@ -1315,11 +1208,12 @@ function parseWindowsStartRequest(value) {
|
|
|
1315
1208
|
"type",
|
|
1316
1209
|
"cwd",
|
|
1317
1210
|
"env"
|
|
1318
|
-
]) || value.type !== "start" || typeof value.cwd !== "string" || !isStringRecord(value.env)) throw new Error("subprocess runner received an invalid Windows start request");
|
|
1211
|
+
], ["control"]) || value.type !== "start" || typeof value.cwd !== "string" || !isStringRecord(value.env) || value.control !== void 0 && value.control !== "pipe") throw new Error("subprocess runner received an invalid Windows start request");
|
|
1319
1212
|
return {
|
|
1320
1213
|
type: "start",
|
|
1321
1214
|
cwd: value.cwd,
|
|
1322
|
-
env: value.env
|
|
1215
|
+
env: value.env,
|
|
1216
|
+
...value.control === "pipe" ? { control: "pipe" } : {}
|
|
1323
1217
|
};
|
|
1324
1218
|
}
|
|
1325
1219
|
/**
|
|
@@ -1477,7 +1371,7 @@ function parseRunnerTargetArgv(argv) {
|
|
|
1477
1371
|
}
|
|
1478
1372
|
/**
|
|
1479
1373
|
* Build direct Linux target stdio, or isolated Windows runner stdio with IPC
|
|
1480
|
-
* on fd 3 and target carriers on fd 4 through fd 6.
|
|
1374
|
+
* on fd 3 and target carriers on fd 4 through fd 6; optional control uses fd 7.
|
|
1481
1375
|
* @param spec - ordinary subprocess request whose stdio modes are preserved.
|
|
1482
1376
|
* @param ipc - whether to isolate the runner and add its private Node IPC descriptor.
|
|
1483
1377
|
* @param stdinCarrier - runner fd 4 carrier; Windows ignore passes an opened null-device fd.
|
|
@@ -1489,8 +1383,14 @@ function runnerStdio(spec, ipc, stdinCarrier = "pipe") {
|
|
|
1489
1383
|
spec.stdio.stdout === "inherit" ? "inherit" : "pipe",
|
|
1490
1384
|
spec.stdio.stderr === "inherit" ? "inherit" : "pipe"
|
|
1491
1385
|
];
|
|
1492
|
-
if (!ipc)
|
|
1493
|
-
|
|
1386
|
+
if (!ipc) {
|
|
1387
|
+
if (spec.stdio.control === "pipe") {
|
|
1388
|
+
while (targetStdio.length < SUBPROCESS_CONTROL_FD) targetStdio.push("ignore");
|
|
1389
|
+
targetStdio.push("overlapped");
|
|
1390
|
+
}
|
|
1391
|
+
return targetStdio;
|
|
1392
|
+
}
|
|
1393
|
+
const runner = [
|
|
1494
1394
|
"ignore",
|
|
1495
1395
|
"ignore",
|
|
1496
1396
|
"ignore",
|
|
@@ -1499,6 +1399,8 @@ function runnerStdio(spec, ipc, stdinCarrier = "pipe") {
|
|
|
1499
1399
|
spec.stdio.stdout === "inherit" ? 1 : "pipe",
|
|
1500
1400
|
spec.stdio.stderr === "inherit" ? 2 : "pipe"
|
|
1501
1401
|
];
|
|
1402
|
+
if (spec.stdio.control === "pipe") runner.push("overlapped");
|
|
1403
|
+
return runner;
|
|
1502
1404
|
}
|
|
1503
1405
|
function windowsEnvironmentValue(env, name) {
|
|
1504
1406
|
for (const key of Object.keys(env).sort()) if (key.toUpperCase() === name) return env[key];
|
|
@@ -1617,7 +1519,7 @@ function targetEnvironment(spec) {
|
|
|
1617
1519
|
validateNoNullByte(`options.env['${key}']`, key);
|
|
1618
1520
|
validateNoNullByte(`options.env['${key}']`, value);
|
|
1619
1521
|
}
|
|
1620
|
-
return env;
|
|
1522
|
+
return controlEnvironment(env, spec.stdio?.control);
|
|
1621
1523
|
}
|
|
1622
1524
|
//#endregion
|
|
1623
|
-
export { bindManagedProcess as C,
|
|
1525
|
+
export { bindManagedProcess as C, createProcessInspector as D, validateSubprocessSpec as E, controlPipe as O, loadLinuxExecve as S, spawnSubprocess as T, parseWindowsRunnerResult as _, resolveWindowsExecutable as a, serializeRunnerError as b, runnerStdio as c, cleanupLinuxLaunchFiles as d, consumeLinuxLaunchRequest as f, linuxLaunchFilesFromLocator as g, isWindowsTerminateRequest as h, parseRunnerTargetArgv as i, spawnRunnerInvocation as l, deserializeRunnerError as m, WINDOWS_RUNNER_SELECTION as n, runnerEnvironment as o, createLinuxLaunchFiles as p, consumeRunnerSelection as r, runnerInvocationAvailable as s, SUBPROCESS_RUNNER_ENV as t, targetEnvironment as u, parseWindowsStartRequest as v, childEnv as w, writeLinuxStartupError as x, readLinuxStartupError as y };
|
package/lib/runner.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { S as loadLinuxExecve, a as resolveWindowsExecutable, b as serializeRunnerError, f as consumeLinuxLaunchRequest, g as linuxLaunchFilesFromLocator, h as isWindowsTerminateRequest, i as parseRunnerTargetArgv, r as consumeRunnerSelection, t as SUBPROCESS_RUNNER_ENV, v as parseWindowsStartRequest, x as writeLinuxStartupError } from "./runner-launch-
|
|
1
|
+
import { S as loadLinuxExecve, a as resolveWindowsExecutable, b as serializeRunnerError, f as consumeLinuxLaunchRequest, g as linuxLaunchFilesFromLocator, h as isWindowsTerminateRequest, i as parseRunnerTargetArgv, r as consumeRunnerSelection, t as SUBPROCESS_RUNNER_ENV, v as parseWindowsStartRequest, x as writeLinuxStartupError } from "./runner-launch-DGV26RBf.js";
|
|
2
2
|
import { closeSync } from "node:fs";
|
|
3
|
+
import { SUBPROCESS_CONTROL_FD } from "@deepseek-ai/dsh-subprocess/control";
|
|
3
4
|
import { Win32Error, closeHandleChecked, isJobEmpty, loadWin32ProcessBindings, pollProcessExit, spawnCurrentTokenJobProcess, terminateJob } from "@deepseek-ai/dsh-win32-process";
|
|
4
5
|
//#region lib/types/spawn-runner.js
|
|
5
6
|
/** One-shot Linux exec bootstrap and Windows Job-owning subprocess runner. */
|
|
6
7
|
const defaultInternals = {
|
|
7
8
|
/* v8 ignore next -- source/built/packaged subprocess smoke executes this only in a replaceable child process. */
|
|
8
|
-
execve: (file, argv, env) => loadLinuxExecve()(file, argv, env),
|
|
9
|
+
execve: (file, argv, env, control) => loadLinuxExecve()(file, argv, env, control),
|
|
9
10
|
loadWin32ProcessBindings,
|
|
10
11
|
spawnCurrentTokenJobProcess,
|
|
11
12
|
closeFileDescriptor: closeSync,
|
|
@@ -58,28 +59,32 @@ function linuxPathNotFoundError(program) {
|
|
|
58
59
|
spawnargs: []
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
|
-
function execLinuxFile(file, argv, env, internals) {
|
|
62
|
+
function execLinuxFile(file, argv, env, internals, control) {
|
|
62
63
|
try {
|
|
63
|
-
return internals.execve(file, argv, env);
|
|
64
|
+
return control === void 0 ? internals.execve(file, argv, env) : internals.execve(file, argv, env, control);
|
|
64
65
|
} catch (error) {
|
|
65
66
|
if (error.code !== "ENOEXEC") throw error;
|
|
66
|
-
return internals.execve("/bin/sh", [
|
|
67
|
+
return control === void 0 ? internals.execve("/bin/sh", [
|
|
67
68
|
"/bin/sh",
|
|
68
69
|
file,
|
|
69
70
|
...argv.slice(1)
|
|
70
|
-
], env)
|
|
71
|
+
], env) : internals.execve("/bin/sh", [
|
|
72
|
+
"/bin/sh",
|
|
73
|
+
file,
|
|
74
|
+
...argv.slice(1)
|
|
75
|
+
], env, control);
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
function execLinuxTarget(request, argv, internals) {
|
|
74
79
|
const program = argv[0];
|
|
75
|
-
if (program.includes("/")) return execLinuxFile(program, argv, request.env, internals);
|
|
80
|
+
if (program.includes("/")) return execLinuxFile(program, argv, request.env, internals, request.control);
|
|
76
81
|
const path = request.env.PATH ?? "/usr/bin:/bin";
|
|
77
82
|
let permissionFailure;
|
|
78
83
|
for (const directory of path.split(":")) {
|
|
79
84
|
const root = directory.startsWith("/") ? directory : `${request.cwd}${request.cwd.endsWith("/") ? "" : "/"}${directory}`;
|
|
80
85
|
const candidate = `${root}${root.endsWith("/") ? "" : "/"}${program}`;
|
|
81
86
|
try {
|
|
82
|
-
return execLinuxFile(candidate, argv, request.env, internals);
|
|
87
|
+
return execLinuxFile(candidate, argv, request.env, internals, request.control);
|
|
83
88
|
} catch (error) {
|
|
84
89
|
const code = error.code;
|
|
85
90
|
if (code === "EACCES") {
|
|
@@ -227,12 +232,18 @@ var WindowsJobRunner = class {
|
|
|
227
232
|
stdio: {
|
|
228
233
|
stdin: 4,
|
|
229
234
|
stdout: 5,
|
|
230
|
-
stderr: 6
|
|
235
|
+
stderr: 6,
|
|
236
|
+
...request.control === "pipe" ? { control: SUBPROCESS_CONTROL_FD } : {}
|
|
231
237
|
}
|
|
232
238
|
});
|
|
233
239
|
this.processHandle = spawned.process;
|
|
234
240
|
this.jobHandle = spawned.job;
|
|
235
|
-
for (const fileDescriptor of [
|
|
241
|
+
for (const fileDescriptor of request.control === "pipe" ? [
|
|
242
|
+
4,
|
|
243
|
+
5,
|
|
244
|
+
6,
|
|
245
|
+
SUBPROCESS_CONTROL_FD
|
|
246
|
+
] : [
|
|
236
247
|
4,
|
|
237
248
|
5,
|
|
238
249
|
6
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Parent-side setup for one explicitly requested inherited control pipe. */
|
|
2
|
+
import type { Duplex, Readable, Writable } from 'node:stream';
|
|
3
|
+
/**
|
|
4
|
+
* Read the optional extra pipe from Node's stdio tuple.
|
|
5
|
+
* @param child - child whose requested extra pipe was allocated by Node.
|
|
6
|
+
* @param control - requested transport, or undefined when absent.
|
|
7
|
+
* @returns the parent duplex endpoint, absent when not requested or native startup failed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function controlPipe(child: {
|
|
10
|
+
readonly stdio: ReadonlyArray<Readable | Writable | null | undefined>;
|
|
11
|
+
}, control?: 'pipe'): Duplex | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Stamp the private marker on a fresh child environment after rejecting a caller override.
|
|
14
|
+
* @param env - newly materialized child environment, owned by the caller.
|
|
15
|
+
* @param control - requested control transport, or undefined when absent.
|
|
16
|
+
* @returns the same environment with the provider-owned launch marker when requested.
|
|
17
|
+
*/
|
|
18
|
+
export declare function controlEnvironment<T extends NodeJS.ProcessEnv>(env: T, control?: 'pipe'): T;
|
|
19
|
+
//# sourceMappingURL=control-spawn.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess';
|
|
12
|
-
import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
12
|
+
import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalEnvironment, SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
13
13
|
import type { SpawnInternals } from './spawn.ts';
|
|
14
14
|
import type { ProcessInspector } from './process-inspector.ts';
|
|
15
15
|
/**
|
|
@@ -24,6 +24,8 @@ export declare class LocalSubprocessRuntime extends SubprocessRuntime {
|
|
|
24
24
|
private live;
|
|
25
25
|
/** Live terminals retained through normal quiescence or host-exit finalization. */
|
|
26
26
|
private terminals;
|
|
27
|
+
/** Caller endpoints retained until close, independently of managed process lifetime. */
|
|
28
|
+
private controlChannels;
|
|
27
29
|
/** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
|
|
28
30
|
internals: SpawnInternals;
|
|
29
31
|
/** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
|
|
@@ -40,6 +42,8 @@ export declare class LocalSubprocessRuntime extends SubprocessRuntime {
|
|
|
40
42
|
spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
|
|
41
43
|
private selectContainmentMode;
|
|
42
44
|
private warnFallback;
|
|
45
|
+
/** @inheritdoc */
|
|
46
|
+
terminalEnvironment(signal?: AbortSignal): Promise<SubprocessTerminalEnvironment>;
|
|
43
47
|
spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
|
|
44
48
|
}
|
|
45
49
|
export default LocalSubprocessRuntime;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
|
|
2
2
|
/** Replace the current process image while preserving the supplied argv and environment. */
|
|
3
|
-
export type LinuxExecve = (file: string, argv: string[], env: Record<string, string
|
|
3
|
+
export type LinuxExecve = (file: string, argv: string[], env: Record<string, string>, control?: 'pipe') => never;
|
|
4
4
|
/**
|
|
5
5
|
* Load libc's execve and fcntl symbols on first use and retain the native bindings.
|
|
6
6
|
* @returns a process-replacing execve operation that throws Node-style errors on failure.
|