1# 2# SPDX-License-Identifier: GPL-2.0-only 3# 4 5import logging 6import signal 7import subprocess 8import errno 9import select 10import bb 11 12logger = logging.getLogger('BitBake.Process') 13 14def subprocess_setup(): 15 # Python installs a SIGPIPE handler by default. This is usually not what 16 # non-Python subprocesses expect. 17 signal.signal(signal.SIGPIPE, signal.SIG_DFL) 18 19class CmdError(RuntimeError): 20 def __init__(self, command, msg=None): 21 self.command = command 22 self.msg = msg 23 24 def __str__(self): 25 if not isinstance(self.command, str): 26 cmd = subprocess.list2cmdline(self.command) 27 else: 28 cmd = self.command 29 30 msg = "Execution of '%s' failed" % cmd 31 if self.msg: 32 msg += ': %s' % self.msg 33 return msg 34 35class NotFoundError(CmdError): 36 def __str__(self): 37 return CmdError.__str__(self) + ": command not found" 38 39class ExecutionError(CmdError): 40 def __init__(self, command, exitcode, stdout = None, stderr = None): 41 CmdError.__init__(self, command) 42 self.exitcode = exitcode 43 self.stdout = stdout 44 self.stderr = stderr 45 self.extra_message = None 46 47 def __str__(self): 48 message = "" 49 if self.stderr: 50 message += self.stderr 51 if self.stdout: 52 message += self.stdout 53 if message: 54 message = ":\n" + message 55 return (CmdError.__str__(self) + 56 " with exit code %s" % self.exitcode + message + (self.extra_message or "")) 57 58class Popen(subprocess.Popen): 59 defaults = { 60 "close_fds": True, 61 "preexec_fn": subprocess_setup, 62 "stdout": subprocess.PIPE, 63 "stderr": subprocess.PIPE, 64 "stdin": subprocess.PIPE, 65 "shell": False, 66 } 67 68 def __init__(self, *args, **kwargs): 69 options = dict(self.defaults) 70 options.update(kwargs) 71 subprocess.Popen.__init__(self, *args, **options) 72 73def _logged_communicate(pipe, log, input, extrafiles): 74 if pipe.stdin: 75 if input is not None: 76 pipe.stdin.write(input) 77 pipe.stdin.close() 78 79 outdata, errdata = [], [] 80 rin = [] 81 82 if pipe.stdout is not None: 83 bb.utils.nonblockingfd(pipe.stdout.fileno()) 84 rin.append(pipe.stdout) 85 if pipe.stderr is not None: 86 bb.utils.nonblockingfd(pipe.stderr.fileno()) 87 rin.append(pipe.stderr) 88 for fobj, _ in extrafiles: 89 bb.utils.nonblockingfd(fobj.fileno()) 90 rin.append(fobj) 91 92 def readextras(selected): 93 for fobj, func in extrafiles: 94 if fobj in selected: 95 try: 96 data = fobj.read() 97 except IOError as err: 98 if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK: 99 data = None 100 if data is not None: 101 func(data) 102 103 def read_all_pipes(log, rin, outdata, errdata): 104 rlist = rin 105 stdoutbuf = b"" 106 stderrbuf = b"" 107 108 try: 109 r,w,e = select.select (rlist, [], [], 1) 110 except OSError as e: 111 if e.errno != errno.EINTR: 112 raise 113 114 readextras(r) 115 116 if pipe.stdout in r: 117 data = stdoutbuf + pipe.stdout.read() 118 if data is not None and len(data) > 0: 119 try: 120 data = data.decode("utf-8") 121 outdata.append(data) 122 log.write(data) 123 log.flush() 124 stdoutbuf = b"" 125 except UnicodeDecodeError: 126 stdoutbuf = data 127 128 if pipe.stderr in r: 129 data = stderrbuf + pipe.stderr.read() 130 if data is not None and len(data) > 0: 131 try: 132 data = data.decode("utf-8") 133 errdata.append(data) 134 log.write(data) 135 log.flush() 136 stderrbuf = b"" 137 except UnicodeDecodeError: 138 stderrbuf = data 139 140 try: 141 # Read all pipes while the process is open 142 while pipe.poll() is None: 143 read_all_pipes(log, rin, outdata, errdata) 144 145 # Process closed, drain all pipes... 146 read_all_pipes(log, rin, outdata, errdata) 147 finally: 148 log.flush() 149 150 if pipe.stdout is not None: 151 pipe.stdout.close() 152 if pipe.stderr is not None: 153 pipe.stderr.close() 154 return ''.join(outdata), ''.join(errdata) 155 156def run(cmd, input=None, log=None, extrafiles=None, **options): 157 """Convenience function to run a command and return its output, raising an 158 exception when the command fails""" 159 160 if not extrafiles: 161 extrafiles = [] 162 163 if isinstance(cmd, str) and not "shell" in options: 164 options["shell"] = True 165 166 try: 167 pipe = Popen(cmd, **options) 168 except OSError as exc: 169 if exc.errno == 2: 170 raise NotFoundError(cmd) 171 else: 172 raise CmdError(cmd, exc) 173 174 if log: 175 stdout, stderr = _logged_communicate(pipe, log, input, extrafiles) 176 else: 177 stdout, stderr = pipe.communicate(input) 178 if not stdout is None: 179 stdout = stdout.decode("utf-8") 180 if not stderr is None: 181 stderr = stderr.decode("utf-8") 182 183 if pipe.returncode != 0: 184 if log: 185 # Don't duplicate the output in the exception if logging it 186 raise ExecutionError(cmd, pipe.returncode, None, None) 187 raise ExecutionError(cmd, pipe.returncode, stdout, stderr) 188 return stdout, stderr 189