commands.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # Copyright (c) 2013-2014 Intel Corporation
  2. #
  3. # Released under the MIT license (see COPYING.MIT)
  4. # DESCRIPTION
  5. # This module is mainly used by scripts/oe-selftest and modules under meta/oeqa/selftest
  6. # It provides a class and methods for running commands on the host in a convienent way for tests.
  7. import os
  8. import sys
  9. import signal
  10. import subprocess
  11. import threading
  12. import logging
  13. from oeqa.utils import CommandError
  14. from oeqa.utils import ftools
  15. import re
  16. import contextlib
  17. # Export test doesn't require bb
  18. try:
  19. import bb
  20. except ImportError:
  21. pass
  22. class Command(object):
  23. def __init__(self, command, bg=False, timeout=None, data=None, **options):
  24. self.defaultopts = {
  25. "stdout": subprocess.PIPE,
  26. "stderr": subprocess.STDOUT,
  27. "stdin": None,
  28. "shell": False,
  29. "bufsize": -1,
  30. }
  31. self.cmd = command
  32. self.bg = bg
  33. self.timeout = timeout
  34. self.data = data
  35. self.options = dict(self.defaultopts)
  36. if isinstance(self.cmd, str):
  37. self.options["shell"] = True
  38. if self.data:
  39. self.options['stdin'] = subprocess.PIPE
  40. self.options.update(options)
  41. self.status = None
  42. self.output = None
  43. self.error = None
  44. self.thread = None
  45. self.log = logging.getLogger("utils.commands")
  46. def run(self):
  47. self.process = subprocess.Popen(self.cmd, **self.options)
  48. def commThread():
  49. self.output, self.error = self.process.communicate(self.data)
  50. self.thread = threading.Thread(target=commThread)
  51. self.thread.start()
  52. self.log.debug("Running command '%s'" % self.cmd)
  53. if not self.bg:
  54. self.thread.join(self.timeout)
  55. self.stop()
  56. def stop(self):
  57. if self.thread.isAlive():
  58. self.process.terminate()
  59. # let's give it more time to terminate gracefully before killing it
  60. self.thread.join(5)
  61. if self.thread.isAlive():
  62. self.process.kill()
  63. self.thread.join()
  64. if not self.output:
  65. self.output = ""
  66. else:
  67. self.output = self.output.decode("utf-8", errors='replace').rstrip()
  68. self.status = self.process.poll()
  69. self.log.debug("Command '%s' returned %d as exit code." % (self.cmd, self.status))
  70. # logging the complete output is insane
  71. # bitbake -e output is really big
  72. # and makes the log file useless
  73. if self.status:
  74. lout = "\n".join(self.output.splitlines()[-20:])
  75. self.log.debug("Last 20 lines:\n%s" % lout)
  76. class Result(object):
  77. pass
  78. def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options):
  79. result = Result()
  80. cmd = Command(command, timeout=timeout, **options)
  81. cmd.run()
  82. result.command = command
  83. result.status = cmd.status
  84. result.output = cmd.output
  85. result.error = cmd.error
  86. result.pid = cmd.process.pid
  87. if result.status and not ignore_status:
  88. if assert_error:
  89. raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output))
  90. else:
  91. raise CommandError(result.status, command, result.output)
  92. return result
  93. def bitbake(command, ignore_status=False, timeout=None, postconfig=None, **options):
  94. if postconfig:
  95. postconfig_file = os.path.join(os.environ.get('BUILDDIR'), 'oeqa-post.conf')
  96. ftools.write_file(postconfig_file, postconfig)
  97. extra_args = "-R %s" % postconfig_file
  98. else:
  99. extra_args = ""
  100. if isinstance(command, str):
  101. cmd = "bitbake " + extra_args + " " + command
  102. else:
  103. cmd = [ "bitbake" ] + [a for a in (command + extra_args.split(" ")) if a not in [""]]
  104. try:
  105. return runCmd(cmd, ignore_status, timeout, **options)
  106. finally:
  107. if postconfig:
  108. os.remove(postconfig_file)
  109. def get_bb_env(target=None, postconfig=None):
  110. if target:
  111. return bitbake("-e %s" % target, postconfig=postconfig).output
  112. else:
  113. return bitbake("-e", postconfig=postconfig).output
  114. def get_bb_vars(variables=None, target=None, postconfig=None):
  115. """Get values of multiple bitbake variables"""
  116. bbenv = get_bb_env(target, postconfig=postconfig)
  117. var_re = re.compile(r'^(export )?(?P<var>\w+)="(?P<value>.*)"$')
  118. unset_re = re.compile(r'^unset (?P<var>\w+)$')
  119. lastline = None
  120. values = {}
  121. for line in bbenv.splitlines():
  122. match = var_re.match(line)
  123. val = None
  124. if match:
  125. val = match.group('value')
  126. else:
  127. match = unset_re.match(line)
  128. if match:
  129. # Handle [unexport] variables
  130. if lastline.startswith('# "'):
  131. val = lastline.split('"')[1]
  132. if val:
  133. var = match.group('var')
  134. if variables is None:
  135. values[var] = val
  136. else:
  137. if var in variables:
  138. values[var] = val
  139. variables.remove(var)
  140. # Stop after all required variables have been found
  141. if not variables:
  142. break
  143. lastline = line
  144. if variables:
  145. # Fill in missing values
  146. for var in variables:
  147. values[var] = None
  148. return values
  149. def get_bb_var(var, target=None, postconfig=None):
  150. return get_bb_vars([var], target, postconfig)[var]
  151. def get_test_layer():
  152. layers = get_bb_var("BBLAYERS").split()
  153. testlayer = None
  154. for l in layers:
  155. if '~' in l:
  156. l = os.path.expanduser(l)
  157. if "/meta-selftest" in l and os.path.isdir(l):
  158. testlayer = l
  159. break
  160. return testlayer
  161. def create_temp_layer(templayerdir, templayername, priority=999, recipepathspec='recipes-*/*'):
  162. os.makedirs(os.path.join(templayerdir, 'conf'))
  163. with open(os.path.join(templayerdir, 'conf', 'layer.conf'), 'w') as f:
  164. f.write('BBPATH .= ":${LAYERDIR}"\n')
  165. f.write('BBFILES += "${LAYERDIR}/%s/*.bb \\' % recipepathspec)
  166. f.write(' ${LAYERDIR}/%s/*.bbappend"\n' % recipepathspec)
  167. f.write('BBFILE_COLLECTIONS += "%s"\n' % templayername)
  168. f.write('BBFILE_PATTERN_%s = "^${LAYERDIR}/"\n' % templayername)
  169. f.write('BBFILE_PRIORITY_%s = "%d"\n' % (templayername, priority))
  170. f.write('BBFILE_PATTERN_IGNORE_EMPTY_%s = "1"\n' % templayername)
  171. @contextlib.contextmanager
  172. def runqemu(pn, ssh=True):
  173. import bb.tinfoil
  174. import bb.build
  175. tinfoil = bb.tinfoil.Tinfoil()
  176. tinfoil.prepare(False)
  177. try:
  178. tinfoil.logger.setLevel(logging.WARNING)
  179. import oeqa.targetcontrol
  180. tinfoil.config_data.setVar("TEST_LOG_DIR", "${WORKDIR}/testimage")
  181. tinfoil.config_data.setVar("TEST_QEMUBOOT_TIMEOUT", "1000")
  182. import oe.recipeutils
  183. recipefile = oe.recipeutils.pn_to_recipe(tinfoil.cooker, pn)
  184. recipedata = oe.recipeutils.parse_recipe(tinfoil.cooker, recipefile, [])
  185. # The QemuRunner log is saved out, but we need to ensure it is at the right
  186. # log level (and then ensure that since it's a child of the BitBake logger,
  187. # we disable propagation so we don't then see the log events on the console)
  188. logger = logging.getLogger('BitBake.QemuRunner')
  189. logger.setLevel(logging.DEBUG)
  190. logger.propagate = False
  191. logdir = recipedata.getVar("TEST_LOG_DIR", True)
  192. qemu = oeqa.targetcontrol.QemuTarget(recipedata)
  193. finally:
  194. # We need to shut down tinfoil early here in case we actually want
  195. # to run tinfoil-using utilities with the running QEMU instance.
  196. # Luckily QemuTarget doesn't need it after the constructor.
  197. tinfoil.shutdown()
  198. # Setup bitbake logger as console handler is removed by tinfoil.shutdown
  199. bblogger = logging.getLogger('BitBake')
  200. bblogger.setLevel(logging.INFO)
  201. console = logging.StreamHandler(sys.stdout)
  202. bbformat = bb.msg.BBLogFormatter("%(levelname)s: %(message)s")
  203. if sys.stdout.isatty():
  204. bbformat.enable_color()
  205. console.setFormatter(bbformat)
  206. bblogger.addHandler(console)
  207. try:
  208. qemu.deploy()
  209. try:
  210. qemu.start(ssh=ssh)
  211. except bb.build.FuncFailed:
  212. raise Exception('Failed to start QEMU - see the logs in %s' % logdir)
  213. yield qemu
  214. finally:
  215. try:
  216. qemu.stop()
  217. except:
  218. pass
  219. def updateEnv(env_file):
  220. """
  221. Source a file and update environment.
  222. """
  223. cmd = ". %s; env -0" % env_file
  224. result = runCmd(cmd)
  225. for line in result.output.split("\0"):
  226. (key, _, value) = line.partition("=")
  227. os.environ[key] = value