@x1a0f3n9/dsh-subprocess-local 0.1.5-rc.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.
@@ -0,0 +1,135 @@
1
+ /** Platform process-table inspection for terminal readiness, signals, and teardown. */
2
+ import type { SubprocessTerminalSignal } from '@x1a0f3n9/dsh-subprocess';
3
+ /** PID plus start identity, preventing teardown escalation after PID reuse. */
4
+ export interface ProcessIdentity {
5
+ pid: number;
6
+ started: string;
7
+ }
8
+ interface FileStatus {
9
+ readonly rdev: number;
10
+ isCharacterDevice(): boolean;
11
+ }
12
+ /**
13
+ * One observation of the platform process table, shared by every question a
14
+ * single readiness poll or teardown pass asks.
15
+ *
16
+ * The table is read at most once, on the first question that needs it — a
17
+ * `/bin/ps` fork on macOS, a `/proc` walk on Linux, a Toolhelp32 enumeration on
18
+ * Windows. Later questions never re-read it, which is what keeps a poll's cost
19
+ * independent of how many descendants the running command spawned. Windows
20
+ * liveness needs no table at all: wait state is a per-handle question there, so
21
+ * a snapshot asked only for liveness never enumerates.
22
+ *
23
+ * A snapshot answers what the process table showed, which is what batch
24
+ * filtering wants and what signalling must not use: {@link ProcessInspector.isAlive}
25
+ * is the fence a signal takes, because it reads current state instead.
26
+ */
27
+ export interface ProcessSnapshot {
28
+ /**
29
+ * Return the root and its transitive descendants as observed, children first.
30
+ * @param rootPid - tree root to descend from.
31
+ * @returns Observed root and descendants, children before parents.
32
+ */
33
+ tree(rootPid: number): ProcessIdentity[];
34
+ /**
35
+ * Return observed members of one POSIX process session.
36
+ * @param sessionId - POSIX session identifier.
37
+ * @returns Observed session members, empty where the platform's table omits session ids.
38
+ */
39
+ session(sessionId: number): ProcessIdentity[];
40
+ /**
41
+ * Return whether the exact identity was a non-quiescent process.
42
+ * @param identity - PID plus start identity to match.
43
+ * @returns Whether that exact identity — not merely that PID — was running.
44
+ */
45
+ alive(identity: ProcessIdentity): boolean;
46
+ }
47
+ /** Injectable OS process operations used by one local PTY session. */
48
+ export interface ProcessInspector {
49
+ foregroundPgid(shellPid: number): number | undefined;
50
+ /**
51
+ * Report whether the foreground group waits on the terminal shell's stdin.
52
+ *
53
+ * @param pgid Foreground process-group identifier.
54
+ * @param shellPid Persistent terminal shell process identifier.
55
+ * @returns Whether a group member is blocked reading the shell's terminal input.
56
+ */
57
+ isStdinWaiting(pgid: number, shellPid: number): boolean;
58
+ /**
59
+ * Read the process table once and answer tree, session, and liveness from it.
60
+ * @returns A process-table observation whose reads are shared.
61
+ */
62
+ snapshot(): ProcessSnapshot;
63
+ /**
64
+ * Return whether the exact identity is a non-quiescent process right now.
65
+ *
66
+ * Reads the narrowest per-identity source the platform offers rather than a
67
+ * whole table, so a signalling round can re-check every target without
68
+ * paying for a scan. Callers filtering many members at once want
69
+ * {@link ProcessSnapshot.alive} instead.
70
+ *
71
+ * @param identity - PID plus start identity to match.
72
+ * @returns Whether that exact identity — not merely that PID — is running.
73
+ */
74
+ isAlive(identity: ProcessIdentity): boolean;
75
+ signalGroup(pgid: number, signal: SubprocessTerminalSignal): void;
76
+ /**
77
+ * Signal one exact process identity, fenced against PID reuse.
78
+ *
79
+ * The fence reads current state immediately before the signal. An observation
80
+ * taken earlier in the same round cannot stand in for it: the observation
81
+ * preserves the original PID-to-start-time pairing, so a recycled PID would
82
+ * still match and take a signal meant for the process that exited.
83
+ *
84
+ * @param identity - PID plus start identity to signal.
85
+ * @param signal - termination signal to deliver.
86
+ */
87
+ signalProcess(identity: ProcessIdentity, signal: 'SIGTERM' | 'SIGKILL'): void;
88
+ }
89
+ /** Testable boundary around filesystem, process-table, and signal syscalls. */
90
+ export interface ProcessInspectorInternals {
91
+ readFile(path: string): string;
92
+ readDir(path: string): string[];
93
+ readLink(path: string): string;
94
+ stat(path: string): FileStatus;
95
+ open(path: string): number;
96
+ read(fd: number, buffer: Buffer, length: number, position: number): number;
97
+ close(fd: number): void;
98
+ exec(file: string, args: string[]): string;
99
+ kill(pid: number, signal: NodeJS.Signals): void;
100
+ }
101
+ interface ProcStat {
102
+ pid: number;
103
+ parentPid: number;
104
+ pgrp: number;
105
+ session: number;
106
+ state: string;
107
+ ttyDevice: number;
108
+ tpgid: number;
109
+ started: string;
110
+ }
111
+ /**
112
+ * Parse fields used from Linux `/proc/<pid>/stat`, including parenthesized comm text.
113
+ * @param text - complete stat line.
114
+ * @returns Parsed identity/group fields, or undefined for malformed input.
115
+ */
116
+ export declare function parseProcStat(text: string): ProcStat | undefined;
117
+ /**
118
+ * Report whether a Linux process group has an executing member. `false`
119
+ * means the group contains only zombie/dead entries; `undefined` means the
120
+ * process table could not prove either outcome.
121
+ * @param processGroupId - POSIX process-group id to inspect.
122
+ * @param internals - injectable process-table operations.
123
+ * @returns Live-member presence, or `undefined` when unavailable/absent.
124
+ */
125
+ export declare function linuxProcessGroupHasLiveMembers(processGroupId: number, internals?: ProcessInspectorInternals): boolean | undefined;
126
+ /**
127
+ * Create the supported platform inspector or fail at plugin load.
128
+ * @param platform - target Node platform.
129
+ * @param arch - target CPU architecture for Linux syscall numbers.
130
+ * @param internals - filesystem/process boundary, injectable for deterministic tests.
131
+ * @returns Platform process inspector.
132
+ */
133
+ export declare function createProcessInspector(platform?: NodeJS.Platform, arch?: NodeJS.Architecture, internals?: ProcessInspectorInternals): ProcessInspector;
134
+ export {};
135
+ //# sourceMappingURL=process-inspector.d.ts.map
@@ -0,0 +1,66 @@
1
+ /** Parent-side invocation and bootstrap state for the private native runner. */
2
+ import type { StdioOptions } from 'node:child_process';
3
+ import type { SubprocessSpawnSpec } from '@x1a0f3n9/dsh-subprocess';
4
+ /** The one private environment variable consumed before target state is restored. */
5
+ export declare const SUBPROCESS_RUNNER_ENV: "DSH_SUBPROCESS_RUNNER";
6
+ /** Sentinel used by the packaged bootstrap for the Windows IPC runner. */
7
+ export declare const WINDOWS_RUNNER_SELECTION: "windows";
8
+ /** Non-empty command tuple used to launch the private runner entry. */
9
+ export type RunnerInvocation = [string, ...string[]];
10
+ /**
11
+ * Resolve the source, built, or packaged entry that calls the same runner core.
12
+ * @returns executable and arguments for the active runtime form.
13
+ */
14
+ export declare function spawnRunnerInvocation(): RunnerInvocation;
15
+ /**
16
+ * Check the concrete runner executable and entry paths without executing a probe mode.
17
+ * @param invocation - resolved executable and runner-entry arguments.
18
+ * @returns whether every concrete executable or entry path is accessible.
19
+ */
20
+ export declare function runnerInvocationAvailable(invocation?: RunnerInvocation): boolean;
21
+ /**
22
+ * Build the bootstrap-safe environment; target overrides arrive through request/IPC.
23
+ * @param selection - private runner selector or Linux launch-request locator.
24
+ * @param invocation - resolved runner invocation whose source form needs the workspace paths map.
25
+ * @returns environment for the runner before target state is restored.
26
+ */
27
+ export declare function runnerEnvironment(selection: string, invocation?: RunnerInvocation): NodeJS.ProcessEnv;
28
+ /**
29
+ * Read and delete the private selector before importing or restoring target state.
30
+ * @param env - mutable environment containing the private selector.
31
+ * @returns the consumed selector, or undefined when no runner was requested.
32
+ */
33
+ export declare function consumeRunnerSelection(env?: NodeJS.ProcessEnv): string | undefined;
34
+ /**
35
+ * Require the private argv delimiter and at least one target argv entry.
36
+ * @param argv - private runner arguments.
37
+ * @returns copied target argv after the private delimiter.
38
+ */
39
+ export declare function parseRunnerTargetArgv(argv: readonly string[]): string[];
40
+ /**
41
+ * Build direct Linux target stdio, or isolated Windows runner stdio with IPC
42
+ * on fd 3 and target carriers on fd 4 through fd 6.
43
+ * @param spec - ordinary subprocess request whose stdio modes are preserved.
44
+ * @param ipc - whether to isolate the runner and add its private Node IPC descriptor.
45
+ * @param stdinCarrier - runner fd 4 carrier; Windows ignore passes an opened null-device fd.
46
+ * @returns child-process stdio options for the runner.
47
+ */
48
+ export declare function runnerStdio(spec: SubprocessSpawnSpec, ipc: boolean, stdinCarrier?: 'pipe' | number): StdioOptions;
49
+ /**
50
+ * Resolve the executable path with libuv/Node Windows spawn search order while
51
+ * preserving the caller's original command-line argv entry separately.
52
+ * @param command - original target argv[0].
53
+ * @param cwd - final target working directory used for relative search roots.
54
+ * @param env - final target environment containing the child PATH.
55
+ * @param exists - injectable non-directory candidate probe used by tests.
56
+ * @param currentEnv - runner environment supplying PATH fallback and cwd-search policy.
57
+ * @returns a resolved application name suitable for `CreateProcessW`, or undefined when no candidate exists.
58
+ */
59
+ export declare function resolveWindowsExecutable(command: string, cwd: string, env: Readonly<Record<string, string>>, exists?: (candidate: string) => boolean, currentEnv?: Readonly<Record<string, string | undefined>>): string | undefined;
60
+ /**
61
+ * Materialize and synchronously validate the final target environment.
62
+ * @param spec - final target argv, cwd, and environment overrides.
63
+ * @returns complete target environment after Node-equivalent validation.
64
+ */
65
+ export declare function targetEnvironment(spec: Pick<SubprocessSpawnSpec, 'argv' | 'cwd' | 'env'>): Record<string, string>;
66
+ //# sourceMappingURL=runner-launch.d.ts.map
@@ -0,0 +1,109 @@
1
+ /** Closed private transports shared by the native subprocess runner. */
2
+ /** Target state restored by the Linux bootstrap after systemd establishes the scope. */
3
+ export interface LinuxLaunchRequest {
4
+ cwd: string;
5
+ env: Record<string, string>;
6
+ }
7
+ /** Bounded Node-shaped error fields allowed across a private runner boundary. */
8
+ export interface SerializedRunnerError {
9
+ name: string;
10
+ message: string;
11
+ code?: string;
12
+ syscall?: string;
13
+ path?: string;
14
+ }
15
+ /** A Linux pre-exec failure published beside its consumed request. */
16
+ export type LinuxStartupError = {
17
+ type: 'error';
18
+ error: SerializedRunnerError;
19
+ };
20
+ /** The only parent-to-runner start message on Windows. */
21
+ export interface WindowsStartRequest {
22
+ type: 'start';
23
+ cwd: string;
24
+ env: Record<string, string>;
25
+ }
26
+ /** The only parent-to-runner control message on Windows. */
27
+ export interface WindowsTerminateRequest {
28
+ type: 'terminate';
29
+ }
30
+ /** Exactly one direct-result branch is sent by a connected Windows runner. */
31
+ export type WindowsRunnerResult = {
32
+ type: 'target-exit';
33
+ exitCode: number;
34
+ } | {
35
+ type: 'error';
36
+ error: SerializedRunnerError;
37
+ };
38
+ /** Private paths owned by one Linux ordinary or PTY spawn. */
39
+ export interface LinuxLaunchFiles {
40
+ directory: string;
41
+ requestPath: string;
42
+ startupErrorPath: string;
43
+ }
44
+ /**
45
+ * Create a private 0700 directory and one complete 0600 launch request.
46
+ * @param request - target cwd and complete environment for the bootstrap.
47
+ * @returns private paths owned by this launch.
48
+ */
49
+ export declare function createLinuxLaunchFiles(request: LinuxLaunchRequest): LinuxLaunchFiles;
50
+ /**
51
+ * Derive the only permitted startup-error path from an absolute request locator.
52
+ * @param requestPath - absolute path to the private launch-request file.
53
+ * @returns validated sibling paths for this launch.
54
+ */
55
+ export declare function linuxLaunchFilesFromLocator(requestPath: string): LinuxLaunchFiles;
56
+ /**
57
+ * Strictly read and remove a one-shot Linux launch request.
58
+ * @param requestPath - private launch-request path to consume.
59
+ * @returns validated target cwd and environment.
60
+ */
61
+ export declare function consumeLinuxLaunchRequest(requestPath: string): LinuxLaunchRequest;
62
+ /**
63
+ * Publish one strict 0600 Linux pre-exec error.
64
+ * @param files - private paths for this launch.
65
+ * @param error - bounded spawn or runner failure to publish.
66
+ */
67
+ export declare function writeLinuxStartupError(files: LinuxLaunchFiles, error: LinuxStartupError): void;
68
+ /**
69
+ * Read the Linux pre-exec error, if the bootstrap published one.
70
+ * @param path - expected startup-error path.
71
+ * @returns the validated failure, or undefined when none was published.
72
+ */
73
+ export declare function readLinuxStartupError(path: string): LinuxStartupError | undefined;
74
+ /**
75
+ * Strictly parse the single Windows start message.
76
+ * @param value - untrusted IPC payload.
77
+ * @returns validated target start request.
78
+ */
79
+ export declare function parseWindowsStartRequest(value: unknown): WindowsStartRequest;
80
+ /**
81
+ * Return true only for the exact, payload-free Windows terminate control.
82
+ * @param value - untrusted IPC payload.
83
+ * @returns whether the payload is the exact terminate request.
84
+ */
85
+ export declare function isWindowsTerminateRequest(value: unknown): value is WindowsTerminateRequest;
86
+ /**
87
+ * Strictly parse one of the two Windows direct-result branches.
88
+ * @param value - untrusted IPC payload.
89
+ * @returns validated direct-result message.
90
+ */
91
+ export declare function parseWindowsRunnerResult(value: unknown): WindowsRunnerResult;
92
+ /**
93
+ * Convert an unknown failure into the bounded cross-process error record.
94
+ * @param error - failure caught at the process boundary.
95
+ * @returns bounded serializable error fields.
96
+ */
97
+ export declare function serializeRunnerError(error: unknown): SerializedRunnerError;
98
+ /**
99
+ * Rebuild a Node-shaped Error from a strict runner record.
100
+ * @param serialized - validated bounded error fields.
101
+ * @returns reconstructed Error with supported Node fields.
102
+ */
103
+ export declare function deserializeRunnerError(serialized: SerializedRunnerError): Error;
104
+ /**
105
+ * Best-effort removal of only the private paths created for this Linux spawn.
106
+ * @param files - exact private paths owned by this launch.
107
+ */
108
+ export declare function cleanupLinuxLaunchFiles(files: LinuxLaunchFiles): void;
109
+ //# sourceMappingURL=runner-protocol.d.ts.map
@@ -0,0 +1,36 @@
1
+ /** One-shot Linux exec bootstrap and Windows Job-owning subprocess runner. */
2
+ import { closeHandleChecked, isJobEmpty, pollProcessExit, spawnCurrentTokenJobProcess, terminateJob } from '@x1a0f3n9/dsh-win32-process';
3
+ import type { CurrentTokenProcessBindings } from '@x1a0f3n9/dsh-win32-process';
4
+ import { resolveWindowsExecutable } from './runner-launch.ts';
5
+ type RunnerHost = Pick<NodeJS.Process, 'env' | 'exitCode' | 'connected' | 'cwd' | 'chdir' | 'on' | 'off' | 'once' | 'disconnect'> & {
6
+ send?: NodeJS.Process['send'];
7
+ };
8
+ /** Injectable operations used by the protocol-owner tests. */
9
+ export interface SpawnRunnerInternals {
10
+ execve(file: string, argv: string[], env: Record<string, string>): never;
11
+ loadWin32ProcessBindings(): CurrentTokenProcessBindings;
12
+ spawnCurrentTokenJobProcess: typeof spawnCurrentTokenJobProcess;
13
+ closeFileDescriptor(fileDescriptor: number): void;
14
+ resolveWindowsExecutable: typeof resolveWindowsExecutable;
15
+ pollProcessExit: typeof pollProcessExit;
16
+ isJobEmpty: typeof isJobEmpty;
17
+ terminateJob: typeof terminateJob;
18
+ closeHandleChecked: typeof closeHandleChecked;
19
+ }
20
+ /**
21
+ * Execute the selected Linux bootstrap or Windows Job runner.
22
+ * @param selection - Windows sentinel or Linux launch-request locator.
23
+ * @param argv - private runner arguments beginning with the target delimiter.
24
+ * @param host - process transport and lifecycle host.
25
+ * @param internals - native and filesystem operations used by the runner.
26
+ */
27
+ export declare function runSpawnRunner(selection: string, argv: readonly string[], host?: RunnerHost, internals?: SpawnRunnerInternals): Promise<void>;
28
+ /**
29
+ * Best-effort reporting for failures before the selected runner established its owner.
30
+ * @param selection - Windows sentinel, Linux launch-request locator, or no selection.
31
+ * @param error - failure raised before normal runner settlement.
32
+ * @param host - process transport and lifecycle host.
33
+ */
34
+ export declare function reportSpawnRunnerFailure(selection: string | undefined, error: unknown, host?: RunnerHost): Promise<void>;
35
+ export {};
36
+ //# sourceMappingURL=spawn-runner.d.ts.map
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Process plumbing for the local subprocess service: ordinary process launch
3
+ * with per-stream stdio dispositions, tail-keep collection with spill
4
+ * files, provider-owned range signalling, and common termination scheduling.
5
+ * POSIX owners stage TERM before KILL; Windows owners terminate immediately.
6
+ * This layer reacts to an abort signal; callers own deadlines, teardown
7
+ * ladders, and cause classification.
8
+ * @module dsh-subprocess-local/spawn
9
+ */
10
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
11
+ import type { CollectedOutput, SubprocessHandle, SubprocessSpawnSpec } from '@x1a0f3n9/dsh-subprocess';
12
+ import type { ManagedProcessLaunch } from './managed-owner.ts';
13
+ type SpawnProcess = (program: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
14
+ /**
15
+ * Build a child environment: explicit caller entries override the scrubbed
16
+ * parent base using the target platform's environment-key semantics. A string
17
+ * deliberately restores or overrides an entry; an explicit `undefined`
18
+ * tombstone removes an ordinary ambient entry.
19
+ * @param extra - explicit caller entries and tombstones, merged after the scrub.
20
+ * @returns the environment to hand to `spawn` for the child process.
21
+ */
22
+ export declare function childEnv(extra?: Readonly<NodeJS.ProcessEnv>): NodeJS.ProcessEnv;
23
+ /** Injectable process, spill, and platform operations. */
24
+ export interface SpawnInternals {
25
+ /** Process spawner (defaults to `node:child_process` `spawn`). */
26
+ spawn?: SpawnProcess;
27
+ /** Directory for spill files (defaults to the OS temp dir). */
28
+ spillDir?: string;
29
+ /** Windows tree-termination runner (defaults to `taskkill /PID <pid> /T /F`). */
30
+ taskkill?: (pid: number) => void;
31
+ /** Host platform override for signalling decisions. */
32
+ platform?: NodeJS.Platform;
33
+ /** Linux process-group member probe (defaults to `/proc` inspection). */
34
+ linuxProcessGroupHasLiveMembers?: (processGroupId: number) => boolean | undefined;
35
+ }
36
+ /**
37
+ * Local-only synchronous final termination used by the owning service during
38
+ * host exit and as the last fallback after failed normal disposal. It is
39
+ * intentionally absent from the public subprocess seam.
40
+ */
41
+ export interface LocalSubprocessHandle extends SubprocessHandle {
42
+ /** Force-terminate the current tree synchronously without starting timers or waits. */
43
+ terminateForHostExit(): void;
44
+ }
45
+ /**
46
+ * Prepare fallible output storage before starting a managed native process.
47
+ * @param internals - optional caller-owned spill directory.
48
+ * @returns binding inputs whose spill directory is ready for use.
49
+ */
50
+ export declare function prepareManagedProcessBinding(internals?: Pick<SpawnInternals, 'spillDir'>): {
51
+ spillDir: string;
52
+ };
53
+ /**
54
+ * Collects one stream with a bounded in-memory tail. With a spill cap, on
55
+ * first overflow a spill file is created and every chunk (including those
56
+ * already collected) is appended there while the full stream remains within
57
+ * the cap; without one, only the in-memory tail is ever retained (the
58
+ * diagnostic-tail shape — a language server's stderr).
59
+ *
60
+ * Tail-keep rationale (pi/OpenCode): errors and final results cluster at the
61
+ * end of command output; the spill file covers the head.
62
+ */
63
+ export declare class OutputCollector {
64
+ private readonly maxBytes;
65
+ private readonly maxSpillBytes;
66
+ private readonly label;
67
+ private readonly spillDir;
68
+ private chunks;
69
+ private bytes;
70
+ private dropped;
71
+ private spillFd;
72
+ private spillFile;
73
+ private spillDisabled;
74
+ /** Total bytes ever pushed (not just retained). */
75
+ private total;
76
+ constructor(maxBytes: number, maxSpillBytes: number | undefined, label: string, spillDir: string);
77
+ /**
78
+ * Ingest one stream chunk, counting it toward the whole-stream total. On
79
+ * first overflow of the in-memory cap a spill file is opened (when spilling
80
+ * is enabled) and every chunk (already-collected ones included) is appended
81
+ * there from then on; the in-memory tail then drops whole chunks from its
82
+ * head (or the head of a single over-cap chunk) until it fits the cap again.
83
+ * @param chunk - the raw bytes from one stream 'data' event.
84
+ */
85
+ push(chunk: Buffer): void;
86
+ /** Open the spill file lazily and append `chunk` (and any prior chunks once). */
87
+ private spillAll;
88
+ /** Stop spilling and remove the file once it can no longer hold the complete stream. */
89
+ private discardSpill;
90
+ /**
91
+ * Incremental read in whole-stream byte coordinates: returns everything
92
+ * pushed since `fromByte`. When `fromByte` has already slid out of the
93
+ * in-memory tail window, the read is `lossy` — it returns the whole
94
+ * retained tail and the gap is only recoverable from the spill file.
95
+ * @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
96
+ * @returns the delta text, the offset for the next read, the `lossy` flag, and the spill path when one was created.
97
+ */
98
+ readFrom(fromByte: number): {
99
+ text: string;
100
+ nextOffset: number;
101
+ lossy: boolean;
102
+ spillPath?: string;
103
+ };
104
+ /**
105
+ * Close the spill file once the stream has ended. A failed close (delayed
106
+ * writeback fault) stops advertising the spill path — the file may be
107
+ * missing its tail — while every in-memory read keeps working. Idempotent;
108
+ * the spawn path seals both collectors at settlement so reads after exit
109
+ * never point at a still-open file.
110
+ */
111
+ seal(): void;
112
+ /**
113
+ * Seal the spill file and return the final output.
114
+ * @returns the final collected output: tail text, truncation flag, and the spill path when intact.
115
+ */
116
+ finalize(): CollectedOutput;
117
+ }
118
+ /**
119
+ * Send `sig` to a detached POSIX process group. Never throws: delivery races
120
+ * process exit and may run in a timer callback, so failures are contained and
121
+ * a missing pid is a no-op.
122
+ * @param pid - the group leader's pid, when the spawn published one.
123
+ * @param sig - the signal to deliver to the whole group.
124
+ */
125
+ export declare function killGroup(pid: number | undefined, sig: NodeJS.Signals): void;
126
+ /**
127
+ * Terminate one Windows process tree with `taskkill /T /F`. Contained like
128
+ * POSIX group signalling — delivery races tree exit, so an absent tree, a
129
+ * nonzero status, or a missing taskkill binary must not break idempotent
130
+ * teardown.
131
+ * @param pid - root process id, when the spawn published one.
132
+ */
133
+ export declare function taskkillProcessTree(pid: number | undefined): void;
134
+ /**
135
+ * Validate the synchronous portion of one ordinary spawn request.
136
+ * @param spec - exact target request.
137
+ * @throws when grace, cancellation, or argv is invalid before launch.
138
+ */
139
+ export declare function validateSubprocessSpec(spec: SubprocessSpawnSpec): void;
140
+ /**
141
+ * Bind platform launch facts to the existing stdio, outcome, abort, and termination lifecycle.
142
+ * @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
143
+ * @param launch - platform streams, direct outcome, and managed-range owner.
144
+ * @param internals - test-only spill-directory override.
145
+ * @returns live subprocess handle.
146
+ */
147
+ export declare function bindManagedProcess(spec: SubprocessSpawnSpec, launch: ManagedProcessLaunch, internals?: Pick<SpawnInternals, 'spillDir'>): LocalSubprocessHandle;
148
+ /**
149
+ * Spawn one detached PGID/taskkill fallback and bind the common lifecycle.
150
+ * @param spec - fully resolved argv, cwd, stdio, grace, cancellation, environment.
151
+ * @param internals - test-only spill-directory, platform, and taskkill overrides.
152
+ * @returns live subprocess handle.
153
+ */
154
+ export declare function spawnSubprocess(spec: SubprocessSpawnSpec, internals?: SpawnInternals): LocalSubprocessHandle;
155
+ export {};
156
+ //# sourceMappingURL=spawn.d.ts.map
@@ -0,0 +1,69 @@
1
+ /** Local node-pty terminal-process implementation for the subprocess seam. */
2
+ import { PassThrough } from 'node:stream';
3
+ import type { IPty } from 'node-pty';
4
+ import type { SubprocessOutcome, SubprocessTerminalForeground, SubprocessTerminalHandle, SubprocessTerminalSignal } from '@x1a0f3n9/dsh-subprocess';
5
+ import type { BoundProcessOwner } from './managed-owner.ts';
6
+ import type { ProcessInspector } from './process-inspector.ts';
7
+ /**
8
+ * A local terminal whose native managed range or fallback process-session
9
+ * ownership stays below the PTY backend.
10
+ * The seam's terminate() promise — no write, inspection, or signal in flight
11
+ * after settlement — holds here without operation tracking only because every
12
+ * handle call completes synchronously under the hood (node-pty write, ps-based
13
+ * inspection). A first genuinely asynchronous step in any handle call must add
14
+ * the tracking a remote provider needs.
15
+ */
16
+ export declare class LocalTerminalHandle implements SubprocessTerminalHandle {
17
+ private readonly terminal;
18
+ private readonly inspector;
19
+ private readonly graceMs;
20
+ private readonly platform;
21
+ private readonly managedOwner?;
22
+ private readonly resolveManagedOutcome?;
23
+ readonly pid: number;
24
+ readonly output: PassThrough;
25
+ readonly done: Promise<SubprocessOutcome>;
26
+ private readonly outcome;
27
+ private readonly dataDisposable;
28
+ private readonly exitDisposable;
29
+ private cleanup;
30
+ private managedOwnerCleaned;
31
+ private exited;
32
+ private trackedDescendants;
33
+ /** The spawned shell's start identity; scans stop adopting members once the root pid no longer carries it. */
34
+ private readonly rootIdentity;
35
+ /**
36
+ * @param terminal - allocated node-pty process.
37
+ * @param inspector - platform process/session operations.
38
+ * @param graceMs - TERM-to-KILL and exit-wait grace.
39
+ * @param platform - host platform; defaults to the running platform, injectable for deterministic tests.
40
+ */
41
+ constructor(terminal: IPty, inspector: ProcessInspector, graceMs: number, platform?: NodeJS.Platform, managedOwner?: BoundProcessOwner | undefined, resolveManagedOutcome?: ((outcome: SubprocessOutcome) => SubprocessOutcome) | undefined);
42
+ /** Whether node-pty has not yet published the top-level exit event. */
43
+ get running(): boolean;
44
+ write(data: string): Promise<void>;
45
+ inspectForeground(): Promise<SubprocessTerminalForeground | undefined>;
46
+ signalForeground(signal: SubprocessTerminalSignal): Promise<number>;
47
+ terminate(): Promise<void>;
48
+ /**
49
+ * Force-terminate the observable session synchronously during Node's exit
50
+ * event. This does not claim quiescence and does not replace terminate().
51
+ */
52
+ terminateForHostExit(): void;
53
+ private forceStopShell;
54
+ private survivors;
55
+ private descendants;
56
+ private waitForMembers;
57
+ private signalMembers;
58
+ private forceStopDescendants;
59
+ private unionMembers;
60
+ private stopDescendants;
61
+ private stopShell;
62
+ private stopShellWindows;
63
+ private waitForWindowsShellExit;
64
+ private closeOnce;
65
+ private cleanupManagedOwner;
66
+ private closeManagedRange;
67
+ private settleExitIfGone;
68
+ }
69
+ //# sourceMappingURL=terminal.d.ts.map
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Windows process-table operations for terminal readiness, signalling, and
3
+ * teardown: Toolhelp32 snapshot enumeration with GetProcessTimes creation-time
4
+ * identity and process-handle wait-state liveness, the shell pid as a pseudo
5
+ * process group (Windows has no POSIX groups), and taskkill tree signalling.
6
+ * The koffi bindings load lazily so
7
+ * non-Windows processes never touch Win32 libraries; all decision logic takes
8
+ * an injectable internals boundary so suites can pin it on any host.
9
+ * @module dsh-subprocess-local/windows-inspector
10
+ */
11
+ import type { SubprocessTerminalSignal } from '@x1a0f3n9/dsh-subprocess';
12
+ import type { ProcessIdentity, ProcessInspector, ProcessSnapshot } from './process-inspector.ts';
13
+ /** One Toolhelp32 process-table row. */
14
+ export interface ProcessEntry {
15
+ pid: number;
16
+ parentPid: number;
17
+ }
18
+ /** Creation identity plus the process object's current wait state. */
19
+ export interface WindowsProcessState {
20
+ /** GetProcessTimes creation identity used to fence PID reuse. */
21
+ started: string;
22
+ /** Whether a zero-time process-handle wait reports the process still running. */
23
+ active: boolean;
24
+ }
25
+ /** Injectable Windows process operations used by one local PTY session. */
26
+ export interface WindowsProcessInspectorInternals {
27
+ /** Enumerate the current process table (pid/parent pairs). */
28
+ snapshot(): ProcessEntry[];
29
+ /** Return one process's creation identity and wait state, or undefined when unreadable. */
30
+ processState(pid: number): WindowsProcessState | undefined;
31
+ /** Terminate one process tree; `force` maps to taskkill `/F`. */
32
+ taskkill(pid: number, force: boolean): void;
33
+ }
34
+ /**
35
+ * Walk a process table from one root in children-first order, retaining only
36
+ * members whose start identity is readable (unreadable members are detector
37
+ * misses, exactly like an unreadable `/proc` entry on Linux).
38
+ * @param entries - the process table snapshot.
39
+ * @param rootPid - the tree root to descend from.
40
+ * @param started - creation-time identity resolver for one member.
41
+ * @returns the root and its current transitive descendants, children first.
42
+ */
43
+ export declare function windowsProcessTree(entries: ProcessEntry[], rootPid: number, started: (pid: number) => string | undefined): ProcessIdentity[];
44
+ /**
45
+ * Windows {@link ProcessInspector}. The shell pid stands in for a foreground
46
+ * process group: it is a stable pseudo-group that lets the prompt-marker
47
+ * readiness path compare foreground identities, while every actual signal
48
+ * targets the console-wide tree through taskkill (SIGINT is delivered by the
49
+ * terminal handle as a `\x03` input write and never reaches this layer).
50
+ */
51
+ export declare class WindowsProcessInspector implements ProcessInspector {
52
+ private readonly internals;
53
+ constructor(internals?: WindowsProcessInspectorInternals);
54
+ foregroundPgid(shellPid: number): number;
55
+ isStdinWaiting(_pgid: number, _shellPid: number): boolean;
56
+ isAlive(identity: ProcessIdentity): boolean;
57
+ snapshot(): ProcessSnapshot;
58
+ signalGroup(pgid: number, signal: SubprocessTerminalSignal): void;
59
+ signalProcess(identity: ProcessIdentity, signal: 'SIGTERM' | 'SIGKILL'): void;
60
+ }
61
+ /**
62
+ * Create the Windows process inspector.
63
+ * @param internals - injectable process operations; defaults to the koffi-backed table.
64
+ * @returns the Windows inspector.
65
+ */
66
+ export declare function createWindowsProcessInspector(internals?: WindowsProcessInspectorInternals): WindowsProcessInspector;
67
+ declare const nativePtr: unique symbol;
68
+ /** Koffi 3 native pointer (a BigInt address), branded so it cannot silently enter numeric contexts. */
69
+ export type NativePtr = bigint & {
70
+ readonly [nativePtr]: true;
71
+ };
72
+ /**
73
+ * True for NULL and INVALID_HANDLE_VALUE returns from Win32 handle APIs.
74
+ * @param value - a handle as koffi may hand it back (pointer, null, or 0n).
75
+ * @returns whether the value signals an invalid handle.
76
+ */
77
+ export declare function isInvalidHandle(value: NativePtr | null | undefined): boolean;
78
+ export {};
79
+ //# sourceMappingURL=windows-inspector.d.ts.map