@deepseek-ai/dsh-subprocess-local 0.1.5-rc.2 → 0.1.6-alpha.1
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 +13 -2
- package/README.zh.md +20 -9
- package/lib/index.js +218 -71
- package/lib/output.js +181 -0
- package/lib/{runner-launch-COYGu0Dl.js → runner-launch-BnjugUhn.js} +63 -183
- 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 +2 -1
- package/lib/types/output.d.ts +85 -0
- package/lib/types/runner-launch.d.ts +4 -2
- package/lib/types/runner-protocol.d.ts +2 -0
- package/lib/types/spawn-runner.d.ts +1 -1
- package/lib/types/spawn.d.ts +1 -74
- package/lib/types/terminal.d.ts +2 -0
- package/package.json +13 -8
package/lib/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { C as bindManagedProcess, D as
|
|
1
|
+
import { C as bindManagedProcess, D as createProcessInspector, E as validateSubprocessSpec, O as controlPipe, S as loadLinuxExecve, T as spawnSubprocess, _ 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-BnjugUhn.js";
|
|
2
|
+
import { prepareManagedProcessBinding } from "./output.js";
|
|
2
3
|
import { closeSync, constants, existsSync, openSync } from "node:fs";
|
|
3
4
|
import { access, stat } from "node:fs/promises";
|
|
5
|
+
import { constants as constants$1, devNull, userInfo } from "node:os";
|
|
4
6
|
import { delimiter, extname, isAbsolute, resolve } from "node:path";
|
|
5
7
|
import * as nodePty from "node-pty";
|
|
6
|
-
import { SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
8
|
+
import { SubprocessExecutableNotFoundError, SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
7
9
|
import { execFile, spawn, spawnSync } from "node:child_process";
|
|
8
|
-
import { randomBytes } from "node:crypto";
|
|
9
|
-
import { constants as constants$1, devNull } from "node:os";
|
|
10
10
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
11
12
|
import { loadWin32ProcessBindings, probeCurrentTokenJobSupport } from "@deepseek-ai/dsh-win32-process";
|
|
12
13
|
import { Buffer } from "node:buffer";
|
|
13
14
|
import { PassThrough } from "node:stream";
|
|
@@ -118,9 +119,24 @@ function probeLinuxManager(internals = {}) {
|
|
|
118
119
|
function probeLinuxNative(internals = {}) {
|
|
119
120
|
return probeLinuxBootstrap(internals) && probeLinuxScope(internals);
|
|
120
121
|
}
|
|
122
|
+
var LinuxScopeStartup = class {
|
|
123
|
+
files;
|
|
124
|
+
kind;
|
|
125
|
+
terminationSignals = /* @__PURE__ */ new Set();
|
|
126
|
+
constructor(files, kind) {
|
|
127
|
+
this.files = files;
|
|
128
|
+
this.kind = kind;
|
|
129
|
+
}
|
|
130
|
+
resolveOutcome(outcome) {
|
|
131
|
+
const startup = readLinuxStartupError(this.files.startupErrorPath);
|
|
132
|
+
if (startup !== void 0) throw deserializeRunnerError(startup.error);
|
|
133
|
+
if (existsSync(this.files.requestPath) && !(outcome.signal !== null && this.terminationSignals.has(outcome.signal))) throw new Error(`${this.kind} scope exited before its bootstrap consumed the launch request`);
|
|
134
|
+
return outcome;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
121
137
|
var SystemdScopeOwner = class {
|
|
122
138
|
unit;
|
|
123
|
-
|
|
139
|
+
startup;
|
|
124
140
|
direct;
|
|
125
141
|
systemctl;
|
|
126
142
|
runSync;
|
|
@@ -128,13 +144,15 @@ var SystemdScopeOwner = class {
|
|
|
128
144
|
sleep;
|
|
129
145
|
establishment = "pending";
|
|
130
146
|
stopped = false;
|
|
147
|
+
terminationRequested = false;
|
|
131
148
|
observation;
|
|
132
149
|
killFailure;
|
|
150
|
+
directKillSettlement;
|
|
133
151
|
wakeGeneration = 0;
|
|
134
152
|
wakeWaiter;
|
|
135
|
-
constructor(unit,
|
|
153
|
+
constructor(unit, startup, direct, systemctl, runSync, query, sleep) {
|
|
136
154
|
this.unit = unit;
|
|
137
|
-
this.
|
|
155
|
+
this.startup = startup;
|
|
138
156
|
this.direct = direct;
|
|
139
157
|
this.systemctl = systemctl;
|
|
140
158
|
this.runSync = runSync;
|
|
@@ -143,9 +161,12 @@ var SystemdScopeOwner = class {
|
|
|
143
161
|
}
|
|
144
162
|
signal(signal) {
|
|
145
163
|
if (this.stopped) return;
|
|
164
|
+
this.terminationRequested = true;
|
|
165
|
+
if (this.direct.running()) this.startup.terminationSignals.add(signal);
|
|
146
166
|
this.observeRequestConsumption();
|
|
147
167
|
const directFallbackRequired = this.establishment === "pending";
|
|
148
|
-
|
|
168
|
+
let directSignalled = false;
|
|
169
|
+
if (directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
|
|
149
170
|
const result = this.runSync(this.systemctl, [
|
|
150
171
|
"--user",
|
|
151
172
|
"kill",
|
|
@@ -159,13 +180,19 @@ var SystemdScopeOwner = class {
|
|
|
159
180
|
});
|
|
160
181
|
this.wakeObservation();
|
|
161
182
|
if (result.error === void 0 && result.status === 0) {
|
|
162
|
-
if (signal === "SIGKILL")
|
|
183
|
+
if (signal === "SIGKILL") {
|
|
184
|
+
this.killFailure = void 0;
|
|
185
|
+
this.directKillSettlement = void 0;
|
|
186
|
+
}
|
|
163
187
|
return;
|
|
164
188
|
}
|
|
165
|
-
if (!directFallbackRequired && this.direct.running()) this.direct.signal(signal);
|
|
189
|
+
if (!directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
|
|
166
190
|
if (signal === "SIGKILL") {
|
|
167
191
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
168
|
-
if (!MISSING_UNIT.test(output))
|
|
192
|
+
if (!MISSING_UNIT.test(output)) {
|
|
193
|
+
this.killFailure = result.error ?? /* @__PURE__ */ new Error(`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
|
|
194
|
+
this.directKillSettlement = directSignalled ? this.direct.settled.then(() => {}, () => {}) : void 0;
|
|
195
|
+
}
|
|
169
196
|
}
|
|
170
197
|
}
|
|
171
198
|
terminateForHostExit() {
|
|
@@ -188,15 +215,40 @@ var SystemdScopeOwner = class {
|
|
|
188
215
|
} catch {}
|
|
189
216
|
}
|
|
190
217
|
observeRequestConsumption() {
|
|
191
|
-
if (this.establishment === "pending" && !existsSync(this.files.requestPath)) this.establishment = "established";
|
|
218
|
+
if (this.establishment === "pending" && !existsSync(this.startup.files.requestPath)) this.establishment = "established";
|
|
192
219
|
}
|
|
193
220
|
absentUnit() {
|
|
194
221
|
this.observeRequestConsumption();
|
|
195
222
|
if (this.establishment === "established") return false;
|
|
196
|
-
if (!this.direct.running() && existsSync(this.files.requestPath)) return false;
|
|
223
|
+
if (!this.direct.running() && existsSync(this.startup.files.requestPath)) return false;
|
|
197
224
|
if (this.killFailure !== void 0) throw this.killFailure;
|
|
198
225
|
return true;
|
|
199
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Prove an active unit with no processes is the empty managed range rather
|
|
229
|
+
* than a launch still placing its payload. systemd ends a scope only on the
|
|
230
|
+
* populated-to-empty transition, so a payload killed before it entered the
|
|
231
|
+
* cgroup leaves the unit active forever. A departed client cannot add another
|
|
232
|
+
* payload; a consumed request proves the payload already entered the scope,
|
|
233
|
+
* even while its direct-process exit notification is pending.
|
|
234
|
+
*/
|
|
235
|
+
emptyRange(tasksCurrent) {
|
|
236
|
+
return this.terminationRequested && tasksCurrent === 0 && (!this.direct.running() || !existsSync(this.startup.files.requestPath));
|
|
237
|
+
}
|
|
238
|
+
/** Release a leftover empty scope so the transient unit is collected and cannot accumulate. */
|
|
239
|
+
releaseEmptyRange() {
|
|
240
|
+
try {
|
|
241
|
+
this.runSync(this.systemctl, [
|
|
242
|
+
"--user",
|
|
243
|
+
"stop",
|
|
244
|
+
this.unit
|
|
245
|
+
], {
|
|
246
|
+
env: managerEnvironment(),
|
|
247
|
+
stdio: "ignore",
|
|
248
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
249
|
+
});
|
|
250
|
+
} catch {}
|
|
251
|
+
}
|
|
200
252
|
parseUnitState(stdout) {
|
|
201
253
|
const values = /* @__PURE__ */ new Map();
|
|
202
254
|
for (const line of stdout.split(/\r?\n/u)) {
|
|
@@ -209,24 +261,32 @@ var SystemdScopeOwner = class {
|
|
|
209
261
|
}
|
|
210
262
|
const loadState = values.get("LoadState");
|
|
211
263
|
const activeState = values.get("ActiveState");
|
|
212
|
-
|
|
264
|
+
const reportedTasks = values.get("TasksCurrent");
|
|
265
|
+
const tasksCurrent = reportedTasks === "[not set]" ? void 0 : reportedTasks;
|
|
266
|
+
if (values.size !== (reportedTasks === void 0 ? 2 : 3) || loadState === void 0 || activeState === void 0) throw new Error(`systemctl returned incomplete state for ${this.unit}: ${JSON.stringify(stdout.trim())}`);
|
|
267
|
+
if (tasksCurrent !== void 0 && !/^\d+$/u.test(tasksCurrent)) throw new Error(`systemctl returned a non-numeric TasksCurrent for ${this.unit}: ${JSON.stringify(tasksCurrent)}`);
|
|
213
268
|
return {
|
|
214
269
|
loadState,
|
|
215
|
-
activeState
|
|
270
|
+
activeState,
|
|
271
|
+
tasksCurrent: tasksCurrent === void 0 ? void 0 : Number(tasksCurrent)
|
|
216
272
|
};
|
|
217
273
|
}
|
|
218
274
|
async rangeActive() {
|
|
219
275
|
this.observeRequestConsumption();
|
|
276
|
+
const generation = this.wakeGeneration;
|
|
277
|
+
const directRunning = this.direct.running();
|
|
220
278
|
const result = await this.query(this.systemctl, [
|
|
221
279
|
"--user",
|
|
222
280
|
"show",
|
|
223
281
|
this.unit,
|
|
224
282
|
"--property=LoadState",
|
|
225
|
-
"--property=ActiveState"
|
|
283
|
+
"--property=ActiveState",
|
|
284
|
+
"--property=TasksCurrent"
|
|
226
285
|
]);
|
|
286
|
+
if (generation !== this.wakeGeneration) return true;
|
|
227
287
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
228
288
|
if (result.status === 0) {
|
|
229
|
-
const { loadState, activeState } = this.parseUnitState(result.stdout);
|
|
289
|
+
const { loadState, activeState, tasksCurrent } = this.parseUnitState(result.stdout);
|
|
230
290
|
if (loadState === "not-found" && activeState === "inactive") return this.absentUnit();
|
|
231
291
|
if (loadState !== "loaded") throw new Error(`systemctl returned unknown state for ${this.unit}: ${JSON.stringify({
|
|
232
292
|
loadState,
|
|
@@ -240,7 +300,19 @@ var SystemdScopeOwner = class {
|
|
|
240
300
|
"reloading",
|
|
241
301
|
"deactivating"
|
|
242
302
|
].includes(activeState)) throw new Error(`systemctl returned unknown ActiveState for ${this.unit}: ${JSON.stringify(activeState)}`);
|
|
243
|
-
if (this.
|
|
303
|
+
if (this.emptyRange(tasksCurrent)) {
|
|
304
|
+
this.releaseEmptyRange();
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (this.killFailure !== void 0) {
|
|
308
|
+
if (directRunning && this.directKillSettlement !== void 0) {
|
|
309
|
+
const settlement = this.directKillSettlement;
|
|
310
|
+
this.directKillSettlement = void 0;
|
|
311
|
+
await settlement;
|
|
312
|
+
return this.rangeActive();
|
|
313
|
+
}
|
|
314
|
+
throw this.killFailure;
|
|
315
|
+
}
|
|
244
316
|
return true;
|
|
245
317
|
}
|
|
246
318
|
if (!MISSING_UNIT.test(output)) {
|
|
@@ -288,7 +360,7 @@ var SystemdScopeOwner = class {
|
|
|
288
360
|
await this.observation;
|
|
289
361
|
}
|
|
290
362
|
cleanup() {
|
|
291
|
-
cleanupLinuxLaunchFiles(this.files);
|
|
363
|
+
cleanupLinuxLaunchFiles(this.startup.files);
|
|
292
364
|
}
|
|
293
365
|
};
|
|
294
366
|
function scopeArgs(unitBase, invocation, argv) {
|
|
@@ -305,7 +377,7 @@ function scopeArgs(unitBase, invocation, argv) {
|
|
|
305
377
|
...argv
|
|
306
378
|
];
|
|
307
379
|
}
|
|
308
|
-
function directOutcome(child,
|
|
380
|
+
function directOutcome(child, startup) {
|
|
309
381
|
return new Promise((resolveOutcome, rejectOutcome) => {
|
|
310
382
|
let settled = false;
|
|
311
383
|
child.once("error", (error) => {
|
|
@@ -317,40 +389,47 @@ function directOutcome(child, files) {
|
|
|
317
389
|
if (settled) return;
|
|
318
390
|
settled = true;
|
|
319
391
|
try {
|
|
320
|
-
|
|
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({
|
|
392
|
+
resolveOutcome(startup.resolveOutcome({
|
|
330
393
|
exitCode,
|
|
331
394
|
signal
|
|
332
|
-
});
|
|
395
|
+
}));
|
|
333
396
|
} catch (error) {
|
|
334
397
|
rejectOutcome(error instanceof Error ? error : new Error(String(error)));
|
|
335
398
|
}
|
|
336
399
|
});
|
|
337
400
|
});
|
|
338
401
|
}
|
|
339
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Send a direct-process signal, distinguishing an absent PID from failed delivery.
|
|
404
|
+
* @param pid - owned direct-process identity whose exit notification can still be pending.
|
|
405
|
+
* @param send - platform signal operation; true means the signal was submitted.
|
|
406
|
+
* @returns whether the signal was submitted or the owned PID is already absent.
|
|
407
|
+
*/
|
|
408
|
+
function signalLinuxDirectProcess(pid, send) {
|
|
340
409
|
try {
|
|
341
|
-
|
|
342
|
-
} catch {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
410
|
+
if (send()) return true;
|
|
411
|
+
} catch {}
|
|
412
|
+
try {
|
|
413
|
+
process.kill(pid, 0);
|
|
414
|
+
return false;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
return error.code === "ESRCH";
|
|
346
417
|
}
|
|
347
418
|
}
|
|
419
|
+
function signalChildGroup(child, signal) {
|
|
420
|
+
let groupSignalled = false;
|
|
421
|
+
try {
|
|
422
|
+
groupSignalled = process.kill(-child.pid, signal);
|
|
423
|
+
} catch {}
|
|
424
|
+
if (groupSignalled && signal === "SIGTERM") return true;
|
|
425
|
+
return signalLinuxDirectProcess(child.pid, () => process.kill(child.pid, signal));
|
|
426
|
+
}
|
|
348
427
|
/**
|
|
349
428
|
* Prepare one Linux PTY scope using the same launch request and bootstrap core.
|
|
350
429
|
* @param spec - terminal target request.
|
|
351
430
|
* @param targetEnv - validated complete target environment.
|
|
352
431
|
* @param internals - optional runner and systemd seams used by tests.
|
|
353
|
-
* @returns invocation
|
|
432
|
+
* @returns invocation and ownership callbacks; requested termination preserves the observed signal even before bootstrap consumption.
|
|
354
433
|
*/
|
|
355
434
|
function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
356
435
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
@@ -358,19 +437,15 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
|
358
437
|
cwd: spec.cwd,
|
|
359
438
|
env: targetEnv
|
|
360
439
|
});
|
|
440
|
+
const startup = new LinuxScopeStartup(files, "terminal");
|
|
361
441
|
const unitBase = unitStem("dsh-terminal");
|
|
362
442
|
return {
|
|
363
443
|
command: internals.systemdRun ?? "systemd-run",
|
|
364
444
|
args: scopeArgs(unitBase, invocation, spec.argv),
|
|
365
445
|
cwd: process.cwd(),
|
|
366
446
|
env: runnerEnvironment(files.requestPath, invocation),
|
|
367
|
-
bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`,
|
|
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
|
-
},
|
|
447
|
+
bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`, startup, direct, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort),
|
|
448
|
+
resolveOutcome: (outcome) => startup.resolveOutcome(outcome),
|
|
374
449
|
cleanup: () => {
|
|
375
450
|
cleanupLinuxLaunchFiles(files);
|
|
376
451
|
}
|
|
@@ -381,14 +456,16 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
|
381
456
|
* @param spec - ordinary target request.
|
|
382
457
|
* @param targetEnv - validated complete target environment.
|
|
383
458
|
* @param internals - optional runner and systemd seams used by tests.
|
|
384
|
-
* @returns
|
|
459
|
+
* @returns streams, result, and scope owner; requested termination preserves the observed signal even before bootstrap consumption.
|
|
385
460
|
*/
|
|
386
461
|
function launchLinuxScope(spec, targetEnv, internals = {}) {
|
|
387
462
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
388
463
|
const files = createLinuxLaunchFiles({
|
|
389
464
|
cwd: spec.cwd,
|
|
390
|
-
env: targetEnv
|
|
465
|
+
env: targetEnv,
|
|
466
|
+
...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
|
|
391
467
|
});
|
|
468
|
+
const startup = new LinuxScopeStartup(files, "subprocess");
|
|
392
469
|
const unitBase = unitStem("dsh-subprocess");
|
|
393
470
|
let child;
|
|
394
471
|
try {
|
|
@@ -402,17 +479,18 @@ function launchLinuxScope(spec, targetEnv, internals = {}) {
|
|
|
402
479
|
cleanupLinuxLaunchFiles(files);
|
|
403
480
|
throw error;
|
|
404
481
|
}
|
|
405
|
-
const
|
|
482
|
+
const direct = directOutcome(child, startup);
|
|
483
|
+
const owner = new SystemdScopeOwner(`${unitBase}.scope`, startup, {
|
|
406
484
|
running: () => child.pid !== void 0 && child.exitCode === null && child.signalCode === null,
|
|
407
|
-
signal: (signal) =>
|
|
408
|
-
|
|
409
|
-
}
|
|
485
|
+
signal: (signal) => signalChildGroup(child, signal),
|
|
486
|
+
settled: direct
|
|
410
487
|
}, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort);
|
|
411
488
|
return {
|
|
412
489
|
stdin: child.stdin,
|
|
413
490
|
stdout: child.stdout,
|
|
414
491
|
stderr: child.stderr,
|
|
415
|
-
|
|
492
|
+
control: controlPipe(child, spec.stdio.control),
|
|
493
|
+
direct,
|
|
416
494
|
owner
|
|
417
495
|
};
|
|
418
496
|
}
|
|
@@ -513,10 +591,25 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
513
591
|
const rangeExit = Promise.withResolvers();
|
|
514
592
|
let directResultType;
|
|
515
593
|
let runnerSpawned = false;
|
|
594
|
+
let runnerExit;
|
|
595
|
+
let ipcDisconnected = false;
|
|
516
596
|
const failInfrastructure = (error) => {
|
|
517
597
|
direct.reject(error);
|
|
518
598
|
rangeExit.reject(error);
|
|
519
599
|
};
|
|
600
|
+
const settleRange = () => {
|
|
601
|
+
if (!runnerSpawned || runnerExit === void 0) return;
|
|
602
|
+
const { exitCode, signal } = runnerExit;
|
|
603
|
+
if (exitCode === 0 && signal === null) {
|
|
604
|
+
if (directResultType !== void 0) {
|
|
605
|
+
rangeExit.resolve();
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!ipcDisconnected) return;
|
|
609
|
+
}
|
|
610
|
+
const status = signal !== null ? `signal ${signal}` : exitCode === null ? "without an exit status" : `exit code ${String(exitCode)}`;
|
|
611
|
+
failInfrastructure(/* @__PURE__ */ new Error(`subprocess-local: Windows Job runner exited with ${status} before proving its managed range empty`));
|
|
612
|
+
};
|
|
520
613
|
const owner = new WindowsJobOwner(child, rangeExit.promise, () => directResultType, failInfrastructure);
|
|
521
614
|
child.on("message", (value) => {
|
|
522
615
|
if (directResultType !== void 0) {
|
|
@@ -538,6 +631,7 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
538
631
|
signal: null
|
|
539
632
|
});
|
|
540
633
|
else direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error), result.error));
|
|
634
|
+
settleRange();
|
|
541
635
|
});
|
|
542
636
|
child.once("spawn", () => {
|
|
543
637
|
runnerSpawned = true;
|
|
@@ -546,7 +640,8 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
546
640
|
child.send({
|
|
547
641
|
type: "start",
|
|
548
642
|
cwd: spec.cwd,
|
|
549
|
-
env: targetEnv
|
|
643
|
+
env: targetEnv,
|
|
644
|
+
...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
|
|
550
645
|
}, (error) => {
|
|
551
646
|
if (error === null) return;
|
|
552
647
|
failInfrastructure(error);
|
|
@@ -565,19 +660,22 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
565
660
|
}
|
|
566
661
|
failInfrastructure(error);
|
|
567
662
|
});
|
|
568
|
-
child.once("
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
663
|
+
child.once("exit", (exitCode, signal) => {
|
|
664
|
+
runnerExit = {
|
|
665
|
+
exitCode,
|
|
666
|
+
signal
|
|
667
|
+
};
|
|
668
|
+
settleRange();
|
|
669
|
+
});
|
|
670
|
+
child.once("disconnect", () => {
|
|
671
|
+
ipcDisconnected = true;
|
|
672
|
+
settleRange();
|
|
576
673
|
});
|
|
577
674
|
return {
|
|
578
675
|
stdin: spec.stdio.stdin === "ignore" ? null : targetStdin,
|
|
579
676
|
stdout: child.stdio[5],
|
|
580
677
|
stderr: child.stdio[6],
|
|
678
|
+
control: controlPipe(child, spec.stdio.control),
|
|
581
679
|
direct: direct.promise,
|
|
582
680
|
owner
|
|
583
681
|
};
|
|
@@ -634,6 +732,7 @@ var LocalTerminalHandle = class {
|
|
|
634
732
|
cleanup;
|
|
635
733
|
managedOwnerCleaned = false;
|
|
636
734
|
exited = false;
|
|
735
|
+
outputPaused = false;
|
|
637
736
|
trackedDescendants = [];
|
|
638
737
|
/** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
|
|
639
738
|
rootIdentity;
|
|
@@ -653,8 +752,20 @@ var LocalTerminalHandle = class {
|
|
|
653
752
|
this.pid = terminal.pid;
|
|
654
753
|
this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
|
|
655
754
|
this.done = this.outcome.promise;
|
|
755
|
+
const resume = () => {
|
|
756
|
+
if (!this.outputPaused) return;
|
|
757
|
+
this.outputPaused = false;
|
|
758
|
+
if (!this.exited) terminal.resume();
|
|
759
|
+
};
|
|
760
|
+
this.output.on("drain", resume);
|
|
761
|
+
this.output.once("close", () => {
|
|
762
|
+
this.output.off("drain", resume);
|
|
763
|
+
});
|
|
656
764
|
this.dataDisposable = terminal.onData((data) => {
|
|
657
|
-
this.output.write(Buffer.from(data, "utf8"))
|
|
765
|
+
if (!this.output.write(Buffer.from(data, "utf8")) && this.cleanup === void 0 && !this.outputPaused) {
|
|
766
|
+
this.outputPaused = true;
|
|
767
|
+
terminal.pause();
|
|
768
|
+
}
|
|
658
769
|
});
|
|
659
770
|
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
|
660
771
|
if (this.exited) return;
|
|
@@ -679,6 +790,10 @@ var LocalTerminalHandle = class {
|
|
|
679
790
|
if (this.exited) throw new Error("terminal process has exited");
|
|
680
791
|
this.terminal.write(data);
|
|
681
792
|
}
|
|
793
|
+
async resize(cols, rows) {
|
|
794
|
+
if (this.exited) throw new Error("terminal process has exited");
|
|
795
|
+
this.terminal.resize(cols, rows);
|
|
796
|
+
}
|
|
682
797
|
async inspectForeground() {
|
|
683
798
|
this.descendants(this.inspector.snapshot());
|
|
684
799
|
const processGroupId = this.inspector.foregroundPgid(this.pid);
|
|
@@ -704,6 +819,10 @@ var LocalTerminalHandle = class {
|
|
|
704
819
|
}
|
|
705
820
|
terminate() {
|
|
706
821
|
if (this.cleanup !== void 0) return this.cleanup;
|
|
822
|
+
if (this.outputPaused) {
|
|
823
|
+
this.outputPaused = false;
|
|
824
|
+
this.terminal.resume();
|
|
825
|
+
}
|
|
707
826
|
const cleanup = this.closeOnce();
|
|
708
827
|
this.cleanup = cleanup;
|
|
709
828
|
cleanup.catch(() => {
|
|
@@ -916,6 +1035,8 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
916
1035
|
live = /* @__PURE__ */ new Set();
|
|
917
1036
|
/** Live terminals retained through normal quiescence or host-exit finalization. */
|
|
918
1037
|
terminals = /* @__PURE__ */ new Set();
|
|
1038
|
+
/** Caller endpoints retained until close, independently of managed process lifetime. */
|
|
1039
|
+
controlChannels = /* @__PURE__ */ new Set();
|
|
919
1040
|
/** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
|
|
920
1041
|
internals = {};
|
|
921
1042
|
/** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
|
|
@@ -957,6 +1078,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
957
1078
|
this.terminals.delete(terminal);
|
|
958
1079
|
}));
|
|
959
1080
|
const outcomes = await Promise.allSettled(pending);
|
|
1081
|
+
await Promise.all([...this.controlChannels].map((control) => new Promise((resolveClose) => {
|
|
1082
|
+
control.once("close", () => {
|
|
1083
|
+
resolveClose();
|
|
1084
|
+
});
|
|
1085
|
+
control.destroy();
|
|
1086
|
+
})));
|
|
1087
|
+
this.controlChannels.clear();
|
|
960
1088
|
const failures = [];
|
|
961
1089
|
for (const outcome of outcomes) if (outcome.status === "rejected") failures.push(outcome.reason);
|
|
962
1090
|
if (failures.length > 0) this.terminateForHostExit();
|
|
@@ -980,7 +1108,7 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
980
1108
|
} catch {}
|
|
981
1109
|
}
|
|
982
1110
|
signal?.throwIfAborted();
|
|
983
|
-
throw new
|
|
1111
|
+
throw new SubprocessExecutableNotFoundError(absolute ? `subprocess-local: command ${JSON.stringify(command)} is not an executable file` : `subprocess-local: command ${JSON.stringify(command)} was not found on PATH`);
|
|
984
1112
|
}
|
|
985
1113
|
executableCandidates(command, env) {
|
|
986
1114
|
const path = environmentValue(env, "PATH") ?? "";
|
|
@@ -998,6 +1126,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
998
1126
|
handle = bindManagedProcess(spec, containmentMode === "linux-scope" ? launchLinuxScope(spec, env) : launchWindowsJob(spec, env), binding);
|
|
999
1127
|
}
|
|
1000
1128
|
this.live.add(handle);
|
|
1129
|
+
const control = handle.control;
|
|
1130
|
+
if (control !== void 0) {
|
|
1131
|
+
this.controlChannels.add(control);
|
|
1132
|
+
control.once("close", () => {
|
|
1133
|
+
this.controlChannels.delete(control);
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1001
1136
|
const release = () => handle.waitForExit().then(() => {
|
|
1002
1137
|
this.live.delete(handle);
|
|
1003
1138
|
});
|
|
@@ -1025,23 +1160,36 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1025
1160
|
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
1161
|
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
1162
|
}
|
|
1163
|
+
/** @inheritdoc */
|
|
1164
|
+
async terminalEnvironment(signal) {
|
|
1165
|
+
signal?.throwIfAborted();
|
|
1166
|
+
const platform = process.platform === "win32" ? "windows" : "posix";
|
|
1167
|
+
const defaultShell = platform === "windows" ? process.env.ComSpec || void 0 : process.env.SHELL || userInfo().shell || void 0;
|
|
1168
|
+
return {
|
|
1169
|
+
platform,
|
|
1170
|
+
...defaultShell === void 0 ? {} : { defaultShell }
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1028
1173
|
async spawnTerminal(spec) {
|
|
1029
1174
|
const file = spec.argv[0];
|
|
1030
1175
|
if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
|
|
1031
1176
|
spec.signal?.throwIfAborted();
|
|
1032
1177
|
const env = targetEnvironment(spec);
|
|
1033
1178
|
const options = {
|
|
1034
|
-
name:
|
|
1179
|
+
name: spec.terminalType,
|
|
1035
1180
|
rows: spec.rows,
|
|
1036
1181
|
cols: spec.cols,
|
|
1037
1182
|
cwd: spec.cwd,
|
|
1038
|
-
env
|
|
1183
|
+
env: {
|
|
1184
|
+
...env,
|
|
1185
|
+
TERM: spec.terminalType
|
|
1186
|
+
}
|
|
1039
1187
|
};
|
|
1040
1188
|
const inspector = this.terminalInspector ?? createProcessInspector();
|
|
1041
1189
|
const scope = this.selectContainmentMode("terminal") === "linux-scope" ? prepareLinuxTerminalScope(spec, {
|
|
1042
1190
|
...env,
|
|
1043
1191
|
PWD: spec.cwd,
|
|
1044
|
-
TERM:
|
|
1192
|
+
TERM: spec.terminalType
|
|
1045
1193
|
}) : void 0;
|
|
1046
1194
|
if (scope !== void 0) {
|
|
1047
1195
|
options.cwd = scope.cwd;
|
|
@@ -1055,17 +1203,16 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1055
1203
|
throw error;
|
|
1056
1204
|
}
|
|
1057
1205
|
let handle;
|
|
1206
|
+
const directSettlement = Promise.withResolvers();
|
|
1058
1207
|
const owner = scope?.bindOwner({
|
|
1059
1208
|
running: () => handle?.running ?? true,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
terminal.kill(signal);
|
|
1063
|
-
} catch {}
|
|
1064
|
-
}
|
|
1209
|
+
settled: directSettlement.promise,
|
|
1210
|
+
signal: (signal) => signalLinuxDirectProcess(terminal.pid, () => process.kill(terminal.pid, signal))
|
|
1065
1211
|
});
|
|
1066
1212
|
handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome);
|
|
1067
1213
|
this.terminals.add(handle);
|
|
1068
1214
|
const release = async () => {
|
|
1215
|
+
directSettlement.resolve();
|
|
1069
1216
|
await handle.terminate();
|
|
1070
1217
|
this.terminals.delete(handle);
|
|
1071
1218
|
};
|