@deepseek-ai/dsh-subprocess-local 0.1.2-alpha.5 → 0.1.3-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 +27 -18
- package/README.zh.md +28 -19
- package/lib/index.js +673 -961
- package/lib/runner-launch-COYGu0Dl.js +1623 -0
- package/lib/runner.js +425 -0
- package/lib/types/bin.d.ts +7 -0
- package/lib/types/index.d.ts +17 -11
- package/lib/types/linux-execve.d.ts +9 -0
- package/lib/types/linux-scope.d.ts +81 -0
- package/lib/types/managed-owner.d.ts +30 -0
- package/lib/types/runner-launch.d.ts +66 -0
- package/lib/types/runner-protocol.d.ts +109 -0
- package/lib/types/spawn-runner.d.ts +36 -0
- package/lib/types/spawn.d.ts +41 -15
- package/lib/types/terminal.d.ts +11 -2
- package/lib/types/windows-job.d.ts +30 -0
- package/package.json +15 -8
package/lib/index.js
CHANGED
|
@@ -1,1011 +1,608 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as bindManagedProcess, D as validateSubprocessSpec, E as spawnSubprocess, O as createProcessInspector, S as loadLinuxExecve, T as prepareManagedProcessBinding, _ as parseWindowsRunnerResult, c as runnerStdio, d as cleanupLinuxLaunchFiles, l as spawnRunnerInvocation, m as deserializeRunnerError, n as WINDOWS_RUNNER_SELECTION, o as runnerEnvironment, p as createLinuxLaunchFiles, s as runnerInvocationAvailable, u as targetEnvironment, w as childEnv, y as readLinuxStartupError } from "./runner-launch-COYGu0Dl.js";
|
|
2
|
+
import { closeSync, constants, existsSync, openSync } from "node:fs";
|
|
2
3
|
import { access, stat } from "node:fs/promises";
|
|
3
|
-
import { delimiter, extname, isAbsolute,
|
|
4
|
+
import { delimiter, extname, isAbsolute, resolve } from "node:path";
|
|
4
5
|
import * as nodePty from "node-pty";
|
|
5
|
-
import { SubprocessRuntime
|
|
6
|
-
import {
|
|
6
|
+
import { SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
7
|
+
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
7
8
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { constants as constants$1,
|
|
9
|
+
import { constants as constants$1, devNull } from "node:os";
|
|
9
10
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import { Buffer as Buffer$1 } from "node:buffer";
|
|
11
|
+
import { loadWin32ProcessBindings, probeCurrentTokenJobSupport } from "@deepseek-ai/dsh-win32-process";
|
|
12
|
+
import { Buffer } from "node:buffer";
|
|
13
13
|
import { PassThrough } from "node:stream";
|
|
14
|
-
//#region lib/types/
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const result = [];
|
|
45
|
-
const visit = (entry) => {
|
|
46
|
-
if (visited.has(entry.pid)) return;
|
|
47
|
-
visited.add(entry.pid);
|
|
48
|
-
for (const child of byParent.get(entry.pid) ?? []) visit(child);
|
|
49
|
-
const identity = started(entry.pid);
|
|
50
|
-
if (identity !== void 0) result.push({
|
|
51
|
-
pid: entry.pid,
|
|
52
|
-
started: identity
|
|
14
|
+
//#region lib/types/linux-scope.js
|
|
15
|
+
/** Linux user-systemd scope launch and managed-range ownership. */
|
|
16
|
+
const SYSTEMCTL_TIMEOUT_MS = 5e3;
|
|
17
|
+
const SCOPE_INITIAL_POLL_INTERVAL_MS = 50;
|
|
18
|
+
const MISSING_UNIT = /\bunit\b[^\r\n]*(?:could not be found|not found|not loaded)/iu;
|
|
19
|
+
function managerEnvironment() {
|
|
20
|
+
const environment = childEnv({ LC_ALL: "C" });
|
|
21
|
+
delete environment.SYSTEMD_LOG_TARGET;
|
|
22
|
+
return environment;
|
|
23
|
+
}
|
|
24
|
+
function quietSystemdEnvironment() {
|
|
25
|
+
return childEnv({
|
|
26
|
+
LC_ALL: "C",
|
|
27
|
+
SYSTEMD_LOG_TARGET: "null"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function querySystemctl(command, args) {
|
|
31
|
+
return new Promise((resolveResult) => {
|
|
32
|
+
execFile(command, [...args], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
env: managerEnvironment(),
|
|
35
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
36
|
+
}, (error, stdout, stderr) => {
|
|
37
|
+
const code = error === null ? 0 : error.code;
|
|
38
|
+
resolveResult({
|
|
39
|
+
status: typeof code === "number" ? code : null,
|
|
40
|
+
stdout,
|
|
41
|
+
stderr,
|
|
42
|
+
...error === null ? {} : { error }
|
|
43
|
+
});
|
|
53
44
|
});
|
|
54
|
-
};
|
|
55
|
-
visit(root);
|
|
56
|
-
return result;
|
|
45
|
+
});
|
|
57
46
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* process group: it is a stable pseudo-group that lets the prompt-marker
|
|
61
|
-
* readiness path compare foreground identities, while every actual signal
|
|
62
|
-
* targets the console-wide tree through taskkill (SIGINT is delivered by the
|
|
63
|
-
* terminal handle as a `\x03` input write and never reaches this layer).
|
|
64
|
-
*/
|
|
65
|
-
var WindowsProcessInspector = class {
|
|
66
|
-
internals;
|
|
67
|
-
constructor(internals = defaultWindowsProcessInternals()) {
|
|
68
|
-
this.internals = internals;
|
|
69
|
-
}
|
|
70
|
-
foregroundPgid(shellPid) {
|
|
71
|
-
return shellPid;
|
|
72
|
-
}
|
|
73
|
-
isStdinWaiting(_pgid, _shellPid) {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
isAlive(identity) {
|
|
77
|
-
const state = this.internals.processState(identity.pid);
|
|
78
|
-
return state?.active === true && state.started === identity.started;
|
|
79
|
-
}
|
|
80
|
-
snapshot() {
|
|
81
|
-
let entries;
|
|
82
|
-
return {
|
|
83
|
-
tree: (rootPid) => windowsProcessTree(entries ??= this.internals.snapshot(), rootPid, (pid) => this.internals.processState(pid)?.started),
|
|
84
|
-
session: () => [],
|
|
85
|
-
alive: (identity) => this.isAlive(identity)
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
signalGroup(pgid, signal) {
|
|
89
|
-
this.internals.taskkill(pgid, signal === "SIGKILL");
|
|
90
|
-
}
|
|
91
|
-
signalProcess(identity, signal) {
|
|
92
|
-
if (this.isAlive(identity)) this.internals.taskkill(identity.pid, signal === "SIGKILL");
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
/**
|
|
96
|
-
* Create the Windows process inspector.
|
|
97
|
-
* @param internals - injectable process operations; defaults to the koffi-backed table.
|
|
98
|
-
* @returns the Windows inspector.
|
|
99
|
-
*/
|
|
100
|
-
function createWindowsProcessInspector(internals = defaultWindowsProcessInternals()) {
|
|
101
|
-
return new WindowsProcessInspector(internals);
|
|
47
|
+
function unitStem(prefix) {
|
|
48
|
+
return `${prefix}-${String(process.pid)}-${randomBytes(6).toString("hex")}`;
|
|
102
49
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (pid <= 0) return;
|
|
106
|
-
spawnSync("taskkill", [
|
|
107
|
-
"/PID",
|
|
108
|
-
String(pid),
|
|
109
|
-
"/T",
|
|
110
|
-
...force ? ["/F"] : []
|
|
111
|
-
], { stdio: "ignore" });
|
|
50
|
+
function sleepWithAbort(delayMs, signal) {
|
|
51
|
+
return setTimeout$1(delayMs, void 0, { signal });
|
|
112
52
|
}
|
|
113
53
|
/**
|
|
114
|
-
*
|
|
115
|
-
* @param
|
|
116
|
-
* @returns whether the
|
|
54
|
+
* Confirm this exact runner entry and libc execve binding without a probe mode.
|
|
55
|
+
* @param internals - optional runner and libc-binding seams used by tests.
|
|
56
|
+
* @returns whether the bootstrap can enter the final target.
|
|
117
57
|
*/
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
58
|
+
function probeLinuxBootstrap(internals = {}) {
|
|
59
|
+
try {
|
|
60
|
+
(internals.loadLinuxExecve ?? loadLinuxExecve)();
|
|
61
|
+
const invocation = internals.runnerInvocation ?? (internals.resolveRunnerInvocation ?? spawnRunnerInvocation)();
|
|
62
|
+
return (internals.runnerAvailable ?? runnerInvocationAvailable)(invocation);
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
122
66
|
}
|
|
123
|
-
const PVOID = koffi.pointer("void");
|
|
124
67
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* re-register the names.
|
|
68
|
+
* Confirm current literal-argv transient-scope support before selecting native launch.
|
|
69
|
+
* @param internals - optional systemd command seams used by tests.
|
|
70
|
+
* @returns whether the current user manager supports the required scope invocation.
|
|
129
71
|
*/
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
72
|
+
function probeLinuxScope(internals = {}) {
|
|
73
|
+
const unitBase = unitStem("dsh-subprocess-probe");
|
|
74
|
+
const result = (internals.spawnSync ?? spawnSync)(internals.systemdRun ?? "systemd-run", [
|
|
75
|
+
"--user",
|
|
76
|
+
"--scope",
|
|
77
|
+
"--quiet",
|
|
78
|
+
"--collect",
|
|
79
|
+
"--expand-environment=no",
|
|
80
|
+
`--unit=${unitBase}`,
|
|
81
|
+
"--",
|
|
82
|
+
internals.systemctl ?? "systemctl",
|
|
83
|
+
"--user",
|
|
84
|
+
"show",
|
|
85
|
+
`${unitBase}.scope`,
|
|
86
|
+
"--property=ActiveState",
|
|
87
|
+
"--value"
|
|
88
|
+
], {
|
|
89
|
+
env: quietSystemdEnvironment(),
|
|
90
|
+
stdio: "ignore",
|
|
91
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
147
92
|
});
|
|
148
|
-
|
|
149
|
-
if (PROCESSENTRY32W.size !== 568) throw new Error(`PROCESSENTRY32W layout mismatch: koffi computed ${PROCESSENTRY32W.size}, Windows headers say 568`);
|
|
150
|
-
/* v8 ignore stop */
|
|
151
|
-
cachedStructs = {
|
|
152
|
-
PROCESSENTRY32W,
|
|
153
|
-
FILETIME
|
|
154
|
-
};
|
|
155
|
-
return cachedStructs;
|
|
93
|
+
return result.error === void 0 && result.status === 0;
|
|
156
94
|
}
|
|
157
|
-
let cachedStructs;
|
|
158
|
-
const TH32CS_SNAPPROCESS = 2;
|
|
159
|
-
const WAIT_OBJECT_0 = 0;
|
|
160
|
-
const WAIT_TIMEOUT = 258;
|
|
161
|
-
let cachedBindings;
|
|
162
95
|
/**
|
|
163
|
-
*
|
|
164
|
-
* @
|
|
96
|
+
* Confirm that the current user manager remains reachable after a positive deep probe.
|
|
97
|
+
* @param internals - optional systemctl seam used by tests.
|
|
98
|
+
* @returns whether one lightweight manager query succeeds.
|
|
165
99
|
*/
|
|
166
|
-
function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"uint32"
|
|
179
|
-
]),
|
|
180
|
-
getProcessTimes: bind("GetProcessTimes", "int", [
|
|
181
|
-
PVOID,
|
|
182
|
-
koffi.pointer(FILETIME),
|
|
183
|
-
koffi.pointer(FILETIME),
|
|
184
|
-
koffi.pointer(FILETIME),
|
|
185
|
-
koffi.pointer(FILETIME)
|
|
186
|
-
]),
|
|
187
|
-
waitForSingleObject: bind("WaitForSingleObject", "uint32", [PVOID, "uint32"]),
|
|
188
|
-
closeHandle: bind("CloseHandle", "int", [PVOID])
|
|
189
|
-
};
|
|
190
|
-
return cachedBindings;
|
|
100
|
+
function probeLinuxManager(internals = {}) {
|
|
101
|
+
const result = (internals.spawnSync ?? spawnSync)(internals.systemctl ?? "systemctl", [
|
|
102
|
+
"--user",
|
|
103
|
+
"show",
|
|
104
|
+
"--property=Version",
|
|
105
|
+
"--value"
|
|
106
|
+
], {
|
|
107
|
+
env: managerEnvironment(),
|
|
108
|
+
stdio: "ignore",
|
|
109
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
110
|
+
});
|
|
111
|
+
return result.error === void 0 && result.status === 0;
|
|
191
112
|
}
|
|
192
113
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* @
|
|
196
|
-
* @param count - element count.
|
|
197
|
-
* @returns the branded allocation pointer.
|
|
114
|
+
* Re-check every Linux native prerequisite for one eligible spawn.
|
|
115
|
+
* @param internals - optional native capability seams used by tests.
|
|
116
|
+
* @returns whether the Linux native containment path is currently available.
|
|
198
117
|
*/
|
|
199
|
-
function
|
|
200
|
-
return
|
|
118
|
+
function probeLinuxNative(internals = {}) {
|
|
119
|
+
return probeLinuxBootstrap(internals) && probeLinuxScope(internals);
|
|
201
120
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
121
|
+
var SystemdScopeOwner = class {
|
|
122
|
+
unit;
|
|
123
|
+
files;
|
|
124
|
+
direct;
|
|
125
|
+
systemctl;
|
|
126
|
+
runSync;
|
|
127
|
+
query;
|
|
128
|
+
sleep;
|
|
129
|
+
establishment = "pending";
|
|
130
|
+
stopped = false;
|
|
131
|
+
observation;
|
|
132
|
+
killFailure;
|
|
133
|
+
wakeGeneration = 0;
|
|
134
|
+
wakeWaiter;
|
|
135
|
+
constructor(unit, files, direct, systemctl, runSync, query, sleep) {
|
|
136
|
+
this.unit = unit;
|
|
137
|
+
this.files = files;
|
|
138
|
+
this.direct = direct;
|
|
139
|
+
this.systemctl = systemctl;
|
|
140
|
+
this.runSync = runSync;
|
|
141
|
+
this.query = query;
|
|
142
|
+
this.sleep = sleep;
|
|
143
|
+
}
|
|
144
|
+
signal(signal) {
|
|
145
|
+
if (this.stopped) return;
|
|
146
|
+
this.observeRequestConsumption();
|
|
147
|
+
const directFallbackRequired = this.establishment === "pending";
|
|
148
|
+
if (directFallbackRequired && this.direct.running()) this.direct.signal(signal);
|
|
149
|
+
const result = this.runSync(this.systemctl, [
|
|
150
|
+
"--user",
|
|
151
|
+
"kill",
|
|
152
|
+
"--kill-whom=all",
|
|
153
|
+
`--signal=${signal}`,
|
|
154
|
+
this.unit
|
|
155
|
+
], {
|
|
156
|
+
encoding: "utf8",
|
|
157
|
+
env: managerEnvironment(),
|
|
158
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
159
|
+
});
|
|
160
|
+
this.wakeObservation();
|
|
161
|
+
if (result.error === void 0 && result.status === 0) {
|
|
162
|
+
if (signal === "SIGKILL") this.killFailure = void 0;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!directFallbackRequired && this.direct.running()) this.direct.signal(signal);
|
|
166
|
+
if (signal === "SIGKILL") {
|
|
167
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
168
|
+
if (!MISSING_UNIT.test(output)) this.killFailure = result.error ?? /* @__PURE__ */ new Error(`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
|
|
221
169
|
}
|
|
222
|
-
} finally {
|
|
223
|
-
bindings.closeHandle(snapshot);
|
|
224
|
-
}
|
|
225
|
-
return entries;
|
|
226
|
-
}
|
|
227
|
-
/** Read one process's creation identity and current wait state. */
|
|
228
|
-
function windowsProcessState(bindings, pid) {
|
|
229
|
-
const { FILETIME } = win32Structs();
|
|
230
|
-
const handle = bindings.openProcess(1052672, 0, pid);
|
|
231
|
-
if (isInvalidHandle(handle)) return void 0;
|
|
232
|
-
try {
|
|
233
|
-
const creation = allocNative(FILETIME, 1);
|
|
234
|
-
const exit = allocNative(FILETIME, 1);
|
|
235
|
-
const kernel = allocNative(FILETIME, 1);
|
|
236
|
-
const user = allocNative(FILETIME, 1);
|
|
237
|
-
/* v8 ignore next -- a GetProcessTimes failure after a successful open races process exit and
|
|
238
|
-
cannot be staged deterministically; the absent-process path is covered and the caller
|
|
239
|
-
treats undefined as a detector miss. */
|
|
240
|
-
if (bindings.getProcessTimes(handle, creation, exit, kernel, user) === 0) return void 0;
|
|
241
|
-
const record = koffi.decode(creation, FILETIME);
|
|
242
|
-
const wait = bindings.waitForSingleObject(handle, 0);
|
|
243
|
-
/* v8 ignore next -- an opened process handle has exactly one of these two
|
|
244
|
-
zero-time wait states; an unexpected Win32 failure is an unreadable process. */
|
|
245
|
-
if (wait !== WAIT_OBJECT_0 && wait !== WAIT_TIMEOUT) return void 0;
|
|
246
|
-
return {
|
|
247
|
-
started: `${record.dwHighDateTime}:${record.dwLowDateTime}`,
|
|
248
|
-
active: wait === WAIT_TIMEOUT
|
|
249
|
-
};
|
|
250
|
-
} finally {
|
|
251
|
-
bindings.closeHandle(handle);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/** The koffi-backed default internals; bindings resolve lazily on first use. */
|
|
255
|
-
function defaultWindowsProcessInternals() {
|
|
256
|
-
return {
|
|
257
|
-
snapshot: () => snapshotWindowsProcesses(win32Bindings()),
|
|
258
|
-
processState: (pid) => windowsProcessState(win32Bindings(), pid),
|
|
259
|
-
taskkill: taskkillTree
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region lib/types/process-inspector.js
|
|
264
|
-
/** Platform process-table inspection for terminal readiness, signals, and teardown. */
|
|
265
|
-
/* v8 ignore start -- thin OS bindings; injected logic is unit-tested and real platform composition exercises them. */
|
|
266
|
-
const DEFAULT_INTERNALS = {
|
|
267
|
-
readFile: (path) => readFileSync(path, "utf8"),
|
|
268
|
-
readDir: (path) => readdirSync(path),
|
|
269
|
-
readLink: (path) => readlinkSync(path, "utf8"),
|
|
270
|
-
stat: (path) => statSync(path),
|
|
271
|
-
open: (path) => openSync(path, "r"),
|
|
272
|
-
read: (fd, buffer, length, position) => readSync(fd, buffer, 0, length, position),
|
|
273
|
-
close: closeSync,
|
|
274
|
-
exec: (file, args) => execFileSync(file, args, { encoding: "utf8" }),
|
|
275
|
-
kill: (pid, signal) => process.kill(pid, signal)
|
|
276
|
-
};
|
|
277
|
-
/**
|
|
278
|
-
* Parse fields used from Linux `/proc/<pid>/stat`, including parenthesized comm text.
|
|
279
|
-
* @param text - complete stat line.
|
|
280
|
-
* @returns Parsed identity/group fields, or undefined for malformed input.
|
|
281
|
-
*/
|
|
282
|
-
function parseProcStat(text) {
|
|
283
|
-
const open = text.indexOf("(");
|
|
284
|
-
const close = text.lastIndexOf(")");
|
|
285
|
-
if (open <= 0 || close <= open) return void 0;
|
|
286
|
-
const pid = Number(text.slice(0, open).trim());
|
|
287
|
-
const rest = text.slice(close + 2).trim().split(/\s+/);
|
|
288
|
-
const state = rest[0] || "";
|
|
289
|
-
const parentPid = Number(rest[1]);
|
|
290
|
-
const pgrp = Number(rest[2]);
|
|
291
|
-
const session = Number(rest[3]);
|
|
292
|
-
const ttyDevice = Number(rest[4]);
|
|
293
|
-
const tpgid = Number(rest[5]);
|
|
294
|
-
const started = rest[19];
|
|
295
|
-
if (![
|
|
296
|
-
pid,
|
|
297
|
-
parentPid,
|
|
298
|
-
pgrp,
|
|
299
|
-
session,
|
|
300
|
-
ttyDevice,
|
|
301
|
-
tpgid
|
|
302
|
-
].every(Number.isSafeInteger) || state.length !== 1 || started === void 0) return void 0;
|
|
303
|
-
return {
|
|
304
|
-
pid,
|
|
305
|
-
parentPid,
|
|
306
|
-
pgrp,
|
|
307
|
-
session,
|
|
308
|
-
state,
|
|
309
|
-
ttyDevice,
|
|
310
|
-
tpgid,
|
|
311
|
-
started
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
function readLinuxStat(internals, pid) {
|
|
315
|
-
try {
|
|
316
|
-
return parseProcStat(internals.readFile(`/proc/${pid}/stat`));
|
|
317
|
-
} catch (_unreadableProcEntry) {
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function linuxDeviceNumber(value) {
|
|
322
|
-
return value >>> 0;
|
|
323
|
-
}
|
|
324
|
-
function readLinuxTerminalDevice(internals, pid, ttyDevice, tid) {
|
|
325
|
-
const terminalDevice = linuxDeviceNumber(ttyDevice);
|
|
326
|
-
if (terminalDevice === 0) return void 0;
|
|
327
|
-
const path = tid === void 0 ? `/proc/${pid}/fd/0` : `/proc/${pid}/task/${tid}/fd/0`;
|
|
328
|
-
try {
|
|
329
|
-
if (internals.readLink(path) === "/dev/tty") return terminalDevice;
|
|
330
|
-
const status = internals.stat(path);
|
|
331
|
-
return status.isCharacterDevice() && linuxDeviceNumber(status.rdev) === terminalDevice ? terminalDevice : void 0;
|
|
332
|
-
} catch (_unreadableStdinDevice) {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Report whether a Linux process group has an executing member. `false`
|
|
338
|
-
* means the group contains only zombie/dead entries; `undefined` means the
|
|
339
|
-
* process table could not prove either outcome.
|
|
340
|
-
* @param processGroupId - POSIX process-group id to inspect.
|
|
341
|
-
* @param internals - injectable process-table operations.
|
|
342
|
-
* @returns Live-member presence, or `undefined` when unavailable/absent.
|
|
343
|
-
*/
|
|
344
|
-
function linuxProcessGroupHasLiveMembers(processGroupId, internals = DEFAULT_INTERNALS) {
|
|
345
|
-
let entries;
|
|
346
|
-
try {
|
|
347
|
-
entries = internals.readDir("/proc");
|
|
348
|
-
} catch (_unreadableProcDirectory) {
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
let matched = false;
|
|
352
|
-
for (const entry of entries) {
|
|
353
|
-
if (!/^\d+$/.test(entry)) continue;
|
|
354
|
-
const stat = readLinuxStat(internals, Number(entry));
|
|
355
|
-
if (stat?.pgrp !== processGroupId) continue;
|
|
356
|
-
matched = true;
|
|
357
|
-
if (!/^[ZXx]$/.test(stat.state)) return true;
|
|
358
170
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
171
|
+
terminateForHostExit() {
|
|
172
|
+
if (this.stopped) return;
|
|
173
|
+
try {
|
|
174
|
+
if (this.direct.running()) this.direct.signal("SIGKILL");
|
|
175
|
+
} catch {}
|
|
176
|
+
try {
|
|
177
|
+
this.runSync(this.systemctl, [
|
|
178
|
+
"--user",
|
|
179
|
+
"kill",
|
|
180
|
+
"--kill-whom=all",
|
|
181
|
+
"--signal=SIGKILL",
|
|
182
|
+
this.unit
|
|
183
|
+
], {
|
|
184
|
+
env: managerEnvironment(),
|
|
185
|
+
stdio: "ignore",
|
|
186
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
187
|
+
});
|
|
188
|
+
} catch {}
|
|
366
189
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
190
|
+
observeRequestConsumption() {
|
|
191
|
+
if (this.establishment === "pending" && !existsSync(this.files.requestPath)) this.establishment = "established";
|
|
192
|
+
}
|
|
193
|
+
absentUnit() {
|
|
194
|
+
this.observeRequestConsumption();
|
|
195
|
+
if (this.establishment === "established") return false;
|
|
196
|
+
if (!this.direct.running() && existsSync(this.files.requestPath)) return false;
|
|
197
|
+
if (this.killFailure !== void 0) throw this.killFailure;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
parseUnitState(stdout) {
|
|
201
|
+
const values = /* @__PURE__ */ new Map();
|
|
202
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
203
|
+
if (line === "") continue;
|
|
204
|
+
const separator = line.indexOf("=");
|
|
205
|
+
if (separator <= 0) throw new Error(`systemctl returned malformed state for ${this.unit}: ${JSON.stringify(stdout.trim())}`);
|
|
206
|
+
const name = line.slice(0, separator);
|
|
207
|
+
if (values.has(name)) throw new Error(`systemctl returned duplicate ${name} for ${this.unit}`);
|
|
208
|
+
values.set(name, line.slice(separator + 1));
|
|
209
|
+
}
|
|
210
|
+
const loadState = values.get("LoadState");
|
|
211
|
+
const activeState = values.get("ActiveState");
|
|
212
|
+
if (values.size !== 2 || loadState === void 0 || activeState === void 0) throw new Error(`systemctl returned incomplete state for ${this.unit}: ${JSON.stringify(stdout.trim())}`);
|
|
376
213
|
return {
|
|
377
|
-
|
|
378
|
-
|
|
214
|
+
loadState,
|
|
215
|
+
activeState
|
|
379
216
|
};
|
|
380
|
-
} catch (_unreadableSyscall) {
|
|
381
|
-
return;
|
|
382
217
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
function syscallWaitsOnStdin(internals, pid, tid, syscall, tables) {
|
|
438
|
-
const [a0 = 0, a1 = 0, a2 = 0] = syscall.args;
|
|
439
|
-
for (const table of tables) {
|
|
440
|
-
if (syscall.number === table.read) return a0 === 0;
|
|
441
|
-
if (syscall.number === table.select || syscall.number === table.pselect) return a0 >= 1 && fdSetHasStdin(internals, pid, a1);
|
|
442
|
-
if (syscall.number === table.poll || syscall.number === table.ppoll) return a1 >= 1 && pollHasStdin(internals, pid, a0, a1);
|
|
443
|
-
if (syscall.number === table.epollWait || syscall.number === table.epollPwait) return a2 >= 1 && epollHasStdin(internals, pid, tid, a0);
|
|
444
|
-
}
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
447
|
-
var PosixProcessInspector = class {
|
|
448
|
-
internals;
|
|
449
|
-
constructor(internals) {
|
|
450
|
-
this.internals = internals;
|
|
218
|
+
async rangeActive() {
|
|
219
|
+
this.observeRequestConsumption();
|
|
220
|
+
const result = await this.query(this.systemctl, [
|
|
221
|
+
"--user",
|
|
222
|
+
"show",
|
|
223
|
+
this.unit,
|
|
224
|
+
"--property=LoadState",
|
|
225
|
+
"--property=ActiveState"
|
|
226
|
+
]);
|
|
227
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
228
|
+
if (result.status === 0) {
|
|
229
|
+
const { loadState, activeState } = this.parseUnitState(result.stdout);
|
|
230
|
+
if (loadState === "not-found" && activeState === "inactive") return this.absentUnit();
|
|
231
|
+
if (loadState !== "loaded") throw new Error(`systemctl returned unknown state for ${this.unit}: ${JSON.stringify({
|
|
232
|
+
loadState,
|
|
233
|
+
activeState
|
|
234
|
+
})}`);
|
|
235
|
+
this.establishment = "established";
|
|
236
|
+
if (activeState === "inactive" || activeState === "failed") return false;
|
|
237
|
+
if (![
|
|
238
|
+
"active",
|
|
239
|
+
"activating",
|
|
240
|
+
"reloading",
|
|
241
|
+
"deactivating"
|
|
242
|
+
].includes(activeState)) throw new Error(`systemctl returned unknown ActiveState for ${this.unit}: ${JSON.stringify(activeState)}`);
|
|
243
|
+
if (this.killFailure !== void 0) throw this.killFailure;
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
if (!MISSING_UNIT.test(output)) {
|
|
247
|
+
if (result.error !== void 0) throw result.error;
|
|
248
|
+
throw new Error(`systemctl could not read ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
|
|
249
|
+
}
|
|
250
|
+
return this.absentUnit();
|
|
251
|
+
}
|
|
252
|
+
wakeObservation() {
|
|
253
|
+
this.wakeGeneration += 1;
|
|
254
|
+
this.wakeWaiter?.resolve();
|
|
255
|
+
this.wakeWaiter = void 0;
|
|
256
|
+
}
|
|
257
|
+
async waitForPoll(delayMs, generation) {
|
|
258
|
+
if (generation !== this.wakeGeneration) return;
|
|
259
|
+
const wake = Promise.withResolvers();
|
|
260
|
+
const waiter = {
|
|
261
|
+
generation,
|
|
262
|
+
resolve: wake.resolve
|
|
263
|
+
};
|
|
264
|
+
const sleepController = new AbortController();
|
|
265
|
+
this.wakeWaiter = waiter;
|
|
266
|
+
try {
|
|
267
|
+
await Promise.race([this.sleep(delayMs, sleepController.signal), wake.promise]);
|
|
268
|
+
} finally {
|
|
269
|
+
sleepController.abort();
|
|
270
|
+
if (this.wakeWaiter === waiter) this.wakeWaiter = void 0;
|
|
271
|
+
}
|
|
451
272
|
}
|
|
452
|
-
|
|
453
|
-
this.
|
|
273
|
+
async waitForExit() {
|
|
274
|
+
if (this.stopped) return;
|
|
275
|
+
this.observation ??= (async () => {
|
|
276
|
+
let pollIntervalMs = SCOPE_INITIAL_POLL_INTERVAL_MS;
|
|
277
|
+
let generation = this.wakeGeneration;
|
|
278
|
+
while (await this.rangeActive()) {
|
|
279
|
+
await this.waitForPoll(pollIntervalMs, generation);
|
|
280
|
+
generation = this.wakeGeneration;
|
|
281
|
+
if (this.establishment === "established") pollIntervalMs = Math.min(pollIntervalMs * 2, SYSTEMCTL_TIMEOUT_MS);
|
|
282
|
+
}
|
|
283
|
+
this.stopped = true;
|
|
284
|
+
})().catch((error) => {
|
|
285
|
+
this.observation = void 0;
|
|
286
|
+
throw error;
|
|
287
|
+
});
|
|
288
|
+
await this.observation;
|
|
454
289
|
}
|
|
455
|
-
|
|
456
|
-
|
|
290
|
+
cleanup() {
|
|
291
|
+
cleanupLinuxLaunchFiles(this.files);
|
|
457
292
|
}
|
|
458
293
|
};
|
|
459
|
-
function
|
|
460
|
-
return
|
|
294
|
+
function scopeArgs(unitBase, invocation, argv) {
|
|
295
|
+
return [
|
|
296
|
+
"--user",
|
|
297
|
+
"--scope",
|
|
298
|
+
"--quiet",
|
|
299
|
+
"--collect",
|
|
300
|
+
"--expand-environment=no",
|
|
301
|
+
`--unit=${unitBase}`,
|
|
302
|
+
"--",
|
|
303
|
+
...invocation,
|
|
304
|
+
"--",
|
|
305
|
+
...argv
|
|
306
|
+
];
|
|
461
307
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
tree(rootPid) {
|
|
470
|
-
return processTree(this.rows, rootPid);
|
|
471
|
-
}
|
|
472
|
-
session(sessionId) {
|
|
473
|
-
return this.rows.flatMap((row) => row.session === sessionId ? [{
|
|
474
|
-
pid: row.pid,
|
|
475
|
-
started: row.started
|
|
476
|
-
}] : []);
|
|
477
|
-
}
|
|
478
|
-
alive(identity) {
|
|
479
|
-
const row = this.byPid.get(identity.pid);
|
|
480
|
-
return row?.started === identity.started && !quiescent(row.state);
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
function processTree(entries, rootPid) {
|
|
484
|
-
const root = new Map(entries.map((entry) => [entry.pid, entry])).get(rootPid);
|
|
485
|
-
if (root === void 0) return [];
|
|
486
|
-
const byParent = /* @__PURE__ */ new Map();
|
|
487
|
-
for (const entry of entries) {
|
|
488
|
-
const children = byParent.get(entry.parentPid) ?? [];
|
|
489
|
-
children.push(entry);
|
|
490
|
-
byParent.set(entry.parentPid, children);
|
|
491
|
-
}
|
|
492
|
-
const visited = /* @__PURE__ */ new Set();
|
|
493
|
-
const result = [];
|
|
494
|
-
const visit = (entry) => {
|
|
495
|
-
if (visited.has(entry.pid)) return;
|
|
496
|
-
visited.add(entry.pid);
|
|
497
|
-
for (const child of byParent.get(entry.pid) ?? []) visit(child);
|
|
498
|
-
result.push({
|
|
499
|
-
pid: entry.pid,
|
|
500
|
-
started: entry.started
|
|
308
|
+
function directOutcome(child, files) {
|
|
309
|
+
return new Promise((resolveOutcome, rejectOutcome) => {
|
|
310
|
+
let settled = false;
|
|
311
|
+
child.once("error", (error) => {
|
|
312
|
+
if (settled) return;
|
|
313
|
+
settled = true;
|
|
314
|
+
rejectOutcome(error);
|
|
501
315
|
});
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const terminalDevice = readLinuxTerminalDevice(this.internals, shellPid, shell.ttyDevice);
|
|
522
|
-
if (terminalDevice === void 0) return false;
|
|
523
|
-
for (const pid of numericEntries(this.internals, "/proc")) {
|
|
524
|
-
const process = readLinuxStat(this.internals, pid);
|
|
525
|
-
if (process?.pgrp !== pgid) continue;
|
|
526
|
-
for (const tid of numericEntries(this.internals, `/proc/${pid}/task`)) {
|
|
527
|
-
const syscall = readSyscall(this.internals, pid, tid);
|
|
528
|
-
if (syscall !== void 0 && syscallWaitsOnStdin(this.internals, pid, tid, syscall, tables) && readLinuxTerminalDevice(this.internals, pid, process.ttyDevice, tid) === terminalDevice) return true;
|
|
316
|
+
child.once("exit", (exitCode, signal) => {
|
|
317
|
+
if (settled) return;
|
|
318
|
+
settled = true;
|
|
319
|
+
try {
|
|
320
|
+
const startup = readLinuxStartupError(files.startupErrorPath);
|
|
321
|
+
if (startup !== void 0) {
|
|
322
|
+
rejectOutcome(deserializeRunnerError(startup.error));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (existsSync(files.requestPath)) {
|
|
326
|
+
rejectOutcome(/* @__PURE__ */ new Error("subprocess scope exited before its bootstrap consumed the launch request"));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
resolveOutcome({
|
|
330
|
+
exitCode,
|
|
331
|
+
signal
|
|
332
|
+
});
|
|
333
|
+
} catch (error) {
|
|
334
|
+
rejectOutcome(error instanceof Error ? error : new Error(String(error)));
|
|
529
335
|
}
|
|
530
|
-
}
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
isAlive(identity) {
|
|
534
|
-
const stat = readLinuxStat(this.internals, identity.pid);
|
|
535
|
-
return stat?.started === identity.started && !quiescent(stat.state);
|
|
536
|
-
}
|
|
537
|
-
snapshot() {
|
|
538
|
-
return new PosixProcessSnapshot(numericEntries(this.internals, "/proc").flatMap((pid) => {
|
|
539
|
-
const stat = readLinuxStat(this.internals, pid);
|
|
540
|
-
return stat === void 0 ? [] : [{
|
|
541
|
-
pid,
|
|
542
|
-
parentPid: stat.parentPid,
|
|
543
|
-
started: stat.started,
|
|
544
|
-
session: stat.session,
|
|
545
|
-
state: stat.state
|
|
546
|
-
}];
|
|
547
|
-
}));
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
function macProcessTable(internals) {
|
|
551
|
-
return internals.exec("/bin/ps", ["-axo", "pid=,ppid=,lstart="]).split("\n").flatMap((line) => {
|
|
552
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(.+?)\s*$/.exec(line);
|
|
553
|
-
if (match?.[1] === void 0 || match[2] === void 0 || match[3] === void 0) return [];
|
|
554
|
-
return [{
|
|
555
|
-
pid: Number(match[1]),
|
|
556
|
-
parentPid: Number(match[2]),
|
|
557
|
-
started: match[3],
|
|
558
|
-
session: void 0,
|
|
559
|
-
state: void 0
|
|
560
|
-
}];
|
|
336
|
+
});
|
|
561
337
|
});
|
|
562
338
|
}
|
|
563
|
-
|
|
564
|
-
|
|
339
|
+
function signalChildGroup(child, signal) {
|
|
340
|
+
try {
|
|
341
|
+
process.kill(-child.pid, signal);
|
|
342
|
+
} catch {
|
|
565
343
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"tpgid=",
|
|
569
|
-
"-p",
|
|
570
|
-
String(shellPid)
|
|
571
|
-
]).trim());
|
|
572
|
-
return Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
573
|
-
} catch (_missingProcess) {
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
isStdinWaiting(_pgid, _shellPid) {
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
isAlive(identity) {
|
|
581
|
-
return macProcessTable(this.internals).some((entry) => entry.pid === identity.pid && entry.started === identity.started);
|
|
582
|
-
}
|
|
583
|
-
snapshot() {
|
|
584
|
-
return new PosixProcessSnapshot(macProcessTable(this.internals));
|
|
344
|
+
child.kill(signal);
|
|
345
|
+
} catch {}
|
|
585
346
|
}
|
|
586
|
-
};
|
|
587
|
-
/**
|
|
588
|
-
* Create the supported platform inspector or fail at plugin load.
|
|
589
|
-
* @param platform - target Node platform.
|
|
590
|
-
* @param arch - target CPU architecture for Linux syscall numbers.
|
|
591
|
-
* @param internals - filesystem/process boundary, injectable for deterministic tests.
|
|
592
|
-
* @returns Platform process inspector.
|
|
593
|
-
*/
|
|
594
|
-
function createProcessInspector(platform = process.platform, arch = process.arch, internals = DEFAULT_INTERNALS) {
|
|
595
|
-
if (platform === "linux") return new LinuxProcessInspector(arch, internals);
|
|
596
|
-
if (platform === "darwin") return new MacProcessInspector(internals);
|
|
597
|
-
if (platform === "win32") return createWindowsProcessInspector();
|
|
598
|
-
throw new Error(`subprocess-local: terminal inspection is unsupported on platform ${platform}`);
|
|
599
347
|
}
|
|
600
|
-
//#endregion
|
|
601
|
-
//#region lib/types/spawn.js
|
|
602
348
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
* @module dsh-subprocess-local/spawn
|
|
349
|
+
* Prepare one Linux PTY scope using the same launch request and bootstrap core.
|
|
350
|
+
* @param spec - terminal target request.
|
|
351
|
+
* @param targetEnv - validated complete target environment.
|
|
352
|
+
* @param internals - optional runner and systemd seams used by tests.
|
|
353
|
+
* @returns invocation facts and ownership callbacks for node-pty.
|
|
609
354
|
*/
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
355
|
+
function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
356
|
+
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
357
|
+
const files = createLinuxLaunchFiles({
|
|
358
|
+
cwd: spec.cwd,
|
|
359
|
+
env: targetEnv
|
|
360
|
+
});
|
|
361
|
+
const unitBase = unitStem("dsh-terminal");
|
|
362
|
+
return {
|
|
363
|
+
command: internals.systemdRun ?? "systemd-run",
|
|
364
|
+
args: scopeArgs(unitBase, invocation, spec.argv),
|
|
365
|
+
cwd: process.cwd(),
|
|
366
|
+
env: runnerEnvironment(files.requestPath, invocation),
|
|
367
|
+
bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`, files, direct, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort),
|
|
368
|
+
resolveOutcome: (outcome) => {
|
|
369
|
+
const startup = readLinuxStartupError(files.startupErrorPath);
|
|
370
|
+
if (startup !== void 0) throw deserializeRunnerError(startup.error);
|
|
371
|
+
if (existsSync(files.requestPath)) throw new Error("terminal scope exited before its bootstrap consumed the launch request");
|
|
372
|
+
return outcome;
|
|
373
|
+
},
|
|
374
|
+
cleanup: () => {
|
|
375
|
+
cleanupLinuxLaunchFiles(files);
|
|
376
|
+
}
|
|
623
377
|
};
|
|
624
|
-
let entries = Object.entries(env);
|
|
625
|
-
for (const [key, value] of Object.entries(extra ?? {})) {
|
|
626
|
-
const normalized = key.toUpperCase();
|
|
627
|
-
entries = entries.filter(([inherited]) => inherited.toUpperCase() !== normalized);
|
|
628
|
-
entries.push([key, value]);
|
|
629
|
-
}
|
|
630
|
-
return Object.fromEntries(entries);
|
|
631
378
|
}
|
|
632
379
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
380
|
+
* Launch one ordinary target inside a transient user scope.
|
|
381
|
+
* @param spec - ordinary target request.
|
|
382
|
+
* @param targetEnv - validated complete target environment.
|
|
383
|
+
* @param internals - optional runner and systemd seams used by tests.
|
|
384
|
+
* @returns direct streams, result, and managed-scope owner.
|
|
637
385
|
*/
|
|
638
|
-
function
|
|
639
|
-
|
|
386
|
+
function launchLinuxScope(spec, targetEnv, internals = {}) {
|
|
387
|
+
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
388
|
+
const files = createLinuxLaunchFiles({
|
|
389
|
+
cwd: spec.cwd,
|
|
390
|
+
env: targetEnv
|
|
391
|
+
});
|
|
392
|
+
const unitBase = unitStem("dsh-subprocess");
|
|
393
|
+
let child;
|
|
394
|
+
try {
|
|
395
|
+
child = (internals.spawn ?? spawn)(internals.systemdRun ?? "systemd-run", scopeArgs(unitBase, invocation, spec.argv), {
|
|
396
|
+
cwd: process.cwd(),
|
|
397
|
+
env: runnerEnvironment(files.requestPath, invocation),
|
|
398
|
+
stdio: runnerStdio(spec, false),
|
|
399
|
+
detached: true
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
cleanupLinuxLaunchFiles(files);
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
const owner = new SystemdScopeOwner(`${unitBase}.scope`, files, {
|
|
406
|
+
running: () => child.pid !== void 0 && child.exitCode === null && child.signalCode === null,
|
|
407
|
+
signal: (signal) => {
|
|
408
|
+
signalChildGroup(child, signal);
|
|
409
|
+
}
|
|
410
|
+
}, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort);
|
|
411
|
+
return {
|
|
412
|
+
stdin: child.stdin,
|
|
413
|
+
stdout: child.stdout,
|
|
414
|
+
stderr: child.stderr,
|
|
415
|
+
direct: directOutcome(child, files),
|
|
416
|
+
owner
|
|
417
|
+
};
|
|
640
418
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
* other local users read command output or pre-create symlinks.
|
|
647
|
-
*/
|
|
648
|
-
function privateSpillDir() {
|
|
649
|
-
defaultSpillDir ??= mkdtempSync(join(tmpdir(), "dsh-subprocess-"));
|
|
650
|
-
return defaultSpillDir;
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region lib/types/windows-job.js
|
|
421
|
+
/** Windows parent-side launch and ownership for the private Job runner. */
|
|
422
|
+
function isWindowsStartCancellationError(error) {
|
|
423
|
+
return error.name === "Error" && error.message === "subprocess target start was cancelled" && error.code === void 0 && error.syscall === void 0 && error.path === void 0;
|
|
651
424
|
}
|
|
652
425
|
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
* the cap; without one, only the in-memory tail is ever retained (the
|
|
657
|
-
* diagnostic-tail shape — a language server's stderr).
|
|
658
|
-
*
|
|
659
|
-
* Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
|
|
660
|
-
* end of command output; the spill file covers the head.
|
|
426
|
+
* Re-check the runner entry, bindings, and current Job capability for every spawn.
|
|
427
|
+
* @param internals - optional runner and Win32 capability seams used by tests.
|
|
428
|
+
* @returns whether the Windows native containment path is currently available.
|
|
661
429
|
*/
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
spillFile;
|
|
672
|
-
spillDisabled;
|
|
673
|
-
/** Total bytes ever pushed (not just retained). */
|
|
674
|
-
total = 0;
|
|
675
|
-
constructor(maxBytes, maxSpillBytes, label, spillDir) {
|
|
676
|
-
this.maxBytes = maxBytes;
|
|
677
|
-
this.maxSpillBytes = maxSpillBytes;
|
|
678
|
-
this.label = label;
|
|
679
|
-
this.spillDir = spillDir;
|
|
680
|
-
this.spillDisabled = maxSpillBytes === void 0;
|
|
681
|
-
}
|
|
682
|
-
/**
|
|
683
|
-
* Ingest one stream chunk, counting it toward the whole-stream total. On
|
|
684
|
-
* first overflow of the in-memory cap a spill file is opened (when spilling
|
|
685
|
-
* is enabled) and every chunk (already-collected ones included) is appended
|
|
686
|
-
* there from then on; the in-memory tail then drops whole chunks from its
|
|
687
|
-
* head (or the head of a single over-cap chunk) until it fits the cap again.
|
|
688
|
-
* @param chunk - the raw bytes from one stream 'data' event.
|
|
689
|
-
*/
|
|
690
|
-
push(chunk) {
|
|
691
|
-
this.total += chunk.length;
|
|
692
|
-
const overflows = this.bytes + chunk.length > this.maxBytes;
|
|
693
|
-
if (!this.spillDisabled && (overflows || this.spillFd !== void 0)) this.spillAll(chunk);
|
|
694
|
-
this.chunks.push(chunk);
|
|
695
|
-
this.bytes += chunk.length;
|
|
696
|
-
while (this.bytes > this.maxBytes) {
|
|
697
|
-
const head = this.chunks[0];
|
|
698
|
-
const excess = this.bytes - this.maxBytes;
|
|
699
|
-
if (head.length <= excess) {
|
|
700
|
-
this.chunks.shift();
|
|
701
|
-
this.bytes -= head.length;
|
|
702
|
-
} else {
|
|
703
|
-
this.chunks[0] = head.subarray(excess);
|
|
704
|
-
this.bytes -= excess;
|
|
705
|
-
}
|
|
706
|
-
this.dropped = true;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
/** Open the spill file lazily and append `chunk` (and any prior chunks once). */
|
|
710
|
-
spillAll(chunk) {
|
|
711
|
-
if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
|
|
712
|
-
this.discardSpill();
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
if (this.spillFd === void 0) {
|
|
716
|
-
this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);
|
|
717
|
-
this.spillFd = openSync(this.spillFile, "wx", 384);
|
|
718
|
-
for (const prior of this.chunks) writeSync(this.spillFd, prior);
|
|
719
|
-
}
|
|
720
|
-
writeSync(this.spillFd, chunk);
|
|
430
|
+
function probeWindowsJob(internals = {}) {
|
|
431
|
+
try {
|
|
432
|
+
const invocation = internals.runnerInvocation ?? (internals.resolveRunnerInvocation ?? spawnRunnerInvocation)();
|
|
433
|
+
if (!(internals.runnerAvailable ?? runnerInvocationAvailable)(invocation)) return false;
|
|
434
|
+
const api = (internals.loadWin32ProcessBindings ?? loadWin32ProcessBindings)();
|
|
435
|
+
(internals.probeCurrentTokenJobSupport ?? probeCurrentTokenJobSupport)(api);
|
|
436
|
+
return true;
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
721
439
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
440
|
+
}
|
|
441
|
+
var WindowsJobOwner = class {
|
|
442
|
+
runner;
|
|
443
|
+
exited;
|
|
444
|
+
directResultType;
|
|
445
|
+
failInfrastructure;
|
|
446
|
+
cancellationReason;
|
|
447
|
+
cancellationReasonSet = false;
|
|
448
|
+
terminationSent = false;
|
|
449
|
+
constructor(runner, exited, directResultType, failInfrastructure) {
|
|
450
|
+
this.runner = runner;
|
|
451
|
+
this.exited = exited;
|
|
452
|
+
this.directResultType = directResultType;
|
|
453
|
+
this.failInfrastructure = failInfrastructure;
|
|
454
|
+
this.exited.catch(() => {});
|
|
455
|
+
}
|
|
456
|
+
signal(_signal, cancellationReason) {
|
|
457
|
+
if (!this.cancellationReasonSet) {
|
|
458
|
+
this.cancellationReason = cancellationReason;
|
|
459
|
+
this.cancellationReasonSet = true;
|
|
733
460
|
}
|
|
734
|
-
if (
|
|
735
|
-
|
|
736
|
-
} catch {}
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Incremental read in whole-stream byte coordinates: returns everything
|
|
740
|
-
* pushed since `fromByte`. When `fromByte` has already slid out of the
|
|
741
|
-
* in-memory tail window, the read is `lossy` — it returns the whole
|
|
742
|
-
* retained tail and the gap is only recoverable from the spill file.
|
|
743
|
-
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
|
|
744
|
-
* @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
|
|
745
|
-
*/
|
|
746
|
-
readFrom(fromByte) {
|
|
747
|
-
const windowStart = this.total - this.bytes;
|
|
748
|
-
const buffer = Buffer.concat(this.chunks);
|
|
749
|
-
const lossy = fromByte < windowStart;
|
|
750
|
-
return {
|
|
751
|
-
text: (lossy ? buffer : buffer.subarray(fromByte - windowStart)).toString("utf8"),
|
|
752
|
-
nextOffset: this.total,
|
|
753
|
-
lossy,
|
|
754
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Close the spill file once the stream has ended. A failed close (delayed
|
|
759
|
-
* writeback fault) stops advertising the spill path — the file may be
|
|
760
|
-
* missing its tail — while every in-memory read keeps working. Idempotent;
|
|
761
|
-
* the spawn path seals both collectors at settlement so reads after exit
|
|
762
|
-
* never point at a still-open file.
|
|
763
|
-
*/
|
|
764
|
-
seal() {
|
|
765
|
-
if (this.spillFd === void 0) return;
|
|
461
|
+
if (this.terminationSent || !this.runner.connected) return;
|
|
462
|
+
this.terminationSent = true;
|
|
766
463
|
try {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
464
|
+
this.runner.send?.({ type: "terminate" }, (error) => {
|
|
465
|
+
if (error === null || this.directResultType() !== void 0) return;
|
|
466
|
+
this.failInfrastructure(error);
|
|
467
|
+
this.terminateForHostExit();
|
|
468
|
+
});
|
|
469
|
+
} catch (error) {
|
|
470
|
+
this.failInfrastructure(error);
|
|
471
|
+
this.terminateForHostExit();
|
|
770
472
|
}
|
|
771
|
-
this.spillFd = void 0;
|
|
772
473
|
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
* @returns the final collected output: tail text, truncation flag, and the spill path when intact.
|
|
776
|
-
*/
|
|
777
|
-
finalize() {
|
|
778
|
-
this.seal();
|
|
779
|
-
return {
|
|
780
|
-
text: Buffer.concat(this.chunks).toString("utf8"),
|
|
781
|
-
truncated: this.dropped,
|
|
782
|
-
...this.spillFile !== void 0 ? { spillPath: this.spillFile } : {}
|
|
783
|
-
};
|
|
474
|
+
mapStartFailure(failure, serialized) {
|
|
475
|
+
return this.cancellationReasonSet && isWindowsStartCancellationError(serialized) ? this.cancellationReason : failure;
|
|
784
476
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
* Terminate one Windows process tree with `taskkill /T /F`. Contained like
|
|
788
|
-
* POSIX group signalling — delivery races tree exit, so an absent tree, a
|
|
789
|
-
* nonzero status, or a missing taskkill binary must not break idempotent
|
|
790
|
-
* teardown.
|
|
791
|
-
* @param pid - root process id; non-positive is a no-op.
|
|
792
|
-
*/
|
|
793
|
-
function taskkillProcessTree(pid) {
|
|
794
|
-
if (pid <= 0) return;
|
|
795
|
-
spawnSync("taskkill", [
|
|
796
|
-
"/PID",
|
|
797
|
-
String(pid),
|
|
798
|
-
"/T",
|
|
799
|
-
"/F"
|
|
800
|
-
], { stdio: "ignore" });
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Signal a detached process tree with platform-correct semantics: POSIX
|
|
804
|
-
* signals the negative process-group id and falls back to the direct child
|
|
805
|
-
* when the group is gone; Windows terminates the tree via taskkill (any
|
|
806
|
-
* signal value force-terminates — Node maps signals to TerminateProcess).
|
|
807
|
-
*/
|
|
808
|
-
function signalTree(platform, pid, sig, child, taskkill) {
|
|
809
|
-
if (platform === "win32") {
|
|
810
|
-
taskkill(pid);
|
|
811
|
-
return;
|
|
477
|
+
async waitForExit() {
|
|
478
|
+
await this.exited;
|
|
812
479
|
}
|
|
813
|
-
|
|
814
|
-
if (pid <= 0) return;
|
|
815
|
-
try {
|
|
816
|
-
process.kill(-pid, sig);
|
|
817
|
-
} catch {
|
|
818
|
-
/* v8 ignore start -- the fallback needs a live child whose group signal fails
|
|
819
|
-
(EPERM-style), which POSIX CI cannot stage; the swallow keeps teardown idempotent. */
|
|
480
|
+
terminateForHostExit() {
|
|
820
481
|
try {
|
|
821
|
-
|
|
482
|
+
this.runner.kill("SIGKILL");
|
|
822
483
|
} catch {}
|
|
823
484
|
}
|
|
824
|
-
}
|
|
485
|
+
};
|
|
825
486
|
/**
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* @param
|
|
830
|
-
* @
|
|
831
|
-
* @returns live subprocess handle.
|
|
832
|
-
* @throws when `graceMs` cannot be represented by one Node timer.
|
|
487
|
+
* Launch one target through a runner that uniquely owns its Job handle.
|
|
488
|
+
* @param spec - ordinary target request.
|
|
489
|
+
* @param targetEnv - validated complete target environment.
|
|
490
|
+
* @param internals - optional runner launch seams used by tests.
|
|
491
|
+
* @returns direct streams, result, and runner-owned managed range.
|
|
833
492
|
*/
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const child = spawn(program, args, {
|
|
849
|
-
cwd: spec.cwd,
|
|
850
|
-
env,
|
|
851
|
-
stdio: [
|
|
852
|
-
stdinMode === "ignore" ? "ignore" : "pipe",
|
|
853
|
-
outMode === "inherit" ? "inherit" : "pipe",
|
|
854
|
-
errMode === "inherit" ? "inherit" : "pipe"
|
|
855
|
-
],
|
|
856
|
-
detached: platform !== "win32"
|
|
857
|
-
});
|
|
858
|
-
const collectStream = (mode, stream, label) => {
|
|
859
|
-
if (!isCollect(mode) || stream === null) return void 0;
|
|
860
|
-
const collector = new OutputCollector(mode.maxBytes, mode.spill?.maxBytes, label, spillDir);
|
|
861
|
-
stream.on("data", (chunk) => {
|
|
862
|
-
collector.push(chunk);
|
|
493
|
+
function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
494
|
+
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
495
|
+
const [command, ...prefix] = invocation;
|
|
496
|
+
const ignoredStdinFd = spec.stdio.stdin === "ignore" ? openSync(devNull, "r") : void 0;
|
|
497
|
+
let child;
|
|
498
|
+
try {
|
|
499
|
+
child = (internals.spawn ?? spawn)(command, [
|
|
500
|
+
...prefix,
|
|
501
|
+
"--",
|
|
502
|
+
...spec.argv
|
|
503
|
+
], {
|
|
504
|
+
cwd: process.cwd(),
|
|
505
|
+
env: runnerEnvironment(WINDOWS_RUNNER_SELECTION, invocation),
|
|
506
|
+
stdio: runnerStdio(spec, true, ignoredStdinFd ?? "pipe")
|
|
863
507
|
});
|
|
864
|
-
|
|
508
|
+
} finally {
|
|
509
|
+
if (ignoredStdinFd !== void 0) closeSync(ignoredStdinFd);
|
|
510
|
+
}
|
|
511
|
+
const targetStdin = child.stdio[4];
|
|
512
|
+
const direct = Promise.withResolvers();
|
|
513
|
+
const rangeExit = Promise.withResolvers();
|
|
514
|
+
let directResultType;
|
|
515
|
+
let runnerSpawned = false;
|
|
516
|
+
const failInfrastructure = (error) => {
|
|
517
|
+
direct.reject(error);
|
|
518
|
+
rangeExit.reject(error);
|
|
865
519
|
};
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const treeAlive = () => {
|
|
875
|
-
/* v8 ignore next -- only a timer callback already queued when the observer settles can enter here;
|
|
876
|
-
the guard is the final defense against probing an id after its tree was confirmed absent. */
|
|
877
|
-
if (treeExitObserved) return false;
|
|
878
|
-
if (pid <= 0) return false;
|
|
879
|
-
if (platform === "win32") return child.exitCode === null && child.signalCode === null;
|
|
520
|
+
const owner = new WindowsJobOwner(child, rangeExit.promise, () => directResultType, failInfrastructure);
|
|
521
|
+
child.on("message", (value) => {
|
|
522
|
+
if (directResultType !== void 0) {
|
|
523
|
+
failInfrastructure(/* @__PURE__ */ new Error("subprocess-local: Windows runner emitted more than one direct result"));
|
|
524
|
+
owner.terminateForHostExit();
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
let result;
|
|
880
528
|
try {
|
|
881
|
-
|
|
882
|
-
if (settled && platform === "linux" && linuxGroupHasLiveMembers(pid) === false) return false;
|
|
883
|
-
return true;
|
|
529
|
+
result = parseWindowsRunnerResult(value);
|
|
884
530
|
} catch (error) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (code === "ESRCH") return false;
|
|
889
|
-
/* v8 ignore start -- EPERM and non-POSIX negative-pid failures are platform defenses; CI runs
|
|
890
|
-
tree-lifecycle tests on POSIX hosts where absence reports ESRCH. */
|
|
891
|
-
if (code === "EPERM") return true;
|
|
892
|
-
return child.exitCode === null && child.signalCode === null;
|
|
531
|
+
failInfrastructure(error);
|
|
532
|
+
owner.terminateForHostExit();
|
|
533
|
+
return;
|
|
893
534
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
* pending escalation before this process-group id can be reused.
|
|
899
|
-
*/
|
|
900
|
-
const observeTreeExit = () => {
|
|
901
|
-
treeExitObservation ??= (async () => {
|
|
902
|
-
while (treeAlive()) await sleepTick();
|
|
903
|
-
treeExitObserved = true;
|
|
904
|
-
if (graceTimer !== void 0) clearTimeout(graceTimer);
|
|
905
|
-
graceTimer = void 0;
|
|
906
|
-
})();
|
|
907
|
-
return treeExitObservation;
|
|
908
|
-
};
|
|
909
|
-
const kill = (sig) => {
|
|
910
|
-
/* v8 ignore next -- the shared exit observer cancels the ordinary dead-tree timer;
|
|
911
|
-
this remains the timer/death race guard and cannot be staged deterministically. */
|
|
912
|
-
if (!treeAlive()) return;
|
|
913
|
-
signalTree(platform, pid, sig, child, taskkill);
|
|
914
|
-
};
|
|
915
|
-
const terminate = () => {
|
|
916
|
-
if (treeExitObserved || graceTimer !== void 0) return;
|
|
917
|
-
observeTreeExit();
|
|
918
|
-
if (treeExitObserved) return;
|
|
919
|
-
kill("SIGTERM");
|
|
920
|
-
graceTimer = setTimeout(() => {
|
|
921
|
-
kill("SIGKILL");
|
|
922
|
-
}, spec.graceMs);
|
|
923
|
-
};
|
|
924
|
-
const terminateForHostExit = () => {
|
|
925
|
-
kill("SIGKILL");
|
|
926
|
-
};
|
|
927
|
-
const onAbort = () => {
|
|
928
|
-
terminate();
|
|
929
|
-
};
|
|
930
|
-
spec.signal?.addEventListener("abort", onAbort, { once: true });
|
|
931
|
-
if (typeof stdinMode === "object" && child.stdin !== null) {
|
|
932
|
-
child.stdin.on("error", () => {});
|
|
933
|
-
child.stdin.end(stdinMode.data);
|
|
934
|
-
}
|
|
935
|
-
const done = new Promise((resolve, reject) => {
|
|
936
|
-
let pipeDrainTimer;
|
|
937
|
-
const settle = (exitCode, signal) => {
|
|
938
|
-
if (settled) return;
|
|
939
|
-
settled = true;
|
|
940
|
-
if (stdoutCollector !== void 0) child.stdout?.destroy();
|
|
941
|
-
if (stderrCollector !== void 0) child.stderr?.destroy();
|
|
942
|
-
stdoutCollector?.seal();
|
|
943
|
-
stderrCollector?.seal();
|
|
944
|
-
cleanup();
|
|
945
|
-
resolve({
|
|
946
|
-
exitCode,
|
|
947
|
-
signal
|
|
948
|
-
});
|
|
949
|
-
};
|
|
950
|
-
child.on("error", (error) => {
|
|
951
|
-
settled = true;
|
|
952
|
-
cleanup();
|
|
953
|
-
reject(error);
|
|
954
|
-
});
|
|
955
|
-
child.on("exit", (exitCode, signal) => {
|
|
956
|
-
pipeDrainTimer = setTimeout(() => {
|
|
957
|
-
settle(exitCode, signal);
|
|
958
|
-
}, spec.graceMs);
|
|
535
|
+
directResultType = result.type;
|
|
536
|
+
if (result.type === "target-exit") direct.resolve({
|
|
537
|
+
exitCode: result.exitCode,
|
|
538
|
+
signal: null
|
|
959
539
|
});
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
540
|
+
else direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error), result.error));
|
|
541
|
+
});
|
|
542
|
+
child.once("spawn", () => {
|
|
543
|
+
runnerSpawned = true;
|
|
544
|
+
try {
|
|
545
|
+
if (child.send === void 0) throw new Error("subprocess-local: Windows runner has no IPC channel");
|
|
546
|
+
child.send({
|
|
547
|
+
type: "start",
|
|
548
|
+
cwd: spec.cwd,
|
|
549
|
+
env: targetEnv
|
|
550
|
+
}, (error) => {
|
|
551
|
+
if (error === null) return;
|
|
552
|
+
failInfrastructure(error);
|
|
553
|
+
owner.terminateForHostExit();
|
|
554
|
+
});
|
|
555
|
+
} catch (error) {
|
|
556
|
+
failInfrastructure(error);
|
|
557
|
+
owner.terminateForHostExit();
|
|
964
558
|
}
|
|
965
559
|
});
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
await observed;
|
|
972
|
-
return true;
|
|
560
|
+
child.once("error", (error) => {
|
|
561
|
+
if (!runnerSpawned) {
|
|
562
|
+
direct.reject(error);
|
|
563
|
+
rangeExit.resolve();
|
|
564
|
+
return;
|
|
973
565
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
try {
|
|
982
|
-
return await Promise.race([observed.then(() => true), aborted.promise]);
|
|
983
|
-
} finally {
|
|
984
|
-
signal.removeEventListener("abort", onAbort);
|
|
566
|
+
failInfrastructure(error);
|
|
567
|
+
});
|
|
568
|
+
child.once("close", (exitCode, signal) => {
|
|
569
|
+
if (!runnerSpawned) return;
|
|
570
|
+
if (exitCode === 0 && signal === null && directResultType !== void 0) {
|
|
571
|
+
rangeExit.resolve();
|
|
572
|
+
return;
|
|
985
573
|
}
|
|
986
|
-
|
|
574
|
+
const status = signal !== null ? `signal ${signal}` : exitCode === null ? "without an exit status" : `exit code ${String(exitCode)}`;
|
|
575
|
+
failInfrastructure(/* @__PURE__ */ new Error(`subprocess-local: Windows Job runner exited with ${status} before proving its managed range empty`));
|
|
576
|
+
});
|
|
987
577
|
return {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
/* v8 ignore stop */
|
|
994
|
-
collected: {
|
|
995
|
-
...stdoutCollector !== void 0 ? { stdout: stdoutCollector } : {},
|
|
996
|
-
...stderrCollector !== void 0 ? { stderr: stderrCollector } : {}
|
|
997
|
-
},
|
|
998
|
-
done,
|
|
999
|
-
terminate,
|
|
1000
|
-
terminateForHostExit,
|
|
1001
|
-
waitForExit
|
|
578
|
+
stdin: spec.stdio.stdin === "ignore" ? null : targetStdin,
|
|
579
|
+
stdout: child.stdio[5],
|
|
580
|
+
stderr: child.stdio[6],
|
|
581
|
+
direct: direct.promise,
|
|
582
|
+
owner
|
|
1002
583
|
};
|
|
1003
584
|
}
|
|
1004
585
|
//#endregion
|
|
1005
586
|
//#region lib/types/terminal.js
|
|
1006
587
|
/** Local node-pty terminal-process implementation for the subprocess seam. */
|
|
1007
|
-
function delay(ms) {
|
|
1008
|
-
return new Promise((resolve) =>
|
|
588
|
+
function delay(ms, signal) {
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
const finish = () => {
|
|
591
|
+
clearTimeout(timer);
|
|
592
|
+
signal?.removeEventListener("abort", finish);
|
|
593
|
+
resolve();
|
|
594
|
+
};
|
|
595
|
+
const timer = setTimeout(finish, ms);
|
|
596
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
async function raceWithDelay(operation, ms, timeout) {
|
|
600
|
+
const controller = new AbortController();
|
|
601
|
+
try {
|
|
602
|
+
return await Promise.race([operation, delay(ms, controller.signal).then(() => timeout)]);
|
|
603
|
+
} finally {
|
|
604
|
+
controller.abort();
|
|
605
|
+
}
|
|
1009
606
|
}
|
|
1010
607
|
function signalName(number) {
|
|
1011
608
|
if (number === void 0 || number === 0) return null;
|
|
@@ -1013,7 +610,8 @@ function signalName(number) {
|
|
|
1013
610
|
return null;
|
|
1014
611
|
}
|
|
1015
612
|
/**
|
|
1016
|
-
* A local terminal whose
|
|
613
|
+
* A local terminal whose native managed range or fallback process-session
|
|
614
|
+
* ownership stays below the PTY backend.
|
|
1017
615
|
* The seam's terminate() promise — no write, inspection, or signal in flight
|
|
1018
616
|
* after settlement — holds here without operation tracking only because every
|
|
1019
617
|
* handle call completes synchronously under the hood (node-pty write, ps-based
|
|
@@ -1025,6 +623,8 @@ var LocalTerminalHandle = class {
|
|
|
1025
623
|
inspector;
|
|
1026
624
|
graceMs;
|
|
1027
625
|
platform;
|
|
626
|
+
managedOwner;
|
|
627
|
+
resolveManagedOutcome;
|
|
1028
628
|
pid;
|
|
1029
629
|
output = new PassThrough();
|
|
1030
630
|
done;
|
|
@@ -1032,6 +632,7 @@ var LocalTerminalHandle = class {
|
|
|
1032
632
|
dataDisposable;
|
|
1033
633
|
exitDisposable;
|
|
1034
634
|
cleanup;
|
|
635
|
+
managedOwnerCleaned = false;
|
|
1035
636
|
exited = false;
|
|
1036
637
|
trackedDescendants = [];
|
|
1037
638
|
/** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
|
|
@@ -1042,27 +643,38 @@ var LocalTerminalHandle = class {
|
|
|
1042
643
|
* @param graceMs - TERM-to-KILL and exit-wait grace.
|
|
1043
644
|
* @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
|
|
1044
645
|
*/
|
|
1045
|
-
constructor(terminal, inspector, graceMs, platform = process.platform) {
|
|
646
|
+
constructor(terminal, inspector, graceMs, platform = process.platform, managedOwner, resolveManagedOutcome) {
|
|
1046
647
|
this.terminal = terminal;
|
|
1047
648
|
this.inspector = inspector;
|
|
1048
649
|
this.graceMs = graceMs;
|
|
1049
650
|
this.platform = platform;
|
|
651
|
+
this.managedOwner = managedOwner;
|
|
652
|
+
this.resolveManagedOutcome = resolveManagedOutcome;
|
|
1050
653
|
this.pid = terminal.pid;
|
|
1051
654
|
this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
|
|
1052
655
|
this.done = this.outcome.promise;
|
|
1053
656
|
this.dataDisposable = terminal.onData((data) => {
|
|
1054
|
-
this.output.write(Buffer
|
|
657
|
+
this.output.write(Buffer.from(data, "utf8"));
|
|
1055
658
|
});
|
|
1056
659
|
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
|
1057
660
|
if (this.exited) return;
|
|
1058
661
|
this.exited = true;
|
|
1059
662
|
this.output.end();
|
|
1060
|
-
|
|
663
|
+
const outcome = {
|
|
1061
664
|
exitCode: exitSignal === void 0 || exitSignal === 0 ? exitCode : null,
|
|
1062
665
|
signal: signalName(exitSignal)
|
|
1063
|
-
}
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
this.outcome.resolve(this.resolveManagedOutcome?.(outcome) ?? outcome);
|
|
669
|
+
} catch (error) {
|
|
670
|
+
this.outcome.reject(error);
|
|
671
|
+
}
|
|
1064
672
|
});
|
|
1065
673
|
}
|
|
674
|
+
/** Whether node-pty has not yet published the top-level exit event. */
|
|
675
|
+
get running() {
|
|
676
|
+
return !this.exited;
|
|
677
|
+
}
|
|
1066
678
|
async write(data) {
|
|
1067
679
|
if (this.exited) throw new Error("terminal process has exited");
|
|
1068
680
|
this.terminal.write(data);
|
|
@@ -1107,6 +719,7 @@ var LocalTerminalHandle = class {
|
|
|
1107
719
|
this.forceStopDescendants();
|
|
1108
720
|
this.forceStopShell();
|
|
1109
721
|
this.forceStopDescendants();
|
|
722
|
+
this.managedOwner?.terminateForHostExit();
|
|
1110
723
|
}
|
|
1111
724
|
forceStopShell() {
|
|
1112
725
|
if (this.exited) return;
|
|
@@ -1218,6 +831,18 @@ var LocalTerminalHandle = class {
|
|
|
1218
831
|
}
|
|
1219
832
|
}
|
|
1220
833
|
async closeOnce() {
|
|
834
|
+
if (this.managedOwner !== void 0) {
|
|
835
|
+
try {
|
|
836
|
+
await this.closeManagedRange(this.managedOwner);
|
|
837
|
+
this.dataDisposable.dispose();
|
|
838
|
+
this.exitDisposable.dispose();
|
|
839
|
+
} finally {
|
|
840
|
+
this.done.finally(() => {
|
|
841
|
+
this.cleanupManagedOwner(this.managedOwner);
|
|
842
|
+
}).catch(() => {});
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
1221
846
|
let survivors = await this.stopDescendants();
|
|
1222
847
|
if (survivors.length > 0) throw new Error(`terminal cleanup failed; surviving pids: ${survivors.map((member) => member.pid).join(", ")}`);
|
|
1223
848
|
await this.stopShell();
|
|
@@ -1227,6 +852,33 @@ var LocalTerminalHandle = class {
|
|
|
1227
852
|
this.dataDisposable.dispose();
|
|
1228
853
|
this.exitDisposable.dispose();
|
|
1229
854
|
}
|
|
855
|
+
cleanupManagedOwner(owner) {
|
|
856
|
+
if (this.managedOwnerCleaned) return;
|
|
857
|
+
this.managedOwnerCleaned = true;
|
|
858
|
+
owner.cleanup?.();
|
|
859
|
+
}
|
|
860
|
+
async closeManagedRange(owner) {
|
|
861
|
+
owner.signal("SIGTERM");
|
|
862
|
+
const observation = owner.waitForExit();
|
|
863
|
+
const first = await raceWithDelay(observation.then(() => ({ kind: "stopped" }), (error) => ({
|
|
864
|
+
kind: "failed",
|
|
865
|
+
error
|
|
866
|
+
})), this.graceMs, { kind: "timeout" });
|
|
867
|
+
if (first.kind !== "stopped") {
|
|
868
|
+
owner.signal("SIGKILL");
|
|
869
|
+
if (first.kind === "failed") {
|
|
870
|
+
try {
|
|
871
|
+
await owner.waitForExit();
|
|
872
|
+
} catch (finalError) {
|
|
873
|
+
throw new AggregateError([first.error, finalError], "terminal managed-range cleanup failed");
|
|
874
|
+
}
|
|
875
|
+
throw first.error;
|
|
876
|
+
}
|
|
877
|
+
await observation;
|
|
878
|
+
}
|
|
879
|
+
if (!this.exited) await raceWithDelay(this.done.then(() => void 0), this.graceMs, void 0);
|
|
880
|
+
if (!this.exited) throw new Error(`terminal cleanup failed; surviving pid: ${this.pid}`);
|
|
881
|
+
}
|
|
1230
882
|
settleExitIfGone() {
|
|
1231
883
|
if (this.platform !== "win32") return;
|
|
1232
884
|
if (this.exited) return;
|
|
@@ -1244,28 +896,32 @@ var LocalTerminalHandle = class {
|
|
|
1244
896
|
//#endregion
|
|
1245
897
|
//#region lib/types/index.js
|
|
1246
898
|
/**
|
|
1247
|
-
* Local Service Provider for the subprocess capability seam. Each spawn
|
|
1248
|
-
*
|
|
1249
|
-
* terminates and joins live
|
|
1250
|
-
* any
|
|
1251
|
-
* limit arrives on the spec, so
|
|
1252
|
-
* caller's config (the bash executor's, the LSP host's, …).
|
|
899
|
+
* Local Service Provider for the subprocess capability seam. Each spawn owns a
|
|
900
|
+
* platform-selected managed range with the spec's per-stream stdio dispositions.
|
|
901
|
+
* Normal disposal terminates and joins live ranges; Node's synchronous exit
|
|
902
|
+
* phase force-stops any ranges the service still owns. It has no config: every
|
|
903
|
+
* disposition and limit arrives on the spec, so deployment-varying choices
|
|
904
|
+
* stay with the caller's config (the bash executor's, the LSP host's, …).
|
|
1253
905
|
* @module @deepseek-ai/dsh-subprocess-local
|
|
1254
906
|
*/
|
|
1255
907
|
/**
|
|
1256
|
-
* Local subprocess service:
|
|
908
|
+
* Local subprocess service: platform-selected managed ranges, Node-shaped stdio
|
|
1257
909
|
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
|
|
1258
|
-
* files), credential-scrubbed environment, and
|
|
1259
|
-
*
|
|
1260
|
-
* JavaScript-observable host exit.
|
|
910
|
+
* files), credential-scrubbed environment, and provider-owned range signalling.
|
|
911
|
+
* POSIX paths stage TERM before KILL; Windows paths terminate immediately.
|
|
912
|
+
* JavaScript-observable host exit also performs synchronous final termination.
|
|
1261
913
|
*/
|
|
1262
914
|
var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
1263
915
|
/** Live handles retained for normal disposal and synchronous host-exit finalization. */
|
|
1264
916
|
live = /* @__PURE__ */ new Set();
|
|
1265
917
|
/** Live terminals retained through normal quiescence or host-exit finalization. */
|
|
1266
918
|
terminals = /* @__PURE__ */ new Set();
|
|
1267
|
-
/** Test hook: spill and platform
|
|
919
|
+
/** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
|
|
1268
920
|
internals = {};
|
|
921
|
+
/** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
|
|
922
|
+
fallbackWarningIssued = false;
|
|
923
|
+
/** Positive-only cache for the expensive Linux bootstrap and scope probe. */
|
|
924
|
+
linuxDeepProbePassed = false;
|
|
1269
925
|
/** Test hook for platform process inspection; production resolves lazily on terminal spawn. */
|
|
1270
926
|
terminalInspector;
|
|
1271
927
|
constructor(ctx) {
|
|
@@ -1276,18 +932,15 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1276
932
|
};
|
|
1277
933
|
process.prependListener("exit", onHostExit);
|
|
1278
934
|
return async () => {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
} finally {
|
|
1282
|
-
process.off("exit", onHostExit);
|
|
1283
|
-
}
|
|
935
|
+
await this.disposeManagedProcesses();
|
|
936
|
+
process.off("exit", onHostExit);
|
|
1284
937
|
};
|
|
1285
938
|
}, "local subprocess teardown");
|
|
1286
939
|
}
|
|
1287
940
|
terminateForHostExit() {
|
|
1288
941
|
for (const handle of this.live) try {
|
|
1289
942
|
handle.terminateForHostExit();
|
|
1290
|
-
} catch (
|
|
943
|
+
} catch (_ordinaryRangeTerminationFailed) {}
|
|
1291
944
|
for (const terminal of this.terminals) try {
|
|
1292
945
|
terminal.terminateForHostExit();
|
|
1293
946
|
} catch (_terminalTerminationFailed) {}
|
|
@@ -1296,13 +949,17 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1296
949
|
const pending = [];
|
|
1297
950
|
for (const handle of this.live) {
|
|
1298
951
|
handle.terminate();
|
|
1299
|
-
pending.push(handle.done.catch(() => {}).then(() =>
|
|
952
|
+
pending.push(Promise.all([handle.done.catch(() => {}), handle.waitForExit()]).then(() => {
|
|
953
|
+
this.live.delete(handle);
|
|
954
|
+
}));
|
|
1300
955
|
}
|
|
1301
|
-
for (const terminal of this.terminals) pending.push(terminal.terminate())
|
|
1302
|
-
|
|
956
|
+
for (const terminal of this.terminals) pending.push(terminal.terminate().then(() => {
|
|
957
|
+
this.terminals.delete(terminal);
|
|
958
|
+
}));
|
|
959
|
+
const outcomes = await Promise.allSettled(pending);
|
|
960
|
+
const failures = [];
|
|
961
|
+
for (const outcome of outcomes) if (outcome.status === "rejected") failures.push(outcome.reason);
|
|
1303
962
|
if (failures.length > 0) this.terminateForHostExit();
|
|
1304
|
-
this.live.clear();
|
|
1305
|
-
this.terminals.clear();
|
|
1306
963
|
if (failures.length === 1) throw failures[0];
|
|
1307
964
|
if (failures.length > 1) throw new AggregateError(failures, "local subprocess teardown failed");
|
|
1308
965
|
}
|
|
@@ -1331,27 +988,82 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1331
988
|
return path.split(delimiter).flatMap((directory) => extensions.map((extension) => resolve(process.cwd(), directory, command + extension)));
|
|
1332
989
|
}
|
|
1333
990
|
spawn(spec) {
|
|
1334
|
-
|
|
991
|
+
validateSubprocessSpec(spec);
|
|
992
|
+
const env = targetEnvironment(spec);
|
|
993
|
+
const containmentMode = this.selectContainmentMode("ordinary");
|
|
994
|
+
let handle;
|
|
995
|
+
if (containmentMode === "fallback") handle = spawnSubprocess(spec, this.internals);
|
|
996
|
+
else {
|
|
997
|
+
const binding = prepareManagedProcessBinding(this.internals);
|
|
998
|
+
handle = bindManagedProcess(spec, containmentMode === "linux-scope" ? launchLinuxScope(spec, env) : launchWindowsJob(spec, env), binding);
|
|
999
|
+
}
|
|
1335
1000
|
this.live.add(handle);
|
|
1336
1001
|
const release = () => handle.waitForExit().then(() => {
|
|
1337
1002
|
this.live.delete(handle);
|
|
1338
1003
|
});
|
|
1339
|
-
handle.done.then(release, release);
|
|
1004
|
+
handle.done.then(release, release).catch(() => {});
|
|
1340
1005
|
return handle;
|
|
1341
1006
|
}
|
|
1007
|
+
selectContainmentMode(kind) {
|
|
1008
|
+
const platform = this.internals.platform ?? process.platform;
|
|
1009
|
+
let fallbackReason;
|
|
1010
|
+
if (platform === "linux") {
|
|
1011
|
+
const available = this.linuxDeepProbePassed ? probeLinuxManager() : probeLinuxNative();
|
|
1012
|
+
if (available) this.linuxDeepProbePassed = true;
|
|
1013
|
+
if (available) return "linux-scope";
|
|
1014
|
+
fallbackReason = "the current user-systemd scope or private bootstrap is unavailable";
|
|
1015
|
+
}
|
|
1016
|
+
if (kind === "ordinary" && platform === "win32") {
|
|
1017
|
+
if (probeWindowsJob()) return "windows-job";
|
|
1018
|
+
}
|
|
1019
|
+
this.warnFallback(platform, kind, fallbackReason);
|
|
1020
|
+
return "fallback";
|
|
1021
|
+
}
|
|
1022
|
+
warnFallback(platform, kind, selectedReason) {
|
|
1023
|
+
if (this.fallbackWarningIssued) return;
|
|
1024
|
+
this.fallbackWarningIssued = true;
|
|
1025
|
+
const reason = selectedReason ?? (platform === "darwin" ? "macOS has no supported persistent process-range owner" : platform === "win32" ? kind === "terminal" ? "Windows ConPTY remains outside Job containment" : "the Win32 Job runner is unavailable" : `platform ${platform} has no native managed range`);
|
|
1026
|
+
this.ctx.logger.warn(`subprocess-local is using weaker process-tree containment because ${reason}; descendants that escape the process group or direct-parent tree are not guaranteed to terminate or delay waitForExit()`);
|
|
1027
|
+
}
|
|
1342
1028
|
async spawnTerminal(spec) {
|
|
1343
1029
|
const file = spec.argv[0];
|
|
1344
1030
|
if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
|
|
1345
1031
|
spec.signal?.throwIfAborted();
|
|
1032
|
+
const env = targetEnvironment(spec);
|
|
1346
1033
|
const options = {
|
|
1347
1034
|
name: "dumb",
|
|
1348
1035
|
rows: spec.rows,
|
|
1349
1036
|
cols: spec.cols,
|
|
1350
1037
|
cwd: spec.cwd,
|
|
1351
|
-
env
|
|
1038
|
+
env
|
|
1352
1039
|
};
|
|
1353
1040
|
const inspector = this.terminalInspector ?? createProcessInspector();
|
|
1354
|
-
const
|
|
1041
|
+
const scope = this.selectContainmentMode("terminal") === "linux-scope" ? prepareLinuxTerminalScope(spec, {
|
|
1042
|
+
...env,
|
|
1043
|
+
PWD: spec.cwd,
|
|
1044
|
+
TERM: "dumb"
|
|
1045
|
+
}) : void 0;
|
|
1046
|
+
if (scope !== void 0) {
|
|
1047
|
+
options.cwd = scope.cwd;
|
|
1048
|
+
options.env = scope.env;
|
|
1049
|
+
}
|
|
1050
|
+
let terminal;
|
|
1051
|
+
try {
|
|
1052
|
+
terminal = nodePty.spawn(scope?.command ?? file, scope?.args ?? [...spec.argv.slice(1)], options);
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
scope?.cleanup();
|
|
1055
|
+
throw error;
|
|
1056
|
+
}
|
|
1057
|
+
let handle;
|
|
1058
|
+
const owner = scope?.bindOwner({
|
|
1059
|
+
running: () => handle?.running ?? true,
|
|
1060
|
+
signal: (signal) => {
|
|
1061
|
+
try {
|
|
1062
|
+
terminal.kill(signal);
|
|
1063
|
+
} catch {}
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome);
|
|
1355
1067
|
this.terminals.add(handle);
|
|
1356
1068
|
const release = async () => {
|
|
1357
1069
|
await handle.terminate();
|