dsh-wsl-desktop 0.3.2 → 0.3.4
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/README.en.md +56 -345
- package/README.md +54 -344
- package/lib/client.js +78 -14
- package/lib/index.js +38 -26
- package/lib/wsl/confinement.js +20 -6
- package/lib/wsl/fence.js +29 -7
- package/lib/wsl/fs.js +12 -6
- package/lib/wsl/paths.js +10 -2
- package/lib/wsl/preset.js +1 -1
- package/lib/wsl/pty.js +1 -1
- package/lib/wsl/shell.js +4 -5
- package/lib/wsl/subprocess.js +1 -1
- package/lib/wsl/terminal-bridge.py +41 -4
- package/lib/wsl/world.js +98 -16
- package/package.json +1 -1
package/lib/wsl/world.js
CHANGED
|
@@ -62,8 +62,16 @@ const LXSS_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss'
|
|
|
62
62
|
*/
|
|
63
63
|
export const WSL_CHILD_ENV = { WSL_UTF8: '1' }
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Environment overrides that keep command output model-readable.
|
|
67
|
+
*
|
|
68
|
+
* ONE owner, shared with the shell executor (which spreads it into every
|
|
69
|
+
* command it runs) — an override added to one copy and not the other used to be
|
|
70
|
+
* a silent divergence two modules could not see. Frozen so a shared object stays
|
|
71
|
+
* shared on purpose: an accidental write now throws instead of leaking into the
|
|
72
|
+
* other consumer's environment.
|
|
73
|
+
*/
|
|
74
|
+
export const ENV_OVERRIDES = Object.freeze({ ...WSL_CHILD_ENV, NO_COLOR: '1', TERM: 'dumb', PAGER: 'cat', GIT_PAGER: 'cat' })
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
77
|
* Decode `wsl.exe` output.
|
|
@@ -398,6 +406,52 @@ export function buildWslExecArgv(plan, argv, options = {}) {
|
|
|
398
406
|
]
|
|
399
407
|
}
|
|
400
408
|
|
|
409
|
+
/** The most wsl.exe processes this module runs at once.
|
|
410
|
+
*
|
|
411
|
+
* Every spawn here lands on ONE shared WSL2 VM, and this file's own notes record it
|
|
412
|
+
* stalling under load (a cold start clears a 30 s ceiling, and a busy sibling
|
|
413
|
+
* distribution stalled probes past theirs). The HTTP route bounds its own methods
|
|
414
|
+
* (lib/index.js `routeInFlight`), but one route call can fan out several wsl.exe
|
|
415
|
+
* children — a command plus its confinement probes, executable lookup and
|
|
416
|
+
* login-shell probe — so the route counter is not a spawn cap. This is.
|
|
417
|
+
*
|
|
418
|
+
* Deliberately a plain FIFO queue rather than a pool: no caller holds a slot while
|
|
419
|
+
* awaiting another wsl.exe call, so a hand-off can never deadlock, and FIFO order
|
|
420
|
+
* keeps an early caller from starving behind later ones.
|
|
421
|
+
*/
|
|
422
|
+
const MAX_CONCURRENT_SPAWNS = 8
|
|
423
|
+
|
|
424
|
+
/** Spawns currently in flight. */
|
|
425
|
+
let activeSpawns = 0
|
|
426
|
+
|
|
427
|
+
/** Callers waiting for a slot; each holds a resolve that hands the slot over. */
|
|
428
|
+
const spawnQueue = []
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Take one spawn slot, waiting for a release when the module is at its cap.
|
|
432
|
+
* @returns {Promise<void>} settled once the caller may spawn.
|
|
433
|
+
*/
|
|
434
|
+
function acquireSpawnSlot() {
|
|
435
|
+
if (activeSpawns < MAX_CONCURRENT_SPAWNS) {
|
|
436
|
+
activeSpawns += 1
|
|
437
|
+
return Promise.resolve()
|
|
438
|
+
}
|
|
439
|
+
return new Promise((resolve) => { spawnQueue.push(resolve) })
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Give a spawn slot back — to the head of the queue when one waits (the count
|
|
444
|
+
* stays at the cap), otherwise back to the pool.
|
|
445
|
+
*/
|
|
446
|
+
function releaseSpawnSlot() {
|
|
447
|
+
const next = spawnQueue.shift()
|
|
448
|
+
if (next !== undefined) {
|
|
449
|
+
next()
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
activeSpawns -= 1
|
|
453
|
+
}
|
|
454
|
+
|
|
401
455
|
/**
|
|
402
456
|
* Run one command inside a distribution.
|
|
403
457
|
*
|
|
@@ -436,7 +490,11 @@ export function runWslShell({ distro, linuxCwd, command, username, loginShell =
|
|
|
436
490
|
'--cd', linuxCwd,
|
|
437
491
|
'-e', 'bash', loginShell ? '-lc' : '-c', script,
|
|
438
492
|
]
|
|
439
|
-
|
|
493
|
+
// Validation stays SYNCHRONOUS: callers rely on a bad distro/username throwing
|
|
494
|
+
// before anything is awaited. Only the spawn itself waits for a slot, so the cap
|
|
495
|
+
// never turns a grammar rejection into a rejected promise.
|
|
496
|
+
return acquireSpawnSlot()
|
|
497
|
+
.then(() => new Promise((resolve, reject) => {
|
|
440
498
|
let child
|
|
441
499
|
try {
|
|
442
500
|
child = spawn(wslPath, argv, {
|
|
@@ -511,7 +569,8 @@ export function runWslShell({ distro, linuxCwd, command, username, loginShell =
|
|
|
511
569
|
child.on('close', (code) => {
|
|
512
570
|
settle(code, undefined)
|
|
513
571
|
})
|
|
514
|
-
|
|
572
|
+
}))
|
|
573
|
+
.finally(releaseSpawnSlot)
|
|
515
574
|
}
|
|
516
575
|
|
|
517
576
|
/** The documented probe ceiling: 60s, because a cold VM's first spawn can exceed 30s. */
|
|
@@ -520,7 +579,7 @@ const PROBE_TIMEOUT_MS = 60_000
|
|
|
520
579
|
/**
|
|
521
580
|
* Ceiling for one parsed-output probe, and the retry that goes with it.
|
|
522
581
|
*
|
|
523
|
-
*
|
|
582
|
+
* docs/CONFINEMENT.md states this policy for exactly these probes — 探针超时 60s + 超时后一次透明
|
|
524
583
|
* 重试:桌面重启后的首个 wsl.exe 冷启动可以超过短上限 — and it names `listLinuxDir`,
|
|
525
584
|
* `checkLinuxPath` and `resolveDistroHome` in the same sentence as the probes that
|
|
526
585
|
* already had it (`resolveIdentity`, `detectRunner`, the NO_NEW_PRIVS probe, the PTY's
|
|
@@ -569,7 +628,11 @@ export async function listLinuxDir(distro, linuxPath, { wslPath, run = runWslShe
|
|
|
569
628
|
// directory that is not empty.
|
|
570
629
|
const result = await probeWithRetry(run, { distro, linuxCwd: '/', command: script, loginShell: false, raw: true, wslPath })
|
|
571
630
|
if (result.exitCode !== 0) {
|
|
572
|
-
|
|
631
|
+
// The probe's own evidence goes into the message: a twice-retried cold-VM stall
|
|
632
|
+
// (exit code null) must read as a timeout, not as "退出码 null" — the transient
|
|
633
|
+
// failure a reader can act on by waking the distribution.
|
|
634
|
+
const stderr = result.stderr.toString('utf8').trim()
|
|
635
|
+
throw new Error(`无法列出 ${path}:${stderr || probeEvidence(result)}`)
|
|
573
636
|
}
|
|
574
637
|
const entries = result.stdout.toString('utf8')
|
|
575
638
|
.split('\0')
|
|
@@ -656,7 +719,10 @@ export async function resolveDistroHome(distro, username, { wslPath, run = runWs
|
|
|
656
719
|
const who = await probeWithRetry(run, { distro, linuxCwd: '/', command: 'id -un', loginShell: false, wslPath })
|
|
657
720
|
user = who.stdout.trim()
|
|
658
721
|
if (user === '' || user.includes('\n')) {
|
|
659
|
-
|
|
722
|
+
// probeEvidence, not a bare exit code: a twice-retried stall reports
|
|
723
|
+
// exitCode null, and "null" is not something a reader can act on. The
|
|
724
|
+
// sibling home probe below already does this for the same reason.
|
|
725
|
+
throw new Error(`无法确定发行版 ${distro} 的默认用户:${who.stderr.trim() || probeEvidence(who)}`)
|
|
660
726
|
}
|
|
661
727
|
}
|
|
662
728
|
// `getent` reads the distro's own NSS user database. The pipeline's exit
|
|
@@ -768,7 +834,7 @@ export class LoginShellUnresolvedError extends Error {
|
|
|
768
834
|
*
|
|
769
835
|
* A probe that did not complete is NOT the empty-field case either. It used to
|
|
770
836
|
* return `/bin/bash` like a real answer, so a stalled relay — the machine
|
|
771
|
-
* transient
|
|
837
|
+
* transient docs/CONFINEMENT.md's probe policy exists for — silently pinned the session's
|
|
772
838
|
* shell with a value the caller could not tell from the distribution's own. The
|
|
773
839
|
* ceiling and the repeat are `probeWithRetry`'s (探针超时 60s + 超时后一次透明重试),
|
|
774
840
|
* the same ruling `listLinuxDir`/`checkLinuxPath`/`resolveDistroHome` follow, and a
|
|
@@ -814,13 +880,29 @@ export async function resolveLoginShell(distro, username, { wslPath, run = runWs
|
|
|
814
880
|
const stderr = typeof shell.stderr === 'string' ? shell.stderr.trim() : ''
|
|
815
881
|
throw new LoginShellUnresolvedError(`无法确定发行版 ${distro} 的登录 shell:${stderr || probeEvidence(shell)}`)
|
|
816
882
|
}
|
|
817
|
-
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
883
|
+
// A MULTI-LINE answer is refused, not truncated to line 1: resolveDistroHome runs
|
|
884
|
+
// the same getent-passwd shape and refuses it because a name getent misparses can
|
|
885
|
+
// dump the whole database — taking line 1 would silently elect the FIRST entry's
|
|
886
|
+
// shell for whichever user the caller asked about. Exactly one line is the only
|
|
887
|
+
// unambiguous answer, and an unresolvable login shell is a SKIP for the terminal
|
|
888
|
+
// controller, while a wrong shell is a session that does not work. The refusal is
|
|
889
|
+
// on the LINE COUNT: a single line that trims to empty is NOT this case — that is
|
|
890
|
+
// the documented /bin/bash fallback below.
|
|
891
|
+
const lines = shell.stdout.trim().split('\n')
|
|
892
|
+
if (lines.length > 1) {
|
|
893
|
+
throw new LoginShellUnresolvedError(
|
|
894
|
+
`无法确定发行版 ${distro} 的登录 shell:getent 返回了 ${lines.length} 行,唯一一行才是无歧义答案`,
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
const resolved = lines[0].trim()
|
|
898
|
+
// EMPTY is the fallback (the only way `resolved` is empty past the refusal above
|
|
899
|
+
// is a single line that trims to nothing); an ANSWERED value is returned as
|
|
900
|
+
// measured, so the test is deliberately `=== ''` and not `startsWith('/')`. A
|
|
901
|
+
// non-path answer such as `nologin` is the distribution's own ruling that this
|
|
902
|
+
// account has no interactive shell, and both callers SPAWN the value
|
|
903
|
+
// (subprocess.js hands it back as the resolved executable, and uses it as
|
|
904
|
+
// spawnTerminal's whole argv), so swapping in an unmeasured /bin/bash would open
|
|
905
|
+
// exactly the shell the account's passwd entry refuses — a value the caller
|
|
906
|
+
// cannot tell from one the probe measured.
|
|
825
907
|
return resolved === '' ? '/bin/bash' : resolved
|
|
826
908
|
}
|