blob: 3d261503eaeef2fa91cba1a34c1a08b7522d7923 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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
23from __future__ import division
Patrick Williamsf1e5d692016-03-30 15:21:19 -050024import time
Patrick Williamsc124f4f2015-09-15 14:41:29 -050025import sys
26try:
27 import bb
28except RuntimeError as exc:
29 sys.exit(str(exc))
30
31from bb.ui import uihelper
32from bb.ui.buildinfohelper import BuildInfoHelper
33
34import bb.msg
35import logging
36import os
37
38# pylint: disable=invalid-name
39# module properties for UI modules are read by bitbake and the contract should not be broken
40
41
42featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES, bb.cooker.CookerFeatures.SEND_DEPENDS_TREE, bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING, bb.cooker.CookerFeatures.SEND_SANITYEVENTS]
43
44logger = logging.getLogger("ToasterLogger")
45interactive = sys.stdout.isatty()
46
Patrick Williamsc124f4f2015-09-15 14:41:29 -050047def _log_settings_from_server(server):
48 # Get values of variables which control our output
49 includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"])
50 if error:
51 logger.error("Unable to get the value of BBINCLUDELOGS variable: %s", error)
52 raise BaseException(error)
53 loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"])
54 if error:
55 logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s", error)
56 raise BaseException(error)
57 consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"])
58 if error:
59 logger.error("Unable to get the value of BB_CONSOLELOG variable: %s", error)
60 raise BaseException(error)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050061 return consolelogfile
Patrick Williamsc124f4f2015-09-15 14:41:29 -050062
Patrick Williamsf1e5d692016-03-30 15:21:19 -050063# create a log file for a single build and direct the logger at it;
64# log file name is timestamped to the millisecond (depending
65# on system clock accuracy) to ensure it doesn't overlap with
66# other log file names
67#
68# returns (log file, path to log file) for a build
69def _open_build_log(log_dir):
70 format_str = "%(levelname)s: %(message)s"
Patrick Williamsc124f4f2015-09-15 14:41:29 -050071
Patrick Williamsf1e5d692016-03-30 15:21:19 -050072 now = time.time()
73 now_ms = int((now - int(now)) * 1000)
74 time_str = time.strftime('build_%Y%m%d_%H%M%S', time.localtime(now))
75 log_file_name = time_str + ('.%d.log' % now_ms)
76 build_log_file_path = os.path.join(log_dir, log_file_name)
77
78 build_log = logging.FileHandler(build_log_file_path)
79
80 logformat = bb.msg.BBLogFormatter(format_str)
81 build_log.setFormatter(logformat)
82
83 bb.msg.addDefaultlogFilter(build_log)
84 logger.addHandler(build_log)
85
86 return (build_log, build_log_file_path)
87
88# stop logging to the build log if it exists
89def _close_build_log(build_log):
90 if build_log:
91 build_log.flush()
92 build_log.close()
93 logger.removeHandler(build_log)
94
95def main(server, eventHandler, params):
96 # set to a logging.FileHandler instance when a build starts;
97 # see _open_build_log()
98 build_log = None
99
100 # set to the log path when a build starts
101 build_log_file_path = None
102
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500103 helper = uihelper.BBUIHelper()
104
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500105 # TODO don't use log output to determine when bitbake has started
106 #
107 # WARNING: this log handler cannot be removed, as localhostbecontroller
108 # relies on output in the toaster_ui.log file to determine whether
109 # the bitbake server has started, which only happens if
110 # this logger is setup here (see the TODO in the loop below)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500111 console = logging.StreamHandler(sys.stdout)
112 format_str = "%(levelname)s: %(message)s"
113 formatter = bb.msg.BBLogFormatter(format_str)
114 bb.msg.addDefaultlogFilter(console)
115 console.setFormatter(formatter)
116 logger.addHandler(console)
117 logger.setLevel(logging.INFO)
118
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500119 # verify and warn
120 build_history_enabled = True
121 inheritlist, _ = server.runCommand(["getVariable", "INHERIT"])
122
123 if not "buildhistory" in inheritlist.split(" "):
124 logger.warn("buildhistory is not enabled. Please enable INHERIT += \"buildhistory\" to see image details.")
125 build_history_enabled = False
126
127 if not params.observe_only:
128 logger.error("ToasterUI can only work in observer mode")
129 return 1
130
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500131 # set to 1 when toasterui needs to shut down
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500132 main.shutdown = 0
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500133
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500134 interrupted = False
135 return_value = 0
136 errors = 0
137 warnings = 0
138 taskfailures = []
139 first = True
140
141 buildinfohelper = BuildInfoHelper(server, build_history_enabled)
142
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500143 # write our own log files into bitbake's log directory;
144 # we're only interested in the path to the parent directory of
145 # this file, as we're writing our own logs into the same directory
146 consolelogfile = _log_settings_from_server(server)
147 log_dir = os.path.dirname(consolelogfile)
148 bb.utils.mkdirhier(log_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500149
150 while True:
151 try:
152 event = eventHandler.waitEvent(0.25)
153 if first:
154 first = False
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500155
156 # TODO don't use log output to determine when bitbake has started
157 #
158 # this is the line localhostbecontroller needs to
159 # see in toaster_ui.log which it uses to decide whether
160 # the bitbake server has started...
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500161 logger.info("ToasterUI waiting for events")
162
163 if event is None:
164 if main.shutdown > 0:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500165 # if shutting down, close any open build log first
166 _close_build_log(build_log)
167
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 break
169 continue
170
171 helper.eventHandler(event)
172
173 # pylint: disable=protected-access
174 # the code will look into the protected variables of the event; no easy way around this
175
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500176 # we treat ParseStarted as the first event of toaster-triggered
177 # builds; that way we get the Build Configuration included in the log
178 # and any errors that occur before BuildStarted is fired
179 if isinstance(event, bb.event.ParseStarted):
180 if not (build_log and build_log_file_path):
181 build_log, build_log_file_path = _open_build_log(log_dir)
182 continue
183
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500184 if isinstance(event, bb.event.BuildStarted):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500185 # command-line builds don't fire a ParseStarted event,
186 # so we have to start the log file for those on BuildStarted instead
187 if not (build_log and build_log_file_path):
188 build_log, build_log_file_path = _open_build_log(log_dir)
189
190 buildinfohelper.store_started_build(event, build_log_file_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500191
192 if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)):
193 buildinfohelper.update_and_store_task(event)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500194 logger.info("Logfile for task %s", event.logfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500195 continue
196
197 if isinstance(event, bb.build.TaskBase):
198 logger.info(event._message)
199
200 if isinstance(event, bb.event.LogExecTTY):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500201 logger.info(event.msg)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500202 continue
203
204 if isinstance(event, logging.LogRecord):
205 if event.levelno == -1:
206 event.levelno = formatter.ERROR
207
208 buildinfohelper.store_log_event(event)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500209
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500210 if event.levelno >= formatter.ERROR:
211 errors = errors + 1
212 elif event.levelno == formatter.WARNING:
213 warnings = warnings + 1
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500214
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500215 # For "normal" logging conditions, don't show note logs from tasks
216 # but do show them if the user has changed the default log level to
217 # include verbose/debug messages
218 if event.taskpid != 0 and event.levelno <= formatter.NOTE:
219 continue
220
221 logger.handle(event)
222 continue
223
224 if isinstance(event, bb.build.TaskFailed):
225 buildinfohelper.update_and_store_task(event)
226 logfile = event.logfile
227 if logfile and os.path.exists(logfile):
228 bb.error("Logfile of failure stored in: %s" % logfile)
229 continue
230
231 # these events are unprocessed now, but may be used in the future to log
232 # timing and error informations from the parsing phase in Toaster
233 if isinstance(event, (bb.event.SanityCheckPassed, bb.event.SanityCheck)):
234 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500235 if isinstance(event, bb.event.ParseProgress):
236 continue
237 if isinstance(event, bb.event.ParseCompleted):
238 continue
239 if isinstance(event, bb.event.CacheLoadStarted):
240 continue
241 if isinstance(event, bb.event.CacheLoadProgress):
242 continue
243 if isinstance(event, bb.event.CacheLoadCompleted):
244 continue
245 if isinstance(event, bb.event.MultipleProviders):
246 logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "",
247 event._item,
248 ", ".join(event._candidates))
249 logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item)
250 continue
251
252 if isinstance(event, bb.event.NoProvider):
253 errors = errors + 1
254 if event._runtime:
255 r = "R"
256 else:
257 r = ""
258
259 if event._dependees:
260 text = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)" % (r, event._item, ", ".join(event._dependees), r)
261 else:
262 text = "Nothing %sPROVIDES '%s'" % (r, event._item)
263
264 logger.error(text)
265 if event._reasons:
266 for reason in event._reasons:
267 logger.error("%s", reason)
268 text += reason
269 buildinfohelper.store_log_error(text)
270 continue
271
272 if isinstance(event, bb.event.ConfigParsed):
273 continue
274 if isinstance(event, bb.event.RecipeParsed):
275 continue
276
277 # end of saved events
278
279 if isinstance(event, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted, bb.runqueue.runQueueTaskSkipped)):
280 buildinfohelper.store_started_task(event)
281 continue
282
283 if isinstance(event, bb.runqueue.runQueueTaskCompleted):
284 buildinfohelper.update_and_store_task(event)
285 continue
286
287 if isinstance(event, bb.runqueue.runQueueTaskFailed):
288 buildinfohelper.update_and_store_task(event)
289 taskfailures.append(event.taskstring)
290 logger.error("Task %s (%s) failed with exit code '%s'",
291 event.taskid, event.taskstring, event.exitcode)
292 continue
293
294 if isinstance(event, (bb.runqueue.sceneQueueTaskCompleted, bb.runqueue.sceneQueueTaskFailed)):
295 buildinfohelper.update_and_store_task(event)
296 continue
297
298
299 if isinstance(event, (bb.event.TreeDataPreparationStarted, bb.event.TreeDataPreparationCompleted)):
300 continue
301
302 if isinstance(event, (bb.event.BuildCompleted, bb.command.CommandFailed)):
303
304 errorcode = 0
305 if isinstance(event, bb.command.CommandFailed):
306 errors += 1
307 errorcode = 1
308 logger.error("Command execution failed: %s", event.error)
309
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500310 # turn off logging to the current build log
311 _close_build_log(build_log)
312
313 # reset ready for next BuildStarted
314 build_log = None
315
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500316 # update the build info helper on BuildCompleted, not on CommandXXX
317 buildinfohelper.update_build_information(event, errors, warnings, taskfailures)
318 buildinfohelper.close(errorcode)
319 # mark the log output; controllers may kill the toasterUI after seeing this log
320 logger.info("ToasterUI build done 1, brbe: %s", buildinfohelper.brbe )
321
322 # we start a new build info
323 if buildinfohelper.brbe is not None:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500324 logger.debug("ToasterUI under BuildEnvironment management - exiting after the build")
325 server.terminateServer()
326 else:
327 logger.debug("ToasterUI prepared for new build")
328 errors = 0
329 warnings = 0
330 taskfailures = []
331 buildinfohelper = BuildInfoHelper(server, build_history_enabled)
332
333 logger.info("ToasterUI build done 2")
334 continue
335
336 if isinstance(event, (bb.command.CommandCompleted,
337 bb.command.CommandFailed,
338 bb.command.CommandExit)):
339 errorcode = 0
340
341 continue
342
343 if isinstance(event, bb.event.MetadataEvent):
344 if event.type == "SinglePackageInfo":
345 buildinfohelper.store_build_package_information(event)
346 elif event.type == "LayerInfo":
347 buildinfohelper.store_layer_info(event)
348 elif event.type == "BuildStatsList":
349 buildinfohelper.store_tasks_stats(event)
350 elif event.type == "ImagePkgList":
351 buildinfohelper.store_target_package_data(event)
352 elif event.type == "MissedSstate":
353 buildinfohelper.store_missed_state_tasks(event)
354 elif event.type == "ImageFileSize":
355 buildinfohelper.update_target_image_file(event)
356 elif event.type == "ArtifactFileSize":
357 buildinfohelper.update_artifact_image_file(event)
358 elif event.type == "LicenseManifestPath":
359 buildinfohelper.store_license_manifest_path(event)
360 else:
361 logger.error("Unprocessed MetadataEvent %s ", str(event))
362 continue
363
364 if isinstance(event, bb.cooker.CookerExit):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500365 # shutdown when bitbake server shuts down
366 main.shutdown = 1
367 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500368
369 # ignore
370 if isinstance(event, (bb.event.BuildBase,
371 bb.event.StampUpdate,
372 bb.event.RecipePreFinalise,
373 bb.runqueue.runQueueEvent,
374 bb.runqueue.runQueueExitWait,
375 bb.event.OperationProgress,
376 bb.command.CommandFailed,
377 bb.command.CommandExit,
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500378 bb.command.CommandCompleted,
379 bb.event.ReachableStamps)):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500380 continue
381
382 if isinstance(event, bb.event.DepTreeGenerated):
383 buildinfohelper.store_dependency_information(event)
384 continue
385
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500386 logger.warn("Unknown event: %s", event)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500387 return_value += 1
388
389 except EnvironmentError as ioerror:
390 # ignore interrupted io
391 if ioerror.args[0] == 4:
392 pass
393 except KeyboardInterrupt:
394 main.shutdown = 1
395 except Exception as e:
396 # print errors to log
397 import traceback
398 from pprint import pformat
399 exception_data = traceback.format_exc()
400 logger.error("%s\n%s" , e, exception_data)
401
402 _, _, tb = sys.exc_info()
403 if tb is not None:
404 curr = tb
405 while curr is not None:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500406 logger.error("Error data dump %s\n%s\n" , traceback.format_tb(curr,1), pformat(curr.tb_frame.f_locals))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500407 curr = curr.tb_next
408
409 # save them to database, if possible; if it fails, we already logged to console.
410 try:
411 buildinfohelper.store_log_exception("%s\n%s" % (str(e), exception_data))
412 except Exception as ce:
413 logger.error("CRITICAL - Failed to to save toaster exception to the database: %s", str(ce))
414
415 # make sure we return with an error
416 return_value += 1
417
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500418 if interrupted and return_value == 0:
419 return_value += 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500420
421 logger.warn("Return value is %d", return_value)
422 return return_value