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