From 4b55b460eaffb35466b614868682fa4f2a545c4e Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 16 Aug 2026 07:01:55 +0200 Subject: [PATCH 1/3] lib.ec.ssh.Paramiko: Pass port and password __client() connects using only the URI's hostname and username. The URI's port is ignored, so ssh://host:2222/... ends up connecting to port 22, and a password carried in the URI is never passed to paramiko, so URI-based password authentication cannot work. The Exec and AsyncSSH clients both honor the port and the password. Pass the port and the password to connect(). The port argument is omitted entirely when the URI carries no port, because getaddrinfo() would interpret a None port as service port 0; without the argument, paramiko falls back to its default of 22. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/ec/ssh/Paramiko.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py index 84ac0d9e..0af7af48 100644 --- a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py +++ b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py @@ -32,10 +32,16 @@ class Paramiko(Base): hostname = self.hostname if hostname is None: raise Exception('Tried to run connect without target hostname') + kwargs: dict[str, Any] = { + 'hostname': hostname, + 'username': self.username, + 'password': self.password, + 'allow_agent': True, + } + if self.port is not None: + kwargs['port'] = self.port try: - ret.connect( - hostname = hostname, username = self.username, allow_agent = True - ) + ret.connect(**kwargs) except Exception as e: log(ERR, f'Failed to connect to {self.hostname} ({str(e)})') raise -- 2.55.0 From 4ca370a8772e5657ca9949e797b7c33214568357 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 16 Aug 2026 07:02:25 +0200 Subject: [PATCH 2/3] lib.ec.ssh.Paramiko: Close remote stdin _run_ssh() writes cmd_input to the remote stdin channel but never closes the write side. The channel stays open until the client process exits, so a remote command that reads stdin (cat, a login shell, ...) never sees EOF and blocks forever - including for cmd_input = None, which is supposed to mean non-interactive with stdin from /dev/null. Call shutdown_write() on the channel after writing the input, or immediately when there is none, so the remote command gets EOF. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/ec/ssh/Paramiko.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py index 0af7af48..b85b20f4 100644 --- a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py +++ b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py @@ -94,5 +94,6 @@ class Paramiko(Base): raise if cmd_input is not None: stdin.write(cmd_input) + stdin.channel.shutdown_write() exit_status = stdout.channel.recv_exit_status() return Result(stdout.read(), stderr.read(), exit_status, cmd = cmd) -- 2.55.0 From e6295f7d2b3902f71a7480b3e6f616664932788f Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 16 Aug 2026 07:02:54 +0200 Subject: [PATCH 3/3] lib.ec.ssh.Paramiko: Avoid recv deadlock _run_ssh() waits for the remote process's exit status before reading stdout and stderr. When a command produces more output than the channel's flow-control window can hold, the server stops sending, the remote process blocks on its write and never exits, and recv_exit_status() blocks forever. Drain stdout and stderr to EOF first - the channels close when the process exits, so reading them also implies completion - and only then query the exit status. Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/ec/ssh/Paramiko.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py index b85b20f4..d1171d6f 100644 --- a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py +++ b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py @@ -95,5 +95,7 @@ class Paramiko(Base): if cmd_input is not None: stdin.write(cmd_input) stdin.channel.shutdown_write() + stdout_data = stdout.read() + stderr_data = stderr.read() exit_status = stdout.channel.recv_exit_status() - return Result(stdout.read(), stderr.read(), exit_status, cmd = cmd) + return Result(stdout_data, stderr_data, exit_status, cmd = cmd) -- 2.55.0