Skip to content

fix(bridge): keep the extension connected on dynamically assigned ports - #127

Open
Asheblog wants to merge 2 commits into
omdsh-dev:mainfrom
Asheblog:fix/bridge-discovery-port
Open

Asheblog wants to merge 2 commits into
omdsh-dev:mainfrom
Asheblog:fix/bridge-discovery-port

Conversation

@Asheblog

@Asheblog Asheblog commented Oct 20, 2026 •

Copy link
Copy Markdown

概述 / Summary

让桥接插件在 DSH 0.2.1-rc.2 + 动态 Web 端口 的桌面环境下重新可被扩展连接,修掉三个叠加缺陷:扩展发现不到地址、事件流握手后立刻 1011、以及所有 unary RPC 报 reading 'signal'。

动机 / Motivation

DSH Desktop 用 dsh web --port 0 启动内嵌 Web 服务,每次启动端口都由系统重新分配(实测连续三次:1646 → 10472 → 1496)。这暴露了几个独立问题。

根因 1:扩展只能探测固定端口

扩展通过 /ext/bridge-config 在固定端口表(3080 / 3081 / 3090 / 14389 / 43189)里找本机 dsh。端口随机后侧栏永远停在「未连接」,所有 browser_* 工具报 no browser extension is connected to the bridge,而直接请求当前端口的 /ext/bridge-config 却返回 200 和正确 wsUrl。

根因 2:0.2.1 的 Peer 槽位传了 undefined

修好发现后,扩展进入「连接中 → 断开 → 重连」循环。用与扩展一致的方式握手复现:

0.0s OPEN
0.0s <- {"t":"hello.ok",...}
0.0s <- {"t":"error","code":"stream-failed","message":"TypeError: Cannot read properties of undefined (reading 'signal')"}
0.0s CLOSED 1011 event stream failed

dsh-api-gateway@0.2.1-rc.2 的 openWireStream 在 $events 分支调用 assertPeerLive(peer)(内部是 peer.signal.aborted),而插件在 wireStream.open(endpoint, payload, uplink, peer, signal) 的第 4 槽传的是 undefined。同一版本还把转发事件流绑定到发起它的 Peer($events/result 校验 client.peer !== peer),所以必须是同一个真实 Peer 对象。

根因 3:unary RPC 同样缺 Peer

连接稳定后,面板一列会话就又报同一条 TypeError:

-> rpc session.list
<- {"t":"rpc.result","ok":false,"error":{"code":"gateway/internal",
    "message":"Cannot read properties of undefined (reading 'signal')"}}

因为 prepareInvocation(request, control) 的第一行也是 assertPeerLive(request.peer, endpoint),而 gateway.invoke({ namespace, method, args, signal }) 从来没带过 peer。

改动内容 / Changes

固定端口发现

  • 新增 src/discovery.ts:在 discoveryPort(默认 43189,0 关闭)上监听回环地址,只回答 GET/HEAD /ext/bridge-config,返回当前实时 Web 端口对应的 wsUrl;其它方法 405、其它路径 404;端口被占用(含 Web 服务自身占用该端口)时只记一条 warning 并跳过,不阻断启动。
  • src/index.ts:Config 增加 discoveryPort(schema 默认 43189,校验 0–65535),在 mountBridge 的 effect 作用域内挂载。

in-process Peer(覆盖 stream + unary)

  • src/remote-host-api.ts:通过 connection.createPeer({ principal: { kind: 'owner' }, interactions: 'answer' }) 为每个桥创建一个 owner Peer。
  • 传给 wireStream.open($events、session/follow、workspace/follow)与 connection.createSharedFetchHandler('/api').fetch(request, peer)($events/result)。
  • TypertGatewayLike.invoke 接受可选 peer,四个 invoke 调用点全部前传。
  • EventGeneration 持有该 Peer 供其流使用。
  • createPeer 保持可选:DSH 0.2.0 没有 Peer 机制,调用形态不变。

测试与文档

  • 新增 tests/discovery.spec.ts:发布、HEAD、405、404、等于 Web 端口时跳过、端口被占用时降级。
  • tests/remote-host-api.spec.ts:新增「0.2.1 Peer 槽位」回归用例;tests/index.spec.ts 更新配置默认值与端口校验。
  • README.md / README.zh.md 增加配置行与「端口发现」小节;cordis.patch.yml 显式写出 discoveryPort: 43189。

