| # Copyright (c) 2009 Google Inc. All rights reserved. |
| # Copyright (c) 2009 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # * Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # * Redistributions in binary form must reproduce the above |
| # copyright notice, this list of conditions and the following disclaimer |
| # in the documentation and/or other materials provided with the |
| # distribution. |
| # * Neither the name of Google Inc. nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import logging |
| import sys |
| import traceback |
| |
| from datetime import datetime, timedelta |
| |
| from webkitpy.common.host import Host |
| from webkitpy.common.system import logutils |
| from webkitpy.common.system.executive import ScriptError |
| from webkitpy.common.system.outputtee import OutputTee |
| |
| _log = logging.getLogger(__name__) |
| |
| |
| # FIXME: This will be caught by "except Exception:" blocks, we should consider |
| # making this inherit from SystemExit instead (or BaseException, except that's not recommended). |
| class TerminateQueue(Exception): |
| pass |
| |
| |
| class QueueEngineDelegate: |
| def queue_log_path(self): |
| raise NotImplementedError('subclasses must implement') |
| |
| def work_item_log_path(self, work_item): |
| raise NotImplementedError('subclasses must implement') |
| |
| def begin_work_queue(self): |
| raise NotImplementedError('subclasses must implement') |
| |
| def should_continue_work_queue(self): |
| raise NotImplementedError('subclasses must implement') |
| |
| def next_work_item(self): |
| raise NotImplementedError('subclasses must implement') |
| |
| def process_work_item(self, work_item): |
| raise NotImplementedError('subclasses must implement') |
| |
| def handle_unexpected_error(self, work_item, message): |
| raise NotImplementedError('subclasses must implement') |
| |
| |
| class QueueEngine: |
| def __init__(self, name, delegate, wakeup_event, seconds_to_sleep=120): |
| self._name = name |
| self._delegate = delegate |
| self._wakeup_event = wakeup_event |
| self._output_tee = OutputTee() |
| self._seconds_to_sleep = seconds_to_sleep |
| |
| log_date_format = "%Y-%m-%d %H:%M:%S" |
| handled_error_code = 2 |
| |
| # Child processes exit with a special code to the parent queue process can detect the error was handled. |
| @classmethod |
| def exit_after_handled_error(cls, error): |
| _log.error(error) |
| sys.exit(cls.handled_error_code) |
| |
| def run(self): |
| self._begin_logging() |
| |
| self._delegate.begin_work_queue() |
| while (self._delegate.should_continue_work_queue()): |
| try: |
| self._ensure_work_log_closed() |
| work_item = self._delegate.next_work_item() |
| if not work_item: |
| self._sleep("No work item.") |
| continue |
| |
| try: |
| if not self._delegate.process_work_item(work_item): |
| _log.warning("Unable to process work item.") |
| continue |
| except ScriptError as e: |
| self._open_work_log(work_item) |
| self._work_log.write(e.message_with_output(output_limit=5000)) |
| # Use a special exit code to indicate that the error was already |
| # handled in the child process and we should just keep looping. |
| if e.exit_code == self.handled_error_code: |
| continue |
| message = "Unexpected failure when processing patch! Please file a bug against webkit-patch.\n%s" % e.message_with_output(output_limit=5000) |
| self._delegate.handle_unexpected_error(work_item, message) |
| except TerminateQueue as e: |
| self._stopping("TerminateQueue exception received.") |
| return 0 |
| except KeyboardInterrupt as e: |
| self._stopping("User terminated queue.") |
| return 1 |
| except Exception as e: |
| traceback.print_exc() |
| # Don't try tell the status bot, in case telling it causes an exception. |
| self._sleep("Exception while preparing queue") |
| self._stopping("Delegate terminated queue.") |
| return 0 |
| |
| def _stopping(self, message): |
| _log.info(message) |
| self._delegate.stop_work_queue(message) |
| logging.getLogger("webkitpy").removeHandler(self._log_handler) |
| # Be careful to shut down our OutputTee or the unit tests will be unhappy. |
| self._ensure_work_log_closed() |
| self._output_tee.remove_log(self._queue_log) |
| |
| def _begin_logging(self): |
| _queue_log_path = self._delegate.queue_log_path() |
| # We are using logging.getLogger("webkitpy") instead of _log since we want to capture all messages logged from webkitpy modules. |
| self._log_handler = logutils.configure_logger_to_log_to_file(logging.getLogger("webkitpy"), _queue_log_path, Host().filesystem) |
| self._queue_log = self._output_tee.add_log(_queue_log_path) |
| self._work_log = None |
| |
| def _open_work_log(self, work_item): |
| work_item_log_path = self._delegate.work_item_log_path(work_item) |
| if not work_item_log_path: |
| return |
| self._work_log = self._output_tee._open_log_file(work_item_log_path) |
| |
| def _ensure_work_log_closed(self): |
| # If we still have a bug log open, close it. |
| if self._work_log: |
| self._work_log.close() |
| self._work_log = None |
| |
| def _now(self): |
| """Overriden by the unit tests to allow testing _sleep_message""" |
| return datetime.now() |
| |
| def _sleep_message(self, message): |
| wake_time = self._now() + timedelta(seconds=self._seconds_to_sleep) |
| if self._seconds_to_sleep < 3 * 60: |
| sleep_duration_text = str(self._seconds_to_sleep) + ' seconds' |
| else: |
| sleep_duration_text = str(round(self._seconds_to_sleep / 60)) + ' minutes' |
| return "%s Sleeping until %s (%s)." % (message, wake_time.strftime(self.log_date_format), sleep_duration_text) |
| |
| def _sleep(self, message): |
| _log.info(self._sleep_message(message)) |
| self._wakeup_event.wait(self._seconds_to_sleep) |
| self._wakeup_event.clear() |