Skip to content

Commit

Permalink
[watchmedo] Make auto-restart restart the sub-process if it terminates (
Browse files Browse the repository at this point in the history
#896)

Fixes #405.
  • Loading branch information
taleinat committed Jun 6, 2022
1 parent 72f2eb7 commit fa806f1
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 3 deletions.
3 changes: 2 additions & 1 deletion changelog.rst
Expand Up @@ -9,7 +9,8 @@ Unreleased
2022-xx-xx • `full history <https://github.com/gorakhargosh/watchdog/compare/v2.1.8...HEAD>`__

- [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 <https://github.com/gorakhargosh/watchdog/pull/896>`__)
- Thanks to our beloved contributors: @samschott, @taleinat

2.1.8
~~~~~
Expand Down
15 changes: 13 additions & 2 deletions src/watchdog/tricks/__init__.py
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
27 changes: 27 additions & 0 deletions 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")
13 changes: 13 additions & 0 deletions tests/test_0_watchmedo.py
Expand Up @@ -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
Expand Down

0 comments on commit fa806f1

Please sign in to comment.