blob: 931c0cdbb58b6e8a6fb6e38895a1459eafbc5f6e [file] [log] [blame]
# Copyright 2017 The Tulsi Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Parses a stream of JSON build event protocol messages from a file."""
import json
class _FileLineReader(object):
"""Reads lines from a streaming file.
This will repeatedly check the file for an entire line to read. It will
buffer partial lines until they are completed.
This is meant for files that are being modified by an long-living external
program.
"""
def __init__(self, file_obj):
"""Creates a new FileLineReader object.
Args:
file_obj: The file object to watch.
Returns:
A FileLineReader instance.
"""
self._file_obj = file_obj
self._buffer = []
def check_for_changes(self):
"""Checks the file for any changes, returning the line read if any."""
line = self._file_obj.readline()
self._buffer.append(line)
# Only parse complete lines.
if not line.endswith('\n'):
return None
full_line = ''.join(self._buffer)
del self._buffer[:]
return full_line
class BazelBuildEvent(object):
"""Represents a Bazel Build Event.
Public Properties:
event_dict: the source dictionary for this event.
id_as_string: 'id' in event_dict as a json string, if any.
children_id_strings: list of all children id strings.
stdout: stdout string, if any.
stderr: stderr string, if any.
files: list of file URIs.
"""
def __init__(self, event_dict):
"""Creates a new BazelBuildEvent object.
Args:
event_dict: Dictionary representing a build event
Returns:
A BazelBuildEvent instance.
"""
self.event_dict = event_dict
self.id_as_string = (json.dumps(event_dict['id']) if 'id' in event_dict
else None)
children = event_dict.get('children', [])
self.children_id_strings = [json.dumps(x) for x in children]
self._children_by_id_string = {}
self.stdout = None
self.stderr = None
self.files = []
if 'progress' in event_dict:
self._update_fields_for_progress(event_dict['progress'])
if 'namedSetOfFiles' in event_dict:
self._update_fields_for_named_set_of_files(event_dict['namedSetOfFiles'])
def _update_fields_for_progress(self, progress_dict):
self.stdout = progress_dict.get('stdout')
self.stderr = progress_dict.get('stderr')
def _update_fields_for_named_set_of_files(self, named_set):
files = named_set.get('files', [])
for file_obj in files:
uri = file_obj.get('uri', '')
if uri.startswith('file://'):
self.files.append(uri[7:])
def is_resolved(self):
"""Returns True iff this build event is considered resolved.
This means all of it's children (and their children, recursively) are
present in the tree.
"""
children_id_strings = self.children_id_strings
if not children_id_strings:
return True
children_by_id = self._children_by_id_string
for child_id in children_id_strings:
if child_id not in children_by_id:
return False
if not children_by_id[child_id].is_resolved():
return False
return True
def resolved_children_events(self):
"""Returns all of the currently resolved child BazelBuildEvents."""
events = []
children_id_strings = self.children_id_strings
if not children_id_strings:
return events
children_by_id = self._children_by_id_string
for child_id in children_id_strings:
if child_id in children_by_id:
events.append(children_by_id[child_id])
return events
def insert_child(self, child_event):
"""Inserts the specified BazelBuildEvent as a child."""
self._children_by_id_string[child_event.id_as_string] = child_event
class _BazelBuildEventTree(object):
"""Class to help resolve a structured tree from a stream of Build Events."""
def __init__(self, root_build_event, warning_handler):
"""Creates a new BazelBuildEventTree object.
Args:
root_build_event: The root BazelBuildEvent
warning_handler: Handler function for warnings accepting a single string.
Returns:
A BazelBuildEventTree instance.
"""
self.root_build_event = root_build_event
self.warning_handler = warning_handler
self.event_parents_by_id = {}
self._index_children_of_event(self.root_build_event)
def insert_child(self, build_event):
"""Inserts the BazelBuildEvent into the tree."""
self._attach_child_event_to_parent(build_event)
self._index_children_of_event(build_event)
def is_complete(self):
"""Returns True iff the root of the tree is considered resolved.
This means all of it's children (and their children, recursively) are
present in the tree.
"""
return self.root_build_event.is_resolved()
def _attach_child_event_to_parent(self, build_event):
"""Attach a build_event to its proper parent using our parent index."""
event_id = build_event.id_as_string
if not event_id:
event_str = json.dumps(build_event.event_dict)
self._warn('BazelBuildEvent %s does not have an id!' % event_str)
return
parent = self.event_parents_by_id.get(event_id, None)
if not parent:
self._warn('Unable to find parent for BazelBuildEvent %s!' % event_id)
return
parent.insert_child(build_event)
def _index_children_of_event(self, build_event):
"""Index the children of build_event so we can easily attach them later."""
event_parents_by_id = self.event_parents_by_id
for child_id in build_event.children_id_strings:
event_parents_by_id[child_id] = build_event
def _warn(self, msg):
"""Logs the warning to the warning_handler if it exists."""
if self.warning_handler:
self.warning_handler(msg)
class BazelBuildEventsWatcher(object):
"""Watches a build events JSON file."""
def __init__(self, json_file, warning_handler=None):
"""Creates a new BazelBuildEventsWatcher object.
Args:
json_file: The JSON file object to watch.
warning_handler: Handler function for warnings accepting a single string.
Returns:
A BazelBuildEventsWatcher instance.
"""
self.file_reader = _FileLineReader(json_file)
self.build_event_tree = None
self.warning_handler = warning_handler
def check_for_new_events(self):
"""Checks the file for new BazelBuildEvents.
Returns:
A list of all new BazelBuildEvents.
"""
new_events = []
while True:
line = self.file_reader.check_for_changes()
if not line:
break
build_event_dict = json.loads(line)
build_event = BazelBuildEvent(build_event_dict)
if not self.build_event_tree:
handler = self.warning_handler
self.build_event_tree = _BazelBuildEventTree(build_event, handler)
else:
self.build_event_tree.insert_child(build_event)
new_events.append(build_event)
return new_events
def is_build_complete(self):
tree = self.build_event_tree
return tree.is_complete() if tree else False