blob: 7c3995cce5cebc14b228cbd1f583a83177c2bb5e [file] [log] [blame]
Brad Bishopc342db32019-05-15 21:57:59 -04001#
2# SPDX-License-Identifier: GPL-2.0-only
3#
4
Patrick Williamsc124f4f2015-09-15 14:41:29 -05005import logging
6import signal
7import subprocess
8import errno
9import select
Andrew Geisslerc9f78652020-09-18 14:11:35 -050010import bb
Patrick Williamsc124f4f2015-09-15 14:41:29 -050011
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):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060025 if not isinstance(self.command, str):
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026 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
Andrew Geissler635e0e42020-08-21 15:58:33 -050045 self.extra_message = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046
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) +
Andrew Geissler635e0e42020-08-21 15:58:33 -050056 " with exit code %s" % self.exitcode + message + (self.extra_message or ""))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050057
58class Popen(subprocess.Popen):
59 defaults = {
60 "close_fds": True,
61 "preexec_fn": subprocess_setup,
62 "stdout": subprocess.PIPE,
63 "stderr": subprocess.STDOUT,
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
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500103 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500140 try:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500141 # Read all pipes while the process is open
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500142 while pipe.poll() is None:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500143 read_all_pipes(log, rin, outdata, errdata)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500144
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500145 # Pocess closed, drain all pipes...
146 read_all_pipes(log, rin, outdata, errdata)
147 finally:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148 log.flush()
149
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500150 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163 if isinstance(cmd, str) and not "shell" in options:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164 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)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500178 if not stdout is None:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600179 stdout = stdout.decode("utf-8")
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500180 if not stderr is None:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600181 stderr = stderr.decode("utf-8")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500182
183 if pipe.returncode != 0:
184 raise ExecutionError(cmd, pipe.returncode, stdout, stderr)
185 return stdout, stderr