Repository navigation
v0.8.8:local_resource_download(远端无网时经本机网络代理下载)/ Local Resource Download via This Machine
中文
新功能
- 🟢 local_resource_download——远端无网时经本机网络完成引导期下载:新增 SSH 高级设置
local_resource_download,类似 VS Code 的Remote.SSH: Local Server Download。三档取值:auto(默认,先探测远端网络,不可达才建隧道)/always(始终走本机代理)/off(远端直连,原行为)。实现为本机纯 JS HTTP CONNECT 代理 + SSH 反向转发:远端的 curl 与 npm install 收到http_proxy=http://127.0.0.1:<远端端口>,出站流量经隧道回到本机网络——远端下载命令本身不改,npm 依赖树照常解析,解决了「只下载 tarball 仍需远端拉依赖」的固有局限。引导期全部网络调用(镜像测速、Node 发行包下载、npm 版本解析、dsh 与 pnpm 安装)都走这条隧道。 - 反向端口动态分配:隧道用的远端端口从与其他反向隧道同一套候选区间分配(不再固定值),与凭据代理共用同一套端口纪律;绑定冲突时换端口重试一次,两次都失败则降级为远端直连,不让引导因此中断。
- 两个入口:面板「高级选项」新增下拉框(与代理/环境变量/跳板机共用
~/.dsh/remote-advanced.json的 SSH 域持久化),CLI 新增--local-resource-download <auto|always|off>。仅对 SSH 连接生效(WSL 有本地文件系统访问,无需此选项)。 - 代理环境变量的八个键清单下沉到基础层
util/proxy-env.ts,供编排层与能力层共用单一实现。
修复
- 镜像测速漏注入代理(阻塞级):
selectMirror此前裸curl探测候选镜像,远端无网时全部不可达即抛MIRROR_ALL_UNREACHABLE——本已注入代理的下载步骤根本执行不到,特性在主场景下完全不生效。现在测速的每次 curl 都带-x。 - 反向转发的监听器泄漏:
ssh-transport.forwardIn的'tcp connection'handler 在绑定前挂上,失败路径只 reject 不卸载。上一项修复引入的换端口重试让这条路径成为常态;累积的孤儿 handler 在该端口被释放又被重新绑定时会与正常 handler 同时accept()同一条连接。现在失败先卸载再抛。 - 隧道被空闲超时打断:
net.Socket.setTimeout计的是空闲时长而非建连时长。原实现建连时设了 30 秒超时却从不解除,隧道建立后任何 30 秒静默(npm keep-alive 间隔、慢速服务器、aarch64 上耗时两分钟的安装)都会打断正在传输的连接,客户端只看到 EOF 并重连——表现为连接反复重连、任务迟迟不结束。现在握手成功即解除超时。 - 代理失败路径泄漏本机端口:本机代理一启动就占端口,而 provision 的
try/finally在其返回之后才建立。forwardIn失败时代理永不停止;现在失败路径就地收尾并降级。 selectMirror之外的可达性探测:auto模式改为逐候选顺序尝试全部 npm 镜像,单点被墙不再误判「远端无网」而白建一条隧道。- 代理服务器收尾挂起:
stop()此前依赖server.close()等连接自然结束,而注入的 SSH 流不归本机管——provision 的finally会永久挂起。现在显式销毁上游与注入流,并closeAllConnections()兜底。 - 日志量级:成功的 CONNECT 不再逐条打印(一次 npm install 会产生上百条,刷屏看起来像死循环),改为收尾汇总「共转发 N 条隧道」;建连失败仍逐条留痕。CONNECT 目标解析同时加固:含空白一律拒绝、IPv6 方括号剥离后再交给 DNS。
- 其余整理:删除
provision拆分后遗留的死变量与不可达分支、消除六处重复的'auto' | 'always' | 'off'字面量联合、runProvisionStages去掉与上下文重复的参数。
文档
- README(中/英)新增「本地资源下载」章节:三档取值、工作原理、CLI 与面板两种入口。
- README 删除「模块职责」表格(面向开发者的模块清单,使用者不需要)。
- 新增 5 个测试文件、共 20 个用例,其中 4 条专为上述缺陷的回归线(换端口重试、失败不泄漏、空闲不打断、测速带代理)。
验证
pnpm run typecheck零错误;scripts/check-plugin.ts全通过;单元测试 663 个用例 660 通过 / 3 skipped(既有 POSIX-only)/ 0 失败。- 真机端到端(OrangePI,aarch64,Node 与 dsh 均未安装):
always模式下引导全流程成功——镜像测速、Node v24.22.1 下载、dsh 0.2.1-rc.4 安装(137.7s)、pnpm 11.7.0 安装全部经隧道完成;auto模式在有公网主机上正确判定「有网」不建隧道。
English
Features
- 🟢 local_resource_download — bootstrap downloads through this machine when the remote has no network: New SSH advanced option
local_resource_download, similar to VS Code'sRemote.SSH: Local Server Download. Three values:auto(default — probe the remote network first, build the tunnel only when unreachable) /always(always use this machine as proxy) /off(remote direct, previous behavior). Implemented as a pure-JS HTTP CONNECT proxy in the local process plus an SSH reverse forward: the remotecurlandnpm installreceivehttp_proxy=http://127.0.0.1:<remote-port>and their outbound traffic returns through the tunnel to the local network — the remote download commands themselves are unchanged and npm dependency trees resolve normally, which fixes the inherent limitation of "downloading the tarball still needs the remote to fetch its dependencies." Every network call during bootstrapping (mirror latency probing, Node distribution download, npm version resolution, dsh and pnpm installs) goes through this tunnel. - Dynamic reverse port allocation: the tunnel's remote port comes from the same candidate range as the other reverse tunnels (no fixed value — the documented
DSH_REMOTE_PROXY=18890example would otherwise collide) and shares the same port discipline as the credential proxy. On bind conflicts it retries with a fresh port, and degrades to remote direct if both attempts fail rather than aborting the bootstrap. - Two entry points: a new dropdown in the panel's advanced options (persisted in the SSH domain of
~/.dsh/remote-advanced.json, shared with proxy/env/jump-host) and a new CLI flag--local-resource-download <auto|always|off>. SSH only — WSL has direct filesystem access and does not need it. - The eight proxy environment variable keys moved down to the foundation layer
util/proxy-env.ts, giving the orchestration and capability layers a single implementation.
Fixes
- Mirror probing missed the proxy (blocking):
selectMirrorprobed candidate mirrors with a barecurl, so on an offline remote every candidate was unreachable and it threwMIRROR_ALL_UNREACHABLE— the later, proxy-injected download steps never ran at all, making the feature useless in its primary scenario. Every probe curl now carries-x. - Reverse-forward listener leak:
ssh-transport.forwardInregistered its'tcp connection'handler before binding and never removed it on the failure path. The port-retry introduced above turns that path into a common one; accumulated orphan handlers wouldaccept()the same connection as the legitimate handler once the port is released and rebound. The handler is now removed before rethrowing. - Tunnels killed by an idle timeout:
net.Socket.setTimeoutmeasures inactivity, not connect time. The original code armed a 30-second timeout at connect and never cleared it, so any 30-second silence after the tunnel was established (npm keep-alive gaps, slow servers, a two-minute install on aarch64) tore down an in-flight transfer; the client saw EOF and reconnected — showing up as connection churn while the task never finished. The timeout is now cleared as soon as the handshake succeeds. - Local port leaked on proxy failure: the local proxy occupies a port as soon as it starts, while provision's
try/finallyis only established after it returns. A failingforwardInleft the proxy running forever; the failure path now cleans up in place and degrades. - Reachability probe:
automode now tries every npm mirror in order, so a single blocked endpoint no longer makes it conclude "no network" and build a tunnel needlessly. - Proxy shutdown hang:
stop()relied onserver.close()waiting for connections to end, but injected SSH streams are not driven locally — provision'sfinallywould hang forever. It now explicitly destroys upstream and injected streams, withcloseAllConnections()as a backstop. - Log volume: successful CONNECTs are no longer logged one by one (a single npm install produces hundreds, which looks like an infinite loop); a single summary line reports the tunnel count at shutdown, while failures are still traced. CONNECT target parsing was hardened too: whitespace is rejected outright and IPv6 brackets are stripped before handing the host to DNS.
- Cleanups: removed the dead variable and unreachable branch left behind by the
provisionsplit, dropped six duplicated'auto' | 'always' | 'off'literal unions, and trimmedrunProvisionStagesparameters that duplicated its context.
Documentation
- README (zh/en) gains a "Local resource download" section covering the three modes, how it works, and both entry points.
- README drops the developer-facing "Module Responsibility" table, which users do not need.
- Five new test files with 20 cases, four of them regression lines for the defects above (port retry, no leak on failure, no idle interruption, proxy-aware probing).
Verification
pnpm run typecheckclean;scripts/check-plugin.tsfully passing; 663 unit tests — 660 passed / 3 skipped (pre-existing POSIX-only) / 0 failed.- Real-host end-to-end (OrangePI, aarch64, with neither Node nor dsh installed): a full bootstrap in
alwaysmode succeeded — mirror probing, Node v24.22.1 download, dsh 0.2.1-rc.4 install (137.7s) and pnpm 11.7.0 install all completed through the tunnel; on a host with internet access,automode correctly detected the network and built no tunnel.
Full Changelog: v0.8.7...v0.8.8