dsh-plugin-wsl-env 0.7.5 → 0.8.0
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/CHANGELOG.md +278 -0
- package/CONTRIBUTING.md +129 -39
- package/README.md +20 -20
- package/docs/ARCHITECTURE.md +6 -3
- package/docs/LIMITATIONS.md +13 -0
- package/docs/README.zh.md +20 -20
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1101 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +40 -0
- package/lib/probe-cache.js +35 -29
- package/lib/sandbox-core.d.ts +79 -0
- package/lib/sandbox-core.js +71 -88
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +144 -155
- package/package.json +68 -20
package/lib/subprocess.js
CHANGED
|
@@ -72,34 +72,34 @@
|
|
|
72
72
|
* composition, not of the Session. A Session whose workspace is a Windows folder
|
|
73
73
|
* gets the same terminal, with that folder mounted at `/mnt/<drive>/…`.
|
|
74
74
|
*
|
|
75
|
+
* This is a TypeScript source built to `lib/subprocess.js`; edit THIS file and
|
|
76
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
77
|
+
* fails when it drifts.
|
|
78
|
+
*
|
|
75
79
|
* @module dsh-plugin-wsl/subprocess
|
|
76
80
|
*/
|
|
77
|
-
|
|
78
81
|
import { randomUUID } from "node:crypto";
|
|
79
82
|
import z from "@deepseek-ai/schemastery";
|
|
80
83
|
import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
|
|
81
84
|
import { isPosixAbsolute, toLinuxPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
82
85
|
import { DEFAULT_WSL_PATH, defaultDistro, wslEnvValue, wslTerminalArgv } from "./wsl.js";
|
|
83
86
|
import { searchSpawnRewrite } from "./search-route.js";
|
|
87
|
+
import { gitSpawnRewrite, translateGitEnv } from "./git-route.js";
|
|
84
88
|
import { searchExecutionHandle } from "./search-exec.js";
|
|
85
89
|
import { sharedAgent } from "./agent-shared.js";
|
|
86
90
|
import { parseTerminalActivity, TERMINAL_ID_ENV, terminalActivityProbe, wrapTerminalHandle } from "./terminal-activity.js";
|
|
87
91
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
88
92
|
import { terminalRoute } from "./terminal-route.js";
|
|
89
|
-
|
|
90
93
|
/**
|
|
91
94
|
* Env names a terminal launch keeps for the distro. `TERM` rides along because
|
|
92
95
|
* the PTY consumer sets it for the host process, and WSL imports no name that
|
|
93
96
|
* `WSLENV` does not list.
|
|
94
97
|
*/
|
|
95
98
|
const SAFE_FORWARD = ["TERM", "NO_COLOR", "PAGER", "GIT_PAGER", "LANG", "LC_ALL"];
|
|
96
|
-
|
|
97
99
|
/** Namespace of the managed facts; every `DSH_*` name in a terminal request is forwarded. */
|
|
98
100
|
const DSH_ENV_PREFIX = "DSH_";
|
|
99
|
-
|
|
100
101
|
/** The one program a launch of this provider's does not pass through. */
|
|
101
102
|
const WSL_PROGRAM = "wsl";
|
|
102
|
-
|
|
103
103
|
/**
|
|
104
104
|
* Subprocess provider that serves a WSL distro as the composition's terminal
|
|
105
105
|
* execution world.
|
|
@@ -110,273 +110,278 @@ const WSL_PROGRAM = "wsl";
|
|
|
110
110
|
* literally the shipped implementation.
|
|
111
111
|
*/
|
|
112
112
|
export class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
static Config = z.object({
|
|
114
|
+
/** Distro name; empty means WSL's own default distro. */
|
|
115
|
+
distro: z.string().default(""),
|
|
116
|
+
/** Path to `wsl.exe`; overridable for a non-standard install. */
|
|
117
|
+
wslPath: z.string().default(DEFAULT_WSL_PATH),
|
|
118
|
+
/**
|
|
119
|
+
* A shell to pin inside the distro, as an absolute Linux path. Empty — the
|
|
120
|
+
* default — leaves the choice to `wsl.exe`, which starts the distro user's
|
|
121
|
+
* login shell exactly as an interactive `wsl.exe` does.
|
|
122
|
+
*/
|
|
123
|
+
shell: z.string().default(""),
|
|
124
|
+
/**
|
|
125
|
+
* Login-shell semantics for a pinned shell. Ignored when no shell is pinned,
|
|
126
|
+
* because then the login shell is the shell.
|
|
127
|
+
*/
|
|
128
|
+
loginShell: z.boolean().default(true),
|
|
129
|
+
/** Fallback directory when a launch names none; accepts any coordinate system. */
|
|
130
|
+
cwd: z.string().default(""),
|
|
131
|
+
/** Windows directory the `wsl.exe` process itself starts in. */
|
|
132
|
+
hostCwd: z.string().volatile(),
|
|
133
|
+
/** Extra env names to forward into the distro through WSLENV. */
|
|
134
|
+
forwardEnv: z.array(z.string()).default([]),
|
|
135
|
+
/**
|
|
136
|
+
* Give a session opened on a WINDOWS folder a host shell instead of the
|
|
137
|
+
* distro's shell started at `/mnt/<drive>/…`. WSL-folder sessions always
|
|
138
|
+
* get the distro shell — that is this provider's purpose. `false` restores
|
|
139
|
+
* the composition-owned terminal for every session.
|
|
140
|
+
*/
|
|
141
|
+
hostSessions: z.boolean().default(true),
|
|
142
|
+
/**
|
|
143
|
+
* Let the controller reclaim an idle distro terminal. The terminal is
|
|
144
|
+
* marked at launch (`DSH_TERMINAL_ID` through the existing `WSLENV`
|
|
145
|
+
* forwarding) and its handle's `inspectActivity` is replaced with a
|
|
146
|
+
* distro-side observation: the resident agent scans `/proc` for the
|
|
147
|
+
* marker — a shell with live children is busy, a shell without children
|
|
148
|
+
* is idle. The shipped observation can only answer `unknown` for a distro
|
|
149
|
+
* terminal, and `unknown` never accumulates idle, which is why these
|
|
150
|
+
* terminals had to be closed by hand. When the agent is out the
|
|
151
|
+
* observation answers `unknown` (no reclaim) — a probe that cannot answer
|
|
152
|
+
* must not authorize a close. `false` restores that posture.
|
|
153
|
+
*/
|
|
154
|
+
terminalIdleReclaim: z.boolean().default(true),
|
|
155
|
+
});
|
|
118
156
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* login shell exactly as an interactive `wsl.exe` does.
|
|
157
|
+
* @param ctx - the cordis context.
|
|
158
|
+
* @param config - the schema-validated config.
|
|
122
159
|
*/
|
|
123
|
-
|
|
160
|
+
constructor(ctx, config) {
|
|
161
|
+
super(ctx);
|
|
162
|
+
this.config = config;
|
|
163
|
+
this.resolvedDistro = config.distro;
|
|
164
|
+
}
|
|
124
165
|
/**
|
|
125
|
-
*
|
|
126
|
-
*
|
|
166
|
+
* The distro a launch uses when the request's directory does not name one,
|
|
167
|
+
* resolving WSL's default on first use.
|
|
168
|
+
*
|
|
169
|
+
* @returns the distro name.
|
|
127
170
|
*/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
|
|
171
|
+
async distro() {
|
|
172
|
+
if (this.resolvedDistro.length === 0)
|
|
173
|
+
this.resolvedDistro = await defaultDistro({ wslPath: this.config.wslPath });
|
|
174
|
+
return this.resolvedDistro;
|
|
175
|
+
}
|
|
176
|
+
/** Windows directory the `wsl.exe` process is started from. */
|
|
177
|
+
hostCwd() {
|
|
178
|
+
return this.config.hostCwd.get() ?? process.env.SystemRoot ?? process.cwd();
|
|
179
|
+
}
|
|
135
180
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* the
|
|
181
|
+
* The distro and Linux directory one terminal request names.
|
|
182
|
+
*
|
|
183
|
+
* The Session workspace the terminal controller passes in is the coordinate
|
|
184
|
+
* the harness already speaks, and it is also the strongest evidence of the
|
|
185
|
+
* intended distro: a UNC path names one outright. Anything else — a Windows
|
|
186
|
+
* drive path or a directory-less request — may legitimately be served by the
|
|
187
|
+
* configured distro.
|
|
188
|
+
*
|
|
189
|
+
* @param cwd - the request's working directory, in any coordinate system.
|
|
190
|
+
* @returns the distro name and its absolute Linux directory.
|
|
140
191
|
*/
|
|
141
|
-
|
|
192
|
+
async terminalTarget(cwd) {
|
|
193
|
+
const text = String(cwd ?? "").trim();
|
|
194
|
+
const named = uncToPosix(text);
|
|
195
|
+
if (named !== undefined)
|
|
196
|
+
return { distro: named.distro, linuxCwd: named.linuxPath };
|
|
197
|
+
const distro = await this.distro();
|
|
198
|
+
if (isPosixAbsolute(text))
|
|
199
|
+
return { distro, linuxCwd: text };
|
|
200
|
+
const mounted = windowsToLinuxMount(text);
|
|
201
|
+
if (mounted !== undefined)
|
|
202
|
+
return { distro, linuxCwd: mounted };
|
|
203
|
+
return { distro, linuxCwd: toLinuxPath(this.config.cwd, { distro }) };
|
|
204
|
+
}
|
|
142
205
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
206
|
+
* Open one interactive shell inside the distro over the host's PTY.
|
|
207
|
+
*
|
|
208
|
+
* Only a `wsl.exe` launch is rewritten. Everything else — a shipped host
|
|
209
|
+
* shell, the PTY backend's `bash -i`, any other consumer's terminal — is the
|
|
210
|
+
* shipped implementation's, which is what keeps replacing a root provider
|
|
211
|
+
* safe.
|
|
212
|
+
*
|
|
213
|
+
* The request's remaining arguments are intentionally not forwarded: the
|
|
214
|
+
* controller's shell profile carries only the program, and a second set of
|
|
215
|
+
* `wsl.exe` arguments would fight the distro and `--cd` this method derives
|
|
216
|
+
* from the Session.
|
|
217
|
+
*
|
|
218
|
+
* @param spec - the terminal request from the PTY consumer.
|
|
219
|
+
* @returns the live terminal handle.
|
|
153
220
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* The distro a launch uses when the request's directory does not name one,
|
|
166
|
-
* resolving WSL's default on first use.
|
|
167
|
-
* @returns the distro name.
|
|
168
|
-
*/
|
|
169
|
-
async distro() {
|
|
170
|
-
if (this.resolvedDistro.length === 0) this.resolvedDistro = await defaultDistro({ wslPath: this.config.wslPath });
|
|
171
|
-
return this.resolvedDistro;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** Windows directory the `wsl.exe` process is started from. */
|
|
175
|
-
hostCwd() {
|
|
176
|
-
return this.config.hostCwd.get() ?? process.env.SystemRoot ?? process.cwd();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* The distro and Linux directory one terminal request names.
|
|
181
|
-
*
|
|
182
|
-
* The Session workspace the terminal controller passes in is the coordinate
|
|
183
|
-
* the harness already speaks, and it is also the strongest evidence of the
|
|
184
|
-
* intended distro: a UNC path names one outright. Anything else — a Windows
|
|
185
|
-
* drive path or a directory-less request — may legitimately be served by the
|
|
186
|
-
* configured distro.
|
|
187
|
-
*
|
|
188
|
-
* @param cwd - the request's working directory, in any coordinate system.
|
|
189
|
-
* @returns the distro name and its absolute Linux directory.
|
|
190
|
-
*/
|
|
191
|
-
async terminalTarget(cwd) {
|
|
192
|
-
const text = String(cwd ?? "").trim();
|
|
193
|
-
const named = uncToPosix(text);
|
|
194
|
-
if (named !== undefined) return { distro: named.distro, linuxCwd: named.linuxPath };
|
|
195
|
-
const distro = await this.distro();
|
|
196
|
-
if (isPosixAbsolute(text)) return { distro, linuxCwd: text };
|
|
197
|
-
const mounted = windowsToLinuxMount(text);
|
|
198
|
-
if (mounted !== undefined) return { distro, linuxCwd: mounted };
|
|
199
|
-
return { distro, linuxCwd: toLinuxPath(this.config.cwd, { distro }) };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Open one interactive shell inside the distro over the host's PTY.
|
|
204
|
-
*
|
|
205
|
-
* Only a `wsl.exe` launch is rewritten. Everything else — a shipped host
|
|
206
|
-
* shell, the PTY backend's `bash -i`, any other consumer's terminal — is the
|
|
207
|
-
* shipped implementation's, which is what keeps replacing a root provider
|
|
208
|
-
* safe.
|
|
209
|
-
*
|
|
210
|
-
* The request's remaining arguments are intentionally not forwarded: the
|
|
211
|
-
* controller's shell profile carries only the program, and a second set of
|
|
212
|
-
* `wsl.exe` arguments would fight the distro and `--cd` this method derives
|
|
213
|
-
* from the Session.
|
|
214
|
-
*
|
|
215
|
-
* @param spec - the terminal request from the PTY consumer.
|
|
216
|
-
* @returns the live terminal handle.
|
|
217
|
-
*/
|
|
218
|
-
async spawnTerminal(spec) {
|
|
219
|
-
if (!this.launchesDistro(spec.argv)) return super.spawnTerminal(spec);
|
|
220
|
-
// A session on a Windows folder is a HOST session: it gets the host shell
|
|
221
|
-
// in its own directory, not the distro's shell at `/mnt/<drive>/…`. The
|
|
222
|
-
// wsl.exe argv still names this provider's launch, so the substitution
|
|
223
|
-
// stays inside the override — every other consumer is untouched.
|
|
224
|
-
if (this.config.hostSessions && terminalRoute(spec.cwd) === "host") {
|
|
225
|
-
return super.spawnTerminal({ ...spec, argv: ["powershell.exe", "-NoLogo"] });
|
|
226
|
-
}
|
|
227
|
-
const target = await this.terminalTarget(spec.cwd);
|
|
228
|
-
const argv = wslTerminalArgv({
|
|
229
|
-
wslPath: this.config.wslPath,
|
|
230
|
-
distro: target.distro,
|
|
231
|
-
shellPath: this.config.shell,
|
|
232
|
-
linuxCwd: target.linuxCwd,
|
|
233
|
-
login: this.config.loginShell,
|
|
234
|
-
});
|
|
235
|
-
// The idle-reclaim mark: one env name per terminal, riding the WSLENV
|
|
236
|
-
// forwarding this launch already does. The distro shell and everything it
|
|
237
|
-
// spawns carry it in /proc/<pid>/environ — what the activity probe scans.
|
|
238
|
-
const terminalId = this.config.terminalIdleReclaim ? randomUUID() : undefined;
|
|
239
|
-
const specWithMark = terminalId === undefined ? spec : { ...spec, env: { ...spec.env, [TERMINAL_ID_ENV]: terminalId } };
|
|
240
|
-
const handle = await super.spawnTerminal({
|
|
241
|
-
...specWithMark,
|
|
242
|
-
argv,
|
|
243
|
-
// A Windows CreateProcess cannot use a Linux path as its working
|
|
244
|
-
// directory; the Linux directory rides on `--cd` instead.
|
|
245
|
-
cwd: this.hostCwd(),
|
|
246
|
-
env: this.forwardedEnv(specWithMark),
|
|
247
|
-
});
|
|
248
|
-
if (terminalId === undefined) return handle;
|
|
249
|
-
return wrapTerminalHandle(handle, this.distroActivity(target.distro, terminalId));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* The replacement `inspectActivity` for one distro terminal: ask the
|
|
254
|
-
* resident agent whether the marked shell is at its prompt.
|
|
255
|
-
*
|
|
256
|
-
* The revision increments only when the reported state changes, so the
|
|
257
|
-
* controller's retention accumulates idle across consecutive `idle`
|
|
258
|
-
* observations and restarts the clock on every transition. Any probe
|
|
259
|
-
* failure — the agent out, the distro stopped, a script error — answers
|
|
260
|
-
* `unknown`, which the retention reads as "not idle": the probe that cannot
|
|
261
|
-
* answer never authorizes a close. The first unexpected (non-availability)
|
|
262
|
-
* failure is logged once, so a broken probe is diagnosable without
|
|
263
|
-
* spamming the log every poll.
|
|
264
|
-
*
|
|
265
|
-
* @param distro - the distro the terminal lives in.
|
|
266
|
-
* @param terminalId - the launch's `DSH_TERMINAL_ID` mark.
|
|
267
|
-
* @returns the seam's activity observation.
|
|
268
|
-
*/
|
|
269
|
-
distroActivity(distro, terminalId) {
|
|
270
|
-
let revision = 0;
|
|
271
|
-
let lastState = "";
|
|
272
|
-
let warned = false;
|
|
273
|
-
return async () => {
|
|
274
|
-
try {
|
|
275
|
-
const probe = terminalActivityProbe(terminalId);
|
|
276
|
-
const result = await sharedAgent(distro).exec({
|
|
277
|
-
cwd: probe.cwd,
|
|
278
|
-
argv: probe.argv,
|
|
279
|
-
maxOutputBytes: 256,
|
|
280
|
-
timeoutMs: 10_000,
|
|
281
|
-
});
|
|
282
|
-
const state = parseTerminalActivity(result.stdout) ?? "unknown";
|
|
283
|
-
if (state !== lastState) {
|
|
284
|
-
revision += 1;
|
|
285
|
-
lastState = state;
|
|
221
|
+
async spawnTerminal(spec) {
|
|
222
|
+
if (!this.launchesDistro(spec.argv))
|
|
223
|
+
return super.spawnTerminal(spec);
|
|
224
|
+
// A session on a Windows folder is a HOST session: it gets the host shell
|
|
225
|
+
// in its own directory, not the distro's shell at `/mnt/<drive>/…`. The
|
|
226
|
+
// wsl.exe argv still names this provider's launch, so the substitution
|
|
227
|
+
// stays inside the override — every other consumer is untouched.
|
|
228
|
+
if (this.config.hostSessions && terminalRoute(spec.cwd) === "host") {
|
|
229
|
+
return super.spawnTerminal({ ...spec, argv: ["powershell.exe", "-NoLogo"] });
|
|
286
230
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
231
|
+
const target = await this.terminalTarget(spec.cwd);
|
|
232
|
+
const argv = wslTerminalArgv({
|
|
233
|
+
wslPath: this.config.wslPath,
|
|
234
|
+
distro: target.distro,
|
|
235
|
+
shellPath: this.config.shell,
|
|
236
|
+
linuxCwd: target.linuxCwd,
|
|
237
|
+
login: this.config.loginShell,
|
|
238
|
+
});
|
|
239
|
+
// The idle-reclaim mark: one env name per terminal, riding the WSLENV
|
|
240
|
+
// forwarding this launch already does. The distro shell and everything it
|
|
241
|
+
// spawns carry it in /proc/<pid>/environ — what the activity probe scans.
|
|
242
|
+
const terminalId = this.config.terminalIdleReclaim ? randomUUID() : undefined;
|
|
243
|
+
const specWithMark = terminalId === undefined ? spec : { ...spec, env: { ...spec.env, [TERMINAL_ID_ENV]: terminalId } };
|
|
244
|
+
const handle = await super.spawnTerminal({
|
|
245
|
+
...specWithMark,
|
|
246
|
+
argv,
|
|
247
|
+
// A Windows CreateProcess cannot use a Linux path as its working
|
|
248
|
+
// directory; the Linux directory rides on `--cd` instead.
|
|
249
|
+
cwd: this.hostCwd(),
|
|
250
|
+
// The terminal spec's env is a string record; forwarding adds `WSLENV`.
|
|
251
|
+
env: this.forwardedEnv(specWithMark),
|
|
252
|
+
});
|
|
253
|
+
if (terminalId === undefined)
|
|
254
|
+
return handle;
|
|
255
|
+
return wrapTerminalHandle(handle, this.distroActivity(target.distro, terminalId));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The replacement `inspectActivity` for one distro terminal: ask the
|
|
259
|
+
* resident agent whether the marked shell is at its prompt.
|
|
260
|
+
*
|
|
261
|
+
* The revision increments only when the reported state changes, so the
|
|
262
|
+
* controller's retention accumulates idle across consecutive `idle`
|
|
263
|
+
* observations and restarts the clock on every transition. Any probe
|
|
264
|
+
* failure — the agent out, the distro stopped, a script error — answers
|
|
265
|
+
* `unknown`, which the retention reads as "not idle": the probe that cannot
|
|
266
|
+
* answer never authorizes a close. The first unexpected (non-availability)
|
|
267
|
+
* failure is logged once, so a broken probe is diagnosable without
|
|
268
|
+
* spamming the log every poll.
|
|
269
|
+
*
|
|
270
|
+
* @param distro - the distro the terminal lives in.
|
|
271
|
+
* @param terminalId - the launch's `DSH_TERMINAL_ID` mark.
|
|
272
|
+
* @returns the seam's activity observation.
|
|
273
|
+
*/
|
|
274
|
+
distroActivity(distro, terminalId) {
|
|
275
|
+
let revision = 0;
|
|
276
|
+
let lastState = "";
|
|
277
|
+
let warned = false;
|
|
278
|
+
return async () => {
|
|
279
|
+
try {
|
|
280
|
+
const probe = terminalActivityProbe(terminalId);
|
|
281
|
+
const result = await sharedAgent(distro).exec({
|
|
282
|
+
cwd: probe.cwd,
|
|
283
|
+
argv: probe.argv,
|
|
284
|
+
maxOutputBytes: 256,
|
|
285
|
+
timeoutMs: 10_000,
|
|
286
|
+
});
|
|
287
|
+
const state = parseTerminalActivity(result.stdout) ?? "unknown";
|
|
288
|
+
if (state !== lastState) {
|
|
289
|
+
revision += 1;
|
|
290
|
+
lastState = state;
|
|
291
|
+
}
|
|
292
|
+
return { state, revision };
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (!(error instanceof AgentUnavailableError) && !warned) {
|
|
296
|
+
warned = true;
|
|
297
|
+
this.ctx.logger?.warn(`wsl-subprocess: the terminal activity probe failed and idle reclamation is paused (${String(error?.message ?? error)})`);
|
|
298
|
+
}
|
|
299
|
+
return { state: "unknown", revision };
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Open one host-side process, rewriting two kinds of launches into the
|
|
305
|
+
* distro: the file-search tool's ripgrep (agent-first, one-shot fallback)
|
|
306
|
+
* and git snapshot commands (one-shot only — snapshot operations are not
|
|
307
|
+
* latency-sensitive, and their stdin relay and collected streams are the
|
|
308
|
+
* shipped handle's). Both decisions are pure coordinate functions
|
|
309
|
+
* (`search-route.js`, `git-route.js`); a spawn neither names (any other
|
|
310
|
+
* program, any drive or relative directory) reaches the shipped
|
|
311
|
+
* implementation verbatim.
|
|
312
|
+
*
|
|
313
|
+
* @param spec - the spawn request.
|
|
314
|
+
* @returns the live search handle.
|
|
315
|
+
*/
|
|
316
|
+
spawn(spec) {
|
|
317
|
+
const gitRewrite = gitSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
|
|
318
|
+
if (gitRewrite !== undefined) {
|
|
319
|
+
// The snapshot environment (GIT_INDEX_FILE / GIT_OBJECT_DIRECTORY /
|
|
320
|
+
// GIT_ALTERNATE_OBJECT_DIRECTORIES and the git hygiene flags) is
|
|
321
|
+
// path-shaped in Windows form; the values are translated into distro
|
|
322
|
+
// paths and every forwarded name rides WSLENV. The user's real index,
|
|
323
|
+
// object store, work tree, and refs stay untouched by design — the
|
|
324
|
+
// snapshot runs in the tool's private scratch.
|
|
325
|
+
const { env, wslenv } = translateGitEnv(spec.env);
|
|
326
|
+
return super.spawn({
|
|
327
|
+
...spec,
|
|
328
|
+
argv: gitRewrite.wslArgv,
|
|
329
|
+
cwd: this.hostCwd(),
|
|
330
|
+
env: wslenv.length > 0 ? { ...env, WSLENV: wslEnvValue(wslenv) } : env,
|
|
331
|
+
});
|
|
292
332
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
333
|
+
const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
|
|
334
|
+
if (rewrite === undefined)
|
|
335
|
+
return super.spawn(spec);
|
|
336
|
+
// The search handle carries the slice `dsh-tool-fs-search` consumes
|
|
337
|
+
// (`done`, `collected`, `kill`); the seam's full handle surface is the
|
|
338
|
+
// one-shot fallback's, so the override's declared return is the cast's.
|
|
339
|
+
return searchExecutionHandle({
|
|
340
|
+
agent: sharedAgent(rewrite.distro),
|
|
341
|
+
cwd: rewrite.linuxCwd,
|
|
342
|
+
argv: rewrite.rgArgv,
|
|
343
|
+
// One budget serves both streams; the stdout budget is the binding one,
|
|
344
|
+
// and a pathological stderr is still bounded by it.
|
|
345
|
+
maxOutputBytes: spec.stdio.stdout?.maxBytes ?? 0,
|
|
346
|
+
signal: spec.signal,
|
|
347
|
+
spawnFallback: () => super.spawn({
|
|
348
|
+
...spec,
|
|
349
|
+
argv: rewrite.wslArgv,
|
|
350
|
+
// `wsl.exe --cd` carries the Linux directory; the host process itself
|
|
351
|
+
// must start in a Windows directory, as with a terminal launch.
|
|
352
|
+
cwd: this.hostCwd(),
|
|
353
|
+
env: this.forwardedEnv(spec),
|
|
354
|
+
}),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The environment one `wsl.exe` launch forwards: the safe list plus every
|
|
359
|
+
* managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
|
|
360
|
+
* WSLENV note in the recipes.
|
|
361
|
+
*
|
|
362
|
+
* @param spec - the launch request whose env is forwarded.
|
|
363
|
+
* @returns the environment the host launch carries.
|
|
364
|
+
*/
|
|
365
|
+
forwardedEnv(spec) {
|
|
366
|
+
const forward = new Set([...SAFE_FORWARD, ...this.config.forwardEnv]);
|
|
367
|
+
const imported = Object.keys(spec.env ?? {}).filter((name) => forward.has(name) || name.startsWith(DSH_ENV_PREFIX));
|
|
368
|
+
return imported.length > 0 ? { ...spec.env, WSLENV: wslEnvValue(imported) } : spec.env;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Whether a terminal request is a `wsl.exe` launch.
|
|
372
|
+
*
|
|
373
|
+
* A bare `argv[0]` is judged by its own name, so the shipped program name and
|
|
374
|
+
* an operator-pinned absolute path both match.
|
|
375
|
+
*
|
|
376
|
+
* @param argv - the request's program and arguments.
|
|
377
|
+
* @returns true when this provider owns the launch.
|
|
378
|
+
*/
|
|
379
|
+
launchesDistro(argv) {
|
|
380
|
+
const program = argv?.[0];
|
|
381
|
+
if (typeof program !== "string" || program.length === 0)
|
|
382
|
+
return false;
|
|
383
|
+
const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
|
|
384
|
+
return base.toLowerCase().replace(/\.exe$/u, "") === WSL_PROGRAM;
|
|
327
385
|
}
|
|
328
|
-
const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
|
|
329
|
-
if (rewrite === undefined) return super.spawn(spec);
|
|
330
|
-
return searchExecutionHandle({
|
|
331
|
-
agent: sharedAgent(rewrite.distro),
|
|
332
|
-
cwd: rewrite.linuxCwd,
|
|
333
|
-
argv: rewrite.rgArgv,
|
|
334
|
-
// One budget serves both streams; the stdout budget is the binding one,
|
|
335
|
-
// and a pathological stderr is still bounded by it.
|
|
336
|
-
maxOutputBytes: spec.stdio?.stdout?.maxBytes ?? 0,
|
|
337
|
-
signal: spec.signal,
|
|
338
|
-
spawnFallback: () =>
|
|
339
|
-
super.spawn({
|
|
340
|
-
...spec,
|
|
341
|
-
argv: rewrite.wslArgv,
|
|
342
|
-
// `wsl.exe --cd` carries the Linux directory; the host process itself
|
|
343
|
-
// must start in a Windows directory, as with a terminal launch.
|
|
344
|
-
cwd: this.hostCwd(),
|
|
345
|
-
env: this.forwardedEnv(spec),
|
|
346
|
-
}),
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* The environment one `wsl.exe` launch forwards: the safe list plus every
|
|
352
|
-
* managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
|
|
353
|
-
* WSLENV note in the recipes.
|
|
354
|
-
* @param spec - the launch request whose env is forwarded.
|
|
355
|
-
* @returns the environment the host launch carries.
|
|
356
|
-
*/
|
|
357
|
-
forwardedEnv(spec) {
|
|
358
|
-
const forward = new Set([...SAFE_FORWARD, ...this.config.forwardEnv]);
|
|
359
|
-
const imported = Object.keys(spec.env ?? {}).filter(
|
|
360
|
-
(name) => forward.has(name) || name.startsWith(DSH_ENV_PREFIX),
|
|
361
|
-
);
|
|
362
|
-
return imported.length > 0 ? { ...spec.env, WSLENV: wslEnvValue(imported) } : spec.env;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Whether a terminal request is a `wsl.exe` launch.
|
|
367
|
-
*
|
|
368
|
-
* A bare `argv[0]` is judged by its own name, so the shipped program name and
|
|
369
|
-
* an operator-pinned absolute path both match.
|
|
370
|
-
*
|
|
371
|
-
* @param argv - the request's program and arguments.
|
|
372
|
-
* @returns true when this provider owns the launch.
|
|
373
|
-
*/
|
|
374
|
-
launchesDistro(argv) {
|
|
375
|
-
const program = argv?.[0];
|
|
376
|
-
if (typeof program !== "string" || program.length === 0) return false;
|
|
377
|
-
const base = program.slice(Math.max(program.lastIndexOf("/"), program.lastIndexOf("\\")) + 1);
|
|
378
|
-
return base.toLowerCase().replace(/\.exe$/u, "") === WSL_PROGRAM;
|
|
379
|
-
}
|
|
380
386
|
}
|
|
381
|
-
|
|
382
387
|
export default WslSubprocessRuntime;
|