pw_watch: Terminate interrupted builds

- Terminate interrupted builds immediately. This saves time since the
  the watcher doesn't have to wait for the previous build to finish
  before starting a new build.
- Remove some unused members of PigweedBuildWatcher.

Change-Id: I2218f711725ceb76a374c9e035a62f8c64224fdb
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/19301
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Wyatt Hepler <hepler@google.com>
diff --git a/pw_watch/py/pw_watch/debounce.py b/pw_watch/py/pw_watch/debounce.py
index dd9005d..73c5e97 100644
--- a/pw_watch/py/pw_watch/debounce.py
+++ b/pw_watch/py/pw_watch/debounce.py
@@ -24,17 +24,17 @@
 class DebouncedFunction(ABC):
     """Function to be run by Debouncer"""
     @abstractmethod
-    def run(self):
+    def run(self) -> None:
         """Run the function"""
 
     @abstractmethod
-    def cancel(self):
+    def cancel(self) -> bool:
         """Cancel an in-progress run of the function.
         Must be called from different thread than run().
         Returns true if run was successfully cancelled, false otherwise"""
 
     @abstractmethod
-    def on_complete(self, cancelled=False):
+    def on_complete(self, cancelled: bool = False) -> bool:
         """Called after run() finishes. If true, cancelled indicates
         cancel() was invoked during the last run()"""
 
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index c3cd72a..c6394a0 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -103,7 +103,6 @@
         self,
         patterns: Sequence[str] = (),
         ignore_patterns: Sequence[str] = (),
-        case_sensitive: bool = False,
         build_commands: Sequence[BuildCommand] = (),
         ignore_dirs=Optional[List[str]],
         charset: WatchCharset = _ASCII_CHARSET,
@@ -112,13 +111,13 @@
 
         self.patterns = patterns
         self.ignore_patterns = ignore_patterns
-        self.case_sensitive = case_sensitive
         self.build_commands = build_commands
         self.ignore_dirs = ignore_dirs or []
         self.ignore_dirs.extend(cmd.build_dir for cmd in self.build_commands)
-        self.cooldown_finish_time = None
         self.charset: WatchCharset = charset
 
+        self._current_build: Optional[subprocess.Popen] = None
+
         self.debouncer = Debouncer(self)
 
         # Track state of a build. These need to be members instead of locals
@@ -216,18 +215,22 @@
         self.builds_succeeded = []
         num_builds = len(self.build_commands)
         _LOG.info('Starting build with %d directories', num_builds)
+
+        env = os.environ.copy()
+        # Force colors in Pigweed subcommands run through the watcher.
+        env['PW_USE_COLOR'] = '1'
+
         for i, cmd in enumerate(self.build_commands, 1):
             _LOG.info('[%d/%d] Starting build: %s', i, num_builds, cmd)
 
             # Run the build. Put a blank before/after for visual separation.
             print()
-            env = os.environ.copy()
-            # Force colors in Pigweed subcommands run through the watcher.
-            env['PW_USE_COLOR'] = '1'
-            result = subprocess.run(['ninja', '-C', *cmd.args()], env=env)
+            self._current_build = subprocess.Popen(
+                ['ninja', '-C', *cmd.args()], env=env)
+            returncode = self._current_build.wait()
             print()
 
-            build_ok = (result.returncode == 0)
+            build_ok = (returncode == 0)
             if build_ok:
                 level = logging.INFO
                 tag = '(OK)'
@@ -240,10 +243,8 @@
 
     # Implementation of DebouncedFunction.cancel()
     def cancel(self):
-        # TODO: Finish implementing this by supporting cancelling the currently
-        # running build. This will require some subprocess shenanigans and
-        # so will leave this for later.
-        return False
+        self._current_build.terminate()
+        return True
 
     # Implementation of DebouncedFunction.run()
     def on_complete(self, cancelled=False):