Skip to content

Commit

Permalink
[watchmedo] Avoid zombie sub-processes when running shell-command wit…
Browse files Browse the repository at this point in the history
…hout --wait
  • Loading branch information
taleinat committed Jun 6, 2022
1 parent fa806f1 commit e933df8
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 6 deletions.
3 changes: 2 additions & 1 deletion changelog.rst
Original file line number Diff line number Diff line change
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.
- [watchmedo] Make auto-restart restart the sub-process if it terminates. (`#896 <https://github.com/gorakhargosh/watchdog/pull/896>`__)
- [watchmedo] Make ``auto-restart`` restart the sub-process if it terminates. (`#896 <https://github.com/gorakhargosh/watchdog/pull/896>`__)
- [watchmedo] Avoid zombie sub-processes when running ``shell-command`` without ``--wait``. (`#897 <https://github.com/gorakhargosh/watchdog/pull/897>`__)
- Thanks to our beloved contributors: @samschott, @taleinat

2.1.8
Expand Down
14 changes: 13 additions & 1 deletion src/watchdog/tricks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ def __init__(self, shell_command=None, patterns=None, ignore_patterns=None,
self.shell_command = shell_command
self.wait_for_process = wait_for_process
self.drop_during_process = drop_during_process

self.process = None
self._process_watchers = set()

def on_any_event(self, event):
from string import Template

if self.drop_during_process and self.process and self.process.poll() is None:
if self.drop_during_process and self.is_process_running():
return

if event.is_directory:
Expand Down Expand Up @@ -151,6 +153,16 @@ def on_any_event(self, event):
self.process = subprocess.Popen(command, shell=True)
if self.wait_for_process:
self.process.wait()
else:
process_watcher = ProcessWatcher(self.process, None)
self._process_watchers.add(process_watcher)
def cleanup():
self._process_watchers.discard(process_watcher)
process_watcher.process_termination_callback = cleanup
process_watcher.start()

def is_process_running(self):
return self._process_watchers or (self.process is not None and self.process.poll())


class AutoRestartTrick(Trick):
Expand Down
6 changes: 2 additions & 4 deletions src/watchdog/utils/process_watcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import time

from watchdog.utils import BaseThread

Expand All @@ -15,11 +14,10 @@ def __init__(self, popen_obj, 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)
if self.stopped_event.wait(timeout=0.1):
return

try:
self.process_termination_callback()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_0_watchmedo.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,37 @@ def test_kill_auto_restart(tmpdir, capfd):
# assert 'KeyboardInterrupt' in cap.err


def test_shell_command_wait_for_completion(tmpdir, capfd):
from watchdog.events import FileModifiedEvent
from watchdog.tricks import ShellCommandTrick
import shlex
import sys
import time
script = make_dummy_script(tmpdir, n=1)
trick = ShellCommandTrick(shlex.join([sys.executable, script]), wait_for_process=True)
assert not trick.is_process_running()
start_time = time.monotonic()
trick.on_any_event(FileModifiedEvent("foo/bar.baz"))
elapsed = time.monotonic() - start_time
assert not trick.is_process_running()
assert elapsed >= 1


def test_shell_command_subprocess_termination_nowait(tmpdir, capfd):
from watchdog.events import FileModifiedEvent
from watchdog.tricks import ShellCommandTrick
import shlex
import sys
import time
script = make_dummy_script(tmpdir, n=1)
trick = ShellCommandTrick(shlex.join([sys.executable, script]), wait_for_process=False)
assert not trick.is_process_running()
trick.on_any_event(FileModifiedEvent("foo/bar.baz"))
assert trick.is_process_running()
time.sleep(2)
assert not trick.is_process_running()


def test_auto_restart_subprocess_termination(tmpdir, capfd):
from watchdog.tricks import AutoRestartTrick
import sys
Expand Down

0 comments on commit e933df8

Please sign in to comment.