From 989e2c93e37a5d3f0acd274d328993c50a7f3488 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 18 Mar 2026 05:14:07 +0100 Subject: [PATCH] lib.SSHClient.run_cmd(): Align prototype with EC Align the prototype of SSHClient.run_cmd() to ExecContext.run(). This is a push towards making the SSHClient code an ExceContext, too. Some arguments still log a warning or outright raise NotImplementedError. Signed-off-by: Jan Lindemann --- src/python/jw/pkg/lib/SSHClient.py | 45 ++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/python/jw/pkg/lib/SSHClient.py b/src/python/jw/pkg/lib/SSHClient.py index 59e40773..afa45237 100644 --- a/src/python/jw/pkg/lib/SSHClient.py +++ b/src/python/jw/pkg/lib/SSHClient.py @@ -5,6 +5,7 @@ from typing import Any import os, abc, shlex, sys from .util import run_cmd +from .log import * from .ExecContext import Result class SSHClient(abc.ABC): @@ -38,10 +39,38 @@ class SSHClient(abc.ABC): async def run_cmd( self, - cmd: list[str], - output_encoding: str|None = None, + args: list[str], + wd: str|None = None, + throw: bool = True, + verbose: bool = False, + cmd_input: str|None = None, + env: dict[str, str]|None = None, + title: str=None, + output_encoding: str|None = None, # None => unchanged; "bytes" => return raw bytes ) -> Result: - stdout_b, stderr_b, status = await self._run_cmd(cmd) + + if wd is not None: + args = ['cd', wd, '&&', *args] + + if verbose: + log(WARNING, f'Verbose SSH commands are not yet implemented') + + interactive = ( + cmd_input == "mode:interactive" + or (cmd_input == "mode:auto" and sys.stdin.isatty()) + ) + + if interactive: + raise NotImplementedError('Interactive SSH is not yet implemented') + + if env is not None: + raise NotImplementedError('Passing an environment to SSH commands is not yet implemented') + + stdout_b, stderr_b, status = await self._run_cmd(args, cmd_input=cmd_input) + + if throw and status: + raise Exception(f'SSH command returned error {status}') + if output_encoding == 'bytes': return stdout_b, stderr_b, status @@ -49,6 +78,7 @@ class SSHClient(abc.ABC): output_encoding = sys.stdout.encoding or "utf-8" stdout_s = stdout_b.decode(output_encoding, errors="replace") if stdout_b is not None else None stderr_s = stderr_b.decode(output_encoding, errors="replace") if stderr_b is not None else None + return stdout_s, stderr_s, status class SSHClientInternal(SSHClient): # export @@ -79,8 +109,10 @@ class SSHClientInternal(SSHClient): # export def __scp(self): return SCPClient(self.__ssh.get_transport()) - async def _run_cmd(self, cmd: list[str]) -> Result: + async def _run_cmd(self, cmd: list[str], cmd_input: str|None) -> Result: stdin, stdout, stderr = self.__ssh.exec_command(shlex.join(cmd), timeout=self.__timeout) + if cmd_input is not None: + stdin.write(cmd_input) exit_status = stdout.channel.recv_exit_status() return stdout.read(), stderr.read(), exit_status @@ -113,9 +145,10 @@ class SSHClientCmd(SSHClient): # export self.__askpass_orig[key] = os.getenv(key) os.environ[key] = val - async def _run_cmd(self, cmd: list[str]) -> Result: + async def _run_cmd(self, cmd: list[str], cmd_input: str|None) -> Result: self.__init_askpass() - return await run_cmd(['ssh', self.hostname, shlex.join(cmd)], output_encoding='bytes') + return await run_cmd(['ssh', self.hostname, shlex.join(cmd)], + output_encoding='bytes', cmd_input=cmd_input) def ssh_client(*args, **kwargs) -> SSHClient: # export try: