dsh-plugin-wsl-env 0.9.0 → 0.10.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 +417 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +84 -68
- package/SUPPORT.md +3 -3
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +83 -16
- package/cordis.patch.yml +41 -35
- package/docs/ARCHITECTURE.md +9 -1
- package/docs/CONFIGURATION.md +7 -4
- package/docs/LIMITATIONS.md +53 -18
- package/docs/PARITY.md +3 -1
- package/docs/PEER-PARITY.md +18 -11
- package/docs/README.zh.md +97 -74
- package/docs/UPSTREAM-FSIO-EXPORT.md +3 -3
- package/docs/UPSTREAM-TERMINAL-TITLE.md +9 -2
- package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
- package/lib/agent-protocol.d.ts +10 -4
- package/lib/agent-protocol.js +10 -3
- package/lib/agent.d.ts +84 -0
- package/lib/agent.js +167 -15
- package/lib/bwrap.d.ts +8 -0
- package/lib/bwrap.js +4 -1
- package/lib/fs-routing.d.ts +40 -4
- package/lib/fs-routing.js +80 -12
- package/lib/fsio-agent.js +1 -1
- package/lib/index.d.ts +134 -0
- package/lib/index.js +191 -3
- package/lib/native-agent.d.ts +124 -0
- package/lib/native-agent.js +198 -0
- package/lib/ports.d.ts +13 -2
- package/lib/ports.js +18 -5
- package/lib/preflight.js +24 -1
- package/lib/session-runtime.d.ts +161 -0
- package/lib/session-runtime.js +220 -0
- package/lib/session-scope.d.ts +124 -0
- package/lib/session-scope.js +191 -0
- package/lib/shell-env.js +10 -3
- package/lib/watcher.d.ts +25 -12
- package/lib/watcher.js +120 -32
- package/lib/workspace-files-route.d.ts +8 -4
- package/lib/workspace-files-route.js +42 -14
- package/lib/workspace-files-wsl.js +25 -8
- package/package.json +34 -25
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side plumbing for the optional native in-distro agent (ADR 0002).
|
|
3
|
+
*
|
|
4
|
+
* The shell agent is the shipped peer. This module is the opt-in path that
|
|
5
|
+
* lets the host run the spike/native binary instead, for the session runtime
|
|
6
|
+
* only:
|
|
7
|
+
*
|
|
8
|
+
* 1. read the binary from a host path (`DSH_WSL_AGENT_NATIVE`);
|
|
9
|
+
* 2. sha256 it and refuse an architecture the binary was not built for;
|
|
10
|
+
* 3. stream it into `~/.cache/dsh-wsl/agent/<sha>/dsh-wsl-agent` inside the
|
|
11
|
+
* distro, chmod 0755, and verify the distro-side digest;
|
|
12
|
+
* 4. hand the resulting Linux path to the session-runtime factory.
|
|
13
|
+
*
|
|
14
|
+
* The binary is never executed from `/mnt/c` (DrvFs is often `noexec`), and a
|
|
15
|
+
* failed copy falls back to the shell agent — this module never throws to the
|
|
16
|
+
* command path.
|
|
17
|
+
*
|
|
18
|
+
* This is a TypeScript source built to `lib/native-agent.js`; edit THIS file
|
|
19
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
20
|
+
* `pnpm test` fails when it drifts.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-plugin-wsl/native-agent
|
|
23
|
+
*/
|
|
24
|
+
/** The host environment variable naming the native binary to opt in. */
|
|
25
|
+
export declare const NATIVE_AGENT_ENV = "DSH_WSL_AGENT_NATIVE";
|
|
26
|
+
/**
|
|
27
|
+
* The in-distro copy/verify script. The digest is embedded so `sh` needs no
|
|
28
|
+
* second argument and the cache path is derived from the content; stdout is
|
|
29
|
+
* the final Linux path the session runtime should execute.
|
|
30
|
+
*
|
|
31
|
+
* @param digest - lowercase hex sha256 of the binary.
|
|
32
|
+
* @returns the `sh -c` body (binary on stdin).
|
|
33
|
+
*/
|
|
34
|
+
export declare function nativeCacheScript(digest: string): string;
|
|
35
|
+
/** The injectable edges. */
|
|
36
|
+
export interface NativeBackendOptions {
|
|
37
|
+
/** The `wsl.exe` path. */
|
|
38
|
+
wslPath: string;
|
|
39
|
+
/** The distro. */
|
|
40
|
+
distro: string;
|
|
41
|
+
/**
|
|
42
|
+
* The host path override (the development variable); empty resolves the
|
|
43
|
+
* packaged binary for the distro's architecture.
|
|
44
|
+
*/
|
|
45
|
+
source: string;
|
|
46
|
+
/** The plugin package root; tests and non-standard layouts substitute it. */
|
|
47
|
+
packageRoot?: string;
|
|
48
|
+
/** Read the binary (tests substitute). */
|
|
49
|
+
readSource?: (path: string) => Promise<Buffer>;
|
|
50
|
+
/** Run a host command and return stdout (tests substitute). */
|
|
51
|
+
run?: (argv: string[]) => Promise<string>;
|
|
52
|
+
/** Copy the binary and return the Linux path (tests substitute). */
|
|
53
|
+
copy?: (script: string, binary: Buffer) => Promise<string>;
|
|
54
|
+
}
|
|
55
|
+
/** A prepared native binary: its in-distro path and the sha256 of its bytes. */
|
|
56
|
+
export interface NativeBackendResult {
|
|
57
|
+
/** The Linux path the session runtime spawns. */
|
|
58
|
+
path: string;
|
|
59
|
+
/** Lowercase hex sha256; the agent's HELLO digest must match it. */
|
|
60
|
+
digest: string;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Copy the binary into the distro and verify it. The result is cached per
|
|
64
|
+
* `(wslPath, distro, source)`; a failure is cached as `undefined` for this
|
|
65
|
+
* process (the caller falls back to the shell agent).
|
|
66
|
+
*
|
|
67
|
+
* @param options - the transport, distro, source and injectables.
|
|
68
|
+
* @returns the Linux binary path and digest, or undefined when unusable.
|
|
69
|
+
*/
|
|
70
|
+
export declare function ensureNativeBackend({ wslPath, distro, source, packageRoot, readSource, run, copy, }: NativeBackendOptions): Promise<NativeBackendResult | undefined>;
|
|
71
|
+
/**
|
|
72
|
+
* Forget the cache; tests only.
|
|
73
|
+
*/
|
|
74
|
+
export declare function clearNativeBackendCache(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Stream the binary to the distro on stdin, run the copy/verify script and
|
|
77
|
+
* return its stdout (the cache path).
|
|
78
|
+
*
|
|
79
|
+
* Exported for its regression check; the copy path is injectable in
|
|
80
|
+
* {@link ensureNativeBackend}.
|
|
81
|
+
*
|
|
82
|
+
* @param wslPath - the `wsl.exe` path.
|
|
83
|
+
* @param distro - the distro.
|
|
84
|
+
* @param script - the copy/verify script.
|
|
85
|
+
* @param binary - the binary bytes.
|
|
86
|
+
* @returns the Linux path printed by the script.
|
|
87
|
+
*/
|
|
88
|
+
export declare function copyBinary(wslPath: string, distro: string, script: string, binary: Buffer): Promise<string>;
|
|
89
|
+
/** The session runtime's agent backend, from `wsl-shell.agentBackend`. */
|
|
90
|
+
export type AgentBackend = "auto" | "native" | "shell";
|
|
91
|
+
/**
|
|
92
|
+
* Record the selected backend. The session runtime is shared by the shell and
|
|
93
|
+
* filesystem providers, so this one process-wide value is what both read when
|
|
94
|
+
* they prepare their runtime; it is set from the `wsl-shell` config.
|
|
95
|
+
*
|
|
96
|
+
* @param mode - the selected backend.
|
|
97
|
+
*/
|
|
98
|
+
export declare function configureAgentBackend(mode: AgentBackend): void;
|
|
99
|
+
/** @returns the configured backend (for diagnostics and tests). */
|
|
100
|
+
export declare function configuredAgentBackend(): AgentBackend;
|
|
101
|
+
/**
|
|
102
|
+
* Select the native binary for a distro when the backend allows it.
|
|
103
|
+
*
|
|
104
|
+
* - `shell`: clear any native entry and use the shipped shell agent.
|
|
105
|
+
* - `auto`: use the native binary when a source exists and copy/verify
|
|
106
|
+
* succeeds; otherwise stay on the shell agent.
|
|
107
|
+
* - `native`: same preparation, but a missing source or a failed preparation
|
|
108
|
+
* throws a remedy instead of silently falling back.
|
|
109
|
+
*
|
|
110
|
+
* The source is the development override `DSH_WSL_AGENT_NATIVE`; the packaged
|
|
111
|
+
* per-arch binary will be added by the packaging task and preferred over it.
|
|
112
|
+
*
|
|
113
|
+
* @param options - the transport, distro, setter and injectable prepare step.
|
|
114
|
+
*/
|
|
115
|
+
export declare function ensureNativeSessionAgent({ wslPath, distro, setNative, ensure, }: {
|
|
116
|
+
/** The `wsl.exe` path. */
|
|
117
|
+
wslPath: string;
|
|
118
|
+
/** The resolved distro. */
|
|
119
|
+
distro: string;
|
|
120
|
+
/** Store (or clear, with an empty path) the distro's native entrypoint. */
|
|
121
|
+
setNative: (distro: string, path: string, digest: string) => void;
|
|
122
|
+
/** Prepare the binary; tests substitute. */
|
|
123
|
+
ensure?: (options: NativeBackendOptions) => Promise<NativeBackendResult | undefined>;
|
|
124
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side plumbing for the optional native in-distro agent (ADR 0002).
|
|
3
|
+
*
|
|
4
|
+
* The shell agent is the shipped peer. This module is the opt-in path that
|
|
5
|
+
* lets the host run the spike/native binary instead, for the session runtime
|
|
6
|
+
* only:
|
|
7
|
+
*
|
|
8
|
+
* 1. read the binary from a host path (`DSH_WSL_AGENT_NATIVE`);
|
|
9
|
+
* 2. sha256 it and refuse an architecture the binary was not built for;
|
|
10
|
+
* 3. stream it into `~/.cache/dsh-wsl/agent/<sha>/dsh-wsl-agent` inside the
|
|
11
|
+
* distro, chmod 0755, and verify the distro-side digest;
|
|
12
|
+
* 4. hand the resulting Linux path to the session-runtime factory.
|
|
13
|
+
*
|
|
14
|
+
* The binary is never executed from `/mnt/c` (DrvFs is often `noexec`), and a
|
|
15
|
+
* failed copy falls back to the shell agent — this module never throws to the
|
|
16
|
+
* command path.
|
|
17
|
+
*
|
|
18
|
+
* This is a TypeScript source built to `lib/native-agent.js`; edit THIS file
|
|
19
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
20
|
+
* `pnpm test` fails when it drifts.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-plugin-wsl/native-agent
|
|
23
|
+
*/
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { readFile } from "node:fs/promises";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { pinnedWindowsEnv } from "./agent.js";
|
|
30
|
+
import { runCapture } from "./wsl.js";
|
|
31
|
+
/** The host environment variable naming the native binary to opt in. */
|
|
32
|
+
export const NATIVE_AGENT_ENV = "DSH_WSL_AGENT_NATIVE";
|
|
33
|
+
/** Distro `uname -m` values mapped to the packaged directory names. */
|
|
34
|
+
const ARCHS = new Map([
|
|
35
|
+
["x86_64", "x86_64"],
|
|
36
|
+
["amd64", "x86_64"],
|
|
37
|
+
["aarch64", "aarch64"],
|
|
38
|
+
["arm64", "aarch64"],
|
|
39
|
+
]);
|
|
40
|
+
/** The plugin package root: `lib/native-agent.js` -> `<root>`. */
|
|
41
|
+
const PACKAGE_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
42
|
+
/**
|
|
43
|
+
* The in-distro copy/verify script. The digest is embedded so `sh` needs no
|
|
44
|
+
* second argument and the cache path is derived from the content; stdout is
|
|
45
|
+
* the final Linux path the session runtime should execute.
|
|
46
|
+
*
|
|
47
|
+
* @param digest - lowercase hex sha256 of the binary.
|
|
48
|
+
* @returns the `sh -c` body (binary on stdin).
|
|
49
|
+
*/
|
|
50
|
+
export function nativeCacheScript(digest) {
|
|
51
|
+
return [
|
|
52
|
+
"set -e",
|
|
53
|
+
"umask 077",
|
|
54
|
+
`dir="$HOME/.cache/dsh-wsl/agent/${digest}"`,
|
|
55
|
+
'target="$dir/dsh-wsl-agent"',
|
|
56
|
+
// A live session already runs this exact binary; rewriting it would fail
|
|
57
|
+
// with ETXTBSY. A valid cache is the same verified artifact, so reuse it.
|
|
58
|
+
`if [ -x "$target" ] && [ "$(sha256sum "$target" | cut -d' ' -f1)" = "${digest}" ]; then printf '%s' "$target"; exit 0; fi`,
|
|
59
|
+
'mkdir -p "$dir"',
|
|
60
|
+
'cat > "$target"',
|
|
61
|
+
'chmod 0755 "$target"',
|
|
62
|
+
"got=$(sha256sum \"$target\" | cut -d' ' -f1)",
|
|
63
|
+
`[ "$got" = "${digest}" ] || { echo digest-mismatch >&2; exit 1; }`,
|
|
64
|
+
'"$target" </dev/null >/dev/null 2>&1 || { echo cannot-execute >&2; exit 1; }',
|
|
65
|
+
'printf "%s" "$target"',
|
|
66
|
+
].join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Copy the binary into the distro and verify it. The result is cached per
|
|
70
|
+
* `(wslPath, distro, source)`; a failure is cached as `undefined` for this
|
|
71
|
+
* process (the caller falls back to the shell agent).
|
|
72
|
+
*
|
|
73
|
+
* @param options - the transport, distro, source and injectables.
|
|
74
|
+
* @returns the Linux binary path and digest, or undefined when unusable.
|
|
75
|
+
*/
|
|
76
|
+
export async function ensureNativeBackend({ wslPath, distro, source, packageRoot = PACKAGE_ROOT, readSource = readFile, run = (argv) => runCapture(argv), copy, }) {
|
|
77
|
+
const key = `${wslPath}\u0000${distro}\u0000${source}\u0000${packageRoot}`;
|
|
78
|
+
const cached = NATIVE_CACHE.get(key);
|
|
79
|
+
if (cached !== undefined)
|
|
80
|
+
return cached;
|
|
81
|
+
const pending = (async () => {
|
|
82
|
+
const arch = (await run([wslPath, "-d", distro, "--exec", "uname", "-m"])).trim();
|
|
83
|
+
const normalized = ARCHS.get(arch);
|
|
84
|
+
if (normalized === undefined)
|
|
85
|
+
return undefined;
|
|
86
|
+
const hostPath = source.length > 0 ? source : join(packageRoot, "agent", "native", `linux-${normalized}`, "dsh-wsl-agent");
|
|
87
|
+
const binary = await readSource(hostPath);
|
|
88
|
+
const digest = createHash("sha256").update(binary).digest("hex");
|
|
89
|
+
const install = copy ?? ((script, bytes) => copyBinary(wslPath, distro, script, bytes));
|
|
90
|
+
const path = await install(nativeCacheScript(digest), binary);
|
|
91
|
+
return path.length > 0 ? { path, digest } : undefined;
|
|
92
|
+
})().catch(() => undefined);
|
|
93
|
+
NATIVE_CACHE.set(key, pending);
|
|
94
|
+
return pending;
|
|
95
|
+
}
|
|
96
|
+
/** The per-process cache; a failure is remembered as undefined. */
|
|
97
|
+
const NATIVE_CACHE = new Map();
|
|
98
|
+
/**
|
|
99
|
+
* Forget the cache; tests only.
|
|
100
|
+
*/
|
|
101
|
+
export function clearNativeBackendCache() {
|
|
102
|
+
NATIVE_CACHE.clear();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Stream the binary to the distro on stdin, run the copy/verify script and
|
|
106
|
+
* return its stdout (the cache path).
|
|
107
|
+
*
|
|
108
|
+
* Exported for its regression check; the copy path is injectable in
|
|
109
|
+
* {@link ensureNativeBackend}.
|
|
110
|
+
*
|
|
111
|
+
* @param wslPath - the `wsl.exe` path.
|
|
112
|
+
* @param distro - the distro.
|
|
113
|
+
* @param script - the copy/verify script.
|
|
114
|
+
* @param binary - the binary bytes.
|
|
115
|
+
* @returns the Linux path printed by the script.
|
|
116
|
+
*/
|
|
117
|
+
export function copyBinary(wslPath, distro, script, binary) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const child = spawn(wslPath, ["-d", distro, "--exec", "sh", "-c", script, "sh"], {
|
|
120
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
121
|
+
windowsHide: true,
|
|
122
|
+
env: pinnedWindowsEnv(process.env),
|
|
123
|
+
});
|
|
124
|
+
let stdout = "";
|
|
125
|
+
let stderr = "";
|
|
126
|
+
child.stdout?.setEncoding("utf8");
|
|
127
|
+
child.stdout?.on("data", (chunk) => {
|
|
128
|
+
stdout += chunk;
|
|
129
|
+
});
|
|
130
|
+
child.stderr?.setEncoding("utf8");
|
|
131
|
+
child.stderr?.on("data", (chunk) => {
|
|
132
|
+
stderr += chunk;
|
|
133
|
+
});
|
|
134
|
+
child.on("error", reject);
|
|
135
|
+
child.on("exit", (code) => {
|
|
136
|
+
if (code === 0)
|
|
137
|
+
resolve(stdout.trim());
|
|
138
|
+
else
|
|
139
|
+
reject(new Error(stderr.trim() || `native agent copy exited with ${code}`));
|
|
140
|
+
});
|
|
141
|
+
// The script prints the cached path and exits WITHOUT reading stdin when
|
|
142
|
+
// the in-distro digest already matches. On Windows, finishing the binary
|
|
143
|
+
// write against that closed pipe surfaces as `write EOF` on stdin, and an
|
|
144
|
+
// unhandled stream error would take down the host process. The exit code
|
|
145
|
+
// and stdout above already decide the result, so absorb the write error.
|
|
146
|
+
child.stdin?.on("error", () => { });
|
|
147
|
+
try {
|
|
148
|
+
child.stdin?.end(binary);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A pipe that is already gone is the same warm-cache case.
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** The process-wide mode; the shell executor's constructor owns it. */
|
|
156
|
+
let backend = "auto";
|
|
157
|
+
/**
|
|
158
|
+
* Record the selected backend. The session runtime is shared by the shell and
|
|
159
|
+
* filesystem providers, so this one process-wide value is what both read when
|
|
160
|
+
* they prepare their runtime; it is set from the `wsl-shell` config.
|
|
161
|
+
*
|
|
162
|
+
* @param mode - the selected backend.
|
|
163
|
+
*/
|
|
164
|
+
export function configureAgentBackend(mode) {
|
|
165
|
+
backend = mode;
|
|
166
|
+
}
|
|
167
|
+
/** @returns the configured backend (for diagnostics and tests). */
|
|
168
|
+
export function configuredAgentBackend() {
|
|
169
|
+
return backend;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Select the native binary for a distro when the backend allows it.
|
|
173
|
+
*
|
|
174
|
+
* - `shell`: clear any native entry and use the shipped shell agent.
|
|
175
|
+
* - `auto`: use the native binary when a source exists and copy/verify
|
|
176
|
+
* succeeds; otherwise stay on the shell agent.
|
|
177
|
+
* - `native`: same preparation, but a missing source or a failed preparation
|
|
178
|
+
* throws a remedy instead of silently falling back.
|
|
179
|
+
*
|
|
180
|
+
* The source is the development override `DSH_WSL_AGENT_NATIVE`; the packaged
|
|
181
|
+
* per-arch binary will be added by the packaging task and preferred over it.
|
|
182
|
+
*
|
|
183
|
+
* @param options - the transport, distro, setter and injectable prepare step.
|
|
184
|
+
*/
|
|
185
|
+
export async function ensureNativeSessionAgent({ wslPath, distro, setNative, ensure = ensureNativeBackend, }) {
|
|
186
|
+
if (backend === "shell") {
|
|
187
|
+
setNative(distro, "", "");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const result = await ensure({ wslPath, distro, source: process.env[NATIVE_AGENT_ENV] ?? "" });
|
|
191
|
+
if (result === undefined) {
|
|
192
|
+
if (backend === "native") {
|
|
193
|
+
throw new Error(`agentBackend: "native" could not prepare a native binary: the packaged binary for this architecture is missing or failed the copy/digest/execute check, and ${NATIVE_AGENT_ENV} is unset or unusable; set a valid ${NATIVE_AGENT_ENV}, or use "auto"/"shell".`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
setNative(distro, result.path, result.digest);
|
|
198
|
+
}
|
package/lib/ports.d.ts
CHANGED
|
@@ -38,7 +38,18 @@ export declare function procNetScript(): string;
|
|
|
38
38
|
*
|
|
39
39
|
* @param runner - the {@link WslAgent}-like runner.
|
|
40
40
|
* @param timeoutMs - the exec budget.
|
|
41
|
-
* @returns listening ports, ascending;
|
|
42
|
-
*
|
|
41
|
+
* @returns listening ports, ascending; undefined when the scan could not
|
|
42
|
+
* answer (agent out, non-zero exit), so a caller can keep the last snapshot.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scanListeningPorts(runner: AgentExecRunner, timeoutMs?: number): Promise<number[] | undefined>;
|
|
45
|
+
/**
|
|
46
|
+
* Snapshot the distro's listening ports, collapsing a failed scan to an empty
|
|
47
|
+
* list. New callers that must distinguish "none listening" from "the scan
|
|
48
|
+
* failed" should use {@linkcode scanListeningPorts}; this wrapper exists for
|
|
49
|
+
* the probe/test shape that only needs the list.
|
|
50
|
+
*
|
|
51
|
+
* @param runner - the agent-like runner.
|
|
52
|
+
* @param timeoutMs - the exec budget.
|
|
53
|
+
* @returns listening ports, ascending; [] on a failed scan.
|
|
43
54
|
*/
|
|
44
55
|
export declare function listeningPorts(runner: AgentExecRunner, timeoutMs?: number): Promise<number[]>;
|
package/lib/ports.js
CHANGED
|
@@ -55,17 +55,30 @@ export function procNetScript() {
|
|
|
55
55
|
*
|
|
56
56
|
* @param runner - the {@link WslAgent}-like runner.
|
|
57
57
|
* @param timeoutMs - the exec budget.
|
|
58
|
-
* @returns listening ports, ascending;
|
|
59
|
-
*
|
|
58
|
+
* @returns listening ports, ascending; undefined when the scan could not
|
|
59
|
+
* answer (agent out, non-zero exit), so a caller can keep the last snapshot.
|
|
60
60
|
*/
|
|
61
|
-
export async function
|
|
61
|
+
export async function scanListeningPorts(runner, timeoutMs = 10000) {
|
|
62
62
|
try {
|
|
63
63
|
const result = await runner.exec({ cwd: "/", argv: ["sh", "-c", procNetScript(), "ports"], timeoutMs });
|
|
64
64
|
if (result.exitCode !== 0)
|
|
65
|
-
return
|
|
65
|
+
return undefined;
|
|
66
66
|
return parseListeningPorts(result.stdout.toString("utf8"));
|
|
67
67
|
}
|
|
68
68
|
catch {
|
|
69
|
-
return
|
|
69
|
+
return undefined;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Snapshot the distro's listening ports, collapsing a failed scan to an empty
|
|
74
|
+
* list. New callers that must distinguish "none listening" from "the scan
|
|
75
|
+
* failed" should use {@linkcode scanListeningPorts}; this wrapper exists for
|
|
76
|
+
* the probe/test shape that only needs the list.
|
|
77
|
+
*
|
|
78
|
+
* @param runner - the agent-like runner.
|
|
79
|
+
* @param timeoutMs - the exec budget.
|
|
80
|
+
* @returns listening ports, ascending; [] on a failed scan.
|
|
81
|
+
*/
|
|
82
|
+
export async function listeningPorts(runner, timeoutMs = 10000) {
|
|
83
|
+
return (await scanListeningPorts(runner, timeoutMs)) ?? [];
|
|
84
|
+
}
|
package/lib/preflight.js
CHANGED
|
@@ -25,8 +25,9 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import z from "@deepseek-ai/schemastery";
|
|
27
27
|
import { isWslUnc } from "./paths.js";
|
|
28
|
-
import { defaultDistro } from "./wsl.js";
|
|
28
|
+
import { defaultDistro, runCapture } from "./wsl.js";
|
|
29
29
|
import { bwrapFailure, bwrapUsable, missingBwrapRemedy } from "./sandbox-core.js";
|
|
30
|
+
import { sessionScopeSweepScript } from "./session-scope.js";
|
|
30
31
|
export const name = "wsl-preflight";
|
|
31
32
|
export const Config = z.object({
|
|
32
33
|
/** Distro to preflight; empty means this machine's default distro. */
|
|
@@ -36,6 +37,25 @@ export const Config = z.object({
|
|
|
36
37
|
/** Whether the session-start sandbox preflight runs at all. */
|
|
37
38
|
sandbox: z.boolean().default(true),
|
|
38
39
|
});
|
|
40
|
+
/** Distros whose stale-scope sweep has already run in this process. */
|
|
41
|
+
const sweptDistros = new Set();
|
|
42
|
+
/**
|
|
43
|
+
* Stop `dsh-wsl-*.scope` units a crashed app left behind. A scope is stale
|
|
44
|
+
* when its cgroup has no `wsl-agent.sh`; a live agent means another session
|
|
45
|
+
* still owns it. Advisory: a failed spawn re-arms the distro for the next
|
|
46
|
+
* session, and the once-per-distro cache keeps the cost off the normal path.
|
|
47
|
+
*
|
|
48
|
+
* @param wslPath - the `wsl.exe` path.
|
|
49
|
+
* @param distro - the resolved distro.
|
|
50
|
+
*/
|
|
51
|
+
const sweepStaleScopes = (wslPath, distro) => {
|
|
52
|
+
if (sweptDistros.has(distro))
|
|
53
|
+
return;
|
|
54
|
+
sweptDistros.add(distro);
|
|
55
|
+
void runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", sessionScopeSweepScript()]).catch(() => {
|
|
56
|
+
sweptDistros.delete(distro);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
39
59
|
/**
|
|
40
60
|
* Mount the preflight service.
|
|
41
61
|
*
|
|
@@ -73,6 +93,9 @@ export async function apply(ctx, config = {}) {
|
|
|
73
93
|
// bwrapUsable answers false on a failed probe — it never rejects — so
|
|
74
94
|
// nothing here can throw except a broken context, which is the caller's
|
|
75
95
|
// bug, not the session's.
|
|
96
|
+
// Clean up what a hard crash left behind before this session creates its
|
|
97
|
+
// runtime; a live agent in another session's cgroup makes its scope safe.
|
|
98
|
+
sweepStaleScopes(wslPath, distro);
|
|
76
99
|
const usable = await bwrapUsable({ wslPath, distro });
|
|
77
100
|
if (usable) {
|
|
78
101
|
ctx.logger?.debug?.(`wsl-preflight: bwrap is usable inside distro "${distro}"`);
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-scoped runtime registry (ADR 0001).
|
|
3
|
+
*
|
|
4
|
+
* One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
|
|
5
|
+
* providers that need it and refcounted: the first `acquire` spawns the agent
|
|
6
|
+
* inside a systemd user scope, the last `release` closes the agent and stops
|
|
7
|
+
* the scope (the cgroup then reaps any background dev server a command left
|
|
8
|
+
* behind). The agent runs with no idle close — its lifetime is the session's —
|
|
9
|
+
* but an explicit client lease still catches a wedged host.
|
|
10
|
+
*
|
|
11
|
+
* The confinement here is the agent's own bwrap mount profile, WITHOUT a PID
|
|
12
|
+
* namespace: commands are the agent's children, so a background process
|
|
13
|
+
* survives the command, `/tmp` is the distro's real `/tmp` under
|
|
14
|
+
* `workspace-write`, and `ps` sees the session. `danger-full-access` gets the
|
|
15
|
+
* scope but no mounts.
|
|
16
|
+
*
|
|
17
|
+
* Nothing in this module touches `wsl.exe` by default except through the
|
|
18
|
+
* injectable factories; `test/session-runtime.test.mjs` drives the refcount,
|
|
19
|
+
* the prefix composition and the stop ordering without systemd.
|
|
20
|
+
*
|
|
21
|
+
* This is a TypeScript source built to `lib/session-runtime.js`; edit THIS
|
|
22
|
+
* file and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
23
|
+
* `pnpm test` fails when it drifts.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-plugin-wsl/session-runtime
|
|
26
|
+
*/
|
|
27
|
+
import { WslAgent } from "./agent.js";
|
|
28
|
+
/**
|
|
29
|
+
* Run the session runtime's preflight through an injected host runner and map
|
|
30
|
+
* the answer to a refusal. Shared by the shell executor and the filesystem
|
|
31
|
+
* provider, so both gate the runtime on the same four requirements.
|
|
32
|
+
*
|
|
33
|
+
* @param options - the transport, distro and runner.
|
|
34
|
+
* @returns the remedy-bearing refusal, or null when the platform is usable.
|
|
35
|
+
*/
|
|
36
|
+
export declare function probeSessionRuntime({ wslPath, distro, run }: {
|
|
37
|
+
/** The `wsl.exe` path. */
|
|
38
|
+
wslPath: string;
|
|
39
|
+
/** The distro name. */
|
|
40
|
+
distro: string;
|
|
41
|
+
/** The host runner; throws when the probe could not run. */
|
|
42
|
+
run: (argv: string[]) => Promise<string>;
|
|
43
|
+
}): Promise<string | null>;
|
|
44
|
+
/** One request for a session runtime. */
|
|
45
|
+
export interface SessionRuntimeRequest {
|
|
46
|
+
/** The distro the runtime lives in. */
|
|
47
|
+
distro: string;
|
|
48
|
+
/** The `wsl.exe` path; defaults to `wsl.exe`. */
|
|
49
|
+
wslPath?: string;
|
|
50
|
+
/** The session workspace root, in any coordinate system. */
|
|
51
|
+
workspaceRoot: string;
|
|
52
|
+
/** The resolved file-effect mode (`read-only`, `workspace-write`, `danger-full-access`). */
|
|
53
|
+
mode: string;
|
|
54
|
+
/** Shadow `/mnt` in the runtime's mount profile. */
|
|
55
|
+
maskWindowsDrive?: boolean;
|
|
56
|
+
/** The agent's client lease; 0 disables. */
|
|
57
|
+
leaseMs?: number;
|
|
58
|
+
}
|
|
59
|
+
/** One live runtime and its refcount. */
|
|
60
|
+
export interface SessionRuntimeEntry {
|
|
61
|
+
/** The registry key (`distro`, workspace, mode, mask). */
|
|
62
|
+
key: string;
|
|
63
|
+
/** The systemd user unit name. */
|
|
64
|
+
unit: string;
|
|
65
|
+
/** The resident agent. */
|
|
66
|
+
agent: WslAgent;
|
|
67
|
+
/** The distro, for the stop command. */
|
|
68
|
+
distro: string;
|
|
69
|
+
/** The `wsl.exe` path, for the stop command. */
|
|
70
|
+
wslPath: string;
|
|
71
|
+
/** How many providers hold the runtime. */
|
|
72
|
+
refs: number;
|
|
73
|
+
}
|
|
74
|
+
/** The injectable edges, so the registry is testable without systemd. */
|
|
75
|
+
export interface SessionRuntimeOptions {
|
|
76
|
+
/** Create the agent; tests substitute a fake. */
|
|
77
|
+
spawnAgent?: (options: {
|
|
78
|
+
/** The distro. */
|
|
79
|
+
distro: string;
|
|
80
|
+
/** The `wsl.exe` path. */
|
|
81
|
+
wslPath: string;
|
|
82
|
+
/** The scope + mount prefix. */
|
|
83
|
+
argvPrefix: string[];
|
|
84
|
+
/** No idle close: the runtime lives for the session. */
|
|
85
|
+
idleMs: number;
|
|
86
|
+
/** The explicit client lease. */
|
|
87
|
+
leaseMs: number;
|
|
88
|
+
/** The prefix has no PID namespace, so USR1 can reach the agent. */
|
|
89
|
+
signalable: boolean;
|
|
90
|
+
}) => WslAgent;
|
|
91
|
+
/** Stop the scope; tests record it. */
|
|
92
|
+
stopScope?: (options: {
|
|
93
|
+
wslPath: string;
|
|
94
|
+
distro: string;
|
|
95
|
+
unit: string;
|
|
96
|
+
}) => void;
|
|
97
|
+
/** The unique token in the unit name; tests pin it. */
|
|
98
|
+
token?: () => string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Set (or clear) the native binary the session runtime should spawn for one
|
|
102
|
+
* distro. The host calls this after the copy/verify step; an empty path
|
|
103
|
+
* clears it and restores the shell agent.
|
|
104
|
+
*
|
|
105
|
+
* @param distro - the distro.
|
|
106
|
+
* @param path - the Linux binary path, or empty to clear.
|
|
107
|
+
* @param digest - the binary's sha256, which the HELLO must present.
|
|
108
|
+
*/
|
|
109
|
+
export declare function setSessionNativeAgent(distro: string, path: string, digest?: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* The refcounted registry. One instance lives for the process; providers share
|
|
112
|
+
* it so the shell and filesystem sides address the same runtime for a session.
|
|
113
|
+
*/
|
|
114
|
+
export declare class SessionRuntimes {
|
|
115
|
+
/** The live runtimes, by key. */
|
|
116
|
+
private readonly runtimes;
|
|
117
|
+
/** The agent factory. */
|
|
118
|
+
private readonly spawnAgent;
|
|
119
|
+
/** The scope stopper. */
|
|
120
|
+
private readonly stopScope;
|
|
121
|
+
/** The unit-token generator. */
|
|
122
|
+
private readonly token;
|
|
123
|
+
/**
|
|
124
|
+
* @param options - the injectable edges; production passes none.
|
|
125
|
+
*/
|
|
126
|
+
constructor(options?: SessionRuntimeOptions);
|
|
127
|
+
/**
|
|
128
|
+
* The key a request addresses: distro, workspace, mode and mask. A policy
|
|
129
|
+
* change is a different runtime, because the mount profile is fixed for the
|
|
130
|
+
* agent's lifetime.
|
|
131
|
+
*
|
|
132
|
+
* @param request - the runtime request.
|
|
133
|
+
* @returns the registry key.
|
|
134
|
+
*/
|
|
135
|
+
static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive">): string;
|
|
136
|
+
/**
|
|
137
|
+
* Acquire the runtime for one key, creating it on first use.
|
|
138
|
+
*
|
|
139
|
+
* @param request - the runtime request.
|
|
140
|
+
* @returns the live entry; the caller holds one reference.
|
|
141
|
+
*/
|
|
142
|
+
acquire(request: SessionRuntimeRequest): SessionRuntimeEntry;
|
|
143
|
+
/**
|
|
144
|
+
* Release one reference. The last release closes the agent and stops the
|
|
145
|
+
* scope, in that order: the agent's exit ends the protocol, the stop reaps
|
|
146
|
+
* whatever the session left running.
|
|
147
|
+
*
|
|
148
|
+
* @param key - the entry's key.
|
|
149
|
+
* @returns resolved once the last holder's teardown finished.
|
|
150
|
+
*/
|
|
151
|
+
release(key: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Release every runtime, for a host shutdown or a test. Sequential on
|
|
154
|
+
* purpose: concurrent closes would race the shared agent's state.
|
|
155
|
+
*
|
|
156
|
+
* @returns resolved once every runtime is torn down.
|
|
157
|
+
*/
|
|
158
|
+
disposeAll(): Promise<void>;
|
|
159
|
+
}
|
|
160
|
+
/** The process-wide registry the providers will share. */
|
|
161
|
+
export declare const sessionRuntimes: SessionRuntimes;
|