fix(desktop): log what ssh did when an SSH connect fails
SshConnection.open() handed the classified error to its caller and logged nothing about the failure. desktop.log showed only "connecting" and then "connection closed", and the renderer shows only friendly copy for the error kind. A connect that dies right after TCP setup (#80836) therefore left no exit code, close signal or stderr anywhere. Log them, redacted, for both the mux and no-mux connect paths.
This commit is contained in:
@@ -567,6 +567,39 @@ test('no-mux: open() classifies auth failure', async () => {
|
||||
await assert.rejects(conn.open(), (err: any) => err.kind === 'auth-failed')
|
||||
})
|
||||
|
||||
test('open() records what the failed ssh did in the desktop log (#80836)', async () => {
|
||||
// The renderer only shows friendly copy for the kind, so the log is the one
|
||||
// place a connect that dies right after TCP setup can be diagnosed from.
|
||||
for (const mux of [false, true]) {
|
||||
const logs: string[] = []
|
||||
|
||||
const spawnFn = scriptedSpawn(args =>
|
||||
args.includes('check') ? { code: 255, stderr: 'no control path' } : { signal: 'SIGTERM', stderr: '' }
|
||||
)
|
||||
|
||||
const controlDir = path.join(os.tmpdir(), `hermes-ssh-connect-log-${process.pid}-${Date.now()}`)
|
||||
|
||||
const conn = new SshConnection(
|
||||
{ host: 'box', user: 'me' },
|
||||
{ spawnFn, mux, controlDir, rememberLog: line => logs.push(line) }
|
||||
)
|
||||
|
||||
await assert.rejects(conn.open())
|
||||
fs.rmSync(controlDir, { recursive: true, force: true })
|
||||
assert.ok(
|
||||
logs.some(line => /connect to me@box:22 failed \(kind=unknown, exit=null, signal=SIGTERM\): \(empty\)/.test(line)),
|
||||
`mux=${mux}: ${logs.join(' | ')}`
|
||||
)
|
||||
}
|
||||
|
||||
const logs: string[] = []
|
||||
const spawnFn = scriptedSpawn([{ code: 255, stderr: 'me@box: Permission denied (publickey).' }])
|
||||
const conn = new SshConnection({ host: 'box', user: 'me' }, { spawnFn, mux: false, rememberLog: line => logs.push(line) })
|
||||
|
||||
await assert.rejects(conn.open())
|
||||
assert.ok(logs.some(line => /failed \(kind=auth-failed, exit=255, signal=none\): me@box: Permission denied/.test(line)))
|
||||
})
|
||||
|
||||
test('runSsh keeps Node close signal on the result', async () => {
|
||||
const spawnFn = () => fakeChild({ signal: 'SIGTERM', stderr: '' })
|
||||
const result: any = await runSsh(['box'], { timeoutMs: 5000, spawnFn })
|
||||
|
||||
@@ -736,6 +736,26 @@ class SshConnection {
|
||||
return err
|
||||
}
|
||||
|
||||
// The classified error only reaches the caller (the renderer shows friendly
|
||||
// copy for its kind), so record what ssh actually did — exit code, close
|
||||
// signal, stderr — in desktop.log. Without it a connect that dies right after
|
||||
// TCP setup leaves nothing to diagnose it by (#80836).
|
||||
_connectFailed(raw) {
|
||||
const err = this._fail(raw, SSH_ERROR.UNREACHABLE)
|
||||
|
||||
if (err?.kind !== 'superseded') {
|
||||
const code = raw && typeof raw === 'object' && 'code' in raw ? String(raw.code) : '?'
|
||||
const stderr = String(sshCloseStderr(raw)).trim().slice(-500) || '(empty)'
|
||||
|
||||
this._logLine(
|
||||
`connect to ${target(this.user, this.host)}:${this.port} failed ` +
|
||||
`(kind=${err.kind}, exit=${code}, signal=${sshCloseSignal(raw) || 'none'}): ${stderr}`
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Open the connection. Mux: start the persistent ControlMaster (idempotent —
|
||||
// a live master is a no-op). No-mux: there is no master; validate auth +
|
||||
// reachability with a one-shot `ssh true` so failures classify identically.
|
||||
@@ -788,11 +808,11 @@ class SshConnection {
|
||||
signal
|
||||
})
|
||||
} catch (error) {
|
||||
throw this._fail(error, SSH_ERROR.UNREACHABLE)
|
||||
throw this._connectFailed(error)
|
||||
}
|
||||
|
||||
if (!sshCloseOk(result)) {
|
||||
throw this._fail(result, SSH_ERROR.UNREACHABLE)
|
||||
throw this._connectFailed(result)
|
||||
}
|
||||
|
||||
this._opened = true
|
||||
@@ -808,11 +828,11 @@ class SshConnection {
|
||||
try {
|
||||
result = await runSsh(args, { timeoutMs: this._connectTimeoutMs, spawnFn: this._spawnFn, signal })
|
||||
} catch (error) {
|
||||
throw this._fail(error, SSH_ERROR.UNREACHABLE)
|
||||
throw this._connectFailed(error)
|
||||
}
|
||||
|
||||
if (!sshCloseOk(result)) {
|
||||
throw this._fail(result, SSH_ERROR.UNREACHABLE)
|
||||
throw this._connectFailed(result)
|
||||
}
|
||||
|
||||
this._opened = true
|
||||
|
||||
Reference in New Issue
Block a user