diff --git a/src/python/jw/pkg/lib/ExecContext.py b/src/python/jw/pkg/lib/ExecContext.py index 398a5d9a..b3a103ee 100644 --- a/src/python/jw/pkg/lib/ExecContext.py +++ b/src/python/jw/pkg/lib/ExecContext.py @@ -313,7 +313,7 @@ class ExecContext(Base): await self.open() try: - ret = Result() + ret = Result(cmd = cmd) with self.CallContext( self, title = title, @@ -421,7 +421,7 @@ class ExecContext(Base): # be returned by CallContext and is very much allowed assert cmd_input is not None, 'Invalid: cmd_input is None' - ret = Result() + ret = Result(cmd = cmd) with self.CallContext( self, title = title, diff --git a/src/python/jw/pkg/lib/Result.py b/src/python/jw/pkg/lib/Result.py index 7a72ff59..aea6223b 100644 --- a/src/python/jw/pkg/lib/Result.py +++ b/src/python/jw/pkg/lib/Result.py @@ -28,6 +28,64 @@ class Result: return ret.strip() return ret + def __try_decode( + self, + stdxxx: bytes | None, + quote = False, + truncate: int | None = None, + annotate: bool = True, + label: str | None = None, + ) -> str: + if label is None: + label = '' + else: + label = f'{label}: ' + if stdxxx is None: + return f'{label}None' + try: + ret = stdxxx.decode()[:truncate].strip() + except UnicodeDecodeError: + chunk = stdxxx[:truncate] + ret = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in chunk) + if (not annotate) or truncate is None or len(stdxxx) <= truncate: + return f'{label}"{ret}"' if quote else label + ret + ret = '{labe}"{ret} ..."' if quote else '{labe}{ret} ...' + ret += f' + {len(stdxxx) - truncate} more bytes' + return ret + + def __summarize( + self, + cmd: list[str] | None = None, + wd: str | None = None, + verbose = True + ) -> str: + if not verbose: + ret = f'{self.__status}: ' + else: + from .util import pretty_cmd + if cmd is None: + cmd = self.__cmd + call = '' + if cmd is not None: + if wd is None: + wd = self.__wd + call = f'"{pretty_cmd(cmd, wd)}" ' + ret = ( + f'Command {call}has not yet exited' if self.__status is None else + f'Command {call}has exited with status {self.__status}' + ) + label, stdxxx, truncate = ( + ('stdout', self.__stdout, 40) if self.status == 0 + else ('stderr', self.__stderr, None) + ) + ret += self.__try_decode( + stdxxx, quote = True, truncate = truncate, annotate = True, label = label + ) + return ret + + def __repr__(self) -> str: + return self.__summarize(verbose = False) + @property def status(self) -> int | None: return self.__status @@ -40,29 +98,6 @@ class Result: def encoding(self, value: str) -> None: self.__encoding = value - def __stdout_footprint(self, quote = False) -> str: - if self.__stdout is None: - ret = '' - else: - max_len = 40 - try: - ret = self.stdout_str[:max_len] - except UnicodeDecodeError: - chunk = self.__stdout[:max_len] - ret = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in chunk) - if quote: - ret = f'"{ret}"' - return ret - - def __repr__(self) -> str: - ret = f'{self.__status}:' - if self.__status is not None: - if self.status != 0: - ret += f' err: {self.stderr_str_or_none}' - else: - ret += f' out: {self.__stdout_footprint(quote=True)}' - return ret - @property def strip(self) -> bool: return self.__strip @@ -96,29 +131,6 @@ class Result: import re return re.search(pattern, err) is not None - def __summarize(self, cmd: list[str] | None, wd: str | None = None) -> str: - from .util import pretty_cmd - if cmd is None: - cmd = self.__cmd - call = '' - if cmd is not None: - if wd is None: - wd = self.__wd - call = f'"{pretty_cmd(cmd, wd)}" ' - ret = ( - f'Command {call}has not yet exited' if self.__status is None else - f'Command {call}has exited with status {self.__status}' - ) - call = pretty_cmd(cmd, wd) - if self.status != 0: - ret += f' -> stderr="{self.__stderr!r}"' - else: - if self.__stdout: - ret += f' -> stdout has {len(self.__stdout)} bytes' - else: - ret += ' -> stdout = None' - return ret - def summarize(self, cmd: list[str] | None = None, wd: str | None = None) -> str: return self.__summarize(cmd, wd) diff --git a/src/python/jw/pkg/lib/ec/Local.py b/src/python/jw/pkg/lib/ec/Local.py index 7466c4c8..7ebb6fa5 100644 --- a/src/python/jw/pkg/lib/ec/Local.py +++ b/src/python/jw/pkg/lib/ec/Local.py @@ -83,7 +83,7 @@ class Local(Base): # PTY merges stdout/stderr stdout = b''.join(stdout_chunks) if stdout_chunks else None - return Result(stdout, None, exit_code) + return Result(stdout, None, exit_code, cmd = cmd) # -- non-interactive mode @@ -149,7 +149,7 @@ class Local(Base): stdout = b''.join(stdout_parts) if stdout_parts else None stderr = b''.join(stderr_parts) if stderr_parts else None - return Result(stdout, stderr, exit_code) + return Result(stdout, stderr, exit_code, cmd = cmd) finally: if cwd is not None: diff --git a/src/python/jw/pkg/lib/ec/ssh/AsyncSSH.py b/src/python/jw/pkg/lib/ec/ssh/AsyncSSH.py index 9ac413f5..eda9e4bb 100644 --- a/src/python/jw/pkg/lib/ec/ssh/AsyncSSH.py +++ b/src/python/jw/pkg/lib/ec/ssh/AsyncSSH.py @@ -273,7 +273,7 @@ class AsyncSSH(Base): ) stdout = b''.join(stdout_parts) if stdout_parts else None - return Result(stdout, None, exit_code) + return Result(stdout, None, exit_code, cmd = cmd) finally: if stdin_reader_installed: @@ -353,7 +353,7 @@ class AsyncSSH(Base): exit_code = completed.returncode if completed.returncode is not None else -1 stdout = b''.join(stdout_parts) if stdout_parts else None - return Result(stdout, None, exit_code) + return Result(stdout, None, exit_code, cmd = cmd) async def _run_ssh( self, @@ -446,7 +446,7 @@ class AsyncSSH(Base): completed.returncode if completed.returncode is not None else -1 ) - return Result(stdout, stderr, exit_code) + return Result(stdout, stderr, exit_code, cmd = cmd) except Exception as e: log(ERR, f'Failed to run command {" ".join(cmd)} ({e})') diff --git a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py index 0ae7e67f..018b6796 100644 --- a/src/python/jw/pkg/lib/ec/ssh/Paramiko.py +++ b/src/python/jw/pkg/lib/ec/ssh/Paramiko.py @@ -84,4 +84,4 @@ class Paramiko(Base): if cmd_input is not None: stdin.write(cmd_input) exit_status = stdout.channel.recv_exit_status() - return Result(stdout.read(), stderr.read(), exit_status) + return Result(stdout.read(), stderr.read(), exit_status, cmd = cmd)