测试 / Testing

cd packages/browser/bridge-browser
pnpm run test       # 158 tests(新增 1 个 Peer 用例 + 6 个 discovery 用例)
pnpm run typecheck
pnpm run build

实机验证(Windows 11 / Chrome 116+ / DSH Desktop 0.2.1-rc.2):

GET http://127.0.0.1:43189/ext/bridge-config
-> 200 {"wsUrl":"ws://127.0.0.1:1496/ext/bridge"}
GET http://127.0.0.1:43189/ext/bridge
-> 404
WS  -> hello.ok,socket 保持打开,session.list 返回 ok

注:仓库里 session-purge(symlink)与 token(0600 权限)两个用例在 Windows 本机失败,与本次改动无关,Linux CI 正常。

影响范围 / Impact

  • Windows / macOS / Linux 上的 DSH Desktop(--port 0)用户:扩展恢复自动发现,且事件流与 RPC 都能正常往返。
  • DSH 0.2.1-rc.2 用户:不再在握手后 1011,也不再出现 reading 'signal'。
  • DSH 0.2.0-rc.2 用户:createPeer 不存在时走原路径,行为不变;dsh web 默认 3080 的部署不会命中额外端口。
  • 安全面:发现端口仅绑定 127.0.0.1、只回一条只读路由、返回值不含 token;WebSocket 认证逻辑未改动。
  • 端口冲突:discoveryPort 被占用或恰好等于 Web 端口时自动跳过并告警。

@Asheblog Asheblog changed the title fix(bridge): publish discovery on a stable port fix(bridge): keep the extension connected on dynamically assigned ports Oct 20, 2026
@graydovee

Copy link
Copy Markdown

感谢这个修复—— 这个反应的bug的确存在

#121 里已经有等价实现,并且更完整:插件在 43189–43198 取首个空闲回环端口发布避免端口冲突,扩展侧配套探测编排;本机同时跑多个 dsh 实例时可以在面板里分别发现并切换。

如果急需这个能力 #121,可以先使用 fork 版本:https://github.com/graydovee/dsh-browser (v0.1.7 已发布,scripts/install.sh / scripts/install.ps1 会一并装好桥插件与扩展)。

DSH Desktop starts its web server with `dsh web --port 0`, so the port that
carries /ext/bridge is reassigned by the OS on every launch. The extension
only probes a fixed port list (3080/3081/3090/14389/43189), so a dynamically
assigned host looks permanently offline: the side panel stays on
"disconnected" and every browser_* tool reports "no browser extension is
connected", even though /ext/bridge-config answers normally.

The bridge now also binds a stable loopback port (`discoveryPort`, default
43189, `0` disables it) and republishes /ext/bridge-config there with the live
WebSocket URL. The listener is loopback-only, answers that single read-only
route, publishes no token, and degrades to one warning when the port is taken
(including when the web server itself already owns it).

- src/discovery.ts: handler, URL helper, and resilient listener
- src/index.ts: mount it inside the effect scope, next to the existing route
- tests: handler/bind/degradation coverage; config defaults updated
- docs: config table row and a port-discovery section (en/zh)
dsh 0.2.1-rc.2 requires every gateway entry point to come from a live Peer:

- `openWireStream` calls `assertPeerLive(peer)` before opening `$events` and
  binds the stream to that Peer, so a later `$events/result` must arrive from
  the same object. The plugin passed `undefined`, the first `$events` open
  threw "Cannot read properties of undefined (reading 'signal')", and the
  socket closed 1011 — every reconnect ended in the same loop.
- `prepareInvocation` starts with the same `assertPeerLive(request.peer)`, so
  every unary call (`session.list` included) failed the same way.

The plugin now creates one in-process owner Peer per bridge and forwards it to
`wireStream.open`, `connection.createSharedFetchHandler('/api').fetch(request,
peer)`, and every `gateway.invoke`. `createPeer` stays optional: dsh 0.2.0 has
no Peer seam and keeps the previous call shape.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants