From 735e4a086755dc456c47d9219c4f65cb3b19ddd3 Mon Sep 17 00:00:00 2001 From: Oleh Prypin Date: Sun, 20 Jun 2021 16:59:28 +0200 Subject: [PATCH] Revert livereload file watching to use polling observer This goes back to the approach that was always used with 'livereload' library (but now just using the 'watchdog' implementation of the same), meaning the same downsides with latency and CPU usage. But we have to do this to reasonably support usages that span virtual filesystems such as non-native Docker and network mounts. This also simplifies the code, as the polling observer already follows symlinks and happens to support watching paths of files directly --- mkdocs/livereload/__init__.py | 60 ++++++-------------------------- mkdocs/tests/livereload_tests.py | 14 ++++---- 2 files changed, 17 insertions(+), 57 deletions(-) diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py index 5ac36e7343..a599b73575 100644 --- a/mkdocs/livereload/__init__.py +++ b/mkdocs/livereload/__init__.py @@ -4,7 +4,6 @@ import mimetypes import os import os.path -import pathlib import re import socketserver import threading @@ -13,7 +12,7 @@ import wsgiref.simple_server import watchdog.events -import watchdog.observers +import watchdog.observers.polling class _LoggerAdapter(logging.LoggerAdapter): @@ -35,7 +34,7 @@ def __init__( port, root, mount_path="/", - build_delay=0.25, + polling_interval=0.5, shutdown_delay=0.25, **kwargs, ): @@ -45,7 +44,7 @@ def __init__( self.root = os.path.abspath(root) self.mount_path = ("/" + mount_path.lstrip("/")).rstrip("/") + "/" self.url = f"http://{self.server_name}:{self.server_port}{self.mount_path}" - self.build_delay = build_delay + self.build_delay = 0.1 self.shutdown_delay = shutdown_delay # To allow custom error pages. self.error_handler = lambda code: None @@ -62,7 +61,7 @@ def __init__( self._shutdown = False self.serve_thread = threading.Thread(target=lambda: self.serve_forever(shutdown_delay)) - self.observer = watchdog.observers.Observer(timeout=shutdown_delay) + self.observer = watchdog.observers.polling.PollingObserver(timeout=polling_interval) def watch(self, path, func=None, recursive=True): """Add the 'path' to watched paths, call the function and reload when any file changes under it.""" @@ -77,57 +76,18 @@ def watch(self, path, func=None, recursive=True): stacklevel=2, ) - def callback(event, allowed_path=None): - if isinstance(event, watchdog.events.DirCreatedEvent): + def callback(event): + if event.is_directory: return - if allowed_path is not None and event.src_path != allowed_path: - return - # Text editors always cause a "file close" event in addition to "modified" when saving - # a file. Some editors also have "swap" functionality that keeps writing into another - # file that's never closed. Prevent such write events from causing a rebuild. - if isinstance(event, watchdog.events.FileModifiedEvent): - # But FileClosedEvent is implemented only on Linux, otherwise we mustn't skip this: - if type(self.observer).__name__ == "InotifyObserver": - return log.debug(str(event)) with self._rebuild_cond: self._to_rebuild[func] = True self._rebuild_cond.notify_all() - dir_handler = watchdog.events.FileSystemEventHandler() - dir_handler.on_any_event = callback - - seen = set() - - def schedule(path): - seen.add(path) - if path.is_file(): - # Watchdog doesn't support watching files, so watch its directory and filter by path - handler = watchdog.events.FileSystemEventHandler() - handler.on_any_event = lambda event: callback(event, allowed_path=os.fspath(path)) - - parent = path.parent - log.debug(f"Watching file '{path}' through directory '{parent}'") - self.observer.schedule(handler, parent) - else: - log.debug(f"Watching directory '{path}'") - self.observer.schedule(dir_handler, path, recursive=recursive) - - schedule(pathlib.Path(path).resolve()) - - def watch_symlink_targets(path_obj): # path is os.DirEntry or pathlib.Path - if path_obj.is_symlink(): - path_obj = pathlib.Path(path_obj).resolve() - if path_obj in seen or not path_obj.exists(): - return - schedule(path_obj) - - if path_obj.is_dir() and recursive: - with os.scandir(os.fspath(path_obj)) as scan: - for entry in scan: - watch_symlink_targets(entry) - - watch_symlink_targets(pathlib.Path(path)) + handler = watchdog.events.FileSystemEventHandler() + handler.on_any_event = callback + log.debug(f"Watching '{path}'") + self.observer.schedule(handler, path, recursive=recursive) def serve(self): self.observer.start() diff --git a/mkdocs/tests/livereload_tests.py b/mkdocs/tests/livereload_tests.py index c4d256973c..c7450587e6 100644 --- a/mkdocs/tests/livereload_tests.py +++ b/mkdocs/tests/livereload_tests.py @@ -38,7 +38,7 @@ def testing_server(root, builder=lambda: None, mount_path="/"): port=0, root=root, mount_path=mount_path, - build_delay=0.1, + polling_interval=0.2, bind_and_activate=False, ) server.setup_environ() @@ -146,7 +146,7 @@ def test_rebuild_after_rename(self, site_dir): self.assertTrue(started_building.wait(timeout=10)) @tempdir() - def test_no_rebuild_on_edit(self, site_dir): + def test_rebuild_on_edit(self, site_dir): started_building = threading.Event() with open(Path(site_dir, "test"), "wb") as f: @@ -159,7 +159,7 @@ def test_no_rebuild_on_edit(self, site_dir): f.write(b"hi\n") f.flush() - self.assertFalse(started_building.wait(timeout=0.2)) + self.assertTrue(started_building.wait(timeout=10)) @tempdir({"foo.docs": "a"}) @tempdir({"foo.site": "original"}) @@ -448,15 +448,15 @@ def wait_for_build(): server.watch(Path(origin_dir, "mkdocs.yml")) time.sleep(0.01) + Path(origin_dir, "unrelated.md").write_text("foo") + self.assertFalse(started_building.wait(timeout=0.5)) + Path(tmp_dir, "mkdocs.yml").write_text("edited") self.assertTrue(wait_for_build()) Path(dest_docs_dir, "subdir", "foo.md").write_text("edited") self.assertTrue(wait_for_build()) - Path(origin_dir, "unrelated.md").write_text("foo") - self.assertFalse(started_building.wait(timeout=0.2)) - @tempdir(["file_dest_1.md", "file_dest_2.md", "file_dest_unused.md"], prefix="tmp_dir") @tempdir(["file_under.md"], prefix="dir_to_link_to") @tempdir() @@ -495,7 +495,7 @@ def wait_for_build(): self.assertTrue(wait_for_build()) Path(tmp_dir, "file_dest_unused.md").write_text("edited") - self.assertFalse(started_building.wait(timeout=0.2)) + self.assertFalse(started_building.wait(timeout=0.5)) @tempdir(prefix="site_dir") @tempdir(["docs/unused.md", "README.md"], prefix="origin_dir")