@deepseek-ai/dsh-subprocess-local 0.1.5-rc.2 → 0.1.6-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +20 -2
- package/README.zh.md +27 -9
- package/lib/index.js +483 -88
- package/lib/output.js +181 -0
- package/lib/{runner-launch-COYGu0Dl.js → runner-launch-DGV26RBf.js} +108 -206
- package/lib/runner.js +21 -10
- package/lib/types/control-spawn.d.ts +19 -0
- package/lib/types/index.d.ts +5 -1
- package/lib/types/linux-execve.d.ts +1 -1
- package/lib/types/linux-scope.d.ts +13 -3
- package/lib/types/managed-owner.d.ts +7 -1
- package/lib/types/output.d.ts +85 -0
- package/lib/types/process-inspector.d.ts +3 -0
- package/lib/types/runner-launch.d.ts +4 -2
- package/lib/types/runner-protocol.d.ts +2 -0
- package/lib/types/shell-activity.d.ts +37 -0
- package/lib/types/spawn-runner.d.ts +1 -1
- package/lib/types/spawn.d.ts +1 -74
- package/lib/types/terminal.d.ts +13 -2
- package/package.json +15 -9
package/lib/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { C as bindManagedProcess, D as
|
|
2
|
-
import {
|
|
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-DGV26RBf.js";
|
|
2
|
+
import { prepareManagedProcessBinding } from "./output.js";
|
|
3
|
+
import { closeSync, constants, existsSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { access, stat } from "node:fs/promises";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
5
|
+
import { constants as constants$1, devNull, tmpdir, userInfo } from "node:os";
|
|
6
|
+
import { basename, delimiter, extname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { createLazyRequire } from "@deepseek-ai/dsh-lazy-require";
|
|
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,24 +144,46 @@ 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;
|
|
141
159
|
this.query = query;
|
|
142
160
|
this.sleep = sleep;
|
|
143
161
|
}
|
|
162
|
+
inspectTaskCount() {
|
|
163
|
+
const result = this.runSync(this.systemctl, [
|
|
164
|
+
"--user",
|
|
165
|
+
"show",
|
|
166
|
+
"--property=LoadState",
|
|
167
|
+
"--property=ActiveState",
|
|
168
|
+
"--property=TasksCurrent",
|
|
169
|
+
this.unit
|
|
170
|
+
], {
|
|
171
|
+
encoding: "utf8",
|
|
172
|
+
env: managerEnvironment(),
|
|
173
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
174
|
+
});
|
|
175
|
+
if (result.error !== void 0 || result.status !== 0 || typeof result.stdout !== "string") return void 0;
|
|
176
|
+
const state = this.parseUnitState(result.stdout);
|
|
177
|
+
return state.loadState === "loaded" && state.activeState === "active" ? state.tasksCurrent : void 0;
|
|
178
|
+
}
|
|
144
179
|
signal(signal) {
|
|
145
180
|
if (this.stopped) return;
|
|
181
|
+
this.terminationRequested = true;
|
|
182
|
+
if (this.direct.running()) this.startup.terminationSignals.add(signal);
|
|
146
183
|
this.observeRequestConsumption();
|
|
147
184
|
const directFallbackRequired = this.establishment === "pending";
|
|
148
|
-
|
|
185
|
+
let directSignalled = false;
|
|
186
|
+
if (directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
|
|
149
187
|
const result = this.runSync(this.systemctl, [
|
|
150
188
|
"--user",
|
|
151
189
|
"kill",
|
|
@@ -159,13 +197,19 @@ var SystemdScopeOwner = class {
|
|
|
159
197
|
});
|
|
160
198
|
this.wakeObservation();
|
|
161
199
|
if (result.error === void 0 && result.status === 0) {
|
|
162
|
-
if (signal === "SIGKILL")
|
|
200
|
+
if (signal === "SIGKILL") {
|
|
201
|
+
this.killFailure = void 0;
|
|
202
|
+
this.directKillSettlement = void 0;
|
|
203
|
+
}
|
|
163
204
|
return;
|
|
164
205
|
}
|
|
165
|
-
if (!directFallbackRequired && this.direct.running()) this.direct.signal(signal);
|
|
206
|
+
if (!directFallbackRequired && this.direct.running()) directSignalled = this.direct.signal(signal);
|
|
166
207
|
if (signal === "SIGKILL") {
|
|
167
208
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
168
|
-
if (!MISSING_UNIT.test(output))
|
|
209
|
+
if (!MISSING_UNIT.test(output)) {
|
|
210
|
+
this.killFailure = result.error ?? /* @__PURE__ */ new Error(`systemctl could not signal ${this.unit}: ${output.trim() || `exit ${String(result.status)}`}`);
|
|
211
|
+
this.directKillSettlement = directSignalled ? this.direct.settled.then(() => {}, () => {}) : void 0;
|
|
212
|
+
}
|
|
169
213
|
}
|
|
170
214
|
}
|
|
171
215
|
terminateForHostExit() {
|
|
@@ -188,15 +232,40 @@ var SystemdScopeOwner = class {
|
|
|
188
232
|
} catch {}
|
|
189
233
|
}
|
|
190
234
|
observeRequestConsumption() {
|
|
191
|
-
if (this.establishment === "pending" && !existsSync(this.files.requestPath)) this.establishment = "established";
|
|
235
|
+
if (this.establishment === "pending" && !existsSync(this.startup.files.requestPath)) this.establishment = "established";
|
|
192
236
|
}
|
|
193
237
|
absentUnit() {
|
|
194
238
|
this.observeRequestConsumption();
|
|
195
239
|
if (this.establishment === "established") return false;
|
|
196
|
-
if (!this.direct.running() && existsSync(this.files.requestPath)) return false;
|
|
240
|
+
if (!this.direct.running() && existsSync(this.startup.files.requestPath)) return false;
|
|
197
241
|
if (this.killFailure !== void 0) throw this.killFailure;
|
|
198
242
|
return true;
|
|
199
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Prove an active unit with no processes is the empty managed range rather
|
|
246
|
+
* than a launch still placing its payload. systemd ends a scope only on the
|
|
247
|
+
* populated-to-empty transition, so a payload killed before it entered the
|
|
248
|
+
* cgroup leaves the unit active forever. A departed client cannot add another
|
|
249
|
+
* payload; a consumed request proves the payload already entered the scope,
|
|
250
|
+
* even while its direct-process exit notification is pending.
|
|
251
|
+
*/
|
|
252
|
+
emptyRange(tasksCurrent) {
|
|
253
|
+
return this.terminationRequested && tasksCurrent === 0 && (!this.direct.running() || !existsSync(this.startup.files.requestPath));
|
|
254
|
+
}
|
|
255
|
+
/** Release a leftover empty scope so the transient unit is collected and cannot accumulate. */
|
|
256
|
+
releaseEmptyRange() {
|
|
257
|
+
try {
|
|
258
|
+
this.runSync(this.systemctl, [
|
|
259
|
+
"--user",
|
|
260
|
+
"stop",
|
|
261
|
+
this.unit
|
|
262
|
+
], {
|
|
263
|
+
env: managerEnvironment(),
|
|
264
|
+
stdio: "ignore",
|
|
265
|
+
timeout: SYSTEMCTL_TIMEOUT_MS
|
|
266
|
+
});
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
200
269
|
parseUnitState(stdout) {
|
|
201
270
|
const values = /* @__PURE__ */ new Map();
|
|
202
271
|
for (const line of stdout.split(/\r?\n/u)) {
|
|
@@ -209,24 +278,32 @@ var SystemdScopeOwner = class {
|
|
|
209
278
|
}
|
|
210
279
|
const loadState = values.get("LoadState");
|
|
211
280
|
const activeState = values.get("ActiveState");
|
|
212
|
-
|
|
281
|
+
const reportedTasks = values.get("TasksCurrent");
|
|
282
|
+
const tasksCurrent = reportedTasks === "[not set]" ? void 0 : reportedTasks;
|
|
283
|
+
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())}`);
|
|
284
|
+
if (tasksCurrent !== void 0 && !/^\d+$/u.test(tasksCurrent)) throw new Error(`systemctl returned a non-numeric TasksCurrent for ${this.unit}: ${JSON.stringify(tasksCurrent)}`);
|
|
213
285
|
return {
|
|
214
286
|
loadState,
|
|
215
|
-
activeState
|
|
287
|
+
activeState,
|
|
288
|
+
tasksCurrent: tasksCurrent === void 0 ? void 0 : Number(tasksCurrent)
|
|
216
289
|
};
|
|
217
290
|
}
|
|
218
291
|
async rangeActive() {
|
|
219
292
|
this.observeRequestConsumption();
|
|
293
|
+
const generation = this.wakeGeneration;
|
|
294
|
+
const directRunning = this.direct.running();
|
|
220
295
|
const result = await this.query(this.systemctl, [
|
|
221
296
|
"--user",
|
|
222
297
|
"show",
|
|
223
298
|
this.unit,
|
|
224
299
|
"--property=LoadState",
|
|
225
|
-
"--property=ActiveState"
|
|
300
|
+
"--property=ActiveState",
|
|
301
|
+
"--property=TasksCurrent"
|
|
226
302
|
]);
|
|
303
|
+
if (generation !== this.wakeGeneration) return true;
|
|
227
304
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
228
305
|
if (result.status === 0) {
|
|
229
|
-
const { loadState, activeState } = this.parseUnitState(result.stdout);
|
|
306
|
+
const { loadState, activeState, tasksCurrent } = this.parseUnitState(result.stdout);
|
|
230
307
|
if (loadState === "not-found" && activeState === "inactive") return this.absentUnit();
|
|
231
308
|
if (loadState !== "loaded") throw new Error(`systemctl returned unknown state for ${this.unit}: ${JSON.stringify({
|
|
232
309
|
loadState,
|
|
@@ -240,7 +317,19 @@ var SystemdScopeOwner = class {
|
|
|
240
317
|
"reloading",
|
|
241
318
|
"deactivating"
|
|
242
319
|
].includes(activeState)) throw new Error(`systemctl returned unknown ActiveState for ${this.unit}: ${JSON.stringify(activeState)}`);
|
|
243
|
-
if (this.
|
|
320
|
+
if (this.emptyRange(tasksCurrent)) {
|
|
321
|
+
this.releaseEmptyRange();
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (this.killFailure !== void 0) {
|
|
325
|
+
if (directRunning && this.directKillSettlement !== void 0) {
|
|
326
|
+
const settlement = this.directKillSettlement;
|
|
327
|
+
this.directKillSettlement = void 0;
|
|
328
|
+
await settlement;
|
|
329
|
+
return this.rangeActive();
|
|
330
|
+
}
|
|
331
|
+
throw this.killFailure;
|
|
332
|
+
}
|
|
244
333
|
return true;
|
|
245
334
|
}
|
|
246
335
|
if (!MISSING_UNIT.test(output)) {
|
|
@@ -288,7 +377,7 @@ var SystemdScopeOwner = class {
|
|
|
288
377
|
await this.observation;
|
|
289
378
|
}
|
|
290
379
|
cleanup() {
|
|
291
|
-
cleanupLinuxLaunchFiles(this.files);
|
|
380
|
+
cleanupLinuxLaunchFiles(this.startup.files);
|
|
292
381
|
}
|
|
293
382
|
};
|
|
294
383
|
function scopeArgs(unitBase, invocation, argv) {
|
|
@@ -305,7 +394,7 @@ function scopeArgs(unitBase, invocation, argv) {
|
|
|
305
394
|
...argv
|
|
306
395
|
];
|
|
307
396
|
}
|
|
308
|
-
function directOutcome(child,
|
|
397
|
+
function directOutcome(child, startup) {
|
|
309
398
|
return new Promise((resolveOutcome, rejectOutcome) => {
|
|
310
399
|
let settled = false;
|
|
311
400
|
child.once("error", (error) => {
|
|
@@ -317,40 +406,47 @@ function directOutcome(child, files) {
|
|
|
317
406
|
if (settled) return;
|
|
318
407
|
settled = true;
|
|
319
408
|
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({
|
|
409
|
+
resolveOutcome(startup.resolveOutcome({
|
|
330
410
|
exitCode,
|
|
331
411
|
signal
|
|
332
|
-
});
|
|
412
|
+
}));
|
|
333
413
|
} catch (error) {
|
|
334
414
|
rejectOutcome(error instanceof Error ? error : new Error(String(error)));
|
|
335
415
|
}
|
|
336
416
|
});
|
|
337
417
|
});
|
|
338
418
|
}
|
|
339
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Send a direct-process signal, distinguishing an absent PID from failed delivery.
|
|
421
|
+
* @param pid - owned direct-process identity whose exit notification can still be pending.
|
|
422
|
+
* @param send - platform signal operation; true means the signal was submitted.
|
|
423
|
+
* @returns whether the signal was submitted or the owned PID is already absent.
|
|
424
|
+
*/
|
|
425
|
+
function signalLinuxDirectProcess(pid, send) {
|
|
340
426
|
try {
|
|
341
|
-
|
|
342
|
-
} catch {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
427
|
+
if (send()) return true;
|
|
428
|
+
} catch {}
|
|
429
|
+
try {
|
|
430
|
+
process.kill(pid, 0);
|
|
431
|
+
return false;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
return error.code === "ESRCH";
|
|
346
434
|
}
|
|
347
435
|
}
|
|
436
|
+
function signalChildGroup(child, signal) {
|
|
437
|
+
let groupSignalled = false;
|
|
438
|
+
try {
|
|
439
|
+
groupSignalled = process.kill(-child.pid, signal);
|
|
440
|
+
} catch {}
|
|
441
|
+
if (groupSignalled && signal === "SIGTERM") return true;
|
|
442
|
+
return signalLinuxDirectProcess(child.pid, () => process.kill(child.pid, signal));
|
|
443
|
+
}
|
|
348
444
|
/**
|
|
349
445
|
* Prepare one Linux PTY scope using the same launch request and bootstrap core.
|
|
350
446
|
* @param spec - terminal target request.
|
|
351
447
|
* @param targetEnv - validated complete target environment.
|
|
352
448
|
* @param internals - optional runner and systemd seams used by tests.
|
|
353
|
-
* @returns invocation
|
|
449
|
+
* @returns invocation and ownership callbacks; requested termination preserves the observed signal even before bootstrap consumption.
|
|
354
450
|
*/
|
|
355
451
|
function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
356
452
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
@@ -358,19 +454,15 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
|
358
454
|
cwd: spec.cwd,
|
|
359
455
|
env: targetEnv
|
|
360
456
|
});
|
|
457
|
+
const startup = new LinuxScopeStartup(files, "terminal");
|
|
361
458
|
const unitBase = unitStem("dsh-terminal");
|
|
362
459
|
return {
|
|
363
460
|
command: internals.systemdRun ?? "systemd-run",
|
|
364
461
|
args: scopeArgs(unitBase, invocation, spec.argv),
|
|
365
462
|
cwd: process.cwd(),
|
|
366
463
|
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
|
-
},
|
|
464
|
+
bindOwner: (direct) => new SystemdScopeOwner(`${unitBase}.scope`, startup, direct, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort),
|
|
465
|
+
resolveOutcome: (outcome) => startup.resolveOutcome(outcome),
|
|
374
466
|
cleanup: () => {
|
|
375
467
|
cleanupLinuxLaunchFiles(files);
|
|
376
468
|
}
|
|
@@ -381,14 +473,16 @@ function prepareLinuxTerminalScope(spec, targetEnv, internals = {}) {
|
|
|
381
473
|
* @param spec - ordinary target request.
|
|
382
474
|
* @param targetEnv - validated complete target environment.
|
|
383
475
|
* @param internals - optional runner and systemd seams used by tests.
|
|
384
|
-
* @returns
|
|
476
|
+
* @returns streams, result, and scope owner; requested termination preserves the observed signal even before bootstrap consumption.
|
|
385
477
|
*/
|
|
386
478
|
function launchLinuxScope(spec, targetEnv, internals = {}) {
|
|
387
479
|
const invocation = internals.runnerInvocation ?? spawnRunnerInvocation();
|
|
388
480
|
const files = createLinuxLaunchFiles({
|
|
389
481
|
cwd: spec.cwd,
|
|
390
|
-
env: targetEnv
|
|
482
|
+
env: targetEnv,
|
|
483
|
+
...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
|
|
391
484
|
});
|
|
485
|
+
const startup = new LinuxScopeStartup(files, "subprocess");
|
|
392
486
|
const unitBase = unitStem("dsh-subprocess");
|
|
393
487
|
let child;
|
|
394
488
|
try {
|
|
@@ -402,17 +496,18 @@ function launchLinuxScope(spec, targetEnv, internals = {}) {
|
|
|
402
496
|
cleanupLinuxLaunchFiles(files);
|
|
403
497
|
throw error;
|
|
404
498
|
}
|
|
405
|
-
const
|
|
499
|
+
const direct = directOutcome(child, startup);
|
|
500
|
+
const owner = new SystemdScopeOwner(`${unitBase}.scope`, startup, {
|
|
406
501
|
running: () => child.pid !== void 0 && child.exitCode === null && child.signalCode === null,
|
|
407
|
-
signal: (signal) =>
|
|
408
|
-
|
|
409
|
-
}
|
|
502
|
+
signal: (signal) => signalChildGroup(child, signal),
|
|
503
|
+
settled: direct
|
|
410
504
|
}, internals.systemctl ?? "systemctl", internals.spawnSync ?? spawnSync, internals.systemctlQuery ?? querySystemctl, internals.sleep ?? sleepWithAbort);
|
|
411
505
|
return {
|
|
412
506
|
stdin: child.stdin,
|
|
413
507
|
stdout: child.stdout,
|
|
414
508
|
stderr: child.stderr,
|
|
415
|
-
|
|
509
|
+
control: controlPipe(child, spec.stdio.control),
|
|
510
|
+
direct,
|
|
416
511
|
owner
|
|
417
512
|
};
|
|
418
513
|
}
|
|
@@ -502,6 +597,7 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
502
597
|
...spec.argv
|
|
503
598
|
], {
|
|
504
599
|
cwd: process.cwd(),
|
|
600
|
+
windowsHide: true,
|
|
505
601
|
env: runnerEnvironment(WINDOWS_RUNNER_SELECTION, invocation),
|
|
506
602
|
stdio: runnerStdio(spec, true, ignoredStdinFd ?? "pipe")
|
|
507
603
|
});
|
|
@@ -513,10 +609,25 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
513
609
|
const rangeExit = Promise.withResolvers();
|
|
514
610
|
let directResultType;
|
|
515
611
|
let runnerSpawned = false;
|
|
612
|
+
let runnerExit;
|
|
613
|
+
let ipcDisconnected = false;
|
|
516
614
|
const failInfrastructure = (error) => {
|
|
517
615
|
direct.reject(error);
|
|
518
616
|
rangeExit.reject(error);
|
|
519
617
|
};
|
|
618
|
+
const settleRange = () => {
|
|
619
|
+
if (!runnerSpawned || runnerExit === void 0) return;
|
|
620
|
+
const { exitCode, signal } = runnerExit;
|
|
621
|
+
if (exitCode === 0 && signal === null) {
|
|
622
|
+
if (directResultType !== void 0) {
|
|
623
|
+
rangeExit.resolve();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!ipcDisconnected) return;
|
|
627
|
+
}
|
|
628
|
+
const status = signal !== null ? `signal ${signal}` : exitCode === null ? "without an exit status" : `exit code ${String(exitCode)}`;
|
|
629
|
+
failInfrastructure(/* @__PURE__ */ new Error(`subprocess-local: Windows Job runner exited with ${status} before proving its managed range empty`));
|
|
630
|
+
};
|
|
520
631
|
const owner = new WindowsJobOwner(child, rangeExit.promise, () => directResultType, failInfrastructure);
|
|
521
632
|
child.on("message", (value) => {
|
|
522
633
|
if (directResultType !== void 0) {
|
|
@@ -538,6 +649,7 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
538
649
|
signal: null
|
|
539
650
|
});
|
|
540
651
|
else direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error), result.error));
|
|
652
|
+
settleRange();
|
|
541
653
|
});
|
|
542
654
|
child.once("spawn", () => {
|
|
543
655
|
runnerSpawned = true;
|
|
@@ -546,7 +658,8 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
546
658
|
child.send({
|
|
547
659
|
type: "start",
|
|
548
660
|
cwd: spec.cwd,
|
|
549
|
-
env: targetEnv
|
|
661
|
+
env: targetEnv,
|
|
662
|
+
...spec.stdio.control === void 0 ? {} : { control: spec.stdio.control }
|
|
550
663
|
}, (error) => {
|
|
551
664
|
if (error === null) return;
|
|
552
665
|
failInfrastructure(error);
|
|
@@ -565,19 +678,22 @@ function launchWindowsJob(spec, targetEnv, internals = {}) {
|
|
|
565
678
|
}
|
|
566
679
|
failInfrastructure(error);
|
|
567
680
|
});
|
|
568
|
-
child.once("
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
681
|
+
child.once("exit", (exitCode, signal) => {
|
|
682
|
+
runnerExit = {
|
|
683
|
+
exitCode,
|
|
684
|
+
signal
|
|
685
|
+
};
|
|
686
|
+
settleRange();
|
|
687
|
+
});
|
|
688
|
+
child.once("disconnect", () => {
|
|
689
|
+
ipcDisconnected = true;
|
|
690
|
+
settleRange();
|
|
576
691
|
});
|
|
577
692
|
return {
|
|
578
693
|
stdin: spec.stdio.stdin === "ignore" ? null : targetStdin,
|
|
579
694
|
stdout: child.stdio[5],
|
|
580
695
|
stderr: child.stdio[6],
|
|
696
|
+
control: controlPipe(child, spec.stdio.control),
|
|
581
697
|
direct: direct.promise,
|
|
582
698
|
owner
|
|
583
699
|
};
|
|
@@ -625,6 +741,9 @@ var LocalTerminalHandle = class {
|
|
|
625
741
|
platform;
|
|
626
742
|
managedOwner;
|
|
627
743
|
resolveManagedOutcome;
|
|
744
|
+
shellActivity;
|
|
745
|
+
onQuiescence;
|
|
746
|
+
observeShellExit;
|
|
628
747
|
pid;
|
|
629
748
|
output = new PassThrough();
|
|
630
749
|
done;
|
|
@@ -634,7 +753,12 @@ var LocalTerminalHandle = class {
|
|
|
634
753
|
cleanup;
|
|
635
754
|
managedOwnerCleaned = false;
|
|
636
755
|
exited = false;
|
|
756
|
+
outputPaused = false;
|
|
637
757
|
trackedDescendants = [];
|
|
758
|
+
activityRevision = 0;
|
|
759
|
+
activityKey = "";
|
|
760
|
+
quiescent = false;
|
|
761
|
+
managedRangeEmpty = false;
|
|
638
762
|
/** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
|
|
639
763
|
rootIdentity;
|
|
640
764
|
/**
|
|
@@ -643,22 +767,44 @@ var LocalTerminalHandle = class {
|
|
|
643
767
|
* @param graceMs - TERM-to-KILL and exit-wait grace.
|
|
644
768
|
* @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
|
|
645
769
|
*/
|
|
646
|
-
constructor(terminal, inspector, graceMs, platform = process.platform, managedOwner, resolveManagedOutcome) {
|
|
770
|
+
constructor(terminal, inspector, graceMs, platform = process.platform, managedOwner, resolveManagedOutcome, shellActivity, onQuiescence, observeShellExit = false) {
|
|
647
771
|
this.terminal = terminal;
|
|
648
772
|
this.inspector = inspector;
|
|
649
773
|
this.graceMs = graceMs;
|
|
650
774
|
this.platform = platform;
|
|
651
775
|
this.managedOwner = managedOwner;
|
|
652
776
|
this.resolveManagedOutcome = resolveManagedOutcome;
|
|
777
|
+
this.shellActivity = shellActivity;
|
|
778
|
+
this.onQuiescence = onQuiescence;
|
|
779
|
+
this.observeShellExit = observeShellExit;
|
|
653
780
|
this.pid = terminal.pid;
|
|
654
|
-
|
|
781
|
+
try {
|
|
782
|
+
this.rootIdentity = inspector.snapshot().tree(this.pid).find((member) => member.pid === this.pid);
|
|
783
|
+
} catch (_rootIdentityUnavailable) {
|
|
784
|
+
this.rootIdentity = void 0;
|
|
785
|
+
}
|
|
655
786
|
this.done = this.outcome.promise;
|
|
787
|
+
const resume = () => {
|
|
788
|
+
if (!this.outputPaused) return;
|
|
789
|
+
this.outputPaused = false;
|
|
790
|
+
if (!this.exited) terminal.resume();
|
|
791
|
+
};
|
|
792
|
+
this.output.on("drain", resume);
|
|
793
|
+
this.output.once("close", () => {
|
|
794
|
+
this.output.off("drain", resume);
|
|
795
|
+
});
|
|
656
796
|
this.dataDisposable = terminal.onData((data) => {
|
|
657
|
-
this.output.write(Buffer.from(data, "utf8"))
|
|
797
|
+
if (!this.output.write(Buffer.from(data, "utf8")) && this.cleanup === void 0 && !this.outputPaused) {
|
|
798
|
+
this.outputPaused = true;
|
|
799
|
+
terminal.pause();
|
|
800
|
+
}
|
|
658
801
|
});
|
|
659
802
|
this.exitDisposable = terminal.onExit(({ exitCode, signal: exitSignal }) => {
|
|
660
803
|
if (this.exited) return;
|
|
661
804
|
this.exited = true;
|
|
805
|
+
if (this.managedOwner !== void 0 && this.observeShellExit) this.managedOwner.waitForExit().then(() => {
|
|
806
|
+
this.managedRangeEmpty = true;
|
|
807
|
+
}).catch(() => {});
|
|
662
808
|
this.output.end();
|
|
663
809
|
const outcome = {
|
|
664
810
|
exitCode: exitSignal === void 0 || exitSignal === 0 ? exitCode : null,
|
|
@@ -677,8 +823,13 @@ var LocalTerminalHandle = class {
|
|
|
677
823
|
}
|
|
678
824
|
async write(data) {
|
|
679
825
|
if (this.exited) throw new Error("terminal process has exited");
|
|
826
|
+
this.shellActivity?.invalidate();
|
|
680
827
|
this.terminal.write(data);
|
|
681
828
|
}
|
|
829
|
+
async resize(cols, rows) {
|
|
830
|
+
if (this.exited) throw new Error("terminal process has exited");
|
|
831
|
+
this.terminal.resize(cols, rows);
|
|
832
|
+
}
|
|
682
833
|
async inspectForeground() {
|
|
683
834
|
this.descendants(this.inspector.snapshot());
|
|
684
835
|
const processGroupId = this.inspector.foregroundPgid(this.pid);
|
|
@@ -688,7 +839,44 @@ var LocalTerminalHandle = class {
|
|
|
688
839
|
inputWaiting: this.inspector.isStdinWaiting(processGroupId, this.pid)
|
|
689
840
|
};
|
|
690
841
|
}
|
|
842
|
+
async inspectActivity() {
|
|
843
|
+
let state = this.quiescent ? "idle" : "unknown";
|
|
844
|
+
let revision = 0;
|
|
845
|
+
if (!this.quiescent) try {
|
|
846
|
+
const shell = this.shellActivity?.inspect(this.pid) ?? {
|
|
847
|
+
state: "unknown",
|
|
848
|
+
revision: 0
|
|
849
|
+
};
|
|
850
|
+
revision = shell.revision;
|
|
851
|
+
const observed = this.inspector.snapshot();
|
|
852
|
+
const descendants = this.descendants(observed);
|
|
853
|
+
const root = observed.tree(this.pid).find((member) => member.pid === this.pid);
|
|
854
|
+
if (this.exited && this.managedRangeEmpty) state = "idle";
|
|
855
|
+
else if (descendants.length > 0) state = "busy";
|
|
856
|
+
else if (this.exited && this.managedOwner === void 0 && this.platform === "linux" && observed.complete === true && root === void 0) state = observed.session(this.pid).some((member) => observed.alive(member)) ? "busy" : "idle";
|
|
857
|
+
else if (observed.complete === true && root?.started === this.rootIdentity?.started && root !== void 0) {
|
|
858
|
+
const foreground = this.inspector.foregroundPgid(this.pid);
|
|
859
|
+
state = foreground === void 0 ? "unknown" : foreground === this.pid ? shell.state : "busy";
|
|
860
|
+
if (state === "idle" && this.managedOwner !== void 0) {
|
|
861
|
+
const tasks = this.managedOwner.inspectTaskCount?.();
|
|
862
|
+
state = tasks === void 0 || tasks < 1 ? "unknown" : tasks === 1 ? "idle" : "busy";
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} catch (_incompleteActivityObservation) {
|
|
866
|
+
state = "unknown";
|
|
867
|
+
}
|
|
868
|
+
const key = `${revision}:${state}`;
|
|
869
|
+
if (key !== this.activityKey) {
|
|
870
|
+
this.activityKey = key;
|
|
871
|
+
this.activityRevision++;
|
|
872
|
+
}
|
|
873
|
+
return {
|
|
874
|
+
state,
|
|
875
|
+
revision: this.activityRevision
|
|
876
|
+
};
|
|
877
|
+
}
|
|
691
878
|
async signalForeground(signal) {
|
|
879
|
+
this.shellActivity?.invalidate();
|
|
692
880
|
const foreground = await this.inspectForeground();
|
|
693
881
|
if (foreground === void 0) throw new Error(`cannot resolve foreground process group for terminal ${this.pid}`);
|
|
694
882
|
if (signal === "SIGKILL" && foreground.processGroupId === this.pid) throw new Error("refusing to SIGKILL the terminal shell; terminate the terminal session instead");
|
|
@@ -704,7 +892,15 @@ var LocalTerminalHandle = class {
|
|
|
704
892
|
}
|
|
705
893
|
terminate() {
|
|
706
894
|
if (this.cleanup !== void 0) return this.cleanup;
|
|
707
|
-
|
|
895
|
+
if (this.outputPaused) {
|
|
896
|
+
this.outputPaused = false;
|
|
897
|
+
this.terminal.resume();
|
|
898
|
+
}
|
|
899
|
+
const cleanup = this.closeOnce().then(() => {
|
|
900
|
+
this.quiescent = true;
|
|
901
|
+
this.shellActivity?.dispose();
|
|
902
|
+
this.onQuiescence?.();
|
|
903
|
+
});
|
|
708
904
|
this.cleanup = cleanup;
|
|
709
905
|
cleanup.catch(() => {
|
|
710
906
|
this.cleanup = void 0;
|
|
@@ -894,6 +1090,164 @@ var LocalTerminalHandle = class {
|
|
|
894
1090
|
}
|
|
895
1091
|
};
|
|
896
1092
|
//#endregion
|
|
1093
|
+
//#region lib/types/shell-activity.js
|
|
1094
|
+
/** Private shell lifecycle files; ordinary output never authorizes terminal reclamation. */
|
|
1095
|
+
function quote(value) {
|
|
1096
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
1097
|
+
}
|
|
1098
|
+
/** Opt-in startup integration and revision tracking for one ordinary interactive shell. */
|
|
1099
|
+
var ShellActivity = class {
|
|
1100
|
+
directory;
|
|
1101
|
+
argv;
|
|
1102
|
+
env;
|
|
1103
|
+
revision = 0;
|
|
1104
|
+
observed = "";
|
|
1105
|
+
invalidated;
|
|
1106
|
+
state = "unknown";
|
|
1107
|
+
/**
|
|
1108
|
+
* @param directory - private startup and status file directory.
|
|
1109
|
+
* @param argv - shell launch preserving supported user startup files.
|
|
1110
|
+
* @param env - environment with any temporary startup redirect.
|
|
1111
|
+
*/
|
|
1112
|
+
constructor(directory, argv, env) {
|
|
1113
|
+
this.directory = directory;
|
|
1114
|
+
this.argv = argv;
|
|
1115
|
+
this.env = env;
|
|
1116
|
+
}
|
|
1117
|
+
/** Invalidate prompt evidence before delivering input or a foreground signal. */
|
|
1118
|
+
invalidate() {
|
|
1119
|
+
this.invalidated = this.read();
|
|
1120
|
+
this.revision++;
|
|
1121
|
+
this.state = "unknown";
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Read the latest top-level shell transition, fenced against input since that transition.
|
|
1125
|
+
* @param pid - original shell process id.
|
|
1126
|
+
* @returns lifecycle evidence; process ownership must be checked separately.
|
|
1127
|
+
*/
|
|
1128
|
+
inspect(pid) {
|
|
1129
|
+
const record = this.read();
|
|
1130
|
+
if (record !== this.observed) {
|
|
1131
|
+
this.observed = record;
|
|
1132
|
+
this.revision++;
|
|
1133
|
+
}
|
|
1134
|
+
const match = /^(\d+):(\d+):(idle|busy)\n?$/u.exec(record);
|
|
1135
|
+
this.state = record === this.invalidated || match?.[1] !== String(pid) ? "unknown" : match[3];
|
|
1136
|
+
return {
|
|
1137
|
+
state: this.state,
|
|
1138
|
+
revision: this.revision
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/** Remove private startup and status files after process quiescence. */
|
|
1142
|
+
dispose() {
|
|
1143
|
+
rmSync(this.directory, {
|
|
1144
|
+
recursive: true,
|
|
1145
|
+
force: true
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
read() {
|
|
1149
|
+
try {
|
|
1150
|
+
return readFileSync(join(this.directory, "state"), "utf8");
|
|
1151
|
+
} catch (_unavailableShellObservation) {
|
|
1152
|
+
return "";
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
/**
|
|
1157
|
+
* Prepare optional Bash or Zsh integration for a plain, non-login interactive launch.
|
|
1158
|
+
* @param spec - terminal request; custom arguments and wrapped executables remain unmodified.
|
|
1159
|
+
* @param env - scrubbed target environment.
|
|
1160
|
+
* @param platform - execution platform.
|
|
1161
|
+
* @returns private integration, or undefined for unsupported launches.
|
|
1162
|
+
*/
|
|
1163
|
+
function prepareShellActivity(spec, env, platform) {
|
|
1164
|
+
if (spec.shellActivity !== true || platform === "win32" || spec.argv.length !== 2 || spec.argv[1] !== "-i") return void 0;
|
|
1165
|
+
const shell = basename(spec.argv[0]);
|
|
1166
|
+
if (shell !== "bash" && shell !== "zsh") return void 0;
|
|
1167
|
+
const directory = mkdtempSync(join(tmpdir(), "dsh-shell-"));
|
|
1168
|
+
const state = quote(join(directory, "state"));
|
|
1169
|
+
const guards = quote(join(directory, "guards"));
|
|
1170
|
+
try {
|
|
1171
|
+
if (shell === "bash") {
|
|
1172
|
+
const rc = join(directory, "bashrc");
|
|
1173
|
+
writeFileSync(rc, [
|
|
1174
|
+
"[[ ! -r ~/.bashrc ]] || builtin source ~/.bashrc",
|
|
1175
|
+
"if (( BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4) )) && [[ ! $(declare -p PROMPT_COMMAND PS0 2>/dev/null) =~ declare\\ -[^[:space:]]*r ]]; then",
|
|
1176
|
+
" __dsh_shell_pid=$BASHPID; __dsh_shell_sequence=0",
|
|
1177
|
+
" __dsh_shell_idle() {",
|
|
1178
|
+
" local result=$?",
|
|
1179
|
+
" if [[ $BASHPID == \"$__dsh_shell_pid\" ]]; then",
|
|
1180
|
+
" (( ++__dsh_shell_sequence ))",
|
|
1181
|
+
" local activity=idle",
|
|
1182
|
+
` builtin trap -p >| ${guards}`,
|
|
1183
|
+
` [[ ! -s ${guards} ]] || activity=unknown`,
|
|
1184
|
+
` builtin printf '%s:%s:%s\\n' "$BASHPID" "$__dsh_shell_sequence" "$activity" >| ${state}`,
|
|
1185
|
+
" fi",
|
|
1186
|
+
" return \"$result\"",
|
|
1187
|
+
" }",
|
|
1188
|
+
" if [[ $(declare -p PROMPT_COMMAND 2>/dev/null) == \"declare -a \"* ]]; then",
|
|
1189
|
+
" PROMPT_COMMAND+=(__dsh_shell_idle)",
|
|
1190
|
+
" else",
|
|
1191
|
+
" PROMPT_COMMAND=\"${PROMPT_COMMAND}\"$'\\n'\"__dsh_shell_idle\"",
|
|
1192
|
+
" fi",
|
|
1193
|
+
` PS0+=${quote(`$(builtin printf '%s:%s:busy' "$__dsh_shell_pid" "$__dsh_shell_sequence" >| ${state})`)}`,
|
|
1194
|
+
"fi",
|
|
1195
|
+
""
|
|
1196
|
+
].join("\n"), {
|
|
1197
|
+
mode: 384,
|
|
1198
|
+
flag: "wx"
|
|
1199
|
+
});
|
|
1200
|
+
return new ShellActivity(directory, [
|
|
1201
|
+
spec.argv[0],
|
|
1202
|
+
"--rcfile",
|
|
1203
|
+
rc,
|
|
1204
|
+
"-i"
|
|
1205
|
+
], env);
|
|
1206
|
+
}
|
|
1207
|
+
writeFileSync(join(directory, ".zshenv"), [
|
|
1208
|
+
env.ZDOTDIR === void 0 ? "unset ZDOTDIR" : `ZDOTDIR=${quote(env.ZDOTDIR)}`,
|
|
1209
|
+
"[[ ! -r ${ZDOTDIR:-$HOME}/.zshenv ]] || builtin source \"${ZDOTDIR:-$HOME}/.zshenv\"",
|
|
1210
|
+
"typeset -g __dsh_shell_pid=$$ __dsh_shell_sequence=0",
|
|
1211
|
+
"__dsh_shell_activity() {",
|
|
1212
|
+
" (( ZSH_SUBSHELL == 0 && $$ == __dsh_shell_pid )) || return",
|
|
1213
|
+
" (( ++__dsh_shell_sequence ))",
|
|
1214
|
+
` builtin printf '%s:%s:%s\\n' "$$" "$__dsh_shell_sequence" "$1" >| ${state}`,
|
|
1215
|
+
" return 0",
|
|
1216
|
+
"}",
|
|
1217
|
+
"__dsh_shell_idle() {",
|
|
1218
|
+
` { builtin trap; zle -F; } >| ${guards}`,
|
|
1219
|
+
" if [[ $CONTEXT != start || -n $BUFFER ]]; then __dsh_shell_activity busy",
|
|
1220
|
+
` elif [[ -s ${guards} || -n \${(k)functions[(I)TRAP*]} ]]; then __dsh_shell_activity unknown`,
|
|
1221
|
+
" else __dsh_shell_activity idle; fi",
|
|
1222
|
+
"}",
|
|
1223
|
+
"__dsh_shell_busy() { __dsh_shell_activity busy }",
|
|
1224
|
+
"__dsh_shell_init() {",
|
|
1225
|
+
" autoload -Uz add-zle-hook-widget add-zsh-hook",
|
|
1226
|
+
" add-zle-hook-widget line-init __dsh_shell_idle",
|
|
1227
|
+
" add-zle-hook-widget line-finish __dsh_shell_busy",
|
|
1228
|
+
" add-zsh-hook preexec __dsh_shell_busy",
|
|
1229
|
+
" precmd_functions=(${precmd_functions:#__dsh_shell_init})",
|
|
1230
|
+
"}",
|
|
1231
|
+
"typeset -ga precmd_functions",
|
|
1232
|
+
"precmd_functions+=(__dsh_shell_init)",
|
|
1233
|
+
""
|
|
1234
|
+
].join("\n"), {
|
|
1235
|
+
mode: 384,
|
|
1236
|
+
flag: "wx"
|
|
1237
|
+
});
|
|
1238
|
+
return new ShellActivity(directory, spec.argv, {
|
|
1239
|
+
...env,
|
|
1240
|
+
ZDOTDIR: directory
|
|
1241
|
+
});
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
rmSync(directory, {
|
|
1244
|
+
recursive: true,
|
|
1245
|
+
force: true
|
|
1246
|
+
});
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
//#endregion
|
|
897
1251
|
//#region lib/types/index.js
|
|
898
1252
|
/**
|
|
899
1253
|
* Local Service Provider for the subprocess capability seam. Each spawn owns a
|
|
@@ -904,6 +1258,7 @@ var LocalTerminalHandle = class {
|
|
|
904
1258
|
* stay with the caller's config (the bash executor's, the LSP host's, …).
|
|
905
1259
|
* @module @deepseek-ai/dsh-subprocess-local
|
|
906
1260
|
*/
|
|
1261
|
+
const requireNodePty = createLazyRequire("node-pty", import.meta.url);
|
|
907
1262
|
/**
|
|
908
1263
|
* Local subprocess service: platform-selected managed ranges, Node-shaped stdio
|
|
909
1264
|
* dispositions (raw pipes, inherit, bounded tail-keep collection with spill
|
|
@@ -916,6 +1271,8 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
916
1271
|
live = /* @__PURE__ */ new Set();
|
|
917
1272
|
/** Live terminals retained through normal quiescence or host-exit finalization. */
|
|
918
1273
|
terminals = /* @__PURE__ */ new Set();
|
|
1274
|
+
/** Caller endpoints retained until close, independently of managed process lifetime. */
|
|
1275
|
+
controlChannels = /* @__PURE__ */ new Set();
|
|
919
1276
|
/** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
|
|
920
1277
|
internals = {};
|
|
921
1278
|
/** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
|
|
@@ -957,6 +1314,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
957
1314
|
this.terminals.delete(terminal);
|
|
958
1315
|
}));
|
|
959
1316
|
const outcomes = await Promise.allSettled(pending);
|
|
1317
|
+
await Promise.all([...this.controlChannels].map((control) => new Promise((resolveClose) => {
|
|
1318
|
+
control.once("close", () => {
|
|
1319
|
+
resolveClose();
|
|
1320
|
+
});
|
|
1321
|
+
control.destroy();
|
|
1322
|
+
})));
|
|
1323
|
+
this.controlChannels.clear();
|
|
960
1324
|
const failures = [];
|
|
961
1325
|
for (const outcome of outcomes) if (outcome.status === "rejected") failures.push(outcome.reason);
|
|
962
1326
|
if (failures.length > 0) this.terminateForHostExit();
|
|
@@ -980,7 +1344,7 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
980
1344
|
} catch {}
|
|
981
1345
|
}
|
|
982
1346
|
signal?.throwIfAborted();
|
|
983
|
-
throw new
|
|
1347
|
+
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
1348
|
}
|
|
985
1349
|
executableCandidates(command, env) {
|
|
986
1350
|
const path = environmentValue(env, "PATH") ?? "";
|
|
@@ -998,6 +1362,13 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
998
1362
|
handle = bindManagedProcess(spec, containmentMode === "linux-scope" ? launchLinuxScope(spec, env) : launchWindowsJob(spec, env), binding);
|
|
999
1363
|
}
|
|
1000
1364
|
this.live.add(handle);
|
|
1365
|
+
const control = handle.control;
|
|
1366
|
+
if (control !== void 0) {
|
|
1367
|
+
this.controlChannels.add(control);
|
|
1368
|
+
control.once("close", () => {
|
|
1369
|
+
this.controlChannels.delete(control);
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1001
1372
|
const release = () => handle.waitForExit().then(() => {
|
|
1002
1373
|
this.live.delete(handle);
|
|
1003
1374
|
});
|
|
@@ -1025,47 +1396,71 @@ var LocalSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1025
1396
|
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
1397
|
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
1398
|
}
|
|
1399
|
+
/** @inheritdoc */
|
|
1400
|
+
async terminalEnvironment(signal) {
|
|
1401
|
+
signal?.throwIfAborted();
|
|
1402
|
+
const platform = process.platform === "win32" ? "windows" : "posix";
|
|
1403
|
+
const defaultShell = platform === "windows" ? process.env.ComSpec || void 0 : process.env.SHELL || userInfo().shell || void 0;
|
|
1404
|
+
return {
|
|
1405
|
+
platform,
|
|
1406
|
+
...defaultShell === void 0 ? {} : { defaultShell }
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1028
1409
|
async spawnTerminal(spec) {
|
|
1029
1410
|
const file = spec.argv[0];
|
|
1030
1411
|
if (file === void 0 || file.length === 0) throw new Error("subprocess-local: terminal argv must contain a program");
|
|
1031
1412
|
spec.signal?.throwIfAborted();
|
|
1413
|
+
const inspector = this.terminalInspector ?? createProcessInspector();
|
|
1414
|
+
const containmentMode = this.selectContainmentMode("terminal");
|
|
1032
1415
|
const env = targetEnvironment(spec);
|
|
1416
|
+
const activity = prepareShellActivity(spec, env, this.internals.platform ?? process.platform);
|
|
1417
|
+
const launch = activity === void 0 ? spec : {
|
|
1418
|
+
...spec,
|
|
1419
|
+
argv: activity.argv,
|
|
1420
|
+
env: activity.env
|
|
1421
|
+
};
|
|
1033
1422
|
const options = {
|
|
1034
|
-
name:
|
|
1423
|
+
name: spec.terminalType,
|
|
1035
1424
|
rows: spec.rows,
|
|
1036
1425
|
cols: spec.cols,
|
|
1037
1426
|
cwd: spec.cwd,
|
|
1038
|
-
env
|
|
1427
|
+
env: {
|
|
1428
|
+
...activity?.env ?? env,
|
|
1429
|
+
TERM: spec.terminalType
|
|
1430
|
+
}
|
|
1039
1431
|
};
|
|
1040
|
-
|
|
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
|
-
}
|
|
1432
|
+
let scope;
|
|
1050
1433
|
let terminal;
|
|
1051
1434
|
try {
|
|
1052
|
-
|
|
1435
|
+
scope = containmentMode === "linux-scope" ? prepareLinuxTerminalScope(launch, {
|
|
1436
|
+
...activity?.env ?? env,
|
|
1437
|
+
PWD: spec.cwd,
|
|
1438
|
+
TERM: spec.terminalType
|
|
1439
|
+
}) : void 0;
|
|
1440
|
+
if (scope !== void 0) {
|
|
1441
|
+
options.cwd = scope.cwd;
|
|
1442
|
+
options.env = scope.env;
|
|
1443
|
+
}
|
|
1444
|
+
terminal = requireNodePty().spawn(scope?.command ?? file, scope?.args ?? [...launch.argv.slice(1)], options);
|
|
1053
1445
|
} catch (error) {
|
|
1054
1446
|
scope?.cleanup();
|
|
1447
|
+
activity?.dispose();
|
|
1055
1448
|
throw error;
|
|
1056
1449
|
}
|
|
1057
1450
|
let handle;
|
|
1451
|
+
const directSettlement = Promise.withResolvers();
|
|
1058
1452
|
const owner = scope?.bindOwner({
|
|
1059
1453
|
running: () => handle?.running ?? true,
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
terminal.kill(signal);
|
|
1063
|
-
} catch {}
|
|
1064
|
-
}
|
|
1454
|
+
settled: directSettlement.promise,
|
|
1455
|
+
signal: (signal) => signalLinuxDirectProcess(terminal.pid, () => process.kill(terminal.pid, signal))
|
|
1065
1456
|
});
|
|
1066
|
-
handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome)
|
|
1457
|
+
handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, this.internals.platform ?? process.platform, owner, scope?.resolveOutcome, activity, () => {
|
|
1458
|
+
this.terminals.delete(handle);
|
|
1459
|
+
}, spec.shellActivity === true);
|
|
1067
1460
|
this.terminals.add(handle);
|
|
1068
1461
|
const release = async () => {
|
|
1462
|
+
directSettlement.resolve();
|
|
1463
|
+
if (spec.shellActivity === true) return;
|
|
1069
1464
|
await handle.terminate();
|
|
1070
1465
|
this.terminals.delete(handle);
|
|
1071
1466
|
};
|