toasterui.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. #
  2. # BitBake ToasterUI Implementation
  3. # based on (No)TTY UI Implementation by Richard Purdie
  4. #
  5. # Handling output to TTYs or files (no TTY)
  6. #
  7. # Copyright (C) 2006-2012 Richard Purdie
  8. # Copyright (C) 2013 Intel Corporation
  9. #
  10. # This program is free software; you can redistribute it and/or modify
  11. # it under the terms of the GNU General Public License version 2 as
  12. # published by the Free Software Foundation.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU General Public License along
  20. # with this program; if not, write to the Free Software Foundation, Inc.,
  21. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  22. from __future__ import division
  23. import time
  24. import sys
  25. try:
  26. import bb
  27. except RuntimeError as exc:
  28. sys.exit(str(exc))
  29. from bb.ui import uihelper
  30. from bb.ui.buildinfohelper import BuildInfoHelper
  31. import bb.msg
  32. import logging
  33. import os
  34. # pylint: disable=invalid-name
  35. # module properties for UI modules are read by bitbake and the contract should not be broken
  36. featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES, bb.cooker.CookerFeatures.SEND_DEPENDS_TREE, bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING, bb.cooker.CookerFeatures.SEND_SANITYEVENTS]
  37. logger = logging.getLogger("ToasterLogger")
  38. interactive = sys.stdout.isatty()
  39. def _log_settings_from_server(server):
  40. # Get values of variables which control our output
  41. includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
  42. if error:
  43. logger.error("Unable to get the value of BBINCLUDELOGS variable: %s", error)
  44. raise BaseException(error)
  45. loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"])
  46. if error:
  47. logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s", error)
  48. raise BaseException(error)
  49. consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"])
  50. if error:
  51. logger.error("Unable to get the value of BB_CONSOLELOG variable: %s", error)
  52. raise BaseException(error)
  53. return consolelogfile
  54. # create a log file for a single build and direct the logger at it;
  55. # log file name is timestamped to the millisecond (depending
  56. # on system clock accuracy) to ensure it doesn't overlap with
  57. # other log file names
  58. #
  59. # returns (log file, path to log file) for a build
  60. def _open_build_log(log_dir):
  61. format_str = "%(levelname)s: %(message)s"
  62. now = time.time()
  63. now_ms = int((now - int(now)) * 1000)
  64. time_str = time.strftime('build_%Y%m%d_%H%M%S', time.localtime(now))
  65. log_file_name = time_str + ('.%d.log' % now_ms)
  66. build_log_file_path = os.path.join(log_dir, log_file_name)
  67. build_log = logging.FileHandler(build_log_file_path)
  68. logformat = bb.msg.BBLogFormatter(format_str)
  69. build_log.setFormatter(logformat)
  70. bb.msg.addDefaultlogFilter(build_log)
  71. logger.addHandler(build_log)
  72. return (build_log, build_log_file_path)
  73. # stop logging to the build log if it exists
  74. def _close_build_log(build_log):
  75. if build_log:
  76. build_log.flush()
  77. build_log.close()
  78. logger.removeHandler(build_log)
  79. _evt_list = [
  80. "bb.build.TaskBase",
  81. "bb.build.TaskFailed",
  82. "bb.build.TaskFailedSilent",
  83. "bb.build.TaskStarted",
  84. "bb.build.TaskSucceeded",
  85. "bb.command.CommandCompleted",
  86. "bb.command.CommandExit",
  87. "bb.command.CommandFailed",
  88. "bb.cooker.CookerExit",
  89. "bb.event.BuildCompleted",
  90. "bb.event.BuildStarted",
  91. "bb.event.CacheLoadCompleted",
  92. "bb.event.CacheLoadProgress",
  93. "bb.event.CacheLoadStarted",
  94. "bb.event.ConfigParsed",
  95. "bb.event.DepTreeGenerated",
  96. "bb.event.LogExecTTY",
  97. "bb.event.MetadataEvent",
  98. "bb.event.MultipleProviders",
  99. "bb.event.NoProvider",
  100. "bb.event.ParseCompleted",
  101. "bb.event.ParseProgress",
  102. "bb.event.RecipeParsed",
  103. "bb.event.SanityCheck",
  104. "bb.event.SanityCheckPassed",
  105. "bb.event.TreeDataPreparationCompleted",
  106. "bb.event.TreeDataPreparationStarted",
  107. "bb.runqueue.runQueueTaskCompleted",
  108. "bb.runqueue.runQueueTaskFailed",
  109. "bb.runqueue.runQueueTaskSkipped",
  110. "bb.runqueue.runQueueTaskStarted",
  111. "bb.runqueue.sceneQueueTaskCompleted",
  112. "bb.runqueue.sceneQueueTaskFailed",
  113. "bb.runqueue.sceneQueueTaskStarted",
  114. "logging.LogRecord"]
  115. def main(server, eventHandler, params):
  116. # set to a logging.FileHandler instance when a build starts;
  117. # see _open_build_log()
  118. build_log = None
  119. # set to the log path when a build starts
  120. build_log_file_path = None
  121. helper = uihelper.BBUIHelper()
  122. # TODO don't use log output to determine when bitbake has started
  123. #
  124. # WARNING: this log handler cannot be removed, as localhostbecontroller
  125. # relies on output in the toaster_ui.log file to determine whether
  126. # the bitbake server has started, which only happens if
  127. # this logger is setup here (see the TODO in the loop below)
  128. console = logging.StreamHandler(sys.stdout)
  129. format_str = "%(levelname)s: %(message)s"
  130. formatter = bb.msg.BBLogFormatter(format_str)
  131. bb.msg.addDefaultlogFilter(console)
  132. console.setFormatter(formatter)
  133. logger.addHandler(console)
  134. logger.setLevel(logging.INFO)
  135. llevel, debug_domains = bb.msg.constructLogOptions()
  136. result, error = server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list])
  137. if not result or error:
  138. logger.error("can't set event mask: %s", error)
  139. return 1
  140. # verify and warn
  141. build_history_enabled = True
  142. inheritlist, _ = server.runCommand(["getVariable", "INHERIT"])
  143. if not "buildhistory" in inheritlist.split(" "):
  144. logger.warning("buildhistory is not enabled. Please enable INHERIT += \"buildhistory\" to see image details.")
  145. build_history_enabled = False
  146. if not params.observe_only:
  147. params.updateFromServer(server)
  148. params.updateToServer(server, os.environ.copy())
  149. cmdline = params.parseActions()
  150. if not cmdline:
  151. print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.")
  152. return 1
  153. if 'msg' in cmdline and cmdline['msg']:
  154. logger.error(cmdline['msg'])
  155. return 1
  156. ret, error = server.runCommand(cmdline['action'])
  157. if error:
  158. logger.error("Command '%s' failed: %s" % (cmdline, error))
  159. return 1
  160. elif ret != True:
  161. logger.error("Command '%s' failed: returned %s" % (cmdline, ret))
  162. return 1
  163. # set to 1 when toasterui needs to shut down
  164. main.shutdown = 0
  165. interrupted = False
  166. return_value = 0
  167. errors = 0
  168. warnings = 0
  169. taskfailures = []
  170. first = True
  171. buildinfohelper = BuildInfoHelper(server, build_history_enabled,
  172. os.getenv('TOASTER_BRBE'))
  173. # write our own log files into bitbake's log directory;
  174. # we're only interested in the path to the parent directory of
  175. # this file, as we're writing our own logs into the same directory
  176. consolelogfile = _log_settings_from_server(server)
  177. log_dir = os.path.dirname(consolelogfile)
  178. bb.utils.mkdirhier(log_dir)
  179. while True:
  180. try:
  181. event = eventHandler.waitEvent(0.25)
  182. if first:
  183. first = False
  184. # TODO don't use log output to determine when bitbake has started
  185. #
  186. # this is the line localhostbecontroller needs to
  187. # see in toaster_ui.log which it uses to decide whether
  188. # the bitbake server has started...
  189. logger.info("ToasterUI waiting for events")
  190. if event is None:
  191. if main.shutdown > 0:
  192. # if shutting down, close any open build log first
  193. _close_build_log(build_log)
  194. break
  195. continue
  196. helper.eventHandler(event)
  197. # pylint: disable=protected-access
  198. # the code will look into the protected variables of the event; no easy way around this
  199. # we treat ParseStarted as the first event of toaster-triggered
  200. # builds; that way we get the Build Configuration included in the log
  201. # and any errors that occur before BuildStarted is fired
  202. if isinstance(event, bb.event.ParseStarted):
  203. if not (build_log and build_log_file_path):
  204. build_log, build_log_file_path = _open_build_log(log_dir)
  205. continue
  206. if isinstance(event, bb.event.BuildStarted):
  207. if not (build_log and build_log_file_path):
  208. build_log, build_log_file_path = _open_build_log(log_dir)
  209. buildinfohelper.store_started_build(event, build_log_file_path)
  210. continue
  211. if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)):
  212. buildinfohelper.update_and_store_task(event)
  213. logger.info("Logfile for task %s", event.logfile)
  214. continue
  215. if isinstance(event, bb.build.TaskBase):
  216. logger.info(event._message)
  217. if isinstance(event, bb.event.LogExecTTY):
  218. logger.info(event.msg)
  219. continue
  220. if isinstance(event, logging.LogRecord):
  221. if event.levelno == -1:
  222. event.levelno = formatter.ERROR
  223. buildinfohelper.store_log_event(event)
  224. if event.levelno >= formatter.ERROR:
  225. errors = errors + 1
  226. elif event.levelno == formatter.WARNING:
  227. warnings = warnings + 1
  228. # For "normal" logging conditions, don't show note logs from tasks
  229. # but do show them if the user has changed the default log level to
  230. # include verbose/debug messages
  231. if event.taskpid != 0 and event.levelno <= formatter.NOTE:
  232. continue
  233. logger.handle(event)
  234. continue
  235. if isinstance(event, bb.build.TaskFailed):
  236. buildinfohelper.update_and_store_task(event)
  237. logfile = event.logfile
  238. if logfile and os.path.exists(logfile):
  239. bb.error("Logfile of failure stored in: %s" % logfile)
  240. continue
  241. # these events are unprocessed now, but may be used in the future to log
  242. # timing and error informations from the parsing phase in Toaster
  243. if isinstance(event, (bb.event.SanityCheckPassed, bb.event.SanityCheck)):
  244. continue
  245. if isinstance(event, bb.event.ParseProgress):
  246. continue
  247. if isinstance(event, bb.event.ParseCompleted):
  248. continue
  249. if isinstance(event, bb.event.CacheLoadStarted):
  250. continue
  251. if isinstance(event, bb.event.CacheLoadProgress):
  252. continue
  253. if isinstance(event, bb.event.CacheLoadCompleted):
  254. continue
  255. if isinstance(event, bb.event.MultipleProviders):
  256. logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
  257. event._item,
  258. ", ".join(event._candidates))
  259. logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item)
  260. continue
  261. if isinstance(event, bb.event.NoProvider):
  262. errors = errors + 1
  263. if event._runtime:
  264. r = "R"
  265. else:
  266. r = ""
  267. if event._dependees:
  268. text = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)" % (r, event._item, ", ".join(event._dependees), r)
  269. else:
  270. text = "Nothing %sPROVIDES '%s'" % (r, event._item)
  271. logger.error(text)
  272. if event._reasons:
  273. for reason in event._reasons:
  274. logger.error("%s", reason)
  275. text += reason
  276. buildinfohelper.store_log_error(text)
  277. continue
  278. if isinstance(event, bb.event.ConfigParsed):
  279. continue
  280. if isinstance(event, bb.event.RecipeParsed):
  281. continue
  282. # end of saved events
  283. if isinstance(event, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted, bb.runqueue.runQueueTaskSkipped)):
  284. buildinfohelper.store_started_task(event)
  285. continue
  286. if isinstance(event, bb.runqueue.runQueueTaskCompleted):
  287. buildinfohelper.update_and_store_task(event)
  288. continue
  289. if isinstance(event, bb.runqueue.runQueueTaskFailed):
  290. buildinfohelper.update_and_store_task(event)
  291. taskfailures.append(event.taskstring)
  292. logger.error("Task %s (%s) failed with exit code '%s'",
  293. event.taskid, event.taskstring, event.exitcode)
  294. continue
  295. if isinstance(event, (bb.runqueue.sceneQueueTaskCompleted, bb.runqueue.sceneQueueTaskFailed)):
  296. buildinfohelper.update_and_store_task(event)
  297. continue
  298. if isinstance(event, (bb.event.TreeDataPreparationStarted, bb.event.TreeDataPreparationCompleted)):
  299. continue
  300. if isinstance(event, (bb.event.BuildCompleted, bb.command.CommandFailed)):
  301. errorcode = 0
  302. if isinstance(event, bb.command.CommandFailed):
  303. errors += 1
  304. errorcode = 1
  305. logger.error("Command execution failed: %s", event.error)
  306. # turn off logging to the current build log
  307. _close_build_log(build_log)
  308. # reset ready for next BuildStarted
  309. build_log = None
  310. # update the build info helper on BuildCompleted, not on CommandXXX
  311. buildinfohelper.update_build_information(event, errors, warnings, taskfailures)
  312. brbe = buildinfohelper.brbe
  313. buildinfohelper.close(errorcode)
  314. # we start a new build info
  315. if params.observe_only:
  316. logger.debug("ToasterUI prepared for new build")
  317. errors = 0
  318. warnings = 0
  319. taskfailures = []
  320. buildinfohelper = BuildInfoHelper(server, build_history_enabled)
  321. else:
  322. main.shutdown = 1
  323. logger.info("ToasterUI build done, brbe: %s", brbe)
  324. continue
  325. if isinstance(event, (bb.command.CommandCompleted,
  326. bb.command.CommandFailed,
  327. bb.command.CommandExit)):
  328. if params.observe_only:
  329. errorcode = 0
  330. else:
  331. main.shutdown = 1
  332. continue
  333. if isinstance(event, bb.event.MetadataEvent):
  334. if event.type == "SinglePackageInfo":
  335. buildinfohelper.store_build_package_information(event)
  336. elif event.type == "LayerInfo":
  337. buildinfohelper.store_layer_info(event)
  338. elif event.type == "BuildStatsList":
  339. buildinfohelper.store_tasks_stats(event)
  340. elif event.type == "ImagePkgList":
  341. buildinfohelper.store_target_package_data(event)
  342. elif event.type == "MissedSstate":
  343. buildinfohelper.store_missed_state_tasks(event)
  344. elif event.type == "ImageFileSize":
  345. buildinfohelper.update_target_image_file(event)
  346. elif event.type == "ArtifactFileSize":
  347. buildinfohelper.update_artifact_image_file(event)
  348. elif event.type == "LicenseManifestPath":
  349. buildinfohelper.store_license_manifest_path(event)
  350. elif event.type == "SetBRBE":
  351. buildinfohelper.brbe = buildinfohelper._get_data_from_event(event)
  352. elif event.type == "OSErrorException":
  353. logger.error(event)
  354. else:
  355. logger.error("Unprocessed MetadataEvent %s ", str(event))
  356. continue
  357. if isinstance(event, bb.cooker.CookerExit):
  358. # shutdown when bitbake server shuts down
  359. main.shutdown = 1
  360. continue
  361. if isinstance(event, bb.event.DepTreeGenerated):
  362. buildinfohelper.store_dependency_information(event)
  363. continue
  364. logger.warning("Unknown event: %s", event)
  365. return_value += 1
  366. except EnvironmentError as ioerror:
  367. # ignore interrupted io
  368. if ioerror.args[0] == 4:
  369. pass
  370. except KeyboardInterrupt:
  371. main.shutdown = 1
  372. except Exception as e:
  373. # print errors to log
  374. import traceback
  375. from pprint import pformat
  376. exception_data = traceback.format_exc()
  377. logger.error("%s\n%s" , e, exception_data)
  378. # save them to database, if possible; if it fails, we already logged to console.
  379. try:
  380. buildinfohelper.store_log_exception("%s\n%s" % (str(e), exception_data))
  381. except Exception as ce:
  382. logger.error("CRITICAL - Failed to to save toaster exception to the database: %s", str(ce))
  383. # make sure we return with an error
  384. return_value += 1
  385. if interrupted and return_value == 0:
  386. return_value += 1
  387. logger.warning("Return value is %d", return_value)
  388. return return_value