jw.pkg.*.run_xxx(): Return exit status

Most run_xxx() return stdout and stderr. There's no way, really, for
the caller to get hold of the exit code of the spawned executable. It
can pass throw=true, catch, and assume a non-zero exit status. But
that's not semantically clean, since the spawned function can well be
a test function which is expected to return a non-zero status code,
and the caller might be interested in what code that was, exactly.

The clearest way to solve this is to return the exit code as well.
This commit does that.

Signed-off-by: Jan Lindemann <jan@janware.com>
This commit is contained in:
Jan Lindemann 2026-03-03 08:34:27 +01:00
commit 565946643b
6 changed files with 25 additions and 25 deletions

View file

@ -24,7 +24,7 @@ async def all_installed_packages() -> Iterable[Package]: # export
opts.append('--queryformat')
tag_str = '|'.join([f'%{{{tag}}}' for tag in query_tags]) + r'\n'
opts.append(tag_str)
package_list_str, stderr = await run_rpm(['-qa', *opts], sudo=False)
package_list_str, stderr, status = await run_rpm(['-qa', *opts], sudo=False)
for package_str in package_list_str.splitlines():
tags = package_str.split('|')
ret.append(Package(name=tags[0], vendor=tags[1], packager=tags[2], url=tags[3]))
@ -32,5 +32,5 @@ async def all_installed_packages() -> Iterable[Package]: # export
async def list_files(pkg: str) -> list[str]: # export
opts: list[str] = []
file_list_str, stderr = await run_rpm(['-ql', pkg, *opts], sudo=False)
file_list_str, stderr, status = await run_rpm(['-ql', pkg, *opts], sudo=False)
return file_list_str.splitlines()

View file

