xref: /openbmc/openbmc/poky/bitbake/lib/bb/process.py (revision ac69b488)
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.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
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        # Pocess 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        raise ExecutionError(cmd, pipe.returncode, stdout, stderr)
185    return stdout, stderr
186