xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oeqa/utils/commands.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun#
2*4882a593Smuzhiyun# Copyright (c) 2013-2014 Intel Corporation
3*4882a593Smuzhiyun#
4*4882a593Smuzhiyun# SPDX-License-Identifier: MIT
5*4882a593Smuzhiyun#
6*4882a593Smuzhiyun
7*4882a593Smuzhiyun# DESCRIPTION
8*4882a593Smuzhiyun# This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest
9*4882a593Smuzhiyun# It provides a class and methods for running commands on the host in a convienent way for tests.
10*4882a593Smuzhiyun
11*4882a593Smuzhiyun
12*4882a593Smuzhiyun
13*4882a593Smuzhiyunimport os
14*4882a593Smuzhiyunimport sys
15*4882a593Smuzhiyunimport signal
16*4882a593Smuzhiyunimport subprocess
17*4882a593Smuzhiyunimport threading
18*4882a593Smuzhiyunimport time
19*4882a593Smuzhiyunimport logging
20*4882a593Smuzhiyunfrom oeqa.utils import CommandError
21*4882a593Smuzhiyunfrom oeqa.utils import ftools
22*4882a593Smuzhiyunimport re
23*4882a593Smuzhiyunimport contextlib
24*4882a593Smuzhiyun# Export test doesn't require bb
25*4882a593Smuzhiyuntry:
26*4882a593Smuzhiyun    import bb
27*4882a593Smuzhiyunexcept ImportError:
28*4882a593Smuzhiyun    pass
29*4882a593Smuzhiyun
30*4882a593Smuzhiyunclass Command(object):
31*4882a593Smuzhiyun    def __init__(self, command, bg=False, timeout=None, data=None, output_log=None, **options):
32*4882a593Smuzhiyun
33*4882a593Smuzhiyun        self.defaultopts = {
34*4882a593Smuzhiyun            "stdout": subprocess.PIPE,
35*4882a593Smuzhiyun            "stderr": subprocess.STDOUT,
36*4882a593Smuzhiyun            "stdin": None,
37*4882a593Smuzhiyun            "shell": False,
38*4882a593Smuzhiyun            "bufsize": -1,
39*4882a593Smuzhiyun        }
40*4882a593Smuzhiyun
41*4882a593Smuzhiyun        self.cmd = command
42*4882a593Smuzhiyun        self.bg = bg
43*4882a593Smuzhiyun        self.timeout = timeout
44*4882a593Smuzhiyun        self.data = data
45*4882a593Smuzhiyun
46*4882a593Smuzhiyun        self.options = dict(self.defaultopts)
47*4882a593Smuzhiyun        if isinstance(self.cmd, str):
48*4882a593Smuzhiyun            self.options["shell"] = True
49*4882a593Smuzhiyun        if self.data:
50*4882a593Smuzhiyun            self.options['stdin'] = subprocess.PIPE
51*4882a593Smuzhiyun        self.options.update(options)
52*4882a593Smuzhiyun
53*4882a593Smuzhiyun        self.status = None
54*4882a593Smuzhiyun        # We collect chunks of output before joining them at the end.
55*4882a593Smuzhiyun        self._output_chunks = []
56*4882a593Smuzhiyun        self._error_chunks = []
57*4882a593Smuzhiyun        self.output = None
58*4882a593Smuzhiyun        self.error = None
59*4882a593Smuzhiyun        self.threads = []
60*4882a593Smuzhiyun
61*4882a593Smuzhiyun        self.output_log = output_log
62*4882a593Smuzhiyun        self.log = logging.getLogger("utils.commands")
63*4882a593Smuzhiyun
64*4882a593Smuzhiyun    def run(self):
65*4882a593Smuzhiyun        self.process = subprocess.Popen(self.cmd, **self.options)
66*4882a593Smuzhiyun
67*4882a593Smuzhiyun        def readThread(output, stream, logfunc):
68*4882a593Smuzhiyun            if logfunc:
69*4882a593Smuzhiyun                for line in stream:
70*4882a593Smuzhiyun                    output.append(line)
71*4882a593Smuzhiyun                    logfunc(line.decode("utf-8", errors='replace').rstrip())
72*4882a593Smuzhiyun            else:
73*4882a593Smuzhiyun                output.append(stream.read())
74*4882a593Smuzhiyun
75*4882a593Smuzhiyun        def readStderrThread():
76*4882a593Smuzhiyun            readThread(self._error_chunks, self.process.stderr, self.output_log.error if self.output_log else None)
77*4882a593Smuzhiyun
78*4882a593Smuzhiyun        def readStdoutThread():
79*4882a593Smuzhiyun            readThread(self._output_chunks, self.process.stdout, self.output_log.info if self.output_log else None)
80*4882a593Smuzhiyun
81*4882a593Smuzhiyun        def writeThread():
82*4882a593Smuzhiyun            try:
83*4882a593Smuzhiyun                self.process.stdin.write(self.data)
84*4882a593Smuzhiyun                self.process.stdin.close()
85*4882a593Smuzhiyun            except OSError as ex:
86*4882a593Smuzhiyun                # It's not an error when the command does not consume all
87*4882a593Smuzhiyun                # of our data. subprocess.communicate() also ignores that.
88*4882a593Smuzhiyun                if ex.errno != EPIPE:
89*4882a593Smuzhiyun                    raise
90*4882a593Smuzhiyun
91*4882a593Smuzhiyun        # We write in a separate thread because then we can read
92*4882a593Smuzhiyun        # without worrying about deadlocks. The additional thread is
93*4882a593Smuzhiyun        # expected to terminate by itself and we mark it as a daemon,
94*4882a593Smuzhiyun        # so even it should happen to not terminate for whatever
95*4882a593Smuzhiyun        # reason, the main process will still exit, which will then
96*4882a593Smuzhiyun        # kill the write thread.
97*4882a593Smuzhiyun        if self.data:
98*4882a593Smuzhiyun            thread = threading.Thread(target=writeThread, daemon=True)
99*4882a593Smuzhiyun            thread.start()
100*4882a593Smuzhiyun            self.threads.append(thread)
101*4882a593Smuzhiyun        if self.process.stderr:
102*4882a593Smuzhiyun            thread = threading.Thread(target=readStderrThread)
103*4882a593Smuzhiyun            thread.start()
104*4882a593Smuzhiyun            self.threads.append(thread)
105*4882a593Smuzhiyun        if self.output_log:
106*4882a593Smuzhiyun            self.output_log.info('Running: %s' % self.cmd)
107*4882a593Smuzhiyun        thread = threading.Thread(target=readStdoutThread)
108*4882a593Smuzhiyun        thread.start()
109*4882a593Smuzhiyun        self.threads.append(thread)
110*4882a593Smuzhiyun
111*4882a593Smuzhiyun        self.log.debug("Running command '%s'" % self.cmd)
112*4882a593Smuzhiyun
113*4882a593Smuzhiyun        if not self.bg:
114*4882a593Smuzhiyun            if self.timeout is None:
115*4882a593Smuzhiyun                for thread in self.threads:
116*4882a593Smuzhiyun                    thread.join()
117*4882a593Smuzhiyun            else:
118*4882a593Smuzhiyun                deadline = time.time() + self.timeout
119*4882a593Smuzhiyun                for thread in self.threads:
120*4882a593Smuzhiyun                    timeout = deadline - time.time()
121*4882a593Smuzhiyun                    if timeout < 0:
122*4882a593Smuzhiyun                        timeout = 0
123*4882a593Smuzhiyun                    thread.join(timeout)
124*4882a593Smuzhiyun            self.stop()
125*4882a593Smuzhiyun
126*4882a593Smuzhiyun    def stop(self):
127*4882a593Smuzhiyun        for thread in self.threads:
128*4882a593Smuzhiyun            if thread.is_alive():
129*4882a593Smuzhiyun                self.process.terminate()
130*4882a593Smuzhiyun            # let's give it more time to terminate gracefully before killing it
131*4882a593Smuzhiyun            thread.join(5)
132*4882a593Smuzhiyun            if thread.is_alive():
133*4882a593Smuzhiyun                self.process.kill()
134*4882a593Smuzhiyun                thread.join()
135*4882a593Smuzhiyun
136*4882a593Smuzhiyun        def finalize_output(data):
137*4882a593Smuzhiyun            if not data:
138*4882a593Smuzhiyun                data = ""
139*4882a593Smuzhiyun            else:
140*4882a593Smuzhiyun                data = b"".join(data)
141*4882a593Smuzhiyun                data = data.decode("utf-8", errors='replace').rstrip()
142*4882a593Smuzhiyun            return data
143*4882a593Smuzhiyun
144*4882a593Smuzhiyun        self.output = finalize_output(self._output_chunks)
145*4882a593Smuzhiyun        self._output_chunks = None
146*4882a593Smuzhiyun        # self.error used to be a byte string earlier, probably unintentionally.
147*4882a593Smuzhiyun        # Now it is a normal string, just like self.output.
148*4882a593Smuzhiyun        self.error = finalize_output(self._error_chunks)
149*4882a593Smuzhiyun        self._error_chunks = None
150*4882a593Smuzhiyun        # At this point we know that the process has closed stdout/stderr, so
151*4882a593Smuzhiyun        # it is safe and necessary to wait for the actual process completion.
152*4882a593Smuzhiyun        self.status = self.process.wait()
153*4882a593Smuzhiyun        self.process.stdout.close()
154*4882a593Smuzhiyun        if self.process.stderr:
155*4882a593Smuzhiyun            self.process.stderr.close()
156*4882a593Smuzhiyun
157*4882a593Smuzhiyun        self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status))
158*4882a593Smuzhiyun        # logging the complete output is insane
159*4882a593Smuzhiyun        # bitbake -e output is really big
160*4882a593Smuzhiyun        # and makes the log file useless
161*4882a593Smuzhiyun        if self.status:
162*4882a593Smuzhiyun            lout = "\n".join(self.output.splitlines()[-20:])
163*4882a593Smuzhiyun            self.log.debug("Last 20 lines:\n%s" % lout)
164*4882a593Smuzhiyun
165*4882a593Smuzhiyun
166*4882a593Smuzhiyunclass Result(object):
167*4882a593Smuzhiyun    pass
168*4882a593Smuzhiyun
169*4882a593Smuzhiyun
170*4882a593Smuzhiyundef runCmd(command, ignore_status=False, timeout=None, assert_error=True, sync=True,
171*4882a593Smuzhiyun          native_sysroot=None, limit_exc_output=0, output_log=None, **options):
172*4882a593Smuzhiyun    result = Result()
173*4882a593Smuzhiyun
174*4882a593Smuzhiyun    if native_sysroot:
175*4882a593Smuzhiyun        extra_paths = "%s/sbin:%s/usr/sbin:%s/usr/bin" % \
176*4882a593Smuzhiyun                      (native_sysroot, native_sysroot, native_sysroot)
177*4882a593Smuzhiyun        nenv = dict(options.get('env', os.environ))
178*4882a593Smuzhiyun        nenv['PATH'] = extra_paths + ':' + nenv.get('PATH', '')
179*4882a593Smuzhiyun        options['env'] = nenv
180*4882a593Smuzhiyun
181*4882a593Smuzhiyun    cmd = Command(command, timeout=timeout, output_log=output_log, **options)
182*4882a593Smuzhiyun    cmd.run()
183*4882a593Smuzhiyun
184*4882a593Smuzhiyun    # tests can be heavy on IO and if bitbake can't write out its caches, we see timeouts.
185*4882a593Smuzhiyun    # call sync around the tests to ensure the IO queue doesn't get too large, taking any IO
186*4882a593Smuzhiyun    # hit here rather than in bitbake shutdown.
187*4882a593Smuzhiyun    if sync:
188*4882a593Smuzhiyun        p = os.environ['PATH']
189*4882a593Smuzhiyun        os.environ['PATH'] = "/usr/bin:/bin:/usr/sbin:/sbin:" + p
190*4882a593Smuzhiyun        os.system("sync")
191*4882a593Smuzhiyun        os.environ['PATH'] = p
192*4882a593Smuzhiyun
193*4882a593Smuzhiyun    result.command = command
194*4882a593Smuzhiyun    result.status = cmd.status
195*4882a593Smuzhiyun    result.output = cmd.output
196*4882a593Smuzhiyun    result.error = cmd.error
197*4882a593Smuzhiyun    result.pid = cmd.process.pid
198*4882a593Smuzhiyun
199*4882a593Smuzhiyun    if result.status and not ignore_status:
200*4882a593Smuzhiyun        exc_output = result.output
201*4882a593Smuzhiyun        if limit_exc_output > 0:
202*4882a593Smuzhiyun            split = result.output.splitlines()
203*4882a593Smuzhiyun            if len(split) > limit_exc_output:
204*4882a593Smuzhiyun                exc_output = "\n... (last %d lines of output)\n" % limit_exc_output + \
205*4882a593Smuzhiyun                             '\n'.join(split[-limit_exc_output:])
206*4882a593Smuzhiyun        if assert_error:
207*4882a593Smuzhiyun            raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, exc_output))
208*4882a593Smuzhiyun        else:
209*4882a593Smuzhiyun            raise CommandError(result.status, command, exc_output)
210*4882a593Smuzhiyun
211*4882a593Smuzhiyun    return result
212*4882a593Smuzhiyun
213*4882a593Smuzhiyun
214*4882a593Smuzhiyundef bitbake(command, ignore_status=False, timeout=None, postconfig=None, output_log=None, **options):
215*4882a593Smuzhiyun
216*4882a593Smuzhiyun    if postconfig:
217*4882a593Smuzhiyun        postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf')
218*4882a593Smuzhiyun        ftools.write_file(postconfig_file, postconfig)
219*4882a593Smuzhiyun        extra_args = "-R %s" % postconfig_file
220*4882a593Smuzhiyun    else:
221*4882a593Smuzhiyun        extra_args = ""
222*4882a593Smuzhiyun
223*4882a593Smuzhiyun    if isinstance(command, str):
224*4882a593Smuzhiyun        cmd = "bitbake " + extra_args + " " + command
225*4882a593Smuzhiyun    else:
226*4882a593Smuzhiyun        cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]]
227*4882a593Smuzhiyun
228*4882a593Smuzhiyun    try:
229*4882a593Smuzhiyun        return runCmd(cmd, ignore_status, timeout, output_log=output_log, **options)
230*4882a593Smuzhiyun    finally:
231*4882a593Smuzhiyun        if postconfig:
232*4882a593Smuzhiyun            os.remove(postconfig_file)
233*4882a593Smuzhiyun
234*4882a593Smuzhiyun
235*4882a593Smuzhiyundef get_bb_env(target=None, postconfig=None):
236*4882a593Smuzhiyun    if target:
237*4882a593Smuzhiyun        return bitbake("-e %s" % target, postconfig=postconfig).output
238*4882a593Smuzhiyun    else:
239*4882a593Smuzhiyun        return bitbake("-e", postconfig=postconfig).output
240*4882a593Smuzhiyun
241*4882a593Smuzhiyundef get_bb_vars(variables=None, target=None, postconfig=None):
242*4882a593Smuzhiyun    """Get values of multiple bitbake variables"""
243*4882a593Smuzhiyun    bbenv = get_bb_env(target, postconfig=postconfig)
244*4882a593Smuzhiyun
245*4882a593Smuzhiyun    if variables is not None:
246*4882a593Smuzhiyun        variables = list(variables)
247*4882a593Smuzhiyun    var_re = re.compile(r'^(export )?(?P<var>\w+(_.*)?)="(?P<value>.*)"$')
248*4882a593Smuzhiyun    unset_re = re.compile(r'^unset (?P<var>\w+)$')
249*4882a593Smuzhiyun    lastline = None
250*4882a593Smuzhiyun    values = {}
251*4882a593Smuzhiyun    for line in bbenv.splitlines():
252*4882a593Smuzhiyun        match = var_re.match(line)
253*4882a593Smuzhiyun        val = None
254*4882a593Smuzhiyun        if match:
255*4882a593Smuzhiyun            val = match.group('value')
256*4882a593Smuzhiyun        else:
257*4882a593Smuzhiyun            match = unset_re.match(line)
258*4882a593Smuzhiyun            if match:
259*4882a593Smuzhiyun                # Handle [unexport] variables
260*4882a593Smuzhiyun                if lastline.startswith('#   "'):
261*4882a593Smuzhiyun                    val = lastline.split('"')[1]
262*4882a593Smuzhiyun        if val:
263*4882a593Smuzhiyun            var = match.group('var')
264*4882a593Smuzhiyun            if variables is None:
265*4882a593Smuzhiyun                values[var] = val
266*4882a593Smuzhiyun            else:
267*4882a593Smuzhiyun                if var in variables:
268*4882a593Smuzhiyun                    values[var] = val
269*4882a593Smuzhiyun                    variables.remove(var)
270*4882a593Smuzhiyun                # Stop after all required variables have been found
271*4882a593Smuzhiyun                if not variables:
272*4882a593Smuzhiyun                    break
273*4882a593Smuzhiyun        lastline = line
274*4882a593Smuzhiyun    if variables:
275*4882a593Smuzhiyun        # Fill in missing values
276*4882a593Smuzhiyun        for var in variables:
277*4882a593Smuzhiyun            values[var] = None
278*4882a593Smuzhiyun    return values
279*4882a593Smuzhiyun
280*4882a593Smuzhiyundef get_bb_var(var, target=None, postconfig=None):
281*4882a593Smuzhiyun    return get_bb_vars([var], target, postconfig)[var]
282*4882a593Smuzhiyun
283*4882a593Smuzhiyundef get_test_layer():
284*4882a593Smuzhiyun    layers = get_bb_var("BBLAYERS").split()
285*4882a593Smuzhiyun    testlayer = None
286*4882a593Smuzhiyun    for l in layers:
287*4882a593Smuzhiyun        if '~' in l:
288*4882a593Smuzhiyun            l = os.path.expanduser(l)
289*4882a593Smuzhiyun        if "/meta-selftest" in l and os.path.isdir(l):
290*4882a593Smuzhiyun            testlayer = l
291*4882a593Smuzhiyun            break
292*4882a593Smuzhiyun    return testlayer
293*4882a593Smuzhiyun
294*4882a593Smuzhiyundef create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
295*4882a593Smuzhiyun    os.makedirs(os.path.join(templayerdir, 'conf'))
296*4882a593Smuzhiyun    with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
297*4882a593Smuzhiyun        f.write('BBPATH .= ":${LAYERDIR}"\n')
298*4882a593Smuzhiyun        f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
299*4882a593Smuzhiyun        f.write('            ${LAYERDIR}/%s/*.bbappend"\n' % recipepathspec)
300*4882a593Smuzhiyun        f.write('BBFILE_COLLECTIONS += "%s"\n' % templayername)
301*4882a593Smuzhiyun        f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
302*4882a593Smuzhiyun        f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
303*4882a593Smuzhiyun        f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
304*4882a593Smuzhiyun        f.write('LAYERSERIES_COMPAT_%s = "${LAYERSERIES_COMPAT_core}"\n' % templayername)
305*4882a593Smuzhiyun
306*4882a593Smuzhiyun@contextlib.contextmanager
307*4882a593Smuzhiyundef runqemu(pn, ssh=True, runqemuparams='', image_fstype=None, launch_cmd=None, qemuparams=None, overrides={}, discard_writes=True):
308*4882a593Smuzhiyun    """
309*4882a593Smuzhiyun    launch_cmd means directly run the command, don't need set rootfs or env vars.
310*4882a593Smuzhiyun    """
311*4882a593Smuzhiyun
312*4882a593Smuzhiyun    import bb.tinfoil
313*4882a593Smuzhiyun    import bb.build
314*4882a593Smuzhiyun
315*4882a593Smuzhiyun    # Need a non-'BitBake' logger to capture the runner output
316*4882a593Smuzhiyun    targetlogger = logging.getLogger('TargetRunner')
317*4882a593Smuzhiyun    targetlogger.setLevel(logging.DEBUG)
318*4882a593Smuzhiyun    handler = logging.StreamHandler(sys.stdout)
319*4882a593Smuzhiyun    targetlogger.addHandler(handler)
320*4882a593Smuzhiyun
321*4882a593Smuzhiyun    tinfoil = bb.tinfoil.Tinfoil()
322*4882a593Smuzhiyun    tinfoil.prepare(config_only=False, quiet=True)
323*4882a593Smuzhiyun    try:
324*4882a593Smuzhiyun        tinfoil.logger.setLevel(logging.WARNING)
325*4882a593Smuzhiyun        import oeqa.targetcontrol
326*4882a593Smuzhiyun        recipedata = tinfoil.parse_recipe(pn)
327*4882a593Smuzhiyun        recipedata.setVar("TEST_LOG_DIR", "${WORKDIR}/testimage")
328*4882a593Smuzhiyun        recipedata.setVar("TEST_QEMUBOOT_TIMEOUT", "1000")
329*4882a593Smuzhiyun        # Tell QemuTarget() whether need find rootfs/kernel or not
330*4882a593Smuzhiyun        if launch_cmd:
331*4882a593Smuzhiyun            recipedata.setVar("FIND_ROOTFS", '0')
332*4882a593Smuzhiyun        else:
333*4882a593Smuzhiyun            recipedata.setVar("FIND_ROOTFS", '1')
334*4882a593Smuzhiyun
335*4882a593Smuzhiyun        for key, value in overrides.items():
336*4882a593Smuzhiyun            recipedata.setVar(key, value)
337*4882a593Smuzhiyun
338*4882a593Smuzhiyun        logdir = recipedata.getVar("TEST_LOG_DIR")
339*4882a593Smuzhiyun
340*4882a593Smuzhiyun        qemu = oeqa.targetcontrol.QemuTarget(recipedata, targetlogger, image_fstype)
341*4882a593Smuzhiyun    finally:
342*4882a593Smuzhiyun        # We need to shut down tinfoil early here in case we actually want
343*4882a593Smuzhiyun        # to run tinfoil-using utilities with the running QEMU instance.
344*4882a593Smuzhiyun        # Luckily QemuTarget doesn't need it after the constructor.
345*4882a593Smuzhiyun        tinfoil.shutdown()
346*4882a593Smuzhiyun
347*4882a593Smuzhiyun    try:
348*4882a593Smuzhiyun        qemu.deploy()
349*4882a593Smuzhiyun        try:
350*4882a593Smuzhiyun            qemu.start(params=qemuparams, ssh=ssh, runqemuparams=runqemuparams, launch_cmd=launch_cmd, discard_writes=discard_writes)
351*4882a593Smuzhiyun        except Exception as e:
352*4882a593Smuzhiyun            msg = str(e) + '\nFailed to start QEMU - see the logs in %s' % logdir
353*4882a593Smuzhiyun            if os.path.exists(qemu.qemurunnerlog):
354*4882a593Smuzhiyun                with open(qemu.qemurunnerlog, 'r') as f:
355*4882a593Smuzhiyun                    msg = msg + "Qemurunner log output from %s:\n%s" % (qemu.qemurunnerlog, f.read())
356*4882a593Smuzhiyun            raise Exception(msg)
357*4882a593Smuzhiyun
358*4882a593Smuzhiyun        yield qemu
359*4882a593Smuzhiyun
360*4882a593Smuzhiyun    finally:
361*4882a593Smuzhiyun        targetlogger.removeHandler(handler)
362*4882a593Smuzhiyun        qemu.stop()
363*4882a593Smuzhiyun
364*4882a593Smuzhiyundef updateEnv(env_file):
365*4882a593Smuzhiyun    """
366*4882a593Smuzhiyun    Source a file and update environment.
367*4882a593Smuzhiyun    """
368*4882a593Smuzhiyun
369*4882a593Smuzhiyun    cmd = ". %s; env -0" % env_file
370*4882a593Smuzhiyun    result = runCmd(cmd)
371*4882a593Smuzhiyun
372*4882a593Smuzhiyun    for line in result.output.split("\0"):
373*4882a593Smuzhiyun        (key, _, value) = line.partition("=")
374*4882a593Smuzhiyun        os.environ[key] = value
375