Skip to content

Commit f00e9df

Browse files
author
Jonathan Visser
committed
Replace watchdog with pyinotify
1 parent 249c38d commit f00e9df

File tree

4 files changed

+103
-109
lines changed

4 files changed

+103
-109
lines changed

nginx_config_reloader/__init__.py

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
import threading
1414
import time
1515
from typing import Optional
16+
from pathlib import Path
1617

17-
import pyinotify
18+
from watchdog.observers import Observer
19+
from watchdog.events import FileSystemEventHandler
1820
from dasbus.loop import EventLoop
1921
from dasbus.signal import Signal
2022

@@ -42,8 +44,8 @@
4244
dbus_loop: Optional[EventLoop] = None
4345

4446

45-
class NginxConfigReloader(pyinotify.ProcessEvent):
46-
def my_init(
47+
class NginxConfigReloader(FileSystemEventHandler):
48+
def __init__(
4749
self,
4850
logger=None,
4951
no_magento_config=False,
@@ -79,36 +81,51 @@ def my_init(
7981
self.applying = False
8082
self._on_config_reload = Signal()
8183

82-
def process_IN_DELETE(self, event):
84+
def on_deleted(self, event):
8385
"""Triggered by inotify on removal of file or removal of dir
8486
8587
If the dir itself is removed, inotify will stop watching and also
8688
trigger IN_IGNORED.
8789
"""
88-
if not event.dir: # Will also capture IN_DELETE_SELF
90+
if not event.is_directory:
8991
self.handle_event(event)
9092

91-
def process_IN_MOVED(self, event):
93+
def on_moved(self, event):
9294
"""Triggered by inotify when a file is moved from or to the dir"""
9395
self.handle_event(event)
9496

95-
def process_IN_CREATE(self, event):
97+
def on_created(self, event):
9698
"""Triggered by inotify when a dir is created in the watch dir"""
97-
if event.dir:
99+
if event.is_directory:
98100
self.handle_event(event)
99101

100-
def process_IN_CLOSE_WRITE(self, event):
102+
def on_modified(self, event):
101103
"""Triggered by inotify when a file is written in the dir"""
102104
self.handle_event(event)
103105

104-
def process_IN_MOVE_SELF(self, event):
105-
"""Triggered by inotify when watched dir is moved"""
106-
raise ListenTargetTerminated
106+
def on_any_event(self, event):
107+
"""Triggered by inotify when watched dir is moved or deleted"""
108+
if event.is_directory and event.event_type in ['moved', 'deleted']:
109+
self.logger.warning(f"Directory {event.src_path} has been {event.event_type}.")
110+
raise ListenTargetTerminated
107111

108112
def handle_event(self, event):
109-
if not any(fnmatch.fnmatch(event.name, pat) for pat in WATCH_IGNORE_FILES):
110-
self.logger.info("{} detected on {}.".format(event.maskname, event.name))
113+
file_path = Path(event.src_path)
114+
if (
115+
file_path.name.endswith(".swx")
116+
or file_path.name.endswith(".swp")
117+
or file_path.name.endswith("~")
118+
):
119+
return
120+
121+
if (event.is_directory):
122+
return
123+
124+
basename = os.path.basename(event.src_path)
125+
if not any(fnmatch.fnmatch(basename, pat) for pat in WATCH_IGNORE_FILES):
126+
self.logger.debug(f"{event.event_type.upper()} detected on {event.src_path}")
111127
self.dirty = True
128+
# Additional handling if necessary
112129

113130
def install_magento_config(self):
114131
# Check if configs are present
@@ -309,6 +326,20 @@ def reload(self, send_signal=True):
309326
if send_signal:
310327
self._on_config_reload.emit()
311328

329+
def start_observer(self):
330+
self.observer = Observer()
331+
self.observer.schedule(
332+
self,
333+
self.dir_to_watch,
334+
recursive=True,
335+
follow_symlink=True
336+
)
337+
self.observer.start()
338+
339+
def stop_observer(self):
340+
self.observer.stop()
341+
self.observer.join()
342+
sys.exit()
312343

313344
class ListenTargetTerminated(BaseException):
314345
pass
@@ -357,20 +388,11 @@ def wait_loop(
357388
"""
358389
dir_to_watch = os.path.abspath(dir_to_watch)
359390

360-
wm = pyinotify.WatchManager()
361-
notifier = pyinotify.Notifier(wm)
362-
363-
class SymlinkChangedHandler(pyinotify.ProcessEvent):
364-
def process_IN_DELETE(self, event):
365-
if event.pathname == dir_to_watch:
366-
raise ListenTargetTerminated("watched directory was deleted")
367-
368391
nginx_config_changed_handler = NginxConfigReloader(
369392
logger=logger,
370393
no_magento_config=no_magento_config,
371394
no_custom_config=no_custom_config,
372395
dir_to_watch=dir_to_watch,
373-
notifier=notifier,
374396
use_systemd=use_systemd,
375397
)
376398

@@ -383,35 +405,27 @@ def process_IN_DELETE(self, event):
383405
dbus_thread = threading.Thread(target=dbus_event_loop)
384406
dbus_thread.start()
385407

386-
while True:
387-
while not os.path.exists(dir_to_watch):
388-
logger.warning(
389-
"Configuration dir {} not found, waiting...".format(dir_to_watch)
390-
)
391-
time.sleep(5)
392-
393-
wm.add_watch(
394-
dir_to_watch,
395-
pyinotify.ALL_EVENTS,
396-
nginx_config_changed_handler,
397-
rec=recursive_watch,
398-
auto_add=True,
399-
)
400-
wm.watch_transient_file(
401-
dir_to_watch, pyinotify.ALL_EVENTS, SymlinkChangedHandler
408+
while not os.path.exists(dir_to_watch):
409+
logger.warning(
410+
"Configuration dir {} not found, waiting...".format(dir_to_watch)
402411
)
403-
404-
# Install initial configuration
405-
nginx_config_changed_handler.reload(send_signal=False)
406-
407-
try:
408-
logger.info("Listening for changes to {}".format(dir_to_watch))
409-
notifier.coalesce_events()
410-
notifier.loop(callback=lambda _: after_loop(nginx_config_changed_handler))
411-
except pyinotify.NotifierError as err:
412-
logger.critical(err)
413-
except ListenTargetTerminated:
414-
logger.warning("Configuration dir lost, waiting for it to reappear")
412+
time.sleep(5)
413+
414+
415+
try:
416+
logger.info(f"Listening for changes to {dir_to_watch}")
417+
nginx_config_changed_handler.start_observer()
418+
while True:
419+
time.sleep(1)
420+
after_loop(nginx_config_changed_handler)
421+
except ListenTargetTerminated:
422+
logger.warning("Configuration dir lost, waiting for it to reappear")
423+
nginx_config_changed_handler.stop_observer()
424+
time.sleep(5)
425+
except KeyboardInterrupt:
426+
logger.info("Shutting down observer.")
427+
nginx_config_changed_handler.stop_observer()
428+
415429

416430

417431
def as_unprivileged_user():

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
pyinotify==0.9.6
1+
# Follow_symlinks isnt released in 6.0.0 yet, so we use the latest current commit
2+
watchdog @ git+https://github.com/gorakhargosh/watchdog.git@f3e78cd4d9500d287bd11ec5d08a1f351601028d
23

34
mock==5.0.1
45
pytest==7.2.1
@@ -8,4 +9,4 @@ black==23.1.0
89
pre-commit==2.21.0
910
pygobject
1011
pygobject-stubs
11-
dasbus==1.7
12+
dasbus==1.7

tests/test_nginx_config_reloader.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,4 +679,6 @@ def _dest(self, name):
679679
class Event:
680680
def __init__(self, name):
681681
self.name = name
682-
self.maskname = "IN_CLOSE_WRITE"
682+
self.event_type = "modified"
683+
self.src_path = name
684+
self.is_directory = False

tests/test_inotify_callbacks.py renamed to tests/test_watchdog_callbacks.py

Lines changed: 32 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,20 @@
44
from tempfile import NamedTemporaryFile, mkdtemp
55

66
import mock
7-
import pyinotify
7+
from watchdog.events import (
8+
DirCreatedEvent,
9+
DirDeletedEvent,
10+
DirMovedEvent,
11+
FileCreatedEvent,
12+
FileDeletedEvent,
13+
FileModifiedEvent,
14+
FileMovedEvent,
15+
)
816

917
import nginx_config_reloader
1018

1119

12-
class TestInotifyCallbacks(unittest.TestCase):
20+
class TestWatchdogCallbacks(unittest.TestCase):
1321
def setUp(self):
1422
patcher = mock.patch("nginx_config_reloader.NginxConfigReloader.handle_event")
1523
self.addCleanup(patcher.stop)
@@ -19,87 +27,58 @@ def setUp(self):
1927
with open(os.path.join(self.dir, "existing_file"), "w") as f:
2028
f.write("blablabla")
2129

22-
wm = pyinotify.WatchManager()
23-
handler = nginx_config_reloader.NginxConfigReloader()
24-
self.notifier = pyinotify.Notifier(wm, default_proc_fun=handler)
25-
wm.add_watch(self.dir, pyinotify.ALL_EVENTS)
30+
self.observer = mock.Mock()
31+
self.handler = nginx_config_reloader.NginxConfigReloader(dir_to_watch=self.dir)
32+
self.handler.observer = self.observer
2633

2734
def tearDown(self):
28-
self.notifier.stop()
2935
shutil.rmtree(self.dir, ignore_errors=True)
3036

31-
def _process_events(self):
32-
while self.notifier.check_events(0):
33-
self.notifier.read_events()
34-
self.notifier.process_events()
35-
36-
def test_that_handle_event_is_called_when_new_file_is_created(self):
37-
with open(os.path.join(self.dir, "testfile"), "w") as f:
38-
f.write("blablabla")
39-
40-
self._process_events()
41-
4237
self.assertEqual(len(self.handle_event.mock_calls), 1)
4338

4439
def test_that_handle_event_is_called_when_new_dir_is_created(self):
45-
mkdtemp(dir=self.dir)
46-
self._process_events()
40+
event = DirCreatedEvent(os.path.join(self.dir, "testdir"))
41+
self.handler.on_created(event)
4742

4843
self.assertEqual(len(self.handle_event.mock_calls), 1)
4944

5045
def test_that_handle_event_is_called_when_a_file_is_removed(self):
51-
os.remove(os.path.join(self.dir, "existing_file"))
52-
53-
self._process_events()
46+
event = FileDeletedEvent(os.path.join(self.dir, "existing_file"))
47+
self.handler.on_deleted(event)
5448

5549
self.assertEqual(len(self.handle_event.mock_calls), 1)
5650

5751
def test_that_handle_event_is_called_when_a_file_is_moved_in(self):
5852
with NamedTemporaryFile(delete=False) as f:
59-
os.rename(f.name, os.path.join(self.dir, "newfile"))
60-
61-
self._process_events()
53+
event = FileMovedEvent(
54+
f.name, os.path.join(self.dir, "newfile")
55+
)
56+
self.handler.on_moved(event)
6257

6358
self.assertEqual(len(self.handle_event.mock_calls), 1)
6459

6560
def test_that_handle_event_is_called_when_a_file_is_moved_out(self):
6661
destdir = mkdtemp()
67-
os.rename(
62+
event = FileMovedEvent(
6863
os.path.join(self.dir, "existing_file"),
6964
os.path.join(destdir, "existing_file"),
7065
)
71-
72-
self._process_events()
66+
self.handler.on_moved(event)
7367

7468
self.assertEqual(len(self.handle_event.mock_calls), 1)
7569

7670
shutil.rmtree(destdir)
7771

7872
def test_that_handle_event_is_called_when_a_file_is_renamed(self):
79-
os.rename(
80-
os.path.join(self.dir, "existing_file"), os.path.join(self.dir, "new_name")
73+
event = FileMovedEvent(
74+
os.path.join(self.dir, "existing_file"),
75+
os.path.join(self.dir, "new_name"),
8176
)
82-
83-
self._process_events()
77+
self.handler.on_moved(event)
8478

8579
self.assertGreaterEqual(len(self.handle_event.mock_calls), 1)
8680

87-
def test_that_listen_target_terminated_is_raised_if_dir_is_renamed(self):
88-
destdir = mkdtemp()
89-
os.rename(self.dir, destdir)
90-
91-
with self.assertRaises(nginx_config_reloader.ListenTargetTerminated):
92-
self._process_events()
93-
94-
shutil.rmtree(destdir)
95-
96-
def test_that_listen_target_terminated_is_not_raised_if_dir_is_removed(self):
97-
shutil.rmtree(self.dir)
98-
99-
self._process_events()
100-
101-
102-
class TestInotifyRecursiveCallbacks(TestInotifyCallbacks):
81+
class TestWatchdogRecursiveCallbacks(TestWatchdogCallbacks):
10382
# Run all callback tests on a subdir
10483
def setUp(self):
10584
patcher = mock.patch("nginx_config_reloader.NginxConfigReloader.handle_event")
@@ -111,11 +90,9 @@ def setUp(self):
11190
with open(os.path.join(self.dir, "existing_file"), "w") as f:
11291
f.write("blablabla")
11392

114-
wm = pyinotify.WatchManager()
115-
handler = nginx_config_reloader.NginxConfigReloader()
116-
self.notifier = pyinotify.Notifier(wm, default_proc_fun=handler)
117-
wm.add_watch(self.rootdir, pyinotify.ALL_EVENTS, rec=True)
93+
self.observer = mock.Mock()
94+
self.handler = nginx_config_reloader.NginxConfigReloader(dir_to_watch=self.dir)
95+
self.handler.observer = self.observer
11896

11997
def tearDown(self):
120-
self.notifier.stop()
121-
shutil.rmtree(self.rootdir, ignore_errors=True)
98+
shutil.rmtree(self.rootdir, ignore_errors=True)

0 commit comments

Comments
 (0)