From 0be65370f65e620f2fe58fd18551f2a77eb3c30c Mon Sep 17 00:00:00 2001 From: "954801926@qq.com" Date: Sun, 6 Sep 2026 13:08:47 +0800 Subject: [PATCH] fix: ssh connect flow never fired + 120s host-key prompt budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Workspace: saved-credential connections now call connect() directly; ConnectFlow only rendered a connecting spinner without ever firing openSession, leaving attempts hung forever (root cause of the stuck '连接中' modal) - Host-key prompt timeout raised 30s -> 120s (a security decision needs time; 30s aborted while users compare fingerprints) - main: unhandledRejection/uncaughtException loggers; ssh diagnostics on error paths --- src/main/index.ts | 7 +++++++ src/main/ssh.ts | 12 +++++++++--- src/renderer/src/workspace/Workspace.tsx | 18 +++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 857aec7..feaf1e1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -41,6 +41,13 @@ function createWindow(): void { } } +process.on('unhandledRejection', (reason) => { + console.error('[main] unhandledRejection:', reason) +}) +process.on('uncaughtException', (err) => { + console.error('[main] uncaughtException:', err) +}) + app.whenReady().then(() => { registerIpc() // OS-level effects (login item, sleep blocker) must apply even if the diff --git a/src/main/ssh.ts b/src/main/ssh.ts index b5bc92b..41983d1 100644 --- a/src/main/ssh.ts +++ b/src/main/ssh.ts @@ -53,7 +53,9 @@ export interface SshServiceDeps { timeoutMs?: { prompt: number; connect: number } } -const DEFAULT_TIMEOUTS = { prompt: 30_000, connect: 15_000 } +// Host-key confirmation is a security decision — users may take time to +// compare fingerprints. 30s timed out on people who merely screenshot it. +const DEFAULT_TIMEOUTS = { prompt: 120_000, connect: 15_000 } /** * Open an ssh session (connect + auth + open shell) for `conn`. @@ -107,7 +109,8 @@ export async function connectSsh( try { fingerprint = fingerprintOf(hostKey) status = deps.knownHosts.check(conn.host, conn.port, hostKey).status - } catch { + } catch (err) { + console.error(`[ssh] knownHosts.check threw: ${(err as Error).message}`) fingerprint = fingerprintOf(hostKey) status = 'new' } @@ -287,7 +290,10 @@ function promptUser( */ export function resolveHostKey(promptId: string, action: 'accept' | 'reject'): void { const pending = pendingPrompts.get(promptId) - if (!pending) return + if (!pending) { + console.error(`[ssh] resolveHostKey: no pending prompt ${promptId}`) + return + } clearTimeout(pending.timer) pendingPrompts.delete(promptId) pending.resolve(action === 'accept') diff --git a/src/renderer/src/workspace/Workspace.tsx b/src/renderer/src/workspace/Workspace.tsx index 43ea67e..3ee836d 100644 --- a/src/renderer/src/workspace/Workspace.tsx +++ b/src/renderer/src/workspace/Workspace.tsx @@ -248,9 +248,21 @@ export default function Workspace({ onOpenSettings }: WorkspaceProps): React.JSX [message] ) - const handleConnectRequest = useCallback((conn: SshConnection): void => { - setRequestedConn(conn) - }, []) + const handleConnectRequest = useCallback( + (conn: SshConnection): void => { + // Ask-at-connect connections go through the secret prompt first; saved + // credentials must connect IMMEDIATELY — routing them through ConnectFlow + // would only ever *render* a "connecting" spinner without firing openSession. + const needsPassword = conn.auth === 'password' && conn.askPasswordAtConnect + const needsPassphrase = conn.auth === 'privateKey' && conn.askPassphraseAtConnect + if (needsPassword || needsPassphrase) { + setRequestedConn(conn) + return + } + void connect(conn, {}) + }, + [connect] + ) const handleConnectCancel = useCallback((): void => { setRequestedConn(null)