From fa806f1bdd838de1ebf42916e9f5e21b8688d8e2 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 6 Jun 2022 13:28:24 +0300 Subject: [PATCH] [watchmedo] Make auto-restart restart the sub-process if it terminates (#896) Fixes #405. --- changelog.rst | 3 ++- src/watchdog/tricks/__init__.py | 15 +++++++++++++-- src/watchdog/utils/process_watcher.py | 27 +++++++++++++++++++++++++++ tests/test_0_watchmedo.py | 13 +++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/watchdog/utils/process_watcher.py diff --git a/changelog.rst b/changelog.rst index 279a0c52..747645cd 100644 --- a/changelog.rst +++ b/changelog.rst @@ -9,7 +9,8 @@ Unreleased 2022-xx-xx • `full history `__ - [fsevents] Fix flakey test to assert that there are no errors when stopping the emitter. -- Thanks to our beloved contributors: @samschott +- [watchmedo] Make auto-restart restart the sub-process if it terminates. (`#896 `__) +- Thanks to our beloved contributors: @samschott, @taleinat 2.1.8 ~~~~~ diff --git a/src/watchdog/tricks/__init__.py b/src/watchdog/tricks/__init__.py index e15f970c..33039240 100644 --- a/src/watchdog/tricks/__init__.py +++ b/src/watchdog/tricks/__init__.py @@ -48,9 +48,9 @@ import subprocess import time -from watchdog.utils import echo from watchdog.events import PatternMatchingEventHandler - +from watchdog.utils import echo +from watchdog.utils.process_watcher import ProcessWatcher logger = logging.getLogger(__name__) echo_events = functools.partial(echo.echo, write=lambda msg: logger.info(msg)) @@ -173,16 +173,24 @@ def __init__(self, command, patterns=None, ignore_patterns=None, self.command = command self.stop_signal = stop_signal self.kill_after = kill_after + self.process = None + self.process_watcher = None def start(self): # windows doesn't have setsid self.process = subprocess.Popen(self.command, preexec_fn=getattr(os, 'setsid', None)) + self.process_watcher = ProcessWatcher(self.process, self._restart) + self.process_watcher.start() def stop(self): if self.process is None: return + if self.process_watcher is not None: + self.process_watcher.stop() + self.process_watcher = None + def kill_process(stop_signal): if hasattr(os, 'getpgid') and hasattr(os, 'killpg'): os.killpg(os.getpgid(self.process.pid), stop_signal) @@ -210,5 +218,8 @@ def kill_process(stop_signal): @echo_events def on_any_event(self, event): + self._restart() + + def _restart(self): self.stop() self.start() diff --git a/src/watchdog/utils/process_watcher.py b/src/watchdog/utils/process_watcher.py new file mode 100644 index 00000000..800d7f8e --- /dev/null +++ b/src/watchdog/utils/process_watcher.py @@ -0,0 +1,27 @@ +import logging +import time + +from watchdog.utils import BaseThread + + +logger = logging.getLogger(__name__) + + +class ProcessWatcher(BaseThread): + def __init__(self, popen_obj, process_termination_callback): + super().__init__() + self.popen_obj = popen_obj + self.process_termination_callback = process_termination_callback + + def run(self): + while True: + if self.stopped_event.is_set(): + return + if self.popen_obj.poll() is not None: + break + time.sleep(0.1) + + try: + self.process_termination_callback() + except Exception: + logger.exception("Error calling process termination callback") diff --git a/tests/test_0_watchmedo.py b/tests/test_0_watchmedo.py index 3ab0ac96..d9aaf3fe 100644 --- a/tests/test_0_watchmedo.py +++ b/tests/test_0_watchmedo.py @@ -74,6 +74,19 @@ def test_kill_auto_restart(tmpdir, capfd): # assert 'KeyboardInterrupt' in cap.err +def test_auto_restart_subprocess_termination(tmpdir, capfd): + from watchdog.tricks import AutoRestartTrick + import sys + import time + script = make_dummy_script(tmpdir, n=2) + a = AutoRestartTrick([sys.executable, script]) + a.start() + time.sleep(5) + a.stop() + cap = capfd.readouterr() + assert cap.out.splitlines(keepends=False).count('+++++ 0') > 1 + + def test_auto_restart_arg_parsing_basic(): args = watchmedo.cli.parse_args(["auto-restart", "-d", ".", "--recursive", "--debug-force-polling", "cmd"]) assert args.func is watchmedo.auto_restart