""" 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}'