@ -115,7 +115,7 @@ class CmdBuild(Cmd): # export
env = await get_profile_env(keep=keep)
try:
stdout, stderr = await run_cmd(
stdout, stderr, status = await run_cmd(
make_cmd,
wd=wd,
throw=True,

View file

@ -35,7 +35,7 @@ class CmdGetAuthInfo(Cmd): # export
if not os.path.isdir(jw_pkg_dir + '/.git'):
log(DEBUG, f'jw-pkg directory is not a Git repo: {jw_pkg_dir}')
return
remotes, stderr = await run_cmd(['git', '-C', jw_pkg_dir, 'remote', '-v'])
remotes, stderr, status = await run_cmd(['git', '-C', jw_pkg_dir, 'remote', '-v'])
result: dict[str, str] = {}
for line in remotes.splitlines():
name, url, typ = re.split(r'\s+', line)

View file

@ -55,7 +55,7 @@ class CmdListRepos(Cmd): # export
cmd_input = (f'-u {username}:{password}').encode('utf-8')
curl_args.extend(['-K-'])
curl_args.append(f'https://api.github.com/users/{args.from_owner}/repos')
repos = await run_curl(curl_args, cmd_input=cmd_input)
repos, stderr, status = await run_curl(curl_args, cmd_input=cmd_input)
for repo in repos:
print(repo['name'])
return
@ -71,7 +71,7 @@ class CmdListRepos(Cmd): # export
api_url = f'{args.base_url}/api/v1/{entities_dir}/{args.from_owner}/repos'
try:
tried.append(api_url)
repos = await run_curl(curl_args + [api_url], cmd_input=cmd_input)
repos, stderr, status = await run_curl(curl_args + [api_url], cmd_input=cmd_input)
for repo in repos:
print(repo['name'])
break

View file

@ -97,5 +97,5 @@ class SSHClientCmd(SSHClient): # export
self.__init_askpass()
cmd_arr = ['ssh']
cmd_arr.append(self.hostname)
stdout, stderr = await run_cmd(['ssh', self.hostname, cmd])
stdout, stderr, status = await run_cmd(['ssh', self.hostname, cmd])
return stdout

View file

@ -76,13 +76,13 @@ async def run_cmd(
def __make_pty_reader(collector: list[bytes], enc_for_verbose: str):
def _read(fd):
data = os.read(fd, 1024)
if not data:
return data
collector.append(data)
ret = os.read(fd, 1024)
if not ret:
return ret
collector.append(ret)
if verbose:
__log(NOTICE, data.decode(enc_for_verbose, errors="replace").rstrip("\n"))
return data
__log(NOTICE, ret.decode(enc_for_verbose, errors="replace").rstrip("\n"))
return ret
return _read
if verbose:
@ -128,16 +128,17 @@ async def run_cmd(
os.environ.update(old_env)
return pty.spawn(args, master_read=reader)
__check_exit_code(await asyncio.to_thread(_spawn))
exit_code = await asyncio.to_thread(_spawn)
__check_exit_code(exit_code)
# PTY merges stdout/stderr
stdout_b = b"".join(stdout_chunks_b) if stdout_chunks_b else None
if want_bytes:
return stdout_b, None
return stdout_b, None, exit_code
stdout_dec_enc = (sys.stdout.encoding or "utf-8") if output_encoding is None else output_encoding
stdout_s = stdout_b.decode(stdout_dec_enc, errors="replace") if stdout_b is not None else None
return stdout_s, None
return stdout_s, None, exit_code
# -- non-interactive mode
stdin = (
@ -197,7 +198,7 @@ async def run_cmd(
stderr_b = b"".join(stderr_parts_b) if stderr_parts_b else None
if want_bytes:
return stdout_b, stderr_b
return stdout_b, stderr_b, exit_code
if output_encoding is None:
stdout_dec_enc = sys.stdout.encoding or "utf-8"
@ -212,7 +213,7 @@ async def run_cmd(
if not want_bytes:
__check_exit_code(exit_code, stdout=stdout_s, stderr=stderr_s)
return stdout_s, stderr_s
return stdout_s, stderr_s, exit_code
finally:
if cwd is not None:
@ -225,10 +226,10 @@ async def run_curl(args: list[str], parse_json: bool=True, wd=None, throw=None,
if not verbose:
cmd.append('-s')
cmd.extend(args)
ret, stderr = await run_cmd(cmd, wd=wd, throw=throw, verbose=verbose, cmd_input=cmd_input)
ret, stderr, status = await run_cmd(cmd, wd=wd, throw=throw, verbose=verbose, cmd_input=cmd_input)
if parse_json:
try:
return json.loads(ret)
ret = json.loads(ret)
except Exception as e:
size = 'unknown number of'
try:
@ -238,7 +239,7 @@ async def run_curl(args: list[str], parse_json: bool=True, wd=None, throw=None,
print(f'Failed to parse {size} bytes output of command '
+ f'>{pretty_cmd(cmd, wd)}< ({str(e)}): "{ret}"', file=sys.stderr)
raise
return ret
return ret, stderr, status
async def run_askpass(askpass_env: list[str], key: AskpassKey, host: str|None=None):
assert host is None # Currently unsupported
@ -260,7 +261,7 @@ async def run_askpass(askpass_env: list[str], key: AskpassKey, host: str|None=No
continue # Can't get user name from SSH_ASKPASS
case AskpassKey.Password:
exe_arg += 'Password'
ret, stderr = await run_cmd([exe, exe_arg], throw=False)
ret, stderr, status = await run_cmd([exe, exe_arg], throw=False)
if ret is not None:
return ret
return None
@ -280,8 +281,7 @@ async def run_sudo(cmd: list[str], mod_env: dict[str, str] = {}, opts: list[str]
cmdline.extend(cmd)
if interactive:
cmd_input = "mode:interactive"
stdout, stderr = await run_cmd(cmdline, throw=True, verbose=verbose, env=env, cmd_input=cmd_input)
return stdout, stderr
return await run_cmd(cmdline, throw=True, verbose=verbose, env=env, cmd_input=cmd_input)
async def get_username(args: Namespace|None=None, url: str|None=None, askpass_env: list[str]=[]) -> str: # export
url_user = None if url is None else urlparse(url).username
@ -329,7 +329,7 @@ async def get_profile_env(throw: bool=True, keep: Iterable[str]|bool=False) -> d
}
# Run bash as a login shell, which sources /etc/profile, then print environment as NUL-separated key=value pairs
cmd = ['/usr/bin/env', '-i', '/bin/bash', '-lc', 'env -0']
stdout, stderr = await run_cmd(cmd, throw=throw, output_encoding="bytes", verbose=True, env=env)
stdout, stderr, status = await run_cmd(cmd, throw=throw, output_encoding="bytes", verbose=True, env=env)
ret: dict[str, str] = {}
for entry in stdout.split(b"\0"):
if not entry: