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