Message ID | 20250627134837.151783-1-ross.burton@arm.com |
---|---|
State | New |
Headers | show |
Series | [v2] tinfoil: add wait_for decorator and build_file_sync() helper | expand |
FWIW I have a patch locally to delete all the code from build_target and use this, but I’d like to write a basic test case first. Ross > On 27 Jun 2025, at 14:48, Ross Burton via lists.openembedded.org <ross.burton=arm.com@lists.openembedded.org> wrote: > > The bitbake worker/server IPC is asynchronous, but tinfoil only has > functionality to wait for a response on the build_targets() call. > > Extract the bulk of the "wait for events and handle errors" logic to a > standalone wait_for wrapper, which is the build_targets code without the > extra_events or event_callback arguments (for now). > > Then use this to create a build_file_sync() helper that just wraps the > existing build_file() with @wait_for. > > Signed-off-by: Ross Burton <ross.burton@arm.com> > --- > bitbake/lib/bb/tinfoil.py | 135 +++++++++++++++++++++++++++++++++++++- > 1 file changed, 134 insertions(+), 1 deletion(-) > > diff --git a/bitbake/lib/bb/tinfoil.py b/bitbake/lib/bb/tinfoil.py > index f48baeb3342..e7fbcbca0a6 100644 > --- a/bitbake/lib/bb/tinfoil.py > +++ b/bitbake/lib/bb/tinfoil.py > @@ -14,7 +14,7 @@ import time > import atexit > import re > from collections import OrderedDict, defaultdict > -from functools import partial > +from functools import partial, wraps > from contextlib import contextmanager > > import bb.cache > @@ -27,6 +27,135 @@ import bb.remotedata > from bb.main import setup_bitbake, BitBakeConfigParameters > import bb.fetch2 > > +def wait_for(f): > + """ > + Wrap a function that makes an asynchronous tinfoil call using > + self.run_command() and wait for events to say that the call has been > + successful, or an error has occurred. > + """ > + @wraps(f) > + def wrapper(self, *args, handle_events=True, extra_events=None, event_callback=None, **kwargs): > + if handle_events: > + # A reasonable set of default events matching up with those we handle below > + eventmask = [ > + 'bb.event.BuildStarted', > + 'bb.event.BuildCompleted', > + 'logging.LogRecord', > + 'bb.event.NoProvider', > + 'bb.command.CommandCompleted', > + 'bb.command.CommandFailed', > + 'bb.build.TaskStarted', > + 'bb.build.TaskFailed', > + 'bb.build.TaskSucceeded', > + 'bb.build.TaskFailedSilent', > + 'bb.build.TaskProgress', > + 'bb.runqueue.runQueueTaskStarted', > + 'bb.runqueue.sceneQueueTaskStarted', > + 'bb.event.ProcessStarted', > + 'bb.event.ProcessProgress', > + 'bb.event.ProcessFinished', > + ] > + if extra_events: > + eventmask.extend(extra_events) > + ret = self.set_event_mask(eventmask) > + > + includelogs = self.config_data.getVar('BBINCLUDELOGS') > + loglines = self.config_data.getVar('BBINCLUDELOGS_LINES') > + > + # Call actual function > + ret = f(self, *args, **kwargs) > + > + if handle_events: > + lastevent = time.time() > + result = False > + # Borrowed from knotty, instead somewhat hackily we use the helper > + # as the object to store "shutdown" on > + helper = bb.ui.uihelper.BBUIHelper() > + helper.shutdown = 0 > + parseprogress = None > + termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet) > + try: > + while True: > + try: > + event = self.wait_event(0.25) > + if event: > + lastevent = time.time() > + if event_callback and event_callback(event): > + continue > + if helper.eventHandler(event): > + if isinstance(event, bb.build.TaskFailedSilent): > + self.logger.warning("Logfile for failed setscene task is %s" % event.logfile) > + elif isinstance(event, bb.build.TaskFailed): > + bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter) > + continue > + if isinstance(event, bb.event.ProcessStarted): > + if self.quiet > 1: > + continue > + parseprogress = bb.ui.knotty.new_progress(event.processname, event.total) > + parseprogress.start(False) > + continue > + if isinstance(event, bb.event.ProcessProgress): > + if self.quiet > 1: > + continue > + if parseprogress: > + parseprogress.update(event.progress) > + else: > + bb.warn("Got ProcessProgress event for something that never started?") > + continue > + if isinstance(event, bb.event.ProcessFinished): > + if self.quiet > 1: > + continue > + if parseprogress: > + parseprogress.finish() > + parseprogress = None > + continue > + if isinstance(event, bb.command.CommandCompleted): > + result = True > + break > + if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)): > + self.logger.error(str(event)) > + result = False > + break > + if isinstance(event, logging.LogRecord): > + if event.taskpid == 0 or event.levelno > logging.INFO: > + self.logger.handle(event) > + continue > + if isinstance(event, bb.event.NoProvider): > + self.logger.error(str(event)) > + result = False > + break > + elif helper.shutdown > 1: > + break > + termfilter.updateFooter() > + if time.time() > (lastevent + (3*60)): > + if not self.run_command('ping', handle_events=False): > + print("\nUnable to ping server and no events, closing down...\n") > + return False > + except KeyboardInterrupt: > + termfilter.clearFooter() > + if helper.shutdown == 1: > + print("\nSecond Keyboard Interrupt, stopping...\n") > + ret = self.run_command("stateForceShutdown") > + if ret and ret[2]: > + self.logger.error("Unable to cleanly stop: %s" % ret[2]) > + elif helper.shutdown == 0: > + print("\nKeyboard Interrupt, closing down...\n") > + interrupted = True > + ret = self.run_command("stateShutdown") > + if ret and ret[2]: > + self.logger.error("Unable to cleanly shutdown: %s" % ret[2]) > + helper.shutdown = helper.shutdown + 1 > + termfilter.clearFooter() > + finally: > + termfilter.finish() > + if helper.failed_tasks: > + result = False > + return result > + else: > + return ret > + > + return wrapper > + > > # We need this in order to shut down the connection to the bitbake server, > # otherwise the process will never properly exit > @@ -700,6 +829,10 @@ class Tinfoil: > """ > return self.run_command('buildFile', buildfile, task, internal) > > + @wait_for > + def build_file_sync(self, *args): > + self.build_file(*args) > + > def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None): > """ > Builds the specified targets. This is equivalent to a normal invocation > -- > 2.43.0 > > > -=-=-=-=-=-=-=-=-=-=-=- > Links: You receive all messages sent to this group. > View/Reply Online (#17720): https://lists.openembedded.org/g/bitbake-devel/message/17720 > Mute This Topic: https://lists.openembedded.org/mt/113861633/6875888 > Group Owner: bitbake-devel+owner@lists.openembedded.org > Unsubscribe: https://lists.openembedded.org/g/bitbake-devel/unsub [ross.burton@arm.com] > -=-=-=-=-=-=-=-=-=-=-=- >
diff --git a/bitbake/lib/bb/tinfoil.py b/bitbake/lib/bb/tinfoil.py index f48baeb3342..e7fbcbca0a6 100644 --- a/bitbake/lib/bb/tinfoil.py +++ b/bitbake/lib/bb/tinfoil.py @@ -14,7 +14,7 @@ import time import atexit import re from collections import OrderedDict, defaultdict -from functools import partial +from functools import partial, wraps from contextlib import contextmanager import bb.cache @@ -27,6 +27,135 @@ import bb.remotedata from bb.main import setup_bitbake, BitBakeConfigParameters import bb.fetch2 +def wait_for(f): + """ + Wrap a function that makes an asynchronous tinfoil call using + self.run_command() and wait for events to say that the call has been + successful, or an error has occurred. + """ + @wraps(f) + def wrapper(self, *args, handle_events=True, extra_events=None, event_callback=None, **kwargs): + if handle_events: + # A reasonable set of default events matching up with those we handle below + eventmask = [ + 'bb.event.BuildStarted', + 'bb.event.BuildCompleted', + 'logging.LogRecord', + 'bb.event.NoProvider', + 'bb.command.CommandCompleted', + 'bb.command.CommandFailed', + 'bb.build.TaskStarted', + 'bb.build.TaskFailed', + 'bb.build.TaskSucceeded', + 'bb.build.TaskFailedSilent', + 'bb.build.TaskProgress', + 'bb.runqueue.runQueueTaskStarted', + 'bb.runqueue.sceneQueueTaskStarted', + 'bb.event.ProcessStarted', + 'bb.event.ProcessProgress', + 'bb.event.ProcessFinished', + ] + if extra_events: + eventmask.extend(extra_events) + ret = self.set_event_mask(eventmask) + + includelogs = self.config_data.getVar('BBINCLUDELOGS') + loglines = self.config_data.getVar('BBINCLUDELOGS_LINES') + + # Call actual function + ret = f(self, *args, **kwargs) + + if handle_events: + lastevent = time.time() + result = False + # Borrowed from knotty, instead somewhat hackily we use the helper + # as the object to store "shutdown" on + helper = bb.ui.uihelper.BBUIHelper() + helper.shutdown = 0 + parseprogress = None + termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet) + try: + while True: + try: + event = self.wait_event(0.25) + if event: + lastevent = time.time() + if event_callback and event_callback(event): + continue + if helper.eventHandler(event): + if isinstance(event, bb.build.TaskFailedSilent): + self.logger.warning("Logfile for failed setscene task is %s" % event.logfile) + elif isinstance(event, bb.build.TaskFailed): + bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter) + continue + if isinstance(event, bb.event.ProcessStarted): + if self.quiet > 1: + continue + parseprogress = bb.ui.knotty.new_progress(event.processname, event.total) + parseprogress.start(False) + continue + if isinstance(event, bb.event.ProcessProgress): + if self.quiet > 1: + continue + if parseprogress: + parseprogress.update(event.progress) + else: + bb.warn("Got ProcessProgress event for something that never started?") + continue + if isinstance(event, bb.event.ProcessFinished): + if self.quiet > 1: + continue + if parseprogress: + parseprogress.finish() + parseprogress = None + continue + if isinstance(event, bb.command.CommandCompleted): + result = True + break + if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)): + self.logger.error(str(event)) + result = False + break + if isinstance(event, logging.LogRecord): + if event.taskpid == 0 or event.levelno > logging.INFO: + self.logger.handle(event) + continue + if isinstance(event, bb.event.NoProvider): + self.logger.error(str(event)) + result = False + break + elif helper.shutdown > 1: + break + termfilter.updateFooter() + if time.time() > (lastevent + (3*60)): + if not self.run_command('ping', handle_events=False): + print("\nUnable to ping server and no events, closing down...\n") + return False + except KeyboardInterrupt: + termfilter.clearFooter() + if helper.shutdown == 1: + print("\nSecond Keyboard Interrupt, stopping...\n") + ret = self.run_command("stateForceShutdown") + if ret and ret[2]: + self.logger.error("Unable to cleanly stop: %s" % ret[2]) + elif helper.shutdown == 0: + print("\nKeyboard Interrupt, closing down...\n") + interrupted = True + ret = self.run_command("stateShutdown") + if ret and ret[2]: + self.logger.error("Unable to cleanly shutdown: %s" % ret[2]) + helper.shutdown = helper.shutdown + 1 + termfilter.clearFooter() + finally: + termfilter.finish() + if helper.failed_tasks: + result = False + return result + else: + return ret + + return wrapper + # We need this in order to shut down the connection to the bitbake server, # otherwise the process will never properly exit @@ -700,6 +829,10 @@ class Tinfoil: """ return self.run_command('buildFile', buildfile, task, internal) + @wait_for + def build_file_sync(self, *args): + self.build_file(*args) + def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None): """ Builds the specified targets. This is equivalent to a normal invocation
The bitbake worker/server IPC is asynchronous, but tinfoil only has functionality to wait for a response on the build_targets() call. Extract the bulk of the "wait for events and handle errors" logic to a standalone wait_for wrapper, which is the build_targets code without the extra_events or event_callback arguments (for now). Then use this to create a build_file_sync() helper that just wraps the existing build_file() with @wait_for. Signed-off-by: Ross Burton <ross.burton@arm.com> --- bitbake/lib/bb/tinfoil.py | 135 +++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 1 deletion(-)