Files
amnezia-web-panel/tests/test_ssh_error_text.py
T
hell 2cb442fe04 fix(ssh): say what happened when a command dies with the transport
Paramiko raises a bare EOFError/SSHException when the transport goes away
mid-command, and `str(exc)` on those is an empty string. run_command passes
that empty string on as the failure reason, and the managers interpolate it
straight into their messages - about 30 `{err}` sites across managers/ - so a
dropped connection surfaces as "Failed to configure container: " with nothing
after the colon, and in the log as "Command exited with code -1: ".

The SSH layer now falls back to the exception type when the message is empty,
and labels the read failure explicitly: "SSH connection lost while running the
command (EOFError)". Three call sites: the two reconnect paths and the
stdout/stderr read.

Seen in practice where a panel manages a host that rate-limits SSH (ufw's
`limit` on port 22): a burst of connections gets dropped mid-command and every
resulting error message was blank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 22:25:44 +07:00

52 lines
1.6 KiB
Python

"""An SSH failure must say what happened.
Paramiko raises bare `EOFError()` / `SSHException()` when the transport dies
mid-command, and `str(exc)` on those is an empty string. That empty string is
what the managers interpolate into their failure messages - there are ~30
`{err}` sites across managers/ - so a dropped connection surfaced as
"Failed to configure container: " with nothing after the colon.
"""
import unittest
from unittest import mock
from managers.ssh_manager import SSHManager
class DeadChannel:
def settimeout(self, timeout):
pass
def recv_exit_status(self):
raise EOFError() # exactly what a dropped transport raises
class DeadStream:
def __init__(self):
self.channel = DeadChannel()
def read(self):
return b''
class ReasonTests(unittest.TestCase):
def test_reason_falls_back_to_the_exception_type(self):
self.assertEqual(SSHManager._reason(EOFError()), 'EOFError')
self.assertEqual(SSHManager._reason(RuntimeError(' ')), 'RuntimeError')
self.assertEqual(SSHManager._reason(RuntimeError('boom')), 'boom')
def test_a_transport_dying_mid_command_is_reported(self):
ssh = SSHManager.__new__(SSHManager)
ssh.client = mock.Mock()
ssh.client.exec_command.return_value = (None, DeadStream(), DeadStream())
ssh.ensure_connected = lambda: None
out, err, code = ssh._run_command_locked('whoami', 10, False)
self.assertEqual((out, code), ('', -1))
self.assertIn('SSH connection lost', err)
self.assertIn('EOFError', err)
if __name__ == '__main__':
unittest.main()