From d0776db01f87b11152663aa08ac5fcc13723468e Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Tue, 17 Mar 2026 14:45:18 +0100 Subject: [PATCH] lib.SSHClient.run_cmd(): Accept cmd: list[str] Make SSHClient accept a list of strings for the cmd argument to align with the other run_cmd() functions in jw-pkg. Signed-off-by: Jan Lindemann --- .../jw/pkg/cmds/projects/CmdListRepos.py | 6 +-- src/python/jw/pkg/lib/SSHClient.py | 50 ++++++++++++++----- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/python/jw/pkg/cmds/projects/CmdListRepos.py b/src/python/jw/pkg/cmds/projects/CmdListRepos.py index fa8811f9..5dc5af2a 100644 --- a/src/python/jw/pkg/cmds/projects/CmdListRepos.py +++ b/src/python/jw/pkg/cmds/projects/CmdListRepos.py @@ -38,9 +38,9 @@ class CmdListRepos(Cmd): # export ssh.set_username(username) if password is not None: ssh.set_password(password) - cmd = f'/opt/jw-pkg/bin/git-srv-admin.sh -u {args.from_owner} -j list-personal-projects' - out = await ssh.run_cmd(cmd) - print(out) + cmd = ['/opt/jw-pkg/bin/git-srv-admin.sh', '-u', args.from_owner, '-j', 'list-personal-projects'] + stdout, stderr, code = await ssh.run_cmd(cmd) + print(stdout) return case 'https': cmd_input = None diff --git a/src/python/jw/pkg/lib/SSHClient.py b/src/python/jw/pkg/lib/SSHClient.py index 873f563f..59e40773 100644 --- a/src/python/jw/pkg/lib/SSHClient.py +++ b/src/python/jw/pkg/lib/SSHClient.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- -import os, abc +from typing import Any + +import os, abc, shlex, sys from .util import run_cmd +from .ExecContext import Result class SSHClient(abc.ABC): @@ -16,12 +19,10 @@ class SSHClient(abc.ABC): return self.__hostname def set_password(self, password: str) -> None: - assert password != 'jan' self.__password = password @property def password(self) -> str: - assert self.__password != 'jan' return self.__password def set_username(self, username: str) -> None: @@ -32,20 +33,37 @@ class SSHClient(abc.ABC): return self.__username @abc.abstractmethod - async def run_cmd(self, cmd: str): + async def _run_cmd(self, cmd: list[str]) -> Result: pass + async def run_cmd( + self, + cmd: list[str], + output_encoding: str|None = None, + ) -> Result: + stdout_b, stderr_b, status = await self._run_cmd(cmd) + if output_encoding == 'bytes': + return stdout_b, stderr_b, status + + if output_encoding is None: + 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 def __init__(self, hostname: str) -> None: super().__init__(hostname=hostname) + self.__timeout: float|None = None # Untested + self.___ssh: Any|None = None def __ssh_connect(self): import paramiko # type: ignore # error: Library stubs not installed for "paramiko" ret = paramiko.SSHClient() ret.set_missing_host_key_policy(paramiko.AutoAddPolicy()) path_to_key=os.path.join(os.environ['HOME'], '.ssh', 'id_rsa') - ret.connect(self.__hostname, key_filename=path_to_key, allow_agent=True) + ret.connect(self.hostname, key_filename=path_to_key, allow_agent=True) s = ret.get_transport().open_session() # set up the agent request handler to handle agent requests from the server paramiko.agent.AgentRequestHandler(s) @@ -54,15 +72,17 @@ class SSHClientInternal(SSHClient): # export @property def __ssh(self): if self.___ssh is None: - self.___ssh = self.__ssh_connect(self.__server) + self.___ssh = self.__ssh_connect() return self.___ssh @property def __scp(self): return SCPClient(self.__ssh.get_transport()) - async def run_cmd(self, cmd: str): - return self.__ssh.exec_command(find_cmd) + async def _run_cmd(self, cmd: list[str]) -> Result: + stdin, stdout, stderr = self.__ssh.exec_command(shlex.join(cmd), timeout=self.__timeout) + exit_status = stdout.channel.recv_exit_status() + return stdout.read(), stderr.read(), exit_status class SSHClientCmd(SSHClient): # export @@ -93,9 +113,13 @@ class SSHClientCmd(SSHClient): # export self.__askpass_orig[key] = os.getenv(key) os.environ[key] = val - async def run_cmd(self, cmd: str): + async def _run_cmd(self, cmd: list[str]) -> Result: self.__init_askpass() - cmd_arr = ['ssh'] - cmd_arr.append(self.hostname) - stdout, stderr, status = await run_cmd(['ssh', self.hostname, cmd]) - return stdout + return await run_cmd(['ssh', self.hostname, shlex.join(cmd)], output_encoding='bytes') + +def ssh_client(*args, **kwargs) -> SSHClient: # export + try: + return SSHClientInternal(*args, **kwargs) + except: + pass + return SSHClientCmd(*args, **kwargs)