import errno import os import pty import selectors import subprocess MAX_ERROR_OUTPUT_BYTES = 8192 def _read_chunk(fd, treat_eio_as_eof=False): try: return os.read(fd, 4096) except OSError as exc: if treat_eio_as_eof and exc.errno == errno.EIO: return b"" raise def _stream_output(fd, process, reporter, treat_eio_as_eof=False): selector = selectors.DefaultSelector() recent_output = bytearray() try: selector.register(fd, selectors.EVENT_READ) while selector.get_map(): for key, _ in selector.select(): data = _read_chunk(key.fileobj, treat_eio_as_eof=treat_eio_as_eof) if not data: selector.unregister(key.fileobj) os.close(key.fileobj) continue reporter._clear() os.write(1, data) reporter._render() recent_output.extend(data) if len(recent_output) > MAX_ERROR_OUTPUT_BYTES: del recent_output[:-MAX_ERROR_OUTPUT_BYTES] finally: selector.close() return_code = process.wait() if return_code != 0: raise subprocess.CalledProcessError(return_code, process.args, output=bytes(recent_output)) def run_command_with_reporter(cmd, cwd=None, reporter=None): if reporter is None: subprocess.run(cmd, cwd=cwd, check=True) return try: master_fd, slave_fd = pty.openpty() except OSError: process = subprocess.Popen( cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) assert process.stdout is not None _stream_output(process.stdout.fileno(), process, reporter) return try: process = subprocess.Popen( cmd, cwd=cwd, stdout=slave_fd, stderr=slave_fd, ) finally: os.close(slave_fd) _stream_output(master_fd, process, reporter, treat_eio_as_eof=True)