@linxin666/dsh-remote-web-ui 0.3.24 → 0.4.1
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.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +263 -129
- package/lib/client.js.map +1 -1
- package/lib/index.js +565 -256
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +47 -18
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +103 -22
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +23 -16
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -1
- package/lib/types/client/plugin-card-seat.js +16 -14
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -26
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +61 -27
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +97 -20
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +24 -17
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -24
package/src/update.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { readFileSync } from 'node:fs'
|
|
13
13
|
import { dirname, join } from 'node:path'
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
|
-
import {
|
|
15
|
+
import { createOutputCapture } from './console-output.ts'
|
|
16
|
+
import { withIdentityEncoding } from './http.ts'
|
|
16
17
|
|
|
17
18
|
/** npm registry base used for version probes. */
|
|
18
19
|
export const NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
@@ -322,7 +323,7 @@ export async function fetchLatestVersion(
|
|
|
322
323
|
const controller = new AbortController()
|
|
323
324
|
const timer = setTimeout(() => { controller.abort() }, timeoutMs)
|
|
324
325
|
try {
|
|
325
|
-
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', { signal: controller.signal })
|
|
326
|
+
const response = await fetchImpl(NPM_REGISTRY + '/' + name.replace('/', '%2F') + '/latest', withIdentityEncoding({ signal: controller.signal }))
|
|
326
327
|
if (!response.ok) return undefined
|
|
327
328
|
const body = await response.json()
|
|
328
329
|
if (typeof body !== 'object' || body === null) return undefined
|
|
@@ -405,10 +406,10 @@ export async function fetchGitHubReleaseNotes(
|
|
|
405
406
|
try {
|
|
406
407
|
const response = await fetchImpl(
|
|
407
408
|
`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`,
|
|
408
|
-
{
|
|
409
|
+
withIdentityEncoding({
|
|
409
410
|
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
|
|
410
411
|
signal: controller.signal,
|
|
411
|
-
},
|
|
412
|
+
}),
|
|
412
413
|
)
|
|
413
414
|
if (!response.ok) return undefined
|
|
414
415
|
const body = await response.json()
|
|
@@ -558,11 +559,24 @@ export interface UpdateRunDeps {
|
|
|
558
559
|
timeoutMs?: number
|
|
559
560
|
/** Platform override (defaults to process.platform; test seam). */
|
|
560
561
|
platform?: NodeJS.Platform
|
|
562
|
+
/** Process-group kill seam (defaults to process.kill; test seam). */
|
|
563
|
+
killImpl?: KillImpl
|
|
561
564
|
}
|
|
562
565
|
|
|
563
566
|
/** Cap on captured pnpm output (keeps error payloads bounded). */
|
|
564
567
|
const OUTPUT_CAP = 16 * 1024
|
|
565
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Characters a package name may contain before it is handed to the update
|
|
571
|
+
* command line. Deliberately looser than the strict family shape (the tests
|
|
572
|
+
* and future callers use short names) but it admits no whitespace, quotes,
|
|
573
|
+
* shell metacharacters, or path separators beyond the scope slash.
|
|
574
|
+
*/
|
|
575
|
+
const SHELL_SAFE_PACKAGE_RE = /^@?[A-Za-z0-9][A-Za-z0-9._/-]*$/
|
|
576
|
+
|
|
577
|
+
/** Seam for the process-group kill (tests inject a recorder). */
|
|
578
|
+
export type KillImpl = (pid: number, signal: NodeJS.Signals) => void
|
|
579
|
+
|
|
566
580
|
/**
|
|
567
581
|
* Windows cmd command-not-found stderr. With shell:true a missing shim
|
|
568
582
|
* exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
|
|
@@ -572,14 +586,14 @@ const OUTPUT_CAP = 16 * 1024
|
|
|
572
586
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i
|
|
573
587
|
|
|
574
588
|
/**
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* accumulator — decoding per \`data\` event would split multi-byte characters.
|
|
589
|
+
* Fold one decoded candidate output into the running total under the display
|
|
590
|
+
* cap (the tail carries the failure reason).
|
|
591
|
+
* @param output - the accumulated text.
|
|
592
|
+
* @param text - the candidate's decoded text.
|
|
580
593
|
*/
|
|
581
|
-
function
|
|
582
|
-
|
|
594
|
+
function capOutput(output: string, text: string): string {
|
|
595
|
+
const combined = output + text
|
|
596
|
+
return combined.length > OUTPUT_CAP ? combined.slice(combined.length - OUTPUT_CAP) : combined
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
/**
|
|
@@ -603,16 +617,24 @@ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
|
|
|
603
617
|
export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
604
618
|
return new Promise((resolve) => {
|
|
605
619
|
const spawnImpl = deps.spawnImpl ?? spawn
|
|
606
|
-
const packages = deps.packages
|
|
607
620
|
const platform = deps.platform ?? process.platform
|
|
621
|
+
// The names come from the profile/anchor manifests and the win32 candidate
|
|
622
|
+
// runs through a shell, so anything outside the npm name shape is dropped
|
|
623
|
+
// instead of being concatenated into a command line (a dependency key
|
|
624
|
+
// containing shell metacharacters would otherwise run as a second command).
|
|
625
|
+
const packages = deps.packages.filter(name => SHELL_SAFE_PACKAGE_RE.test(name))
|
|
608
626
|
// Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
|
|
609
627
|
// start .cmd files directly (ENOENT even when installed), so route
|
|
610
628
|
// them through cmd.exe there. POSIX spawns stay shell-free.
|
|
629
|
+
// POSIX: the child leads its own process group (detached) so the timeout
|
|
630
|
+
// can signal pnpm's whole tree — pnpm spawns lifecycle-script children that
|
|
631
|
+
// a direct SIGTERM to the wrapper leaves running against the profile.
|
|
611
632
|
const spawnOptions: Record<string, unknown> = {
|
|
612
633
|
cwd: deps.profileDir,
|
|
613
634
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
614
|
-
...(platform === 'win32' ? { shell: true } : {}),
|
|
635
|
+
...(platform === 'win32' ? { shell: true } : { detached: true }),
|
|
615
636
|
}
|
|
637
|
+
const killImpl: KillImpl = deps.killImpl ?? process.kill
|
|
616
638
|
// Ordered fallback chain: each is tried only when the previous one is
|
|
617
639
|
// missing on PATH (ENOENT on the spawn error event, never on close).
|
|
618
640
|
// `--latest` is required: dsh plugin writes exact-version specs (e.g.
|
|
@@ -633,14 +655,21 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
633
655
|
// (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
|
|
634
656
|
// run against `currentOutput` so a previous candidate's 'not recognized'
|
|
635
657
|
// stderr cannot misclassify a real failure in the next one.
|
|
658
|
+
// Each candidate accumulates raw bytes and is decoded exactly once
|
|
659
|
+
// (console-output.ts): decoding per `data` event splits a multi-byte
|
|
660
|
+
// character that straddles two reads into replacement characters, which is
|
|
661
|
+
// what the panel then shows as garbled pnpm diagnostics.
|
|
636
662
|
let output = ''
|
|
637
|
-
let
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
663
|
+
let capture = createOutputCapture(OUTPUT_CAP)
|
|
664
|
+
let candidateFolded = false
|
|
665
|
+
const append = (chunk: Buffer): void => { capture.push(chunk) }
|
|
666
|
+
/** This candidate's decoded text (idempotent). */
|
|
667
|
+
const candidateText = (): string => capture.read()
|
|
668
|
+
/** Fold this candidate's text into the running output exactly once. */
|
|
669
|
+
const foldCandidate = (): void => {
|
|
670
|
+
if (candidateFolded) return
|
|
671
|
+
candidateFolded = true
|
|
672
|
+
output = capOutput(output, capture.read())
|
|
644
673
|
}
|
|
645
674
|
let currentChild: ReturnType<typeof spawnImpl> | undefined
|
|
646
675
|
// Terminal guard: once a result is produced the promise is settled and no
|
|
@@ -661,14 +690,31 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
661
690
|
const pid = (currentChild as { pid?: number } | undefined)?.pid
|
|
662
691
|
if (pid !== undefined && pid > 0) {
|
|
663
692
|
try {
|
|
664
|
-
|
|
693
|
+
// A spawn failure arrives as an asynchronous 'error' event, which
|
|
694
|
+
// the try/catch cannot see; an emitter with no error listener throws
|
|
695
|
+
// and would take the host process down on this path.
|
|
696
|
+
const killer = spawnImpl('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' })
|
|
697
|
+
killer.on?.('error', () => {})
|
|
665
698
|
} catch {
|
|
666
699
|
// Best-effort kill; fall through to the timeout result.
|
|
667
700
|
}
|
|
668
701
|
}
|
|
669
702
|
} else {
|
|
670
|
-
|
|
703
|
+
// Detached child: its process-group id is its pid, so the negative pid
|
|
704
|
+
// reaches pnpm's grandchildren too. A group kill throws when the child
|
|
705
|
+
// already exited; fall back to the direct kill.
|
|
706
|
+
const pid = (currentChild as { pid?: number } | undefined)?.pid
|
|
707
|
+
if (pid !== undefined && pid > 0) {
|
|
708
|
+
try {
|
|
709
|
+
killImpl(-pid, 'SIGTERM')
|
|
710
|
+
} catch {
|
|
711
|
+
try { currentChild?.kill('SIGTERM') } catch { /* already gone */ }
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
currentChild?.kill('SIGTERM')
|
|
715
|
+
}
|
|
671
716
|
}
|
|
717
|
+
foldCandidate()
|
|
672
718
|
finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
|
|
673
719
|
}, deps.timeoutMs ?? 10 * 60_000)
|
|
674
720
|
const runCandidate = (index: number): void => {
|
|
@@ -684,7 +730,8 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
684
730
|
return
|
|
685
731
|
}
|
|
686
732
|
const candidate = candidates[index]
|
|
687
|
-
|
|
733
|
+
capture = createOutputCapture(OUTPUT_CAP)
|
|
734
|
+
candidateFolded = false
|
|
688
735
|
const child = spawnImpl(candidate.command, candidate.args, spawnOptions)
|
|
689
736
|
currentChild = child
|
|
690
737
|
let settled = false
|
|
@@ -697,6 +744,7 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
697
744
|
child.stderr?.on('data', append)
|
|
698
745
|
child.on('error', (error: Error) => {
|
|
699
746
|
once(() => {
|
|
747
|
+
foldCandidate()
|
|
700
748
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
701
749
|
// Command missing: try the next candidate in the chain.
|
|
702
750
|
runCandidate(index + 1)
|
|
@@ -711,11 +759,13 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
711
759
|
// 'not recognized' or localized message instead of ENOENT — keep the
|
|
712
760
|
// chain going. Checked against this candidate's own output so a prior
|
|
713
761
|
// fallback's stderr cannot misclassify a real failure here.
|
|
714
|
-
if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(
|
|
762
|
+
if (platform === 'win32' && (code === 9009 || (code !== 0 && WIN_CMD_MISSING_RE.test(candidateText())))) {
|
|
763
|
+
foldCandidate()
|
|
715
764
|
runCandidate(index + 1)
|
|
716
765
|
return
|
|
717
766
|
}
|
|
718
767
|
settled = true
|
|
768
|
+
foldCandidate()
|
|
719
769
|
finish({
|
|
720
770
|
ok: code === 0,
|
|
721
771
|
exitCode: code,
|