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
package/lib/watcher.js
CHANGED
|
@@ -8,21 +8,33 @@
|
|
|
8
8
|
* whenever something under `target` may have changed. That is cheap to honor
|
|
9
9
|
* from inside the distro, where the kernel does know.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* When `inotifywait` is installed and the target is an unbounded directory,
|
|
12
|
+
* the watcher uses inotify: recursive kernel watches, one `C` per 0.5s of
|
|
13
|
+
* events (a build's event storm is coalesced, the first event is immediate).
|
|
14
|
+
* Otherwise it falls back to the long-lived polling loop: every cadence,
|
|
12
15
|
* `find <dir> -newer <stamp>` — the directory itself is in the scan, so
|
|
13
16
|
* creations AND deletions (which bump the parent's mtime) are both seen — and
|
|
14
|
-
* one `C` line per hit.
|
|
15
|
-
*
|
|
17
|
+
* one `C` line per hit. A FILE target uses the `mtime:size` fingerprint (so a
|
|
18
|
+
* rename-over or an in-place write is seen), and a positive `maxDepth` keeps
|
|
19
|
+
* the polling scan because inotify cannot bound its own walk.
|
|
16
20
|
*
|
|
17
21
|
* The loop speaks three one-letter lines, the watcher protocol:
|
|
18
22
|
*
|
|
19
23
|
* - `R` — READY: the activation barrier; the host resolves the watch promise
|
|
20
24
|
* on it, never before.
|
|
21
|
-
* - `C` — CHANGED:
|
|
22
|
-
* - `E` — ERROR: the watched
|
|
25
|
+
* - `C` — CHANGED: the target may have changed.
|
|
26
|
+
* - `E` — ERROR: the watched path no longer exists, and the loop has
|
|
23
27
|
* exited. Without it a vanished target would sit armed forever — `find`
|
|
24
28
|
* over a missing directory prints nothing, indistinguishable from "no
|
|
25
29
|
* changes" — so the loop says so and the host reports it.
|
|
30
|
+
* - `P <pid>` — the loop shell's PID, sent before READY. The host uses it to
|
|
31
|
+
* stop the loop with a second `wsl.exe` on close: killing the transport
|
|
32
|
+
* alone does not reliably close the distro-side pipe.
|
|
33
|
+
* - `H` — HEARTBEAT: liveness only, ignored by the host. It exists so the
|
|
34
|
+
* loop notices a closed stdout pipe: the host's close kills `wsl.exe` but
|
|
35
|
+
* cannot signal the distro-side shell, and without a write that shell
|
|
36
|
+
* would sleep and poll forever. The `PIPE` trap turns that write's
|
|
37
|
+
* SIGPIPE into a clean exit through the stamp-removing EXIT trap.
|
|
26
38
|
*
|
|
27
39
|
* Known blind spot, accepted and documented: a file created with an mtime
|
|
28
40
|
* older than the stamp (`cp -p`, `tar -x`) is not seen until something else
|
|
@@ -38,14 +50,15 @@ import { spawn } from "node:child_process";
|
|
|
38
50
|
import { createInterface } from "node:readline";
|
|
39
51
|
/**
|
|
40
52
|
* The shell loop the watcher runs, parameterised by argv so no path can break
|
|
41
|
-
* quoting: `$1` is the Linux directory, `$2` the poll
|
|
42
|
-
* Pure text, so it is unit-testable.
|
|
53
|
+
* quoting: `$1` is the Linux path (a directory or a file), `$2` the poll
|
|
54
|
+
* interval in seconds. Pure text, so it is unit-testable.
|
|
43
55
|
*
|
|
44
56
|
* @param intervalSeconds - poll cadence; must be >= 1.
|
|
45
|
-
* @param maxDepth - bound the scan's depth, 1 = the
|
|
46
|
-
* itself and its direct children; 0 (the default) scans the whole
|
|
47
|
-
* which is the contract's semantics
|
|
48
|
-
*
|
|
57
|
+
* @param maxDepth - bound the scan's depth for a DIRECTORY target, 1 = the
|
|
58
|
+
* directory itself and its direct children; 0 (the default) scans the whole
|
|
59
|
+
* tree, which is the contract's semantics. Positive values select the
|
|
60
|
+
* polling scan (inotify is used only for the unbounded walk). A file target
|
|
61
|
+
* ignores it.
|
|
49
62
|
* @returns the `sh -c` script body.
|
|
50
63
|
*/
|
|
51
64
|
export function watchLoopScript(intervalSeconds = 2, maxDepth = 0) {
|
|
@@ -56,37 +69,90 @@ export function watchLoopScript(intervalSeconds = 2, maxDepth = 0) {
|
|
|
56
69
|
const depthArgs = depth > 0 ? ` -maxdepth ${depth}` : "";
|
|
57
70
|
// `find` lists the directory itself first: a child's creation or deletion
|
|
58
71
|
// bumps the parent's mtime, so both edges are seen, not just modifications.
|
|
72
|
+
// A FILE target has no tree to walk; its `mtime:size` fingerprint answers the
|
|
73
|
+
// same one-way question ("may have changed") and covers a rename-over too,
|
|
74
|
+
// since the path is re-stat'ed each tick.
|
|
59
75
|
return [
|
|
60
76
|
'D="$1"',
|
|
61
77
|
'S="${TMPDIR:-/tmp}/wsl-watch.$$.stamp"',
|
|
78
|
+
'FIFO="${TMPDIR:-/tmp}/wsl-watch.$$.fifo"',
|
|
79
|
+
'IW=""',
|
|
62
80
|
'touch "$S"',
|
|
63
|
-
// The stamp
|
|
64
|
-
// EXIT trap covers plain exits; a signal alone would end the
|
|
65
|
-
// running it, so TERM/INT/HUP are trapped to a plain
|
|
66
|
-
|
|
67
|
-
"trap '
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
// The stamp/FIFO are the loop's private state, so every way out removes
|
|
82
|
+
// them. The EXIT trap covers plain exits; a signal alone would end the
|
|
83
|
+
// shell WITHOUT running it, so TERM/INT/HUP/PIPE are trapped to a plain
|
|
84
|
+
// exit first. `pkill -P $$` also reaps inotifywait/sleep/find.
|
|
85
|
+
"trap 'kill \"$IW\" 2>/dev/null; pkill -P $$ 2>/dev/null; exec 3>&- 2>/dev/null; rm -f \"$FIFO\" \"$S\" 2>/dev/null' EXIT",
|
|
86
|
+
"trap 'exit 0' TERM INT HUP PIPE",
|
|
87
|
+
'F=""',
|
|
88
|
+
'[ -f "$D" ] && F=$(stat -c "%Y:%s" -- "$D" 2>/dev/null || :)',
|
|
89
|
+
// Prefer inotify for an unbounded DIRECTORY watch: events are immediate and
|
|
90
|
+
// the kernel does the walking. A file target uses the fingerprint loop
|
|
91
|
+
// (it also reports a removal as E), and a bounded scan (maxDepth > 0)
|
|
92
|
+
// keeps the polling loop, which honors the bound.
|
|
93
|
+
`if [ -d "$D" ] && [ ${depth} -eq 0 ] && command -v inotifywait >/dev/null 2>&1; then`,
|
|
94
|
+
' if mkfifo "$FIFO" 2>/dev/null; then',
|
|
95
|
+
' exec 3<>"$FIFO"',
|
|
96
|
+
' inotifywait -m -q -r -e create,delete,modify,move,attrib --format "%e" "$D" >"$FIFO" 2>/dev/null &',
|
|
97
|
+
' IW=$!',
|
|
98
|
+
' fi',
|
|
99
|
+
'fi',
|
|
100
|
+
// The PID precedes READY so a close can stop the loop even if the pipe
|
|
101
|
+
// stays open; killing wsl.exe alone does not close it reliably.
|
|
102
|
+
"printf 'P %s\\n' \"$$\"",
|
|
71
103
|
"printf 'R\\n'",
|
|
72
|
-
//
|
|
73
|
-
//
|
|
104
|
+
// One C per half second: a build's event storm becomes a coarse
|
|
105
|
+
// invalidation, while the first event is immediate. A quiet tree blocks
|
|
106
|
+
// in read; the close stops the shell through the P line, not the pipe.
|
|
107
|
+
'if [ -n "$IW" ]; then',
|
|
108
|
+
// A one-second marker on the same FIFO wakes the blocking read so the
|
|
109
|
+
// TERM trap and the vanished-target check run even on a quiet tree. The
|
|
110
|
+
// marker is not an event: it is skipped, and only checked against the
|
|
111
|
+
// target's existence.
|
|
112
|
+
' ( while :; do sleep 1; printf \'H\\n\' >"$FIFO" 2>/dev/null; done ) &',
|
|
113
|
+
' while read -r ev <&3; do',
|
|
114
|
+
' if [ "$ev" = "H" ]; then',
|
|
115
|
+
' [ -d "$D" ] || { printf \'E\\n\'; exit 1; }',
|
|
116
|
+
' continue',
|
|
117
|
+
' fi',
|
|
118
|
+
' [ -d "$D" ] || { printf \'E\\n\'; exit 1; }',
|
|
119
|
+
" printf 'C\\n'",
|
|
120
|
+
' sleep 0.5',
|
|
121
|
+
' done',
|
|
122
|
+
' exec 3>&-',
|
|
123
|
+
'fi',
|
|
124
|
+
// Polling fallback: no inotifywait, a bounded scan, or inotifywait exited
|
|
125
|
+
// (a vanished target or an inotify limit). The stamp is the baseline.
|
|
74
126
|
"while :; do",
|
|
75
|
-
' if [
|
|
76
|
-
|
|
127
|
+
' if [ -d "$D" ]; then',
|
|
128
|
+
` if [ -n "$(find "$D"${depthArgs} -newer "$S" -print -quit 2>/dev/null)" ]; then`,
|
|
129
|
+
' touch "$S"',
|
|
130
|
+
" printf 'C\\n'",
|
|
131
|
+
' fi',
|
|
132
|
+
' elif [ -f "$D" ]; then',
|
|
133
|
+
' cur=$(stat -c "%Y:%s" -- "$D" 2>/dev/null || :)',
|
|
134
|
+
' if [ -z "$cur" ]; then',
|
|
135
|
+
// The file was removed (or became unreadable): the target is gone.
|
|
136
|
+
" printf 'E\\n'",
|
|
137
|
+
' exit 1',
|
|
138
|
+
' fi',
|
|
139
|
+
' if [ "$cur" != "$F" ]; then',
|
|
140
|
+
' F="$cur"',
|
|
141
|
+
" printf 'C\\n'",
|
|
142
|
+
' fi',
|
|
143
|
+
' else',
|
|
144
|
+
// Neither a directory nor a file: say so and die, rather than spin.
|
|
77
145
|
" printf 'E\\n'",
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
'
|
|
82
|
-
" printf 'C\\n'",
|
|
83
|
-
" fi",
|
|
146
|
+
' exit 1',
|
|
147
|
+
' fi',
|
|
148
|
+
// Liveness: the host ignores H; the write dies with the pipe.
|
|
149
|
+
" printf 'H\\n'",
|
|
84
150
|
` sleep ${seconds}`,
|
|
85
|
-
|
|
151
|
+
'done',
|
|
86
152
|
].join("\n");
|
|
87
153
|
}
|
|
88
154
|
/**
|
|
89
|
-
* Arm one in-distro watcher over a Linux directory.
|
|
155
|
+
* Arm one in-distro watcher over a Linux directory or file.
|
|
90
156
|
*
|
|
91
157
|
* @param options - the watch request.
|
|
92
158
|
* @returns resolves once observation is active, with the async close function
|
|
@@ -118,8 +184,15 @@ export function armDistroWatcher({ wslPath, distro, linuxPath, onChange, onError
|
|
|
118
184
|
const detail = stderrTail.trim();
|
|
119
185
|
return detail.length > 0 ? `${message}: ${detail}` : message;
|
|
120
186
|
};
|
|
187
|
+
let loopPid;
|
|
121
188
|
const lines = createInterface({ input: child.stdout });
|
|
122
189
|
lines.on("line", (line) => {
|
|
190
|
+
if (line.startsWith("P ")) {
|
|
191
|
+
const pid = Number.parseInt(line.slice(2), 10);
|
|
192
|
+
if (Number.isInteger(pid) && pid > 0)
|
|
193
|
+
loopPid = pid;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
123
196
|
if (line === "R") {
|
|
124
197
|
if (!settled && !signal?.aborted) {
|
|
125
198
|
settled = true;
|
|
@@ -128,7 +201,7 @@ export function armDistroWatcher({ wslPath, distro, linuxPath, onChange, onError
|
|
|
128
201
|
return;
|
|
129
202
|
}
|
|
130
203
|
if (line === "E") {
|
|
131
|
-
fail(new Error(describe(`the watched
|
|
204
|
+
fail(new Error(describe(`the watched path "${linuxPath}" no longer exists inside "${distro}", so the watch has ended`)));
|
|
132
205
|
return;
|
|
133
206
|
}
|
|
134
207
|
if (line === "C") {
|
|
@@ -139,10 +212,25 @@ export function armDistroWatcher({ wslPath, distro, linuxPath, onChange, onError
|
|
|
139
212
|
if (closed)
|
|
140
213
|
return;
|
|
141
214
|
closed = true;
|
|
215
|
+
const pid = loopPid;
|
|
142
216
|
child.removeAllListeners("exit");
|
|
143
217
|
lines.close();
|
|
144
218
|
child.stdout?.destroy();
|
|
145
219
|
child.kill();
|
|
220
|
+
if (pid !== undefined) {
|
|
221
|
+
// A second wsl.exe reaches the same distro namespace; the loop's TERM
|
|
222
|
+
// trap exits through the EXIT cleanup, which removes the stamp.
|
|
223
|
+
try {
|
|
224
|
+
const stopper = doSpawn(wslPath, ["-d", distro, "--exec", "sh", "-c", 'kill -TERM "$1" 2>/dev/null', "sh", String(pid)]);
|
|
225
|
+
stopper.stdout?.destroy();
|
|
226
|
+
stopper.stderr?.destroy();
|
|
227
|
+
stopper.on("error", () => { });
|
|
228
|
+
stopper.unref?.();
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Best effort: the transport kill already happened.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
146
234
|
await new Promise((resolveClose) => {
|
|
147
235
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
148
236
|
return resolveClose();
|
|
@@ -42,12 +42,14 @@ export declare function linuxJoin(linuxRoot: string, path: string): string;
|
|
|
42
42
|
/**
|
|
43
43
|
* The record invocation for one path: line 1 is the no-follow kind (the
|
|
44
44
|
* upstream lstat gate), line 2 the follow-stat fields (the upstream final
|
|
45
|
-
* stat), tab-joined.
|
|
45
|
+
* stat), tab-joined. The path is resolved and contained first — the GUI's
|
|
46
|
+
* workspace is a boundary for every read, not just for listings.
|
|
46
47
|
*
|
|
47
48
|
* @param file - absolute Linux path.
|
|
49
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
48
50
|
* @returns the agent argv.
|
|
49
51
|
*/
|
|
50
|
-
export declare function statRecordArgv(file: string): string[];
|
|
52
|
+
export declare function statRecordArgv(file: string, root: string): string[];
|
|
51
53
|
/**
|
|
52
54
|
* Parse one stat record (kind + follow fields, tab-joined).
|
|
53
55
|
*
|
|
@@ -77,9 +79,10 @@ export declare function wireKind(kind: string): string;
|
|
|
77
79
|
* @param file - absolute Linux path.
|
|
78
80
|
* @param offset - first line to return (1-based).
|
|
79
81
|
* @param limit - maximum lines to return.
|
|
82
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
80
83
|
* @returns the agent argv.
|
|
81
84
|
*/
|
|
82
|
-
export declare function pageArgv(file: string, offset: number, limit: number): string[];
|
|
85
|
+
export declare function pageArgv(file: string, offset: number, limit: number, root: string): string[];
|
|
83
86
|
/**
|
|
84
87
|
* The byte-window invocation: the stat record rides stderr, the window rides
|
|
85
88
|
* stdout. `$1` is the file, `$2` the byte offset, `$3` the byte count.
|
|
@@ -87,9 +90,10 @@ export declare function pageArgv(file: string, offset: number, limit: number): s
|
|
|
87
90
|
* @param file - absolute Linux path.
|
|
88
91
|
* @param offset - byte offset (0-based).
|
|
89
92
|
* @param length - byte count.
|
|
93
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
90
94
|
* @returns the agent argv.
|
|
91
95
|
*/
|
|
92
|
-
export declare function bytesArgv(file: string, offset: number, length: number): string[];
|
|
96
|
+
export declare function bytesArgv(file: string, offset: number, length: number, root: string): string[];
|
|
93
97
|
/**
|
|
94
98
|
* Parse the stat record a read or byte-window relayed on stderr.
|
|
95
99
|
*
|
|
@@ -64,21 +64,43 @@ export function linuxJoin(linuxRoot, path) {
|
|
|
64
64
|
const joined = text.length === 0 ? base : `${base}/${text}`.replace(/\/\.(?=\/|$)/gu, "");
|
|
65
65
|
return joined.length === 0 ? "/" : joined;
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The containment guard every distro-side workspace read shares. `$1` is the
|
|
69
|
+
* requested file, `$2` the workspace root; both are canonicalized with
|
|
70
|
+
* `realpath -L`, and a target that is neither the root itself nor below it is
|
|
71
|
+
* refused with exit 3 (the `workspace-file/outside-workspace` refusal). The
|
|
72
|
+
* resolved file is left in `real` for the rest of the script.
|
|
73
|
+
*
|
|
74
|
+
* A root of `/` admits every absolute path: the workspace IS the distro. The
|
|
75
|
+
* `-L` spelling matches the listing guard's, so symlinked workspace roots and
|
|
76
|
+
* `..` segments resolve the same way on both sides.
|
|
77
|
+
*/
|
|
78
|
+
const CONTAIN_FN = [
|
|
79
|
+
'contain() {',
|
|
80
|
+
' real=$(realpath -L -- "$1") || exit 2',
|
|
81
|
+
' root=$(realpath -L -- "$2") || exit 2',
|
|
82
|
+
' case "$root" in /) ;; *) case "$real" in "$root"|"$root"/*) ;; *) echo "OUTSIDE:$real" >&2; exit 3;; esac;; esac',
|
|
83
|
+
'}',
|
|
84
|
+
].join("\n");
|
|
67
85
|
/**
|
|
68
86
|
* The record invocation for one path: line 1 is the no-follow kind (the
|
|
69
87
|
* upstream lstat gate), line 2 the follow-stat fields (the upstream final
|
|
70
|
-
* stat), tab-joined.
|
|
88
|
+
* stat), tab-joined. The path is resolved and contained first — the GUI's
|
|
89
|
+
* workspace is a boundary for every read, not just for listings.
|
|
71
90
|
*
|
|
72
91
|
* @param file - absolute Linux path.
|
|
92
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
73
93
|
* @returns the agent argv.
|
|
74
94
|
*/
|
|
75
|
-
export function statRecordArgv(file) {
|
|
95
|
+
export function statRecordArgv(file, root) {
|
|
76
96
|
const script = [
|
|
77
|
-
|
|
78
|
-
'
|
|
97
|
+
CONTAIN_FN,
|
|
98
|
+
'contain "$1" "$2"',
|
|
99
|
+
'k=$(stat -c %F -- "$real") || exit 2',
|
|
100
|
+
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
79
101
|
"printf '%s\\t%s\\n' \"$k\" \"$s\"",
|
|
80
102
|
].join("\n");
|
|
81
|
-
return ["sh", "-c", script, "sh", file];
|
|
103
|
+
return ["sh", "-c", script, "sh", file, root];
|
|
82
104
|
}
|
|
83
105
|
/**
|
|
84
106
|
* Parse one stat record (kind + follow fields, tab-joined).
|
|
@@ -115,16 +137,19 @@ export function wireKind(kind) {
|
|
|
115
137
|
* @param file - absolute Linux path.
|
|
116
138
|
* @param offset - first line to return (1-based).
|
|
117
139
|
* @param limit - maximum lines to return.
|
|
140
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
118
141
|
* @returns the agent argv.
|
|
119
142
|
*/
|
|
120
|
-
export function pageArgv(file, offset, limit) {
|
|
143
|
+
export function pageArgv(file, offset, limit, root) {
|
|
121
144
|
const script = [
|
|
122
|
-
|
|
123
|
-
'
|
|
145
|
+
CONTAIN_FN,
|
|
146
|
+
'contain "$1" "$4"',
|
|
147
|
+
'k=$(stat -c %F -- "$real") || exit 2',
|
|
148
|
+
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
124
149
|
"printf '%s\\t%s\\n' \"$k\" \"$s\" >&2",
|
|
125
|
-
'sed -n "${2},$(( $2 + $3 ))p" -- "$
|
|
150
|
+
'sed -n "${2},$(( $2 + $3 ))p" -- "$real"',
|
|
126
151
|
].join("\n");
|
|
127
|
-
return ["sh", "-c", script, "sh", file, String(offset), String(limit)];
|
|
152
|
+
return ["sh", "-c", script, "sh", file, String(offset), String(limit), root];
|
|
128
153
|
}
|
|
129
154
|
/**
|
|
130
155
|
* The byte-window invocation: the stat record rides stderr, the window rides
|
|
@@ -133,15 +158,18 @@ export function pageArgv(file, offset, limit) {
|
|
|
133
158
|
* @param file - absolute Linux path.
|
|
134
159
|
* @param offset - byte offset (0-based).
|
|
135
160
|
* @param length - byte count.
|
|
161
|
+
* @param root - the workspace root in Linux form (containment boundary).
|
|
136
162
|
* @returns the agent argv.
|
|
137
163
|
*/
|
|
138
|
-
export function bytesArgv(file, offset, length) {
|
|
164
|
+
export function bytesArgv(file, offset, length, root) {
|
|
139
165
|
const script = [
|
|
140
|
-
|
|
166
|
+
CONTAIN_FN,
|
|
167
|
+
'contain "$1" "$4"',
|
|
168
|
+
's=$(stat -L -c %s\\t%D\\t%i\\t%Y\\t%Z -- "$real") || exit 2',
|
|
141
169
|
"printf '%s\\n' \"$s\" >&2",
|
|
142
|
-
'tail -c "+$(( $2 + 1 ))" -- "$
|
|
170
|
+
'tail -c "+$(( $2 + 1 ))" -- "$real" | head -c "$3"',
|
|
143
171
|
].join("\n");
|
|
144
|
-
return ["sh", "-c", script, "sh", file, String(offset), String(length)];
|
|
172
|
+
return ["sh", "-c", script, "sh", file, String(offset), String(length), root];
|
|
145
173
|
}
|
|
146
174
|
/**
|
|
147
175
|
* Parse the stat record a read or byte-window relayed on stderr.
|
|
@@ -162,11 +162,13 @@ export async function distroStat({ runner, distro, linuxRoot, path, distroWorksp
|
|
|
162
162
|
const linuxFile = linuxJoin(linuxRoot, path);
|
|
163
163
|
const result = await runner.exec({
|
|
164
164
|
cwd: "/",
|
|
165
|
-
argv: statRecordArgv(linuxFile),
|
|
165
|
+
argv: statRecordArgv(linuxFile, linuxRoot),
|
|
166
166
|
maxOutputBytes: 4096,
|
|
167
167
|
timeoutMs: 30_000,
|
|
168
168
|
signal,
|
|
169
169
|
});
|
|
170
|
+
if (result.exitCode === 3)
|
|
171
|
+
throw outsideWorkspace(path);
|
|
170
172
|
if (result.exitCode !== 0)
|
|
171
173
|
throw notFound(path);
|
|
172
174
|
const record = parseStatRecord(result.stdout);
|
|
@@ -186,11 +188,13 @@ export async function distroRead({ runner, distro, linuxRoot, path, offset, limi
|
|
|
186
188
|
const linuxFile = linuxJoin(linuxRoot, path);
|
|
187
189
|
const result = await runner.exec({
|
|
188
190
|
cwd: "/",
|
|
189
|
-
argv: pageArgv(linuxFile, offset, limit),
|
|
191
|
+
argv: pageArgv(linuxFile, offset, limit, linuxRoot),
|
|
190
192
|
maxOutputBytes: (maxBytes + 1) * 4,
|
|
191
193
|
timeoutMs: 60_000,
|
|
192
194
|
signal,
|
|
193
195
|
});
|
|
196
|
+
if (result.exitCode === 3)
|
|
197
|
+
throw outsideWorkspace(path);
|
|
194
198
|
if (result.exitCode === 2)
|
|
195
199
|
throw notFound(path);
|
|
196
200
|
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
@@ -213,16 +217,27 @@ export async function distroRead({ runner, distro, linuxRoot, path, offset, limi
|
|
|
213
217
|
catch {
|
|
214
218
|
throw notText(path);
|
|
215
219
|
}
|
|
216
|
-
|
|
217
|
-
|
|
220
|
+
// `pageArgv` reads one line past the page so EOF is decidable from the
|
|
221
|
+
// yield; that line is a probe, not part of the answer. Split it off before
|
|
222
|
+
// counting, and count bytes on the page the caller actually receives (a
|
|
223
|
+
// CRLF file's line count is not a byte budget).
|
|
224
|
+
const parts = text.length === 0 ? [] : text.split("\n");
|
|
225
|
+
if (text.endsWith("\n"))
|
|
226
|
+
parts.pop();
|
|
227
|
+
const count = parts.length;
|
|
228
|
+
const truncated = count > limit;
|
|
229
|
+
const kept = truncated ? parts.slice(0, limit) : parts;
|
|
230
|
+
const pageText = truncated ? `${kept.join("\n")}\n` : text;
|
|
231
|
+
const lines = kept.length;
|
|
232
|
+
const bytes = Buffer.byteLength(pageText, "utf8");
|
|
218
233
|
if (bytes > maxBytes) {
|
|
219
234
|
throw new RemoteError("workspace-file/too-large", `lines ${offset}-${offset + limit - 1} of "${path}" exceed the ${maxBytes} byte cap`, { path, limit: maxBytes });
|
|
220
235
|
}
|
|
221
|
-
if (
|
|
236
|
+
if (pageText.includes(String.fromCharCode(0)))
|
|
222
237
|
throw notText(path);
|
|
223
|
-
const eof =
|
|
238
|
+
const eof = !truncated;
|
|
224
239
|
const stat = { absolutePath: posixToUnc(distro, linuxFile), version: record.version };
|
|
225
|
-
return { ...stat, offset, text, lines, eof };
|
|
240
|
+
return { ...stat, offset, text: pageText, lines, eof };
|
|
226
241
|
}
|
|
227
242
|
/**
|
|
228
243
|
* Read one byte window (or the capped whole file) through the runner.
|
|
@@ -248,11 +263,13 @@ export async function distroReadBytes({ runner, distro, linuxRoot, path, offset,
|
|
|
248
263
|
throw windowTooLarge(path, window.length, maxBytes);
|
|
249
264
|
const result = await runner.exec({
|
|
250
265
|
cwd: "/",
|
|
251
|
-
argv: bytesArgv(linuxFile, window.offset, window.length),
|
|
266
|
+
argv: bytesArgv(linuxFile, window.offset, window.length, linuxRoot),
|
|
252
267
|
maxOutputBytes: window.length + 1024,
|
|
253
268
|
timeoutMs: 60_000,
|
|
254
269
|
signal,
|
|
255
270
|
});
|
|
271
|
+
if (result.exitCode === 3)
|
|
272
|
+
throw outsideWorkspace(path);
|
|
256
273
|
const record = parseStatRecordFromStderr(result.stderr);
|
|
257
274
|
if (record === undefined)
|
|
258
275
|
throw notFound(path);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "\u8ba9 DeepSeek Harness \u4ee5 WSL \u5b50\u7cfb\u7edf\u4f5c\u4e3a\u4f1a\u8bdd\u7684\u6267\u884c\u73af\u5883\uff1ashell\u3001\u6587\u4ef6\u8bfb\u5199\u4e0e\u547d\u4ee4\u6267\u884c\u5747\u5728\u5b50\u7cfb\u7edf\u5185\u5b8c\u6210\uff0c\u76ee\u5f55\u9009\u62e9\u5668\u4e0e GUI \u7ec8\u7aef\u540c\u6837\u5b9a\u4f4d\u5230\u5b50\u7cfb\u7edf\u8def\u5f84\uff0c\u5e76\u901a\u8fc7\u5b50\u7cfb\u7edf\u5185\u7684 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
"lib/**/*.js",
|
|
94
94
|
"lib/**/*.d.ts",
|
|
95
95
|
"agent/*.sh",
|
|
96
|
+
"agent/native/linux-*/dsh-wsl-agent",
|
|
96
97
|
"locale/**/*.json",
|
|
97
98
|
"cordis.patch.yml",
|
|
98
99
|
"examples/**/*.yml",
|
|
@@ -118,9 +119,9 @@
|
|
|
118
119
|
"lint:types": "tsc --noEmit",
|
|
119
120
|
"build": "node scripts/build.mjs",
|
|
120
121
|
"lint:build": "node test/build-freshness.mjs",
|
|
121
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs",
|
|
122
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs",
|
|
122
123
|
"test:syntax": "node test/syntax.mjs",
|
|
123
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
|
|
124
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs",
|
|
124
125
|
"lint:style": "node test/style.mjs",
|
|
125
126
|
"prepublishOnly": "pnpm test",
|
|
126
127
|
"probe": "bash test/probe/run.sh",
|
|
@@ -161,40 +162,45 @@
|
|
|
161
162
|
"url": "https://github.com/Big-Dao/dsh-plugin-wsl-env/issues"
|
|
162
163
|
},
|
|
163
164
|
"devDependencies": {
|
|
164
|
-
"@deepseek-ai/cordis": "4.0.
|
|
165
|
+
"@deepseek-ai/cordis": "4.0.5",
|
|
165
166
|
"@deepseek-ai/cordis-plugin-include": "*",
|
|
166
167
|
"@deepseek-ai/cordis-plugin-loader": "*",
|
|
167
|
-
"@deepseek-ai/dsh-api-workspace-files": "0.2.
|
|
168
|
-
"@deepseek-ai/dsh-bash-local": "0.2.
|
|
169
|
-
"@deepseek-ai/dsh-fs": "0.2.
|
|
170
|
-
"@deepseek-ai/dsh-fs-local": "0.2.
|
|
171
|
-
"@deepseek-ai/dsh-fs-sandbox": "0.2.
|
|
172
|
-
"@deepseek-ai/dsh-host-directory-picker": "0.2.
|
|
173
|
-
"@deepseek-ai/dsh-sandbox": "0.2.
|
|
174
|
-
"@deepseek-ai/dsh-sandbox-policy": "0.2.
|
|
175
|
-
"@deepseek-ai/dsh-subprocess": "0.2.
|
|
176
|
-
"@deepseek-ai/dsh-subprocess-local": "0.2.
|
|
177
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
168
|
+
"@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
|
|
169
|
+
"@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
|
|
170
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.2",
|
|
171
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
|
|
172
|
+
"@deepseek-ai/dsh-fs-sandbox": "0.2.1-rc.2",
|
|
173
|
+
"@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
|
|
174
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
|
|
175
|
+
"@deepseek-ai/dsh-sandbox-policy": "0.2.1-rc.2",
|
|
176
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
|
|
177
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
|
|
178
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
|
|
178
179
|
"@types/js-yaml": "^4.0.9",
|
|
179
180
|
"@types/node": "~22.20.5",
|
|
180
181
|
"js-yaml": "4.3.2",
|
|
181
182
|
"typescript": "~6.0.3"
|
|
182
183
|
},
|
|
183
184
|
"peerDependencies": {
|
|
184
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
185
|
-
"@deepseek-ai/dsh-
|
|
186
|
-
"@deepseek-ai/dsh-
|
|
187
|
-
"@deepseek-ai/dsh-fs
|
|
188
|
-
"@deepseek-ai/dsh-
|
|
189
|
-
"@deepseek-ai/dsh-
|
|
190
|
-
"@deepseek-ai/dsh-
|
|
191
|
-
"@deepseek-ai/dsh-subprocess
|
|
192
|
-
"@deepseek-ai/
|
|
185
|
+
"@deepseek-ai/cordis": "~4.0.5",
|
|
186
|
+
"@deepseek-ai/dsh-api-workspace-files": "0.2.1-rc.2",
|
|
187
|
+
"@deepseek-ai/dsh-bash-local": "0.2.1-rc.2",
|
|
188
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.2",
|
|
189
|
+
"@deepseek-ai/dsh-fs-local": "0.2.1-rc.2",
|
|
190
|
+
"@deepseek-ai/dsh-host-directory-picker": "0.2.1-rc.2",
|
|
191
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
|
|
192
|
+
"@deepseek-ai/dsh-subprocess": "0.2.1-rc.2",
|
|
193
|
+
"@deepseek-ai/dsh-subprocess-local": "0.2.1-rc.2",
|
|
194
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
|
|
195
|
+
"@deepseek-ai/schemastery": "~3.18.5"
|
|
193
196
|
},
|
|
194
197
|
"peerDependenciesMeta": {
|
|
195
198
|
"@deepseek-ai/cordis": {
|
|
196
199
|
"optional": true
|
|
197
200
|
},
|
|
201
|
+
"@deepseek-ai/dsh-api-workspace-files": {
|
|
202
|
+
"optional": true
|
|
203
|
+
},
|
|
198
204
|
"@deepseek-ai/dsh-bash-local": {
|
|
199
205
|
"optional": true
|
|
200
206
|
},
|
|
@@ -216,6 +222,9 @@
|
|
|
216
222
|
"@deepseek-ai/dsh-subprocess-local": {
|
|
217
223
|
"optional": true
|
|
218
224
|
},
|
|
225
|
+
"@deepseek-ai/dsh-typert-protocol": {
|
|
226
|
+
"optional": true
|
|
227
|
+
},
|
|
219
228
|
"@deepseek-ai/schemastery": {
|
|
220
229
|
"optional": true
|
|
221
230
|
}
|