# # Copyright BitBake Contributors # # SPDX-License-Identifier: GPL-2.0-only # import logging import signal import subprocess import errno import select import bb logger = logging.getLogger('BitBake.Process') def subprocess_setup(): # Python installs a SIGPIPE handler by default. This is usually not what # non-Python subprocesses expect. signal.signal(signal.SIGPIPE, signal.SIG_DFL) class CmdError(RuntimeError): def __init__(self, command, msg=None): self.command = command self.msg = msg def __str__(self): if not isinstance(self.command, str): cmd = subprocess.list2cmdline(self.command) else: cmd = self.command msg = "Execution of '%s' failed" % cmd if self.msg: msg += ': %s' % self.msg return msg class NotFoundError(CmdError): def __str__(self): return CmdError.__str__(self) + ": command not found" class ExecutionError(CmdError): def __init__(self, command, exitcode, stdout = None, stderr = None): CmdError.__init__(self, command) self.exitcode = exitcode self.stdout = stdout self.stderr = stderr self.extra_message = None def __str__(self): message = "" if self.stderr: message += self.stderr if self.stdout: message += self.stdout if message: message = ":\n" + message return (CmdError.__str__(self) + " with exit code %s" % self.exitcode + message + (self.extra_message or "")) class Popen(subprocess.Popen): defaults = { "close_fds": True, "preexec_fn": subprocess_setup, "stdout": subprocess.PIPE, "stderr": subprocess.PIPE, "stdin": subprocess.PIPE, "shell": False, } def __init__(self, *args, **kwargs): options = dict(self.defaults) options.update(kwargs) subprocess.Popen.__init__(self, *args, **options) def _logged_communicate(pipe, log, input, extrafiles): if pipe.stdin: if input is not None: pipe.stdin.write(input) pipe.stdin.close() outdata, errdata = [], [] rin = [] if pipe.stdout is not None: bb.utils.nonblockingfd(pipe.stdout.fileno()) rin.append(pipe.stdout) if pipe.stderr is not None: bb.utils.nonblockingfd(pipe.stderr.fileno()) rin.append(pipe.stderr) for fobj, _ in extrafiles: bb.utils.nonblockingfd(fobj.fileno()) rin.append(fobj) def readextras(selected): for fobj, func in extrafiles: if fobj in selected: try: data = fobj.read() except IOError as err: if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK: data = None if data is not None: func(data) def read_all_pipes(log, rin, outdata, errdata): rlist = rin stdoutbuf = b"" stderrbuf = b"" try: r,w,e = select.select (rlist, [], [], 1) except OSError as e: if e.errno != errno.EINTR: raise readextras(r) if pipe.stdout in r: data = stdoutbuf + pipe.stdout.read() if data is not None and len(data) > 0: try: data = data.decode("utf-8") outdata.append(data) log.write(data) log.flush() stdoutbuf = b"" except UnicodeDecodeError: stdoutbuf = data if pipe.stderr in r: data = stderrbuf + pipe.stderr.read() if data is not None and len(data) > 0: try: data = data.decode("utf-8") errdata.append(data) log.write(data) log.flush() stderrbuf = b"" except UnicodeDecodeError: stderrbuf = data try: # Read all pipes while the process is open while pipe.poll() is None: read_all_pipes(log, rin, outdata, errdata) # Process closed, drain all pipes... read_all_pipes(log, rin, outdata, errdata) finally: log.flush() if pipe.stdout is not None: pipe.stdout.close() if pipe.stderr is not None: pipe.stderr.close() return ''.join(outdata), ''.join(errdata) def run(cmd, input=None, log=None, extrafiles=None, **options): """Convenience function to run a command and return its output, raising an exception when the command fails""" if not extrafiles: extrafiles = [] if isinstance(cmd, str) and not "shell" in options: options["shell"] = True try: pipe = Popen(cmd, **options) except OSError as exc: if exc.errno == 2: raise NotFoundError(cmd) else: raise CmdError(cmd, exc) if log: stdout, stderr = _logged_communicate(pipe, log, input, extrafiles) else: stdout, stderr = pipe.communicate(input) if not stdout is None: stdout = stdout.decode("utf-8") if not stderr is None: stderr = stderr.decode("utf-8") if pipe.returncode != 0: if log: # Don't duplicate the output in the exception if logging it raise ExecutionError(cmd, pipe.returncode, None, None) raise ExecutionError(cmd, pipe.returncode, stdout, stderr) return stdout, stderr