95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""
|
|
Run commands and handle returncodes
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from typing import Callable, List, Optional, TextIO
|
|
|
|
from .filesystem import pushd
|
|
from .logging import get_logger
|
|
|
|
|
|
def do_command(
|
|
cmd: List[str],
|
|
cwd: Optional[str] = None,
|
|
*,
|
|
fd_out: Optional[TextIO] = sys.stdout,
|
|
pbar: Optional[Callable[[str], None]] = None,
|
|
) -> None:
|
|
"""
|
|
Run command as subprocess, polls process output pipes and
|
|
either streams outputs to supplied output stream or sends
|
|
each line (stripped) to the supplied progress bar callback hook.
|
|
|
|
Raises ``RuntimeError`` on non-zero return code or execption ``OSError``.
|
|
|
|
:param cmd: command and args.
|
|
:param cwd: directory in which to run command, if unspecified,
|
|
run command in the current working directory.
|
|
:param fd_out: when supplied, streams to this output stream,
|
|
else writes to sys.stdout.
|
|
:param pbar: optional callback hook to tqdm, which takes
|
|
single ``str`` arguent, see:
|
|
https://github.com/tqdm/tqdm#hooks-and-callbacks.
|
|
|
|
"""
|
|
get_logger().debug('cmd: %s\ncwd: %s', ' '.join(cmd), cwd)
|
|
try:
|
|
# NB: Using this rather than cwd arg to Popen due to windows behavior
|
|
with pushd(cwd if cwd is not None else '.'):
|
|
# TODO: replace with subprocess.run in later Python versions?
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
bufsize=1,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT, # avoid buffer overflow
|
|
env=os.environ,
|
|
universal_newlines=True,
|
|
)
|
|
while proc.poll() is None:
|
|
if proc.stdout is not None:
|
|
line = proc.stdout.readline()
|
|
if fd_out is not None:
|
|
fd_out.write(line)
|
|
if pbar is not None:
|
|
pbar(line.strip())
|
|
|
|
stdout, _ = proc.communicate()
|
|
if stdout:
|
|
if len(stdout) > 0:
|
|
if fd_out is not None:
|
|
fd_out.write(stdout)
|
|
if pbar is not None:
|
|
pbar(stdout.strip())
|
|
|
|
if proc.returncode != 0: # throw RuntimeError + msg
|
|
serror = ''
|
|
try:
|
|
serror = os.strerror(proc.returncode)
|
|
except (ArithmeticError, ValueError):
|
|
pass
|
|
msg = 'Command {}\n\t{} {}'.format(
|
|
cmd, returncode_msg(proc.returncode), serror
|
|
)
|
|
raise RuntimeError(msg)
|
|
except OSError as e:
|
|
msg = 'Command: {}\nfailed with error {}\n'.format(cmd, str(e))
|
|
raise RuntimeError(msg) from e
|
|
|
|
|
|
def returncode_msg(retcode: int) -> str:
|
|
"""interpret retcode"""
|
|
if retcode < 0:
|
|
sig = -1 * retcode
|
|
return f'terminated by signal {sig}'
|
|
if retcode <= 125:
|
|
return 'error during processing'
|
|
if retcode == 126: # shouldn't happen
|
|
return ''
|
|
if retcode == 127:
|
|
return 'program not found'
|
|
sig = retcode - 128
|
|
return f'terminated by signal {sig}'
|