@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.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/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 +396 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +604 -262
- 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 +56 -32
- 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 +143 -28
- 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 +104 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +143 -0
- 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/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- 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 -33
- 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 +70 -40
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +136 -25
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +192 -0
- 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/console-output.ts +97 -0
- 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 -29
package/src/update.ts
CHANGED
|
@@ -12,6 +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 { createOutputCapture } from './console-output.ts'
|
|
16
|
+
import { withIdentityEncoding } from './http.ts'
|
|
15
17
|
|
|
16
18
|
/** npm registry base used for version probes. */
|
|
17
19
|
export const NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
@@ -321,7 +323,7 @@ export async function fetchLatestVersion(
|
|
|
321
323
|
const controller = new AbortController()
|
|
322
324
|
const timer = setTimeout(() => { controller.abort() }, timeoutMs)
|
|
323
325
|
try {
|
|
324
|
-
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 }))
|
|
325
327
|
if (!response.ok) return undefined
|
|
326
328
|
const body = await response.json()
|
|
327
329
|
if (typeof body !== 'object' || body === null) return undefined
|
|
@@ -404,10 +406,10 @@ export async function fetchGitHubReleaseNotes(
|
|
|
404
406
|
try {
|
|
405
407
|
const response = await fetchImpl(
|
|
406
408
|
`https://api.github.com/repos/${UPDATE_RELEASE_REPO}/releases/tags/v${version}`,
|
|
407
|
-
{
|
|
409
|
+
withIdentityEncoding({
|
|
408
410
|
headers: { accept: 'application/vnd.github+json', 'user-agent': 'dsh-web-update' },
|
|
409
411
|
signal: controller.signal,
|
|
410
|
-
},
|
|
412
|
+
}),
|
|
411
413
|
)
|
|
412
414
|
if (!response.ok) return undefined
|
|
413
415
|
const body = await response.json()
|
|
@@ -557,11 +559,24 @@ export interface UpdateRunDeps {
|
|
|
557
559
|
timeoutMs?: number
|
|
558
560
|
/** Platform override (defaults to process.platform; test seam). */
|
|
559
561
|
platform?: NodeJS.Platform
|
|
562
|
+
/** Process-group kill seam (defaults to process.kill; test seam). */
|
|
563
|
+
killImpl?: KillImpl
|
|
560
564
|
}
|
|
561
565
|
|
|
562
566
|
/** Cap on captured pnpm output (keeps error payloads bounded). */
|
|
563
567
|
const OUTPUT_CAP = 16 * 1024
|
|
564
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
|
+
|
|
565
580
|
/**
|
|
566
581
|
* Windows cmd command-not-found stderr. With shell:true a missing shim
|
|
567
582
|
* exits with code 1 or 9009 (cmd cannot report ENOENT), so the fallback chain
|
|
@@ -571,20 +586,14 @@ const OUTPUT_CAP = 16 * 1024
|
|
|
571
586
|
const WIN_CMD_MISSING_RE = /not recognized as an internal or external command|不是内部或外部命令/i
|
|
572
587
|
|
|
573
588
|
/**
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
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.
|
|
577
593
|
*/
|
|
578
|
-
function
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
} catch {
|
|
582
|
-
try {
|
|
583
|
-
return new TextDecoder('gbk').decode(chunk)
|
|
584
|
-
} catch {
|
|
585
|
-
return chunk.toString('utf8')
|
|
586
|
-
}
|
|
587
|
-
}
|
|
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
|
|
588
597
|
}
|
|
589
598
|
|
|
590
599
|
/**
|
|
@@ -608,16 +617,24 @@ const MIN_RELEASE_AGE_OVERRIDE = '--config.minimumReleaseAge=0'
|
|
|
608
617
|
export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
609
618
|
return new Promise((resolve) => {
|
|
610
619
|
const spawnImpl = deps.spawnImpl ?? spawn
|
|
611
|
-
const packages = deps.packages
|
|
612
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))
|
|
613
626
|
// Windows ships pnpm/corepack/npx as .cmd shims; Node's spawn cannot
|
|
614
627
|
// start .cmd files directly (ENOENT even when installed), so route
|
|
615
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.
|
|
616
632
|
const spawnOptions: Record<string, unknown> = {
|
|
617
633
|
cwd: deps.profileDir,
|
|
618
634
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
619
|
-
...(platform === 'win32' ? { shell: true } : {}),
|
|
635
|
+
...(platform === 'win32' ? { shell: true } : { detached: true }),
|
|
620
636
|
}
|
|
637
|
+
const killImpl: KillImpl = deps.killImpl ?? process.kill
|
|
621
638
|
// Ordered fallback chain: each is tried only when the previous one is
|
|
622
639
|
// missing on PATH (ENOENT on the spawn error event, never on close).
|
|
623
640
|
// `--latest` is required: dsh plugin writes exact-version specs (e.g.
|
|
@@ -638,14 +655,21 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
638
655
|
// (its tail is subject to OUTPUT_CAP). The win32 missing-command test must
|
|
639
656
|
// run against `currentOutput` so a previous candidate's 'not recognized'
|
|
640
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.
|
|
641
662
|
let output = ''
|
|
642
|
-
let
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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())
|
|
649
673
|
}
|
|
650
674
|
let currentChild: ReturnType<typeof spawnImpl> | undefined
|
|
651
675
|
// Terminal guard: once a result is produced the promise is settled and no
|
|
@@ -666,14 +690,31 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
666
690
|
const pid = (currentChild as { pid?: number } | undefined)?.pid
|
|
667
691
|
if (pid !== undefined && pid > 0) {
|
|
668
692
|
try {
|
|
669
|
-
|
|
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', () => {})
|
|
670
698
|
} catch {
|
|
671
699
|
// Best-effort kill; fall through to the timeout result.
|
|
672
700
|
}
|
|
673
701
|
}
|
|
674
702
|
} else {
|
|
675
|
-
|
|
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
|
+
}
|
|
676
716
|
}
|
|
717
|
+
foldCandidate()
|
|
677
718
|
finish({ ok: false, exitCode: null, output, error: 'update timed out; install process killed', errorCode: 'timeout' })
|
|
678
719
|
}, deps.timeoutMs ?? 10 * 60_000)
|
|
679
720
|
const runCandidate = (index: number): void => {
|
|
@@ -689,7 +730,8 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
689
730
|
return
|
|
690
731
|
}
|
|
691
732
|
const candidate = candidates[index]
|
|
692
|
-
|
|
733
|
+
capture = createOutputCapture(OUTPUT_CAP)
|
|
734
|
+
candidateFolded = false
|
|
693
735
|
const child = spawnImpl(candidate.command, candidate.args, spawnOptions)
|
|
694
736
|
currentChild = child
|
|
695
737
|
let settled = false
|
|
@@ -702,6 +744,7 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
702
744
|
child.stderr?.on('data', append)
|
|
703
745
|
child.on('error', (error: Error) => {
|
|
704
746
|
once(() => {
|
|
747
|
+
foldCandidate()
|
|
705
748
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
706
749
|
// Command missing: try the next candidate in the chain.
|
|
707
750
|
runCandidate(index + 1)
|
|
@@ -716,11 +759,13 @@ export function runUpdate(deps: UpdateRunDeps): Promise<UpdateRunResult> {
|
|
|
716
759
|
// 'not recognized' or localized message instead of ENOENT — keep the
|
|
717
760
|
// chain going. Checked against this candidate's own output so a prior
|
|
718
761
|
// fallback's stderr cannot misclassify a real failure here.
|
|
719
|
-
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()
|
|
720
764
|
runCandidate(index + 1)
|
|
721
765
|
return
|
|
722
766
|
}
|
|
723
767
|
settled = true
|
|
768
|
+
foldCandidate()
|
|
724
769
|
finish({
|
|
725
770
|
ok: code === 0,
|
|
726
771
|
exitCode: code,
|