pw_watch: Automatically re-run build on interrupt This changes the watcher to automatically re-run the build in the case that there was an interruption-- either from a file change or a user build request. Additionally, this changes the watcher startup to trigger a build automatically, instead of waiting for a first change or keypress. Change-Id: Ifb19b4c1a15c78e77fad94a6a92704c03bb87988
diff --git a/pw_watch/py/pw_watch/debounce.py b/pw_watch/py/pw_watch/debounce.py index 66cf36f..87a2ff6 100644 --- a/pw_watch/py/pw_watch/debounce.py +++ b/pw_watch/py/pw_watch/debounce.py
@@ -47,11 +47,12 @@ class State(enum.Enum): - IDLE = 1 - DEBOUNCING = 2 - RUNNING = 3 - INTERRUPTED = 4 - COOLDOWN = 5 + IDLE = 1 # ------- Transistions to: DEBOUNCING + DEBOUNCING = 2 # - Transistions to: RUNNING + RUNNING = 3 # ---- Transistions to: INTERRUPTED or COOLDOWN + INTERRUPTED = 4 #- Transistions to: RERUN + COOLDOWN = 5 #---- Transistions to: IDLE + RERUN = 6 #------- Transistions to: IDLE (but triggers a press) class Debouncer: @@ -68,45 +69,54 @@ self.cooldown_seconds = 1 self.cooldown_timer = None + self.rerun_event_description = None + self.lock = threading.Lock() - def press(self, idle_message=None): + def press(self, event_description=None): """Try to run the function for the class. If the function is recently started, this may push out the deadline for actually starting. If the function is already running, will interrupt the function""" - _LOG.debug('Press - state = %s', str(self.state)) with self.lock: - if self.state == State.IDLE: - if idle_message: - _LOG.info(idle_message) - self._start_debounce_timer() - self._transition(State.DEBOUNCING) + self._press_unlocked(event_description) - elif self.state == State.DEBOUNCING: - self._start_debounce_timer() + def _press_unlocked(self, event_description=None): + _LOG.debug('Press - state = %s', str(self.state)) + if self.state == State.IDLE: + if event_description: + _LOG.info(event_description) + self._start_debounce_timer() + self._transition(State.DEBOUNCING) - elif self.state == State.RUNNING: - # Function is already running, so do nothing. - # TODO: It may make sense to queue an automatic re-build - # when an interruption is detected. Follow up on this after - # using the interruptable watcher in practice for awhile. + elif self.state == State.DEBOUNCING: + self._start_debounce_timer() - # Push an empty line to flush ongoing I/O in subprocess. - print() - print() - _LOG.error('File change detected while running') - _LOG.error('Build may be inconsistent or broken') - print() - self.function.cancel() - self._transition(State.INTERRUPTED) + elif self.state == State.RUNNING: + # When the function is already running but we get an incoming + # event, go into the INTERRUPTED state to signal that we should + # re-try running afterwards. - elif self.state == State.INTERRUPTED: - # Function is running but was already interrupted. Do nothing. - _LOG.debug('Ignoring press - interrupted') + # Push an empty line to flush ongoing I/O in subprocess. + print() - elif self.state == State.COOLDOWN: - # Function just finished and we are cooling down, so do nothing. - _LOG.debug('Ignoring press - cooldown') + # Surround the error message with newlines to make it stand out. + print() + _LOG.error('Event while running: %s', event_description) + print() + + self.function.cancel() + self._transition(State.INTERRUPTED) + self.rerun_event_description = event_description + + elif self.state == State.INTERRUPTED: + # Function is running but was already interrupted. Do nothing. + _LOG.debug('Ignoring press - interrupted') + + elif self.state == State.COOLDOWN: + # Function just finished and we are cooling down; so trigger rerun. + _LOG.debug('Got event in cooldown; scheduling rerun') + self._transition(State.RERUN) + self.rerun_event_description = event_description def _transition(self, new_state): _LOG.debug('State: %s -> %s', str(self.state), str(new_state)) @@ -135,8 +145,12 @@ _LOG.debug('Finished running debounced function') with self.lock: - self.function.on_complete(self.state == State.INTERRUPTED) - self._transition(State.COOLDOWN) + if self.state == State.RUNNING: + self.function.on_complete(cancelled=False) + self._transition(State.COOLDOWN) + elif self.state == State.INTERRUPTED: + self.function.on_complete(cancelled=True) + self._transition(State.RERUN) self._start_cooldown_timer() except KeyboardInterrupt: self.function.on_keyboard_interrupt() @@ -152,6 +166,13 @@ try: with self.lock: self.cooldown_timer = None + rerun = (self.state == State.RERUN) self._transition(State.IDLE) + + # If we were in the RERUN state, then re-trigger the event. + if rerun: + self._press_unlocked('Rerunning: %s' % + self.rerun_event_description) + except KeyboardInterrupt: self.function.on_keyboard_interrupt()
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py index 1fb230a..8103dd0 100755 --- a/pw_watch/py/pw_watch/watch.py +++ b/pw_watch/py/pw_watch/watch.py
@@ -34,7 +34,7 @@ import pw_cli.env import pw_cli.plugins -from pw_watch.debounce import DebouncedFunction, Debouncer, State +from pw_watch.debounce import DebouncedFunction, Debouncer _COLOR = pw_cli.color.colors() _LOG = logging.getLogger(__name__) @@ -141,8 +141,7 @@ try: while True: _ = input() - if self.debouncer.state == State.IDLE: - self.debouncer.press('Manual build triggered...') + self.debouncer.press('Manual build requested...') except KeyboardInterrupt: _exit_due_to_interrupt() @@ -200,8 +199,7 @@ if self.matching_path is None: self.matching_path = matching_path - self.debouncer.press('File change detected: %s; debouncing...' % - matching_path) + self.debouncer.press('File change detected') # Implementation of DebouncedFunction.run() # @@ -434,10 +432,7 @@ _LOG.info('Directory to watch: %s', path_to_log) _LOG.info('Watching for file changes. Ctrl-C exits.') - # Make a nice non-logging banner to motivate the user. - print() - print(_COLOR.green(' WATCHER IS READY: GO WRITE SOME CODE!')) - print() + event_handler.debouncer.press('Triggering initial build...') try: while observer.isAlive():