dsh-plugin-wsl-env 0.1.10 → 0.3.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 +268 -0
- package/README.md +21 -2
- package/agent/wsl-agent.sh +498 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.md +37 -1
- package/docs/CONFIGURATION.md +12 -1
- package/docs/LIMITATIONS.md +70 -5
- package/docs/PARITY.md +70 -0
- package/docs/PEER-PARITY.md +60 -0
- package/docs/README.zh.md +31 -20
- package/docs/UPSTREAM-FSIO-EXPORT.md +87 -0
- package/lib/agent-confined.js +81 -0
- package/lib/agent-errors.js +33 -0
- package/lib/agent-exec.js +204 -0
- package/lib/agent-protocol.js +174 -0
- package/lib/agent-shared.js +32 -0
- package/lib/agent.js +564 -0
- package/lib/bwrap.js +74 -0
- package/lib/fs-publish.js +42 -0
- package/lib/fs-substrate.js +285 -0
- package/lib/fsio-agent.js +481 -0
- package/lib/fsio-text.js +153 -0
- package/lib/index.js +430 -12
- package/lib/ports.js +61 -0
- package/lib/sandbox.js +35 -28
- package/lib/shell-env.js +28 -2
- package/lib/subprocess.js +15 -0
- package/lib/terminal-route.js +31 -0
- package/lib/watcher.js +127 -0
- package/package.json +13 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An agent-backed implementation of the shell execution handle.
|
|
3
|
+
*
|
|
4
|
+
* `LocalBashExecutor.executeArgv` builds its handle around a subprocess spawn:
|
|
5
|
+
* live output readers with byte offsets, spill files beyond the collect
|
|
6
|
+
* budget, a deadline that kills the Windows process tree. This module builds
|
|
7
|
+
* the SAME handle shape around one agent `exec` — the shape `dsh-tool-bash`
|
|
8
|
+
* consumes, so the tool layer cannot tell which path ran. The deliberate
|
|
9
|
+
* deviations, documented here rather than hidden:
|
|
10
|
+
*
|
|
11
|
+
* - **No spill files; the capture cap is the agent's.** The EXEC frame
|
|
12
|
+
* carries the caller's per-stream budget and the AGENT cuts stdout and
|
|
13
|
+
* stderr at it, so a chatty command costs bounded distro memory AND bounded
|
|
14
|
+
* host memory; the RES line's flags say which streams were cut and the
|
|
15
|
+
* handle reports them as `truncated: true`. What the budget spares is the
|
|
16
|
+
* upstream spill machinery: the capture is still whole-in-memory, just
|
|
17
|
+
* whole-at-a-bounded-size.
|
|
18
|
+
* - **Killed commands report `SIGTERM`**, not a raw exit status: the
|
|
19
|
+
* in-distro timeout and `kill()` both mean "we stopped it", and upstream
|
|
20
|
+
* reports its own kills the same way.
|
|
21
|
+
*
|
|
22
|
+
* Pure shaping over an injected runner, so the handle semantics are
|
|
23
|
+
* unit-testable without a distro.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-plugin-wsl/agent-exec
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { CwdError } from "./agent-errors.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A memory-backed output reader in the seam's shape.
|
|
32
|
+
* @param {Buffer} buffer - the complete stream.
|
|
33
|
+
* @returns {{readFrom: (fromByte: number) => {text: string, lossy: boolean, nextOffset: number}}}
|
|
34
|
+
*/
|
|
35
|
+
function memoryReader(buffer) {
|
|
36
|
+
return {
|
|
37
|
+
readFrom(fromByte) {
|
|
38
|
+
const start = Math.min(fromByte, buffer.length);
|
|
39
|
+
return {
|
|
40
|
+
text: buffer.subarray(start).toString("utf8"),
|
|
41
|
+
lossy: false,
|
|
42
|
+
nextOffset: buffer.length,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Run one command through the agent and present it as an execution handle.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} request - the execution request.
|
|
52
|
+
* @param {object} request.agent - the {@link WslAgent}-like runner: `exec`
|
|
53
|
+
* accepting `{cwd, argv, timeoutMs, signal}` and resolving
|
|
54
|
+
* `{exitCode, stdout, stderr}`; throws `AgentUnavailableError` when out.
|
|
55
|
+
* @param {string} request.cwd - Linux workdir.
|
|
56
|
+
* @param {string[]} request.argv - the distro-side argv, bwrap-wrapped when confined.
|
|
57
|
+
* @param {number} [request.timeoutMs] - whole-command budget; the agent TERMs
|
|
58
|
+
* at the ceiling second and KILLs after its grace.
|
|
59
|
+
* @param {number} [request.maxOutputBytes] - per-stream capture ceiling the
|
|
60
|
+
* agent enforces; the handle reports a cut stream as `truncated: true`.
|
|
61
|
+
* @param {AbortSignal} [request.signal] - the caller's cancellation.
|
|
62
|
+
* @param {(proc: object) => void} [request.onStarted] - provider-facts hook,
|
|
63
|
+
* called synchronously before the handle can settle.
|
|
64
|
+
* @returns {object} the execution handle (`status`, `exitCode`, `signal`,
|
|
65
|
+
* `observed`, `done`, `readOutput`, `kill`, `result`).
|
|
66
|
+
*/
|
|
67
|
+
export function agentExecutionHandle({ agent, cwd, argv, timeoutMs = 0, maxOutputBytes = 0, signal, onStarted }) {
|
|
68
|
+
// Our own controller is the kill channel; the caller's signal races it.
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
let killed = false;
|
|
72
|
+
let callerAborted = false;
|
|
73
|
+
if (signal) {
|
|
74
|
+
if (signal.aborted) callerAborted = true;
|
|
75
|
+
else signal.addEventListener("abort", () => { callerAborted = true; controller.abort(signal.reason); }, { once: true });
|
|
76
|
+
}
|
|
77
|
+
if (timeoutMs > 0) {
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
timedOut = true;
|
|
80
|
+
controller.abort(new Error("BASH_TIMEOUT"));
|
|
81
|
+
}, timeoutMs).unref?.();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const settle = agent.exec({ cwd, argv, timeoutMs, maxOutputBytes, signal: controller.signal }).then(
|
|
85
|
+
(result) => ({ ok: true, result }),
|
|
86
|
+
(error) => {
|
|
87
|
+
// A workdir the distro cannot enter is the command's own failure, not
|
|
88
|
+
// the agent's: synthesize the relay-shaped stderr the one-shot path
|
|
89
|
+
// produces, so the executor's `workdirFailure` check reports it the
|
|
90
|
+
// same way. An agent that is actually OUT propagates for fallback.
|
|
91
|
+
if (error instanceof CwdError) {
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
result: {
|
|
95
|
+
exitCode: 1,
|
|
96
|
+
stdout: Buffer.alloc(0),
|
|
97
|
+
stderr: Buffer.from(`<3>WSL: CreateProcessCommon: chdir(${error.linuxPath}) failed: No such file or directory`, "utf8"),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { ok: false, error };
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/** @type {Buffer} */
|
|
106
|
+
let stdout = Buffer.alloc(0);
|
|
107
|
+
/** @type {Buffer} */
|
|
108
|
+
let stderr = Buffer.alloc(0);
|
|
109
|
+
/** The agent's per-stream capture-cap flags, surfaced as `truncated`. */
|
|
110
|
+
let stdoutTruncated = false;
|
|
111
|
+
let stderrTruncated = false;
|
|
112
|
+
let exitOutcome = { exitCode: null, signal: "SIGTERM" };
|
|
113
|
+
|
|
114
|
+
let settleError;
|
|
115
|
+
const done = settle.then(({ ok, result, error }) => {
|
|
116
|
+
if (ok) {
|
|
117
|
+
stdout = result.stdout;
|
|
118
|
+
stderr = result.stderr;
|
|
119
|
+
stdoutTruncated = result.truncated?.stdout === true;
|
|
120
|
+
stderrTruncated = result.truncated?.stderr === true;
|
|
121
|
+
// The distro timeout and kill() both mean "we stopped it"; a genuine
|
|
122
|
+
// exit keeps its code.
|
|
123
|
+
if (timedOut || killed || callerAborted) exitOutcome = { exitCode: null, signal: "SIGTERM" };
|
|
124
|
+
else exitOutcome = { exitCode: result.exitCode, signal: null };
|
|
125
|
+
} else if (timedOut || killed) {
|
|
126
|
+
// OUR kill (the in-distro timeout or kill()): the agent's rejection is
|
|
127
|
+
// the abort plumbing, not a failure — settle as the killed outcome the
|
|
128
|
+
// one-shot path reports for the same situation.
|
|
129
|
+
stdout = Buffer.alloc(0);
|
|
130
|
+
stderr = Buffer.alloc(0);
|
|
131
|
+
} else {
|
|
132
|
+
// The caller's cancellation rejections propagate (upstream keeps
|
|
133
|
+
// cancellation as a rejection), as does a genuinely out-of-service
|
|
134
|
+
// agent — that one is the executor's fallback cue.
|
|
135
|
+
settleError = error;
|
|
136
|
+
}
|
|
137
|
+
return exitOutcome;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const stdoutReader = () => memoryReader(stdout);
|
|
141
|
+
const stderrReader = () => memoryReader(stderr);
|
|
142
|
+
|
|
143
|
+
let stdoutOffset = 0;
|
|
144
|
+
let stderrOffset = 0;
|
|
145
|
+
|
|
146
|
+
/** @type {object} */
|
|
147
|
+
const proc = {
|
|
148
|
+
status: "running",
|
|
149
|
+
exitCode: null,
|
|
150
|
+
signal: null,
|
|
151
|
+
observed: {
|
|
152
|
+
get stdout() { return stdoutReader(); },
|
|
153
|
+
get stderr() { return stderrReader(); },
|
|
154
|
+
},
|
|
155
|
+
done: done.then(() => {
|
|
156
|
+
proc.status = exitOutcome.signal !== null || exitOutcome.exitCode === null ? "killed" : "completed";
|
|
157
|
+
proc.exitCode = exitOutcome.exitCode;
|
|
158
|
+
proc.signal = exitOutcome.signal;
|
|
159
|
+
}),
|
|
160
|
+
readOutput() {
|
|
161
|
+
const out = stdoutReader().readFrom(stdoutOffset);
|
|
162
|
+
const err = stderrReader().readFrom(stderrOffset);
|
|
163
|
+
stdoutOffset = out.nextOffset;
|
|
164
|
+
stderrOffset = err.nextOffset;
|
|
165
|
+
const separator = out.text.length > 0 && !out.text.endsWith("\n") ? "\n" : "";
|
|
166
|
+
return {
|
|
167
|
+
delta: out.text + (err.text.length > 0 ? `${separator}[stderr]\n${err.text}` : ""),
|
|
168
|
+
lossy: false,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
kill() {
|
|
172
|
+
if (proc.status !== "running") return false;
|
|
173
|
+
// Upstream marks the handle killed SYNCHRONOUSLY, before the process is
|
|
174
|
+
// observed to die — the second kill() must already be a no-op.
|
|
175
|
+
proc.status = "killed";
|
|
176
|
+
killed = true;
|
|
177
|
+
controller.abort(new Error("killed"));
|
|
178
|
+
return true;
|
|
179
|
+
},
|
|
180
|
+
result() {
|
|
181
|
+
return done.then(() => {
|
|
182
|
+
// The spawn-equivalent failure (the agent being out) rejects here,
|
|
183
|
+
// exactly like upstream's `running.done` rejection carried by result().
|
|
184
|
+
if (settleError !== undefined) throw settleError;
|
|
185
|
+
// `truncated` is REQUIRED by the bash tool's canonical result — it
|
|
186
|
+
// copies the field unconditionally, and an `undefined` fails the wire's
|
|
187
|
+
// lossless-JSON snapshot ("value is not lossless JSON"). The agent's
|
|
188
|
+
// capture-cap flags are what make it TRUE instead of a perma-lie.
|
|
189
|
+
const output = (text, truncated) => ({ text, lossy: false, truncated });
|
|
190
|
+
return {
|
|
191
|
+
exitCode: proc.exitCode,
|
|
192
|
+
signal: proc.signal,
|
|
193
|
+
timedOut,
|
|
194
|
+
aborted: callerAborted && !timedOut,
|
|
195
|
+
timeoutMs,
|
|
196
|
+
stdout: output(stdout.toString("utf8"), stdoutTruncated),
|
|
197
|
+
stderr: output(stderr.toString("utf8"), stderrTruncated),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
onStarted?.(proc);
|
|
203
|
+
return proc;
|
|
204
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol for the resident in-distro agent (`agent/wsl-agent.sh`).
|
|
3
|
+
*
|
|
4
|
+
* Pure by construction — no Node API, no I/O — so the frame codecs and the
|
|
5
|
+
* line parser are unit-testable without a distro. The protocol itself is line
|
|
6
|
+
* oriented with `|` field separators and single-line base64 payloads; see the
|
|
7
|
+
* agent script's header for the grammar. Base64 was chosen over JSON because
|
|
8
|
+
* the agent must run under a bare POSIX sh with nothing but coreutils.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-wsl/agent-protocol
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The protocol version this host side speaks; the agent must match. */
|
|
14
|
+
export const PROTOCOL_VERSION = 3;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upper bound on one `read` op's payload, set by the HOST looping over windowed
|
|
18
|
+
* requests. Keeping each frame's decoded payload here means the agent's RES
|
|
19
|
+
* line stays modest even for a large file; the host accumulates chunks.
|
|
20
|
+
*/
|
|
21
|
+
export const FS_READ_CHUNK_BYTES = 1024 * 1024;
|
|
22
|
+
|
|
23
|
+
/** The agent identifies itself with this name in its HELLO line. */
|
|
24
|
+
export const AGENT_NAME = "wsl-agent";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Rough ceiling on one protocol line, chosen well above the executor's own
|
|
28
|
+
* 64 KB output budget spill: stdout and stderr ride ONE line each, so a frame
|
|
29
|
+
* is as large as the largest capture the caller asked for. Documented so a
|
|
30
|
+
* future streaming design does not inherit it silently.
|
|
31
|
+
*/
|
|
32
|
+
export const MAX_FRAME_BYTES = 256 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Encode a buffer as the protocol's single-line base64 (no line wraps, no
|
|
36
|
+
* padding surprises — standard base64 including `=` padding).
|
|
37
|
+
* @param {string|Uint8Array} value - the payload to encode.
|
|
38
|
+
* @returns {string} the wire form.
|
|
39
|
+
*/
|
|
40
|
+
export function encodeB64(value) {
|
|
41
|
+
return Buffer.from(value).toString("base64");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Decode one protocol base64 field.
|
|
46
|
+
* @param {string} value - the wire form.
|
|
47
|
+
* @returns {Buffer} the decoded bytes; a malformed field decodes to empty
|
|
48
|
+
* rather than throwing, mirroring the agent's own lenient decode.
|
|
49
|
+
*/
|
|
50
|
+
export function decodeB64(value) {
|
|
51
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value)) return Buffer.alloc(0);
|
|
52
|
+
return Buffer.from(value, "base64");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the request lines for one EXEC frame: a header line naming the cwd,
|
|
57
|
+
* the whole-request timeout in whole seconds (0 = none), the argv word count
|
|
58
|
+
* and the per-stream output capture budget in bytes (0 = unlimited), followed
|
|
59
|
+
* by one base64 line per argv word.
|
|
60
|
+
* @param {object} request - the execution request.
|
|
61
|
+
* @param {string} request.id - caller-chosen request id (unique per agent).
|
|
62
|
+
* @param {string} request.cwd - Linux path to cd into before exec.
|
|
63
|
+
* @param {string[]} request.argv - the argv to execute, already bwrap-wrapped.
|
|
64
|
+
* @param {number} [request.timeoutMs] - in-distro timeout; sub-second
|
|
65
|
+
* remainders round up, and `<= 0` disables the timeout.
|
|
66
|
+
* @param {number} [request.maxOutputBytes] - per-stream capture ceiling the
|
|
67
|
+
* AGENT enforces: stdout and stderr are each cut at this many bytes and the
|
|
68
|
+
* RES line reports what was cut. 0 (the default) keeps the historical
|
|
69
|
+
* unbounded capture.
|
|
70
|
+
* @returns {string[]} the lines to write to the agent's stdin, in order.
|
|
71
|
+
*/
|
|
72
|
+
export function encodeExecFrame({ id, cwd, argv, timeoutMs = 0, maxOutputBytes = 0 }) {
|
|
73
|
+
const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
|
|
74
|
+
const cap = maxOutputBytes > 0 ? Math.floor(maxOutputBytes) : 0;
|
|
75
|
+
const lines = [
|
|
76
|
+
`EXEC|${id}|${encodeB64(cwd)}|${timeoutSeconds}|${argv.length}|${cap}`,
|
|
77
|
+
...argv.map((word) => encodeB64(word)),
|
|
78
|
+
];
|
|
79
|
+
return lines;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build a SETENV request line. Note the agent's decode strips trailing
|
|
84
|
+
* newlines from the value — the same leniency the shell itself applies.
|
|
85
|
+
* @param {string} key - the variable name.
|
|
86
|
+
* @param {string} value - the variable value.
|
|
87
|
+
* @returns {string} the line to write.
|
|
88
|
+
*/
|
|
89
|
+
export function encodeSetEnv(key, value) {
|
|
90
|
+
return `SETENV|${key}|${encodeB64(value)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** @param {string} id - the request id to interrupt. @returns {string} */
|
|
94
|
+
export function encodeKill(id) {
|
|
95
|
+
return `KILL|${id}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the request lines for one FS frame — the filesystem-substrate request
|
|
100
|
+
* that lets the model's file tools run inside the distro. The shape mirrors
|
|
101
|
+
* EXEC: a header naming the op, the timeout in whole seconds and the argument
|
|
102
|
+
* count, followed by one base64 line per argument.
|
|
103
|
+
*
|
|
104
|
+
* Ops (see `agent/wsl-agent.sh` for the server side and the wire grammar):
|
|
105
|
+
* `stat`, `lstat`, `list`, `realpath`, `read`, `write`. Responses reuse the
|
|
106
|
+
* EXEC `ACK`/`RES`/`ERR` lines, so the request machinery (pending map, KILL,
|
|
107
|
+
* the watchdog) is shared unchanged.
|
|
108
|
+
*
|
|
109
|
+
* The argument contract: callers pass PLAINTEXT values here; this frame is the
|
|
110
|
+
* single base64 boundary, and the agent decodes each argument once. A write's
|
|
111
|
+
* content is no exception — the agent pipes its still-encoded wire line
|
|
112
|
+
* straight to `base64 -d`, so raw bytes never pass through a shell variable.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} request - the filesystem request.
|
|
115
|
+
* @param {string} request.id - caller-chosen request id (unique per agent).
|
|
116
|
+
* @param {string} request.op - one of the op names above.
|
|
117
|
+
* @param {string[]} request.args - op arguments, base64-line encoded here.
|
|
118
|
+
* @param {number} [request.timeoutMs] - in-distro timeout; sub-second
|
|
119
|
+
* remainders round up, and `<= 0` disables the timeout.
|
|
120
|
+
* @returns {string[]} the lines to write to the agent's stdin, in order.
|
|
121
|
+
*/
|
|
122
|
+
export function encodeFsFrame({ id, op, args, timeoutMs = 0 }) {
|
|
123
|
+
const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
|
|
124
|
+
return [
|
|
125
|
+
`FS|${id}|${op}|${timeoutSeconds}|${args.length}`,
|
|
126
|
+
...args.map((word) => encodeB64(word)),
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The line parser's verdict for one line of agent stdout. */
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse one line of the agent's stdout.
|
|
134
|
+
* @param {string} line - a line without its terminator.
|
|
135
|
+
* @returns {object} one of:
|
|
136
|
+
* - `{ type: "hello", name, version }`
|
|
137
|
+
* - `{ type: "pong" }`
|
|
138
|
+
* - `{ type: "ack", id }` — the agent dequeued the request for execution;
|
|
139
|
+
* the host arms its stuck-request watchdog here, so queue time never
|
|
140
|
+
* counts against a budget
|
|
141
|
+
* - `{ type: "result", id, exitCode, stdout, stderr }` — outputs as Buffers
|
|
142
|
+
* - `{ type: "agentError", id, reason, message }` — `id` is `""` when the
|
|
143
|
+
* agent reports a request-less problem (a protocol violation)
|
|
144
|
+
* - `{ type: "unknown", line }` — never thrown; the host decides whether an
|
|
145
|
+
* unrecognized line is fatal. (Anything during the handshake is; after it,
|
|
146
|
+
* garbage on stdout is logged and dropped, because a `RES` for a request
|
|
147
|
+
* the host already gave up on is expected noise.)
|
|
148
|
+
*/
|
|
149
|
+
export function parseAgentLine(line) {
|
|
150
|
+
if (line.startsWith("HELLO|")) {
|
|
151
|
+
const [, name, version] = line.split("|");
|
|
152
|
+
return { type: "hello", name, version: Number(version) };
|
|
153
|
+
}
|
|
154
|
+
if (line === "PONG") return { type: "pong" };
|
|
155
|
+
if (line.startsWith("ACK|")) return { type: "ack", id: line.slice(4) };
|
|
156
|
+
if (line.startsWith("RES|")) {
|
|
157
|
+
const [, id, exitCode, stdout, stderr, outTruncated, errTruncated] = line.split("|");
|
|
158
|
+
return {
|
|
159
|
+
type: "result",
|
|
160
|
+
id,
|
|
161
|
+
exitCode: Number(exitCode),
|
|
162
|
+
stdout: decodeB64(stdout ?? ""),
|
|
163
|
+
stderr: decodeB64(stderr ?? ""),
|
|
164
|
+
// The capture-cap flags the agent appends when the EXEC frame carried a
|
|
165
|
+
// budget; frames without them (fakes, FS ops) are simply not truncated.
|
|
166
|
+
truncated: { stdout: outTruncated === "1", stderr: errTruncated === "1" },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (line.startsWith("ERR|")) {
|
|
170
|
+
const [, id, reason, message] = line.split("|");
|
|
171
|
+
return { type: "agentError", id: id ?? "", reason: reason ?? "protocol", message: decodeB64(message ?? "").toString("utf8") };
|
|
172
|
+
}
|
|
173
|
+
return { type: "unknown", line };
|
|
174
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-distro resident agent shared by every provider in this process.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `lib/index.js` so modules the index does not import back
|
|
5
|
+
* (`shell-env`, future consumers) can use the same peer without a cycle: the
|
|
6
|
+
* shell executor's commands, the filesystem's in-distro side work and the
|
|
7
|
+
* port snapshot all queue on ONE agent per distro, and the per-distro startup
|
|
8
|
+
* cost is paid once per process.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-plugin-wsl/agent-shared
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { toLinuxPath } from "./paths.js";
|
|
15
|
+
import { WslAgent } from "./agent.js";
|
|
16
|
+
|
|
17
|
+
const AGENT_SCRIPT_PATH = fileURLToPath(new URL("../agent/wsl-agent.sh", import.meta.url));
|
|
18
|
+
const sharedAgents = new Map();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The shared agent for one distro, created on first use.
|
|
22
|
+
* @param distro - the distro name.
|
|
23
|
+
* @returns {WslAgent} the shared agent.
|
|
24
|
+
*/
|
|
25
|
+
export function sharedAgent(distro) {
|
|
26
|
+
let agent = sharedAgents.get(distro);
|
|
27
|
+
if (agent === undefined) {
|
|
28
|
+
agent = new WslAgent({ distro, scriptPath: toLinuxPath(AGENT_SCRIPT_PATH) });
|
|
29
|
+
sharedAgents.set(distro, agent);
|
|
30
|
+
}
|
|
31
|
+
return agent;
|
|
32
|
+
}
|