dsh-plugin-wsl-env 0.11.0 → 0.13.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 +181 -0
- package/CONTRIBUTING.md +29 -7
- package/README.md +124 -138
- package/SECURITY.md +11 -1
- package/SUPPORT.md +29 -2
- package/cordis.patch.yml +17 -19
- package/docs/ARCHITECTURE.md +30 -29
- package/docs/CONFIGURATION.md +81 -21
- package/docs/LIMITATIONS.md +71 -6
- package/docs/MIGRATION.md +98 -0
- package/docs/PARITY.md +1 -1
- package/docs/PERFORMANCE.md +60 -0
- package/docs/QUICKSTART.md +125 -0
- package/docs/README.zh.md +117 -137
- package/docs/ROADMAP-1.0.md +780 -0
- package/docs/ROADMAP.md +4 -0
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +80 -0
- package/docs/UPSTREAM-LSP-SEAM.md +90 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +95 -0
- package/lib/agent-errors.d.ts +19 -0
- package/lib/agent-errors.js +22 -0
- package/lib/agent-exec.js +2 -2
- package/lib/agent.d.ts +68 -5
- package/lib/agent.js +129 -22
- package/lib/distro-route.d.ts +75 -0
- package/lib/distro-route.js +86 -0
- package/lib/fs-routing.d.ts +4 -1
- package/lib/index.d.ts +166 -37
- package/lib/index.js +353 -75
- package/lib/paths.d.ts +15 -0
- package/lib/paths.js +28 -0
- package/lib/preflight.d.ts +46 -3
- package/lib/preflight.js +83 -14
- package/lib/shell-env.d.ts +9 -3
- package/lib/shell-env.js +121 -43
- package/lib/subprocess.d.ts +12 -3
- package/lib/subprocess.js +21 -3
- package/package.json +23 -15
- package/scripts/diagnose.sh +9 -88
- package/scripts/doctor.mjs +548 -0
- package/scripts/profile-preset.mjs +254 -0
- package/lib/workspace-files-route.d.ts +0 -136
- package/lib/workspace-files-route.js +0 -225
- package/lib/workspace-files-wsl.d.ts +0 -257
- package/lib/workspace-files-wsl.js +0 -418
package/lib/agent.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* command or a write that was in flight has UNKNOWN state — replaying it
|
|
11
11
|
* would run non-idempotent work twice or report a create that actually
|
|
12
12
|
* succeeded as a refusal — so it fails instead, and the caller re-reads
|
|
13
|
-
* before retrying. A
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* before retrying. A failure that spends the rebuild leaves the agent
|
|
14
|
+
* `dead` with a bounded revival: the next call after a 2/4/8-second backoff
|
|
15
|
+
* gets a fresh transport and its own single rebuild; after three deaths the
|
|
16
|
+
* state is permanent and every call rejects with a restart remedy. While
|
|
17
|
+
* dead, calls reject with {@link AgentUnavailableError} — the executor's cue
|
|
18
|
+
* to fall back to the one-shot `wsl.exe` path it shipped with, so the worst
|
|
19
|
+
* case of the agent is "today's behaviour, plus one failed round trip".
|
|
18
20
|
* 2. **The in-distro timeout is the real one.** The EXEC frame carries the
|
|
19
21
|
* timeout in whole seconds and the agent SIGTERMs the child itself, so a
|
|
20
22
|
* timed-out command dies where it lives instead of leaving an orphan
|
|
@@ -38,7 +40,7 @@
|
|
|
38
40
|
* @module dsh-plugin-wsl/agent
|
|
39
41
|
*/
|
|
40
42
|
import { spawn } from "node:child_process";
|
|
41
|
-
import { AgentUnavailableError, CwdError } from "./agent-errors.js";
|
|
43
|
+
import { AgentUnavailableError, BASH_TIMEOUT_REASON, CwdError, SpawnError } from "./agent-errors.js";
|
|
42
44
|
import { AGENT_NAME, MAX_FRAME_BYTES, PROTOCOL_VERSION, encodeExecFrame, encodeFsFrame, encodeKill, encodeSetEnv, parseAgentLine } from "./agent-protocol.js";
|
|
43
45
|
/**
|
|
44
46
|
* A zeroed per-kind metrics record.
|
|
@@ -58,6 +60,19 @@ function emptyAgentMetrics() {
|
|
|
58
60
|
const HANDSHAKE_TIMEOUT_MS = 15000;
|
|
59
61
|
/** Extra time the host watchdog waits past the request's own in-distro timeout. */
|
|
60
62
|
const WATCHDOG_GRACE_MS = 13000;
|
|
63
|
+
/**
|
|
64
|
+
* How many times a resident that reached `dead` may be revived before the
|
|
65
|
+
* failure becomes permanent. Each revival gets the one permitted rebuild.
|
|
66
|
+
*/
|
|
67
|
+
const MAX_REVIVALS = 3;
|
|
68
|
+
/** The first revival delay; doubles per death, capped at 30 s. */
|
|
69
|
+
const DEFAULT_REVIVAL_BACKOFF_MS = 2000;
|
|
70
|
+
/**
|
|
71
|
+
* How long a cancel waits before the out-of-band USR1. Long enough for the
|
|
72
|
+
* agent's own timeout kill to answer, short enough that a killed command still
|
|
73
|
+
* dies promptly.
|
|
74
|
+
*/
|
|
75
|
+
const CANCEL_SIGNAL_GRACE_MS = 60;
|
|
61
76
|
/**
|
|
62
77
|
* How many requests one resident may hold in flight before refusing more. The
|
|
63
78
|
* agent is serial, so a large queue only delays every entry; the cap keeps a
|
|
@@ -171,7 +186,11 @@ export class WslAgent {
|
|
|
171
186
|
this.startSettler = null;
|
|
172
187
|
this.nextRequestId = 0;
|
|
173
188
|
this.pending = new Map();
|
|
189
|
+
this.lastAckedSeq = 0;
|
|
174
190
|
this.rebuildUsed = false;
|
|
191
|
+
this.deaths = 0;
|
|
192
|
+
this.deadUntil = 0;
|
|
193
|
+
this.revivalBackoffMs = config.revivalBackoffMs ?? DEFAULT_REVIVAL_BACKOFF_MS;
|
|
175
194
|
this.idleTimer = null;
|
|
176
195
|
this.lastError = "";
|
|
177
196
|
this.requestMetrics = emptyAgentMetrics();
|
|
@@ -189,6 +208,42 @@ export class WslAgent {
|
|
|
189
208
|
get unavailableReason() {
|
|
190
209
|
return this.lastError;
|
|
191
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Enter the dead state and schedule the next bounded revival attempt.
|
|
213
|
+
*
|
|
214
|
+
* The delay doubles per death (2 s, 4 s, 8 s, … capped at 30 s). After
|
|
215
|
+
* {@link MAX_REVIVALS} deaths the state stays permanent and the refusal
|
|
216
|
+
* carries the restart remedy.
|
|
217
|
+
*/
|
|
218
|
+
markDead() {
|
|
219
|
+
this.deaths += 1;
|
|
220
|
+
this.state = "dead";
|
|
221
|
+
const delay = Math.min(this.revivalBackoffMs * 2 ** Math.max(0, this.deaths - 1), 30_000);
|
|
222
|
+
this.deadUntil = this.deaths > MAX_REVIVALS ? 0 : Date.now() + delay;
|
|
223
|
+
if (this.deaths > MAX_REVIVALS) {
|
|
224
|
+
this.lastError = `${this.lastError}; permanently out after ${this.deaths} deaths — restart the DSH app`;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.lastError = `${this.lastError}; reviving in ${Math.round(delay / 1000)}s (attempt ${this.deaths}/${MAX_REVIVALS})`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* A read-only lifecycle snapshot for diagnostics. Includes the live pid, the
|
|
232
|
+
* last failure and the remaining revival budget; it never starts or stops
|
|
233
|
+
* anything.
|
|
234
|
+
*
|
|
235
|
+
* @returns the status snapshot.
|
|
236
|
+
*/
|
|
237
|
+
status() {
|
|
238
|
+
return {
|
|
239
|
+
distro: this.distro,
|
|
240
|
+
state: this.state,
|
|
241
|
+
pid: this.agentPid,
|
|
242
|
+
lastError: this.lastError,
|
|
243
|
+
deaths: this.deaths,
|
|
244
|
+
pending: this.pending.size,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
192
247
|
/**
|
|
193
248
|
* Start the process and wait for the handshake. Re-entrant: concurrent
|
|
194
249
|
* callers share the one start.
|
|
@@ -197,7 +252,19 @@ export class WslAgent {
|
|
|
197
252
|
*/
|
|
198
253
|
start() {
|
|
199
254
|
if (this.state === "dead") {
|
|
200
|
-
|
|
255
|
+
if (this.deaths > MAX_REVIVALS) {
|
|
256
|
+
return Promise.reject(new AgentUnavailableError(`agent is permanently out after ${this.deaths} deaths (${this.lastError}); restart the DSH app, and delete ~/.cache/dsh-wsl/agent inside the distro if a stale binary is suspected`));
|
|
257
|
+
}
|
|
258
|
+
const wait = this.deadUntil - Date.now();
|
|
259
|
+
if (wait > 0) {
|
|
260
|
+
return Promise.reject(new AgentUnavailableError(`agent is down (${this.lastError}); reviving in ${Math.ceil(wait / 1000)}s, attempt ${this.deaths}/${MAX_REVIVALS}`));
|
|
261
|
+
}
|
|
262
|
+
// Bounded revival: the next call gets a fresh transport and its own
|
|
263
|
+
// single rebuild. Frames from the dead generation were settled in
|
|
264
|
+
// handleProcessExit; nothing is replayed here.
|
|
265
|
+
this.state = "idle";
|
|
266
|
+
this.rebuildUsed = false;
|
|
267
|
+
this.deadUntil = 0;
|
|
201
268
|
}
|
|
202
269
|
if (this.state === "ready")
|
|
203
270
|
return Promise.resolve();
|
|
@@ -381,9 +448,10 @@ export class WslAgent {
|
|
|
381
448
|
const pending = this.pending.get(message.id);
|
|
382
449
|
if (pending) {
|
|
383
450
|
pending.started = true;
|
|
451
|
+
this.lastAckedSeq = pending.seq;
|
|
384
452
|
this.recordAck(pending);
|
|
385
453
|
if (pending.cancelled)
|
|
386
|
-
this.
|
|
454
|
+
this.armCancelSignal(pending);
|
|
387
455
|
pending.armWatchdog?.();
|
|
388
456
|
}
|
|
389
457
|
break;
|
|
@@ -392,6 +460,7 @@ export class WslAgent {
|
|
|
392
460
|
const pending = this.pending.get(message.id);
|
|
393
461
|
if (!pending)
|
|
394
462
|
break; // answered after its caller gave up — noise
|
|
463
|
+
pending.answered = true;
|
|
395
464
|
this.clearPending(pending);
|
|
396
465
|
this.pending.delete(message.id);
|
|
397
466
|
this.rearmIdleTimer();
|
|
@@ -406,6 +475,7 @@ export class WslAgent {
|
|
|
406
475
|
const pending = this.pending.get(message.id);
|
|
407
476
|
if (!pending)
|
|
408
477
|
break;
|
|
478
|
+
pending.answered = true;
|
|
409
479
|
this.clearPending(pending);
|
|
410
480
|
this.pending.delete(message.id);
|
|
411
481
|
this.rearmIdleTimer();
|
|
@@ -414,10 +484,16 @@ export class WslAgent {
|
|
|
414
484
|
pending.settled = true;
|
|
415
485
|
// A cwd rejection is the command's own failure, not the agent's — it
|
|
416
486
|
// becomes a CwdError so the execution layer can report it exactly like
|
|
417
|
-
// the one-shot path's relay text.
|
|
487
|
+
// the one-shot path's relay text. A `spawn` rejection is the same
|
|
488
|
+
// class of failure on the native agent (it cannot distinguish a
|
|
489
|
+
// missing workdir), so it gets its own class and the executor retries
|
|
490
|
+
// it one-shot.
|
|
418
491
|
if (message.reason === "cwd") {
|
|
419
492
|
pending.reject(new CwdError(message.message));
|
|
420
493
|
}
|
|
494
|
+
else if (message.reason === "spawn") {
|
|
495
|
+
pending.reject(new SpawnError(message.message));
|
|
496
|
+
}
|
|
421
497
|
else {
|
|
422
498
|
pending.reject(new AgentUnavailableError(`agent rejected request ${message.id} (${message.reason}): ${message.message}`));
|
|
423
499
|
}
|
|
@@ -511,12 +587,12 @@ export class WslAgent {
|
|
|
511
587
|
pending.settled = true;
|
|
512
588
|
pending.reject(error);
|
|
513
589
|
}
|
|
514
|
-
this.
|
|
590
|
+
this.markDead();
|
|
515
591
|
});
|
|
516
592
|
return;
|
|
517
593
|
}
|
|
518
|
-
this.lastError = `agent exited again (code=${code} signal=${signalName ?? "none"})
|
|
519
|
-
this.
|
|
594
|
+
this.lastError = `agent exited again (code=${code} signal=${signalName ?? "none"})`;
|
|
595
|
+
this.markDead();
|
|
520
596
|
for (const pending of this.pending.values()) {
|
|
521
597
|
this.clearPending(pending);
|
|
522
598
|
if (pending.settled)
|
|
@@ -544,6 +620,7 @@ export class WslAgent {
|
|
|
544
620
|
}
|
|
545
621
|
const pending = {
|
|
546
622
|
id,
|
|
623
|
+
seq: Number(id.slice(1)),
|
|
547
624
|
kind,
|
|
548
625
|
sentAt: performance.now(),
|
|
549
626
|
measured: false,
|
|
@@ -562,6 +639,8 @@ export class WslAgent {
|
|
|
562
639
|
started: false,
|
|
563
640
|
cancelled: false,
|
|
564
641
|
settled: false,
|
|
642
|
+
answered: false,
|
|
643
|
+
signalTimer: null,
|
|
565
644
|
armWatchdog: timeoutMs > 0
|
|
566
645
|
? () => {
|
|
567
646
|
if (pending.watchdog)
|
|
@@ -598,8 +677,6 @@ export class WslAgent {
|
|
|
598
677
|
// confined resident, a build without the USR1 trap); the signal is what
|
|
599
678
|
// interrupts the command the agent is blocked on right now.
|
|
600
679
|
this.writeLines([encodeKill(pending.id)]);
|
|
601
|
-
if (pending.started)
|
|
602
|
-
this.signalAgent("SIGUSR1");
|
|
603
680
|
if (pending.settled)
|
|
604
681
|
return;
|
|
605
682
|
pending.settled = true;
|
|
@@ -608,6 +685,15 @@ export class WslAgent {
|
|
|
608
685
|
// cleans the map.
|
|
609
686
|
this.clearPending(pending);
|
|
610
687
|
pending.reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
688
|
+
// Arm AFTER clearPending: the clear releases the per-request timers,
|
|
689
|
+
// and the signal is the one timer that must outlive this rejection.
|
|
690
|
+
// A host timeout is excluded: the distro-side watcher already kills
|
|
691
|
+
// that command, and a USR1 delivered after it settled would hit the
|
|
692
|
+
// request that took its place (observed as an exit-138 "completed").
|
|
693
|
+
const reason = pending.signal?.reason;
|
|
694
|
+
const timedOut = reason instanceof Error && reason.message === BASH_TIMEOUT_REASON;
|
|
695
|
+
if (pending.started && !timedOut)
|
|
696
|
+
this.armCancelSignal(pending);
|
|
611
697
|
};
|
|
612
698
|
signal.addEventListener("abort", pending.onAbort, { once: true });
|
|
613
699
|
}
|
|
@@ -632,9 +718,6 @@ export class WslAgent {
|
|
|
632
718
|
* @throws {AgentUnavailableError} when the agent is out for this process.
|
|
633
719
|
*/
|
|
634
720
|
async exec({ cwd, argv, timeoutMs = 0, maxOutputBytes = 0, env, signal }) {
|
|
635
|
-
if (this.state === "dead") {
|
|
636
|
-
throw new AgentUnavailableError(this.lastError);
|
|
637
|
-
}
|
|
638
721
|
await this.start();
|
|
639
722
|
const id = `r${this.nextRequestId++}`;
|
|
640
723
|
const lines = [
|
|
@@ -657,9 +740,6 @@ export class WslAgent {
|
|
|
657
740
|
* @throws {AgentUnavailableError} when the agent is out for this process.
|
|
658
741
|
*/
|
|
659
742
|
async fs({ op, args, timeoutMs = 0, signal }) {
|
|
660
|
-
if (this.state === "dead") {
|
|
661
|
-
throw new AgentUnavailableError(this.lastError);
|
|
662
|
-
}
|
|
663
743
|
await this.start();
|
|
664
744
|
const id = `r${this.nextRequestId++}`;
|
|
665
745
|
const lines = encodeFsFrame({ id, op, args, timeoutMs });
|
|
@@ -674,8 +754,6 @@ export class WslAgent {
|
|
|
674
754
|
* @returns resolved on PONG.
|
|
675
755
|
*/
|
|
676
756
|
async ping(signal) {
|
|
677
|
-
if (this.state === "dead")
|
|
678
|
-
throw new AgentUnavailableError(this.lastError);
|
|
679
757
|
await this.start();
|
|
680
758
|
if (signal?.aborted)
|
|
681
759
|
throw signal.reason instanceof Error ? signal.reason : new Error("aborted");
|
|
@@ -899,9 +977,38 @@ export class WslAgent {
|
|
|
899
977
|
clearPending(pending) {
|
|
900
978
|
if (pending.watchdog)
|
|
901
979
|
clearTimeout(pending.watchdog);
|
|
980
|
+
if (pending.signalTimer)
|
|
981
|
+
clearTimeout(pending.signalTimer);
|
|
982
|
+
pending.signalTimer = null;
|
|
902
983
|
if (pending.signal && pending.onAbort)
|
|
903
984
|
pending.signal.removeEventListener("abort", pending.onAbort);
|
|
904
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Deliver the out-of-band cancel signal after a short grace, and only while
|
|
988
|
+
* the request it was meant for is still the agent's current one.
|
|
989
|
+
*
|
|
990
|
+
* The signal has no request id: it interrupts whatever command the agent is
|
|
991
|
+
* blocked in. A timeout request is usually already being killed by the
|
|
992
|
+
* in-distro watcher, so an immediate signal can land after the agent moved
|
|
993
|
+
* on — the next request then dies with exit 138 (SIGUSR1) and looks
|
|
994
|
+
* `completed`. A newer ACK means the agent has answered the old request, so
|
|
995
|
+
* the signal is dropped; an answered request is dropped too.
|
|
996
|
+
*
|
|
997
|
+
* @param pending - the cancelled request.
|
|
998
|
+
*/
|
|
999
|
+
armCancelSignal(pending) {
|
|
1000
|
+
if (pending.signalTimer !== null)
|
|
1001
|
+
return;
|
|
1002
|
+
pending.signalTimer = setTimeout(() => {
|
|
1003
|
+
pending.signalTimer = null;
|
|
1004
|
+
if (pending.answered || this.pending.get(pending.id) !== pending)
|
|
1005
|
+
return;
|
|
1006
|
+
if (this.lastAckedSeq > pending.seq)
|
|
1007
|
+
return;
|
|
1008
|
+
this.signalAgent("SIGUSR1");
|
|
1009
|
+
}, CANCEL_SIGNAL_GRACE_MS);
|
|
1010
|
+
pending.signalTimer.unref?.();
|
|
1011
|
+
}
|
|
905
1012
|
}
|
|
906
1013
|
/** The read-only FS ops: replaying them across a rebuild changes nothing. */
|
|
907
1014
|
const REPLAYABLE_FS_OPS = new Set(["stat", "lstat", "list", "realpath", "read"]);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which distro an operation belongs to.
|
|
3
|
+
*
|
|
4
|
+
* The pinned name has three spellings: an explicit distro wins, `"default"`
|
|
5
|
+
* forces WSL's own default, and the empty string or `"auto"` follows the
|
|
6
|
+
* workspace coordinate when the caller has one. The workspace coordinate is a
|
|
7
|
+
* `\\wsl.localhost\<distro>\...` path; a Linux, drive-letter or relative path
|
|
8
|
+
* carries no distro evidence and falls back to WSL's default.
|
|
9
|
+
*
|
|
10
|
+
* Pure by construction: the caller resolves the default distro once and the
|
|
11
|
+
* selection stays unit-testable without `wsl.exe`.
|
|
12
|
+
*
|
|
13
|
+
* This is a TypeScript source built to `lib/distro-route.js`; edit THIS file and
|
|
14
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
15
|
+
* fails when it drifts.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-plugin-wsl/distro-route
|
|
18
|
+
*/
|
|
19
|
+
/** Why a distro was selected, for diagnostics and for the docs. */
|
|
20
|
+
export type DistroSource = "pinned" | "workspace" | "default";
|
|
21
|
+
/** One distro selection. */
|
|
22
|
+
export interface DistroChoice {
|
|
23
|
+
/** The distro name to use. */
|
|
24
|
+
distro: string;
|
|
25
|
+
/** Whether the name came from the config, the path, or WSL's default. */
|
|
26
|
+
source: DistroSource;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The distro a UNC path names.
|
|
30
|
+
*
|
|
31
|
+
* @param value - any path spelling.
|
|
32
|
+
* @returns the distro name, or undefined for a non-WSL path.
|
|
33
|
+
*/
|
|
34
|
+
export declare function distroOfUnc(value: string | undefined): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* The explicit `distro` config value, or undefined for `""`, `"auto"` and
|
|
37
|
+
* `"default"`.
|
|
38
|
+
*
|
|
39
|
+
* @param configured - the row's `distro` value.
|
|
40
|
+
* @returns the pinned distro, or undefined when the value is a mode.
|
|
41
|
+
*/
|
|
42
|
+
export declare function pinnedDistro(configured: string | undefined): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Whether `distro: ""`/`"auto"` asks the workspace coordinate to decide.
|
|
45
|
+
*
|
|
46
|
+
* @param configured - the row's `distro` value.
|
|
47
|
+
* @returns true for the automatic modes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function isAutoDistro(configured: string | undefined): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Whether `distro: "default"` forces WSL's own default.
|
|
52
|
+
*
|
|
53
|
+
* @param configured - the row's `distro` value.
|
|
54
|
+
* @returns true for the explicit default mode.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isDefaultDistro(configured: string | undefined): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Select the distro for one operation.
|
|
59
|
+
*
|
|
60
|
+
* Order: an explicit distro wins; `"default"` takes the fallback; otherwise the
|
|
61
|
+
* first candidate that names a UNC distro wins, and the fallback closes the
|
|
62
|
+
* list. The candidate order matters — a request's workdir is stronger evidence
|
|
63
|
+
* than the configured default cwd.
|
|
64
|
+
*
|
|
65
|
+
* @param input - the configured value, the path candidates, and WSL's default.
|
|
66
|
+
* @returns the distro and why it was chosen.
|
|
67
|
+
*/
|
|
68
|
+
export declare function selectDistro({ configured, candidates, fallback }: {
|
|
69
|
+
/** The row's `distro` value. */
|
|
70
|
+
configured: string | undefined;
|
|
71
|
+
/** Paths in any coordinate system, strongest evidence first. */
|
|
72
|
+
candidates: ReadonlyArray<string | undefined>;
|
|
73
|
+
/** WSL's default distro, already resolved by the caller. */
|
|
74
|
+
fallback: string;
|
|
75
|
+
}): DistroChoice;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which distro an operation belongs to.
|
|
3
|
+
*
|
|
4
|
+
* The pinned name has three spellings: an explicit distro wins, `"default"`
|
|
5
|
+
* forces WSL's own default, and the empty string or `"auto"` follows the
|
|
6
|
+
* workspace coordinate when the caller has one. The workspace coordinate is a
|
|
7
|
+
* `\\wsl.localhost\<distro>\...` path; a Linux, drive-letter or relative path
|
|
8
|
+
* carries no distro evidence and falls back to WSL's default.
|
|
9
|
+
*
|
|
10
|
+
* Pure by construction: the caller resolves the default distro once and the
|
|
11
|
+
* selection stays unit-testable without `wsl.exe`.
|
|
12
|
+
*
|
|
13
|
+
* This is a TypeScript source built to `lib/distro-route.js`; edit THIS file and
|
|
14
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
15
|
+
* fails when it drifts.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-plugin-wsl/distro-route
|
|
18
|
+
*/
|
|
19
|
+
import { uncToPosix } from "./paths.js";
|
|
20
|
+
/**
|
|
21
|
+
* The distro a UNC path names.
|
|
22
|
+
*
|
|
23
|
+
* @param value - any path spelling.
|
|
24
|
+
* @returns the distro name, or undefined for a non-WSL path.
|
|
25
|
+
*/
|
|
26
|
+
export function distroOfUnc(value) {
|
|
27
|
+
const text = String(value ?? "").trim();
|
|
28
|
+
return text.length === 0 ? undefined : uncToPosix(text)?.distro;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The explicit `distro` config value, or undefined for `""`, `"auto"` and
|
|
32
|
+
* `"default"`.
|
|
33
|
+
*
|
|
34
|
+
* @param configured - the row's `distro` value.
|
|
35
|
+
* @returns the pinned distro, or undefined when the value is a mode.
|
|
36
|
+
*/
|
|
37
|
+
export function pinnedDistro(configured) {
|
|
38
|
+
const text = String(configured ?? "").trim();
|
|
39
|
+
if (text.length === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
const mode = text.toLowerCase();
|
|
42
|
+
return mode === "auto" || mode === "default" ? undefined : text;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Whether `distro: ""`/`"auto"` asks the workspace coordinate to decide.
|
|
46
|
+
*
|
|
47
|
+
* @param configured - the row's `distro` value.
|
|
48
|
+
* @returns true for the automatic modes.
|
|
49
|
+
*/
|
|
50
|
+
export function isAutoDistro(configured) {
|
|
51
|
+
const text = String(configured ?? "").trim().toLowerCase();
|
|
52
|
+
return text.length === 0 || text === "auto";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether `distro: "default"` forces WSL's own default.
|
|
56
|
+
*
|
|
57
|
+
* @param configured - the row's `distro` value.
|
|
58
|
+
* @returns true for the explicit default mode.
|
|
59
|
+
*/
|
|
60
|
+
export function isDefaultDistro(configured) {
|
|
61
|
+
return String(configured ?? "").trim().toLowerCase() === "default";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Select the distro for one operation.
|
|
65
|
+
*
|
|
66
|
+
* Order: an explicit distro wins; `"default"` takes the fallback; otherwise the
|
|
67
|
+
* first candidate that names a UNC distro wins, and the fallback closes the
|
|
68
|
+
* list. The candidate order matters — a request's workdir is stronger evidence
|
|
69
|
+
* than the configured default cwd.
|
|
70
|
+
*
|
|
71
|
+
* @param input - the configured value, the path candidates, and WSL's default.
|
|
72
|
+
* @returns the distro and why it was chosen.
|
|
73
|
+
*/
|
|
74
|
+
export function selectDistro({ configured, candidates, fallback }) {
|
|
75
|
+
const pinned = pinnedDistro(configured);
|
|
76
|
+
if (pinned !== undefined)
|
|
77
|
+
return { distro: pinned, source: "pinned" };
|
|
78
|
+
if (isDefaultDistro(configured))
|
|
79
|
+
return { distro: fallback, source: "default" };
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
const named = distroOfUnc(candidate);
|
|
82
|
+
if (named !== undefined)
|
|
83
|
+
return { distro: named, source: "workspace" };
|
|
84
|
+
}
|
|
85
|
+
return { distro: fallback, source: "default" };
|
|
86
|
+
}
|
package/lib/fs-routing.d.ts
CHANGED
|
@@ -45,7 +45,10 @@ export interface RoutingConfig {
|
|
|
45
45
|
cwd?: string;
|
|
46
46
|
/** The host backend's overwrite-diff byte bound. */
|
|
47
47
|
diffBasisMaxBytes?: number;
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* The distro mode for routed targets: empty/`"auto"` follows each target's
|
|
50
|
+
* UNC distro, `"default"` forces WSL's default, and an explicit name pins.
|
|
51
|
+
*/
|
|
49
52
|
distro?: string;
|
|
50
53
|
/** The `wsl.exe` path, used only to resolve the default distro. */
|
|
51
54
|
wslPath?: string;
|