From d191a4340efdb3699ecae77f6112a2fddb67c175 Mon Sep 17 00:00:00 2001 From: delta <528491526@qq.com> Date: Fri, 9 Oct 2026 22:08:22 +0800 Subject: [PATCH] fix(mobile): give up on an unanswered connection check after 10 s When the saved desktop was unreachable and its packets were dropped (e.g. the desktop listens on a Tailscale address and the phone's Tailscale is not up yet), reopening the Android app stayed on "connecting" until the platform's TCP timeout, minutes on a phone, with no button to retry or pick another desktop. The session check now fails with TIMEOUT after 10 s, which leads to the offline page (retry / change desktop), and the connecting screen offers "change desktop" too. Closes #47 --- .../src/renderer/src/api/remote-connection.ts | 4 +++- .../src/features/remote/connect-page.tsx | 13 +++++++++--- docs/PROGRESS.md | 1 + packages/shared/src/remote-client.test.ts | 20 +++++++++++++++++++ packages/shared/src/remote-client.ts | 17 +++++++++++++++- 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/src/api/remote-connection.ts b/apps/desktop/src/renderer/src/api/remote-connection.ts index adbf6a6..8f29816 100644 --- a/apps/desktop/src/renderer/src/api/remote-connection.ts +++ b/apps/desktop/src/renderer/src/api/remote-connection.ts @@ -56,7 +56,9 @@ export function createRemoteConnection({ return (chunk) => decoder.decode(chunk, { stream: true }) }, delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), - onUnauthorized: () => unauthorized.forEach((listener) => listener()) + onUnauthorized: () => unauthorized.forEach((listener) => listener()), + // An unreachable desktop otherwise keeps the connect page on "connecting" for minutes. + sessionTimeoutMs: 10_000 }) return { diff --git a/apps/desktop/src/renderer/src/features/remote/connect-page.tsx b/apps/desktop/src/renderer/src/features/remote/connect-page.tsx index 759ed19..531b596 100644 --- a/apps/desktop/src/renderer/src/features/remote/connect-page.tsx +++ b/apps/desktop/src/renderer/src/features/remote/connect-page.tsx @@ -70,9 +70,16 @@ export function ConnectPage({

连接 DevHub

{checking ? ( -

- 正在连接… -

+ <> +

+ 正在连接… +

+ {choosesAddress && ( + + )} + ) : ( <> {alert && ( diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 05ffc56..6e567b8 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -46,6 +46,7 @@ M4-3 Android: - #36 已完成(v1.4.0):Android 图标与启动画面由桌面端 SVG 生成。 - #37 已完成(v1.4.0):App 内扫码连接(`@capacitor/barcode-scanner`,不依赖 Google Play 服务,minSdk 26);实际识别待真机验证。 - #41 已完成,待合并:桌面端早于 1.4(不支持 App 的 CORS)时,App 提示升级桌面端,而不是「检查网络」(真机反馈)。 +- #47 已完成,待合并:连接检查 10 秒超时(此前桌面不可达且丢包时一直「正在连接…」,真机反馈),连接中也可「更换桌面端」;已在模拟器验证,真机上的具体诱因(推测为 Tailscale 未连上)未确认。 桌面修复: diff --git a/packages/shared/src/remote-client.test.ts b/packages/shared/src/remote-client.test.ts index 059ef36..922e981 100644 --- a/packages/shared/src/remote-client.test.ts +++ b/packages/shared/src/remote-client.test.ts @@ -152,6 +152,26 @@ describe('remote api', () => { headers: { authorization: 'Bearer secret' } }) }) + + it('gives up on a session check the desktop never answers', async () => { + const delay = vi.fn(async () => undefined) + const hanging = client(() => new Promise(() => undefined), { + delay, + sessionTimeoutMs: 10_000 + }) + await expect(hanging.session()).rejects.toMatchObject({ code: 'TIMEOUT', status: 0 }) + expect(delay).toHaveBeenCalledWith(10_000) + }) + + it('returns a session answered before the timeout', async () => { + let elapse: (() => void) | undefined + const answered = client(async () => json(200, { protocol: 1, allowTerminal: true }), { + delay: () => new Promise((resolve) => (elapse = resolve)), + sessionTimeoutMs: 10_000 + }) + await expect(answered.session()).resolves.toEqual({ protocol: 1, allowTerminal: true }) + elapse?.() + }) }) describe('remote events', () => { diff --git a/packages/shared/src/remote-client.ts b/packages/shared/src/remote-client.ts index 56c07ab..702a38a 100644 --- a/packages/shared/src/remote-client.ts +++ b/packages/shared/src/remote-client.ts @@ -40,6 +40,11 @@ export interface RemoteClientOptions { onUnauthorized?: () => void /** Waits between event stream reconnects; the last value repeats. */ retryDelaysMs?: readonly number[] + /** + * Gives up on `session()` after this long (`TIMEOUT`). Without it the check waits as long as + * the platform does: minutes on a phone whose packets to the desktop are dropped. + */ + sessionTimeoutMs?: number } /** A failed remote call. `code` is the core's `DevhubError` code or a transport code. */ @@ -112,7 +117,17 @@ export function createRemoteClient(options: RemoteClientOptions): RemoteClient { api, events: createEventStream(options, () => url('/events'), auth), async session() { - const { status, body } = await request('/session', { method: 'GET' }) + const pending = request('/session', { method: 'GET' }) + const { sessionTimeoutMs } = options + const { status, body } = + sessionTimeoutMs === undefined + ? await pending + : await Promise.race([ + pending, + options.delay(sessionTimeoutMs).then((): never => { + throw new RemoteError('TIMEOUT', 'DevHub 没有响应,请检查网络和远程访问设置', 0) + }) + ]) if (status === 200 && isSession(body)) return body if (isIpcResult(body) && !body.ok) { throw new RemoteError(body.error.code, body.error.message, status)