@qlzt/dsh-subprocess-local 0.0.0-stage → 0.2.1-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/lib/runner.js ADDED
@@ -0,0 +1,436 @@
1
+ import { S as loadLinuxExecve, a as resolveWindowsExecutable, b as serializeRunnerError, f as consumeLinuxLaunchRequest, g as linuxLaunchFilesFromLocator, h as isWindowsTerminateRequest, i as parseRunnerTargetArgv, r as consumeRunnerSelection, t as SUBPROCESS_RUNNER_ENV, v as parseWindowsStartRequest, x as writeLinuxStartupError } from "./runner-launch-B2zsQ1Dz.js";
2
+ import { closeSync } from "node:fs";
3
+ import { SUBPROCESS_CONTROL_FD } from "@qlzt/dsh-subprocess/control";
4
+ import { Win32Error, closeHandleChecked, isJobEmpty, loadWin32ProcessBindings, pollProcessExit, spawnCurrentTokenJobProcess, terminateJob } from "@qlzt/dsh-win32-process";
5
+ //#region lib/types/spawn-runner.js
6
+ /** One-shot Linux exec bootstrap and Windows Job-owning subprocess runner. */
7
+ const defaultInternals = {
8
+ /* v8 ignore next -- source/built/packaged subprocess smoke executes this only in a replaceable child process. */
9
+ execve: (file, argv, env, control) => loadLinuxExecve()(file, argv, env, control),
10
+ loadWin32ProcessBindings,
11
+ spawnCurrentTokenJobProcess,
12
+ closeFileDescriptor: closeSync,
13
+ resolveWindowsExecutable,
14
+ pollProcessExit,
15
+ isJobEmpty,
16
+ terminateJob,
17
+ closeHandleChecked
18
+ };
19
+ const NODE_SPAWN_DETAIL_CODES = new Set(["EACCES", "ENOENT"]);
20
+ const WINDOWS_SPAWN_ERROR_CODES = new Map([
21
+ [2, "ENOENT"],
22
+ [3, "ENOENT"],
23
+ [267, "ENOENT"],
24
+ [5, "EPERM"],
25
+ [193, "EFTYPE"],
26
+ [740, "EACCES"]
27
+ ]);
28
+ function nodeSpawnError(syscall, code, path) {
29
+ return {
30
+ name: "Error",
31
+ message: `${syscall} ${code}`,
32
+ code,
33
+ syscall,
34
+ ...path === void 0 ? {} : { path }
35
+ };
36
+ }
37
+ function asSpawnError(error, program) {
38
+ const serialized = serializeRunnerError(error);
39
+ if (!(error instanceof Win32Error)) return serialized.code === void 0 ? serialized : nodeSpawnError(`spawn ${program}`, serialized.code, program);
40
+ const code = WINDOWS_SPAWN_ERROR_CODES.get(error.win32Code) ?? "UNKNOWN";
41
+ if (NODE_SPAWN_DETAIL_CODES.has(code)) return nodeSpawnError(`spawn ${program}`, code, program);
42
+ return nodeSpawnError("spawn", code);
43
+ }
44
+ function windowsPathNotFoundError(program) {
45
+ return nodeSpawnError(`spawn ${program}`, "ENOENT", program);
46
+ }
47
+ function windowsStartCancelledError() {
48
+ return {
49
+ name: "Error",
50
+ message: "subprocess target start was cancelled"
51
+ };
52
+ }
53
+ function linuxPathNotFoundError(program) {
54
+ return Object.assign(/* @__PURE__ */ new Error(`spawn ${program} ENOENT`), {
55
+ code: "ENOENT",
56
+ errno: -2,
57
+ syscall: `spawn ${program}`,
58
+ path: program,
59
+ spawnargs: []
60
+ });
61
+ }
62
+ function execLinuxFile(file, argv, env, internals, control) {
63
+ try {
64
+ return control === void 0 ? internals.execve(file, argv, env) : internals.execve(file, argv, env, control);
65
+ } catch (error) {
66
+ if (error.code !== "ENOEXEC") throw error;
67
+ return control === void 0 ? internals.execve("/bin/sh", [
68
+ "/bin/sh",
69
+ file,
70
+ ...argv.slice(1)
71
+ ], env) : internals.execve("/bin/sh", [
72
+ "/bin/sh",
73
+ file,
74
+ ...argv.slice(1)
75
+ ], env, control);
76
+ }
77
+ }
78
+ function execLinuxTarget(request, argv, internals) {
79
+ const program = argv[0];
80
+ if (program.includes("/")) return execLinuxFile(program, argv, request.env, internals, request.control);
81
+ const path = request.env.PATH ?? "/usr/bin:/bin";
82
+ let permissionFailure;
83
+ for (const directory of path.split(":")) {
84
+ const root = directory.startsWith("/") ? directory : `${request.cwd}${request.cwd.endsWith("/") ? "" : "/"}${directory}`;
85
+ const candidate = `${root}${root.endsWith("/") ? "" : "/"}${program}`;
86
+ try {
87
+ return execLinuxFile(candidate, argv, request.env, internals, request.control);
88
+ } catch (error) {
89
+ const code = error.code;
90
+ if (code === "EACCES") {
91
+ permissionFailure ??= error;
92
+ continue;
93
+ }
94
+ if (code === "ENOENT" || code === "ENOTDIR") continue;
95
+ throw error;
96
+ }
97
+ }
98
+ throw permissionFailure ?? linuxPathNotFoundError(program);
99
+ }
100
+ function runLinux(locator, argv, host, internals) {
101
+ const files = linuxLaunchFilesFromLocator(locator);
102
+ let request;
103
+ try {
104
+ request = consumeLinuxLaunchRequest(files.requestPath);
105
+ } catch (error) {
106
+ writeLinuxStartupError(files, {
107
+ type: "error",
108
+ error: serializeRunnerError(error)
109
+ });
110
+ host.exitCode = 127;
111
+ return;
112
+ }
113
+ try {
114
+ host.chdir(request.cwd);
115
+ execLinuxTarget({
116
+ ...request,
117
+ cwd: host.cwd()
118
+ }, argv, internals);
119
+ } catch (error) {
120
+ writeLinuxStartupError(files, {
121
+ type: "error",
122
+ error: asSpawnError(error, argv[0])
123
+ });
124
+ host.exitCode = 127;
125
+ }
126
+ }
127
+ function sendMessage(host, result) {
128
+ return new Promise((resolve, reject) => {
129
+ if (!host.connected || host.send === void 0) {
130
+ reject(/* @__PURE__ */ new Error("subprocess runner IPC is not connected"));
131
+ return;
132
+ }
133
+ try {
134
+ host.send(result, (error) => {
135
+ if (error === null) resolve();
136
+ else reject(error);
137
+ });
138
+ } catch (error) {
139
+ reject(error instanceof Error ? error : new Error(String(error)));
140
+ }
141
+ });
142
+ }
143
+ var WindowsJobRunner = class {
144
+ argv;
145
+ host;
146
+ internals;
147
+ api;
148
+ processHandle;
149
+ jobHandle;
150
+ pollTimer;
151
+ startSeen = false;
152
+ terminateRequested = false;
153
+ resultStarted = false;
154
+ resultDelivered = false;
155
+ finished = false;
156
+ completion = Promise.withResolvers();
157
+ constructor(argv, host, internals) {
158
+ this.argv = argv;
159
+ this.host = host;
160
+ this.internals = internals;
161
+ }
162
+ run() {
163
+ if (!this.host.connected || this.host.send === void 0) {
164
+ this.finish(127);
165
+ return this.completion.promise;
166
+ }
167
+ this.host.on("message", this.onMessage);
168
+ this.host.once("disconnect", this.onDisconnect);
169
+ return this.completion.promise;
170
+ }
171
+ onMessage = (value) => {
172
+ if (this.finished) return;
173
+ if (isWindowsTerminateRequest(value)) {
174
+ this.requestTermination();
175
+ return;
176
+ }
177
+ if (this.startSeen) {
178
+ this.runnerFailure(/* @__PURE__ */ new Error("subprocess runner received more than one Windows start request"));
179
+ return;
180
+ }
181
+ let request;
182
+ try {
183
+ request = parseWindowsStartRequest(value);
184
+ } catch (error) {
185
+ this.runnerFailure(error);
186
+ return;
187
+ }
188
+ this.startSeen = true;
189
+ this.start(request);
190
+ };
191
+ onDisconnect = () => {
192
+ if (this.finished) return;
193
+ this.releaseOwnedJob();
194
+ this.finish(127, false);
195
+ };
196
+ async start(request) {
197
+ if (this.terminateRequested) {
198
+ await this.publishTerminalResult({
199
+ type: "error",
200
+ error: windowsStartCancelledError()
201
+ }, 0);
202
+ return;
203
+ }
204
+ await new Promise((resolveImmediate) => {
205
+ setImmediate(resolveImmediate);
206
+ });
207
+ if (this.finished) return;
208
+ if (this.terminateRequested) {
209
+ await this.publishTerminalResult({
210
+ type: "error",
211
+ error: windowsStartCancelledError()
212
+ }, 0);
213
+ return;
214
+ }
215
+ try {
216
+ const [command, ...args] = this.argv;
217
+ const applicationName = this.internals.resolveWindowsExecutable(command, request.cwd, request.env, void 0, { ...this.host.env });
218
+ if (applicationName === void 0) {
219
+ await this.publishTerminalResult({
220
+ type: "error",
221
+ error: windowsPathNotFoundError(command)
222
+ }, 0);
223
+ return;
224
+ }
225
+ this.api = this.internals.loadWin32ProcessBindings();
226
+ const spawned = this.internals.spawnCurrentTokenJobProcess(this.api, {
227
+ command,
228
+ applicationName,
229
+ args,
230
+ cwd: request.cwd,
231
+ env: request.env,
232
+ stdio: {
233
+ stdin: 4,
234
+ stdout: 5,
235
+ stderr: 6,
236
+ ...request.control === "pipe" ? { control: SUBPROCESS_CONTROL_FD } : {}
237
+ }
238
+ });
239
+ this.processHandle = spawned.process;
240
+ this.jobHandle = spawned.job;
241
+ for (const fileDescriptor of request.control === "pipe" ? [
242
+ 4,
243
+ 5,
244
+ 6,
245
+ SUBPROCESS_CONTROL_FD
246
+ ] : [
247
+ 4,
248
+ 5,
249
+ 6
250
+ ]) this.internals.closeFileDescriptor(fileDescriptor);
251
+ this.pollTimer = setInterval(() => {
252
+ this.poll();
253
+ }, 10);
254
+ } catch (error) {
255
+ if (this.jobHandle === void 0 && error instanceof Win32Error && error.api === "CreateProcessW") {
256
+ await this.publishTerminalResult({
257
+ type: "error",
258
+ error: asSpawnError(error, this.argv[0])
259
+ }, 0);
260
+ return;
261
+ }
262
+ await this.runnerFailure(error);
263
+ }
264
+ }
265
+ requestTermination() {
266
+ if (this.terminateRequested) return;
267
+ this.terminateRequested = true;
268
+ try {
269
+ this.terminateOwnedJob();
270
+ } catch (error) {
271
+ this.runnerFailure(error);
272
+ }
273
+ }
274
+ terminateOwnedJob() {
275
+ const job = this.jobHandle;
276
+ if (job === void 0) return;
277
+ /* v8 ignore next -- a Job handle is assigned only after the bindings are loaded;
278
+ * the guard above is the only reachable empty-owner state. */
279
+ if (this.api === void 0) return;
280
+ this.internals.terminateJob(this.api, job, 1);
281
+ }
282
+ poll() {
283
+ if (this.finished) return;
284
+ /* v8 ignore next -- poll is installed only after start() stores the bindings; retained as a defensive invariant guard. */
285
+ if (this.api === void 0) return;
286
+ try {
287
+ if (this.processHandle !== void 0) {
288
+ const exitCode = this.internals.pollProcessExit(this.api, this.processHandle);
289
+ if (exitCode !== void 0) {
290
+ this.internals.closeHandleChecked(this.api, this.processHandle, "ordinary direct process");
291
+ this.processHandle = void 0;
292
+ this.publishTerminalResult({
293
+ type: "target-exit",
294
+ exitCode
295
+ });
296
+ }
297
+ }
298
+ if (this.jobHandle !== void 0 && this.internals.isJobEmpty(this.api, this.jobHandle)) {
299
+ this.internals.closeHandleChecked(this.api, this.jobHandle, "ordinary process Job");
300
+ this.jobHandle = void 0;
301
+ if (this.resultDelivered) this.finish(0);
302
+ }
303
+ } catch (error) {
304
+ this.runnerFailure(error);
305
+ }
306
+ }
307
+ async publishTerminalResult(result, exitCode) {
308
+ /* v8 ignore next -- each state transition has a single result call site; the guard contains only re-entrant internal defects. */
309
+ if (this.finished || this.resultStarted) return;
310
+ this.resultStarted = true;
311
+ try {
312
+ await sendMessage(this.host, result);
313
+ this.resultDelivered = true;
314
+ } catch {
315
+ this.releaseOwnedJob();
316
+ this.finish(127, false);
317
+ return;
318
+ }
319
+ if (exitCode !== void 0) {
320
+ this.finish(exitCode);
321
+ return;
322
+ }
323
+ if (this.jobHandle === void 0) this.finish(0);
324
+ }
325
+ async runnerFailure(error) {
326
+ /* v8 ignore next -- callers stop/detach on finish; this guard contains only an already-queued internal callback. */
327
+ if (this.finished) return;
328
+ if (!this.resultStarted) {
329
+ this.resultStarted = true;
330
+ try {
331
+ await sendMessage(this.host, {
332
+ type: "error",
333
+ error: serializeRunnerError(error)
334
+ });
335
+ this.resultDelivered = true;
336
+ } catch {}
337
+ }
338
+ this.releaseOwnedJob();
339
+ this.finish(127);
340
+ }
341
+ releaseOwnedJob() {
342
+ if (this.pollTimer !== void 0) clearInterval(this.pollTimer);
343
+ this.pollTimer = void 0;
344
+ const api = this.api;
345
+ if (api === void 0) return;
346
+ if (this.jobHandle !== void 0) {
347
+ try {
348
+ this.internals.terminateJob(api, this.jobHandle, 1);
349
+ } catch {}
350
+ try {
351
+ this.internals.closeHandleChecked(api, this.jobHandle, "ordinary process Job cleanup");
352
+ } catch {}
353
+ this.jobHandle = void 0;
354
+ }
355
+ if (this.processHandle !== void 0) {
356
+ try {
357
+ this.internals.closeHandleChecked(api, this.processHandle, "ordinary direct process cleanup");
358
+ } catch {}
359
+ this.processHandle = void 0;
360
+ }
361
+ }
362
+ finish(exitCode, disconnect = true) {
363
+ if (this.finished) return;
364
+ this.finished = true;
365
+ if (this.pollTimer !== void 0) clearInterval(this.pollTimer);
366
+ this.pollTimer = void 0;
367
+ this.host.off("message", this.onMessage);
368
+ this.host.off("disconnect", this.onDisconnect);
369
+ this.host.exitCode = exitCode;
370
+ if (disconnect && this.host.connected) this.host.disconnect();
371
+ this.completion.resolve();
372
+ }
373
+ };
374
+ /**
375
+ * Execute the selected Linux bootstrap or Windows Job runner.
376
+ * @param selection - Windows sentinel or Linux launch-request locator.
377
+ * @param argv - private runner arguments beginning with the target delimiter.
378
+ * @param host - process transport and lifecycle host.
379
+ * @param internals - native and filesystem operations used by the runner.
380
+ */
381
+ async function runSpawnRunner(selection, argv, host = process, internals = defaultInternals) {
382
+ Reflect.deleteProperty(host.env, SUBPROCESS_RUNNER_ENV);
383
+ const targetArgv = parseRunnerTargetArgv(argv);
384
+ if (selection === "windows") {
385
+ await new WindowsJobRunner(targetArgv, host, internals).run();
386
+ return;
387
+ }
388
+ runLinux(selection, targetArgv, host, internals);
389
+ }
390
+ /**
391
+ * Best-effort reporting for failures before the selected runner established its owner.
392
+ * @param selection - Windows sentinel, Linux launch-request locator, or no selection.
393
+ * @param error - failure raised before normal runner settlement.
394
+ * @param host - process transport and lifecycle host.
395
+ */
396
+ async function reportSpawnRunnerFailure(selection, error, host = process) {
397
+ if (selection === "windows") {
398
+ try {
399
+ await sendMessage(host, {
400
+ type: "error",
401
+ error: serializeRunnerError(error)
402
+ });
403
+ } catch {}
404
+ host.exitCode = 127;
405
+ if (host.connected) host.disconnect();
406
+ return;
407
+ }
408
+ if (selection !== void 0) try {
409
+ writeLinuxStartupError(linuxLaunchFilesFromLocator(selection), {
410
+ type: "error",
411
+ error: serializeRunnerError(error)
412
+ });
413
+ } catch {}
414
+ host.exitCode = 127;
415
+ }
416
+ //#endregion
417
+ //#region lib/types/bin.js
418
+ /** Thin executable/importable entry for the provider-private runner core. */
419
+ /**
420
+ * Run a selector already removed by a packaging bootstrap.
421
+ * @param selection - private runner selector or Linux launch-request locator.
422
+ */
423
+ async function runSelectedSubprocessRunner(selection) {
424
+ try {
425
+ await runSpawnRunner(selection, process.argv.slice(2));
426
+ } catch (error) {
427
+ await reportSpawnRunnerFailure(selection, error);
428
+ }
429
+ }
430
+ if (import.meta.main) {
431
+ const selection = consumeRunnerSelection();
432
+ if (selection === void 0) process.exitCode = 127;
433
+ else runSelectedSubprocessRunner(selection);
434
+ }
435
+ //#endregion
436
+ export { runSelectedSubprocessRunner };
@@ -0,0 +1,7 @@
1
+ /** Thin executable/importable entry for the provider-private runner core. */
2
+ /**
3
+ * Run a selector already removed by a packaging bootstrap.
4
+ * @param selection - private runner selector or Linux launch-request locator.
5
+ */
6
+ export declare function runSelectedSubprocessRunner(selection: string): Promise<void>;
7
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Parent-side setup for one explicitly requested inherited control pipe. */
2
+ import type { Duplex, Readable, Writable } from 'node:stream';
3
+ /**
4
+ * Read the optional extra pipe from Node's stdio tuple.
5
+ * @param child - child whose requested extra pipe was allocated by Node.
6
+ * @param control - requested transport, or undefined when absent.
7
+ * @returns the parent duplex endpoint, absent when not requested or native startup failed.
8
+ */
9
+ export declare function controlPipe(child: {
10
+ readonly stdio: ReadonlyArray<Readable | Writable | null | undefined>;
11
+ }, control?: 'pipe'): Duplex | undefined;
12
+ /**
13
+ * Stamp the private marker on a fresh child environment after rejecting a caller override.
14
+ * @param env - newly materialized child environment, owned by the caller.
15
+ * @param control - requested control transport, or undefined when absent.
16
+ * @returns the same environment with the provider-owned launch marker when requested.
17
+ */
18
+ export declare function controlEnvironment<T extends NodeJS.ProcessEnv>(env: T, control?: 'pipe'): T;
19
+ //# sourceMappingURL=control-spawn.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Local Service Provider for the subprocess capability seam. Each spawn owns a
3
+ * platform-selected managed range with the spec's per-stream stdio dispositions.
4
+ * Normal disposal terminates and joins live ranges; Node's synchronous exit
5
+ * phase force-stops any ranges the service still owns. It has no config: every
6
+ * disposition and limit arrives on the spec, so deployment-varying choices
7
+ * stay with the caller's config (the bash executor's, the LSP host's, …).
8
+ * @module @qlzt/dsh-subprocess-local
9
+ */
10
+ import { Context } from '@deepseek-ai/cordis';
11
+ import { SubprocessRuntime } from '@qlzt/dsh-subprocess';
12
+ import type { SubprocessHandle, SubprocessSpawnSpec, SubprocessTerminalHandle, SubprocessTerminalEnvironment, SubprocessTerminalSpawnSpec } from '@qlzt/dsh-subprocess';
13
+ import type { SpawnInternals } from './spawn.ts';
14
+ import type { ProcessInspector } from './process-inspector.ts';
15
+ /**
16
+ * Local subprocess service: platform-selected managed ranges, Node-shaped stdio
17
+ * dispositions (raw pipes, inherit, bounded tail-keep collection with spill
18
+ * files), credential-scrubbed environment, and provider-owned range signalling.
19
+ * POSIX paths stage TERM before KILL; Windows paths terminate immediately.
20
+ * JavaScript-observable host exit also performs synchronous final termination.
21
+ */
22
+ export declare class LocalSubprocessRuntime extends SubprocessRuntime {
23
+ /** Live handles retained for normal disposal and synchronous host-exit finalization. */
24
+ private live;
25
+ /** Live terminals retained through normal quiescence or host-exit finalization. */
26
+ private terminals;
27
+ /** Caller endpoints retained until close, independently of managed process lifetime. */
28
+ private controlChannels;
29
+ /** Test hook: process, spill, and platform operations forwarded to spawnSubprocess. */
30
+ internals: SpawnInternals;
31
+ /** Provider-lifetime latch suppressing repeated weaker-containment warnings. */
32
+ private fallbackWarningIssued;
33
+ /** Positive-only cache for the expensive Linux bootstrap and scope probe. */
34
+ private linuxDeepProbePassed;
35
+ /** Test hook for platform process inspection; production resolves lazily on terminal spawn. */
36
+ terminalInspector: ProcessInspector | undefined;
37
+ constructor(ctx: Context);
38
+ /** Spill failures reach the plugin logger; the log line is the only trace of why a result has no spill path. */
39
+ private readonly reportSpillFailure;
40
+ private terminateForHostExit;
41
+ private disposeManagedProcesses;
42
+ resolveExecutable(command: string, env?: Readonly<Record<string, string>>, signal?: AbortSignal): Promise<string>;
43
+ private executableCandidates;
44
+ spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
45
+ private selectContainmentMode;
46
+ private warnFallback;
47
+ /** @inheritdoc */
48
+ terminalEnvironment(signal?: AbortSignal): Promise<SubprocessTerminalEnvironment>;
49
+ spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle>;
50
+ }
51
+ export default LocalSubprocessRuntime;
52
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,9 @@
1
+ /** Lazy libc execve and descriptor bindings used by the one-shot Linux bootstrap. */
2
+ /** Replace the current process image while preserving the supplied argv and environment. */
3
+ export type LinuxExecve = (file: string, argv: string[], env: Record<string, string>, control?: 'pipe') => never;
4
+ /**
5
+ * Load libc's execve and fcntl symbols on first use and retain the native bindings.
6
+ * @returns a process-replacing execve operation that throws Node-style errors on failure.
7
+ */
8
+ export declare function loadLinuxExecve(): LinuxExecve;
9
+ //# sourceMappingURL=linux-execve.d.ts.map
@@ -0,0 +1,91 @@
1
+ /** Linux user-systemd scope launch and managed-range ownership. */
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import type { SubprocessOutcome, SubprocessSpawnSpec, SubprocessTerminalSpawnSpec } from '@qlzt/dsh-subprocess';
4
+ import { loadLinuxExecve } from './linux-execve.ts';
5
+ import type { BoundProcessOwner, ManagedProcessLaunch } from './managed-owner.ts';
6
+ import type { RunnerInvocation } from './runner-launch.ts';
7
+ /** Test seams for systemd command execution. */
8
+ export interface LinuxScopeInternals {
9
+ spawn?: typeof spawn;
10
+ spawnSync?: typeof spawnSync;
11
+ systemctlQuery?: (command: string, args: readonly string[]) => Promise<SystemctlResult>;
12
+ systemdRun?: string;
13
+ systemctl?: string;
14
+ runnerInvocation?: RunnerInvocation;
15
+ resolveRunnerInvocation?: () => RunnerInvocation;
16
+ runnerAvailable?: (invocation: RunnerInvocation) => boolean;
17
+ loadLinuxExecve?: typeof loadLinuxExecve;
18
+ sleep?: (delayMs: number, signal?: AbortSignal) => Promise<void>;
19
+ }
20
+ interface SystemctlResult {
21
+ status: number | null;
22
+ stdout: string;
23
+ stderr: string;
24
+ error?: Error;
25
+ }
26
+ /**
27
+ * Confirm this exact runner entry and libc execve binding without a probe mode.
28
+ * @param internals - optional runner and libc-binding seams used by tests.
29
+ * @returns whether the bootstrap can enter the final target.
30
+ */
31
+ export declare function probeLinuxBootstrap(internals?: LinuxScopeInternals): boolean;
32
+ /**
33
+ * Confirm current literal-argv transient-scope support before selecting native launch.
34
+ * @param internals - optional systemd command seams used by tests.
35
+ * @returns whether the current user manager supports the required scope invocation.
36
+ */
37
+ export declare function probeLinuxScope(internals?: LinuxScopeInternals): boolean;
38
+ /**
39
+ * Confirm that the current user manager remains reachable after a positive deep probe.
40
+ * @param internals - optional systemctl seam used by tests.
41
+ * @returns whether one lightweight manager query succeeds.
42
+ */
43
+ export declare function probeLinuxManager(internals?: LinuxScopeInternals): boolean;
44
+ /**
45
+ * Re-check every Linux native prerequisite for one eligible spawn.
46
+ * @param internals - optional native capability seams used by tests.
47
+ * @returns whether the Linux native containment path is currently available.
48
+ */
49
+ export declare function probeLinuxNative(internals?: LinuxScopeInternals): boolean;
50
+ interface DirectRange {
51
+ running(): boolean;
52
+ /** True for group TERM delivery, direct signal submission, or proven direct-PID absence. */
53
+ signal(signal: 'SIGTERM' | 'SIGKILL'): boolean;
54
+ /** Direct exit/error settlement, independent of output drain and managed-range completion. */
55
+ settled: Promise<unknown>;
56
+ }
57
+ /**
58
+ * Send a direct-process signal, distinguishing an absent PID from failed delivery.
59
+ * @param pid - owned direct-process identity whose exit notification can still be pending.
60
+ * @param send - platform signal operation; true means the signal was submitted.
61
+ * @returns whether the signal was submitted or the owned PID is already absent.
62
+ */
63
+ export declare function signalLinuxDirectProcess(pid: number, send: () => boolean): boolean;
64
+ /** Linux PTY invocation and owner for the exact one-shot scope/bootstrap. */
65
+ export interface LinuxTerminalScopeLaunch {
66
+ command: string;
67
+ args: string[];
68
+ cwd: string;
69
+ env: NodeJS.ProcessEnv;
70
+ bindOwner: (direct: DirectRange) => BoundProcessOwner;
71
+ resolveOutcome: (outcome: SubprocessOutcome) => SubprocessOutcome;
72
+ cleanup: () => void;
73
+ }
74
+ /**
75
+ * Prepare one Linux PTY scope using the same launch request and bootstrap core.
76
+ * @param spec - terminal target request.
77
+ * @param targetEnv - validated complete target environment.
78
+ * @param internals - optional runner and systemd seams used by tests.
79
+ * @returns invocation and ownership callbacks; requested termination preserves the observed signal even before bootstrap consumption.
80
+ */
81
+ export declare function prepareLinuxTerminalScope(spec: SubprocessTerminalSpawnSpec, targetEnv: Record<string, string>, internals?: LinuxScopeInternals): LinuxTerminalScopeLaunch;
82
+ /**
83
+ * Launch one ordinary target inside a transient user scope.
84
+ * @param spec - ordinary target request.
85
+ * @param targetEnv - validated complete target environment.
86
+ * @param internals - optional runner and systemd seams used by tests.
87
+ * @returns streams, result, and scope owner; requested termination preserves the observed signal even before bootstrap consumption.
88
+ */
89
+ export declare function launchLinuxScope(spec: SubprocessSpawnSpec, targetEnv: Record<string, string>, internals?: LinuxScopeInternals): ManagedProcessLaunch;
90
+ export {};
91
+ //# sourceMappingURL=linux-scope.d.ts.map
@@ -0,0 +1,36 @@
1
+ /** Minimal managed-range ownership bound to one ordinary subprocess handle. */
2
+ import type { Duplex, Readable, Writable } from 'node:stream';
3
+ import type { SubprocessOutcome } from '@qlzt/dsh-subprocess';
4
+ /** Platform owner used by termination and whole-range settlement. */
5
+ export interface BoundProcessOwner {
6
+ /** Signal the managed range; `cancellationReason` is used only before Windows target commit. */
7
+ signal(signal: 'SIGTERM' | 'SIGKILL', cancellationReason?: unknown): void;
8
+ /** Wait for the same managed range to become empty; reject when it cannot be observed. */
9
+ waitForExit(): Promise<void>;
10
+ /**
11
+ * Count tasks in the native range, including descendants outside the observable process tree.
12
+ * @returns the current count, or undefined when observation is unavailable.
13
+ */
14
+ inspectTaskCount?(): number | undefined;
15
+ /** Synchronously force final termination during JavaScript-observable host exit. */
16
+ terminateForHostExit(): void;
17
+ /** Release provider-private protocol artifacts after outcome and range settlement. */
18
+ cleanup?(): void;
19
+ }
20
+ /** Platform launch facts consumed by the common stdio and result lifecycle. */
21
+ export interface ManagedProcessLaunch {
22
+ stdin: Writable | null;
23
+ stdout: Readable | null;
24
+ stderr: Readable | null;
25
+ control?: Duplex | undefined;
26
+ direct: Promise<SubprocessOutcome>;
27
+ owner: BoundProcessOwner;
28
+ }
29
+ /**
30
+ * Apply an optional abort bound to one shared wait promise.
31
+ * @param pending - managed-range wait shared by all callers.
32
+ * @param signal - optional caller cancellation signal.
33
+ * @returns whether the managed-range wait completed before cancellation.
34
+ */
35
+ export declare function waitWithAbort(pending: Promise<void>, signal?: AbortSignal): Promise<boolean>;
36
+ //# sourceMappingURL=managed-owner.d.ts.map