diff options
author | Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> | 2018-07-18 16:39:07 +0900 |
---|---|---|
committer | Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> | 2018-07-18 18:59:03 +0900 |
commit | a950a985e0fa05cf4f7b1b1bbc1de6db338e821c (patch) | |
tree | d8fc611d27effd61b34d18a5c69334d3d3cac549 /buildstream/_context.py | |
parent | 308d59be60f3410db26fc691e830df5d2cc3decb (diff) | |
download | buildstream-a950a985e0fa05cf4f7b1b1bbc1de6db338e821c.tar.gz |
_context.py: Adding apis for message recording and task logging.
Added the new Context.recorded_messages() context manager, this
causes messages to be logged in a dedicated log file before being
propagated to the log handler, and also provides an open log handle
for the capturing and logging of stdout/stderr from child processes.
This comes with the additional accessors:
Context.get_log_handle()
Context.get_log_filename()
Diffstat (limited to 'buildstream/_context.py')
-rw-r--r-- | buildstream/_context.py | 139 |
1 files changed, 138 insertions, 1 deletions
diff --git a/buildstream/_context.py b/buildstream/_context.py index 5cc7f434c..fbb0a9686 100644 --- a/buildstream/_context.py +++ b/buildstream/_context.py @@ -31,6 +31,7 @@ from ._message import Message, MessageType from ._profile import Topics, profile_start, profile_end from ._artifactcache import ArtifactCache from ._workspaces import Workspaces +from .plugin import _plugin_lookup # Context() @@ -121,6 +122,8 @@ class Context(): self._projects = [] self._project_overrides = {} self._workspaces = None + self._log_handle = None + self._log_filename = None # load() # @@ -385,8 +388,11 @@ class Context(): if message.depth is None: message.depth = len(list(self._message_depth)) + # If we are recording messages, dump a copy into the open log file. + self._record_message(message) + # Send it off to the log handler (can be the frontend, - # or it can be the child task which will log and propagate + # or it can be the child task which will propagate # to the frontend) assert self._message_handler @@ -456,6 +462,137 @@ class Context(): self._pop_message_depth() self.message(message) + # recorded_messages() + # + # Records all messages in a log file while the context manager + # is active. + # + # In addition to automatically writing all messages to the + # specified logging file, an open file handle for process stdout + # and stderr will be available via the Context.get_log_handle() API, + # and the full logfile path will be available via the + # Context.get_log_filename() API. + # + # Args: + # filename (str): A logging directory relative filename, + # the pid and .log extension will be automatically + # appended + # + # Yields: + # (str): The fully qualified log filename + # + @contextmanager + def recorded_messages(self, filename): + + # We dont allow recursing in this context manager, and + # we also do not allow it in the main process. + assert self._log_handle is None + assert self._log_filename is None + assert not utils._is_main_process() + + # Create the fully qualified logfile in the log directory, + # appending the pid and .log extension at the end. + self._log_filename = os.path.join(self.logdir, + '{}.{}.log'.format(filename, os.getpid())) + + # Ensure the directory exists first + directory = os.path.dirname(self._log_filename) + os.makedirs(directory, exist_ok=True) + + with open(self._log_filename, 'a') as logfile: + + # Write one last line to the log and flush it to disk + def flush_log(): + + # If the process currently had something happening in the I/O stack + # then trying to reenter the I/O stack will fire a runtime error. + # + # So just try to flush as well as we can at SIGTERM time + try: + logfile.write('\n\nForcefully terminated\n') + logfile.flush() + except RuntimeError: + os.fsync(logfile.fileno()) + + self._log_handle = logfile + with _signals.terminator(flush_log): + yield self._log_filename + + self._log_handle = None + self._log_filename = None + + # get_log_handle() + # + # Fetches the active log handle, this will return the active + # log file handle when the Context.recorded_messages() context + # manager is active + # + # Returns: + # (file): The active logging file handle, or None + # + def get_log_handle(self): + return self._log_handle + + # get_log_filename() + # + # Fetches the active log filename, this will return the active + # log filename when the Context.recorded_messages() context + # manager is active + # + # Returns: + # (str): The active logging filename, or None + # + def get_log_filename(self): + return self._log_filename + + # _record_message() + # + # Records the message if recording is enabled + # + # Args: + # message (Message): The message to record + # + def _record_message(self, message): + + if self._log_handle is None: + return + + INDENT = " " + EMPTYTIME = "--:--:--" + template = "[{timecode: <8}] {type: <7}" + + # If this message is associated with a plugin, print what + # we know about the plugin. + plugin_name = "" + if message.unique_id: + template += " {plugin}" + plugin = _plugin_lookup(message.unique_id) + plugin_name = plugin.name + + template += ": {message}" + + detail = '' + if message.detail is not None: + template += "\n\n{detail}" + detail = message.detail.rstrip('\n') + detail = INDENT + INDENT.join(detail.splitlines(True)) + + timecode = EMPTYTIME + if message.message_type in (MessageType.SUCCESS, MessageType.FAIL): + hours, remainder = divmod(int(message.elapsed.total_seconds()), 60**2) + minutes, seconds = divmod(remainder, 60) + timecode = "{0:02d}:{1:02d}:{2:02d}".format(hours, minutes, seconds) + + text = template.format(timecode=timecode, + plugin=plugin_name, + type=message.message_type.upper(), + message=message.message, + detail=detail) + + # Write to the open log file + self._log_handle.write('{}\n'.format(text)) + self._log_handle.flush() + # _push_message_depth() / _pop_message_depth() # # For status messages, send the depth of timed |