__all__ = ( "CommandError", "CommandLogger", "CommandExecutor", ) import logging import os import subprocess import threading from pathlib import Path logger = logging.getLogger("command") class CommandError(RuntimeError): @property def output(self): return self.args[0] class CommandLogger(threading.Thread): def __init__(self, level: int): super().__init__() self.daemon = False self.level = level self._read_fd, self._write_fd = os.pipe() self._reader_pipe = os.fdopen(self._read_fd) self.lines = [] self.start() def fileno(self): return self._write_fd def run(self): for line in iter(self._reader_pipe.readline, ""): logger.log(self.level, line.strip("\n")) self.lines.append(line) self._reader_pipe.close() def close(self): os.close(self._write_fd) class CommandExecutor: _logger = logger def __init__(self, command: str): self._command = command def run(self, basepath: Path): self._logger.debug(self._command) out_logger = CommandLogger(logging.TRACE) err_logger = CommandLogger(logging.TRACE) try: result = subprocess.run( self._command.split(" "), cwd=basepath, stdout=out_logger, # type: ignore stderr=err_logger, # type: ignore ) finally: out_logger.close() err_logger.close() if result.returncode != 0: raise CommandError("".join(err_logger.lines or out_logger.lines))