Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make auto-restart restart the sub-process if it terminates #896

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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