Skip to content

Commit

Permalink
Revert livereload file watching to use polling observer (#2477)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
oprypin committed Jul 8, 2021
1 parent 06d8b47 commit cdb1f71
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 57 deletions.
60 changes: 10 additions & 50 deletions mkdocs/livereload/__init__.py
Expand Up @@ -4,7 +4,6 @@
import mimetypes
import os
import os.path
import pathlib
import re
import socketserver
import threading
Expand All @@ -13,7 +12,7 @@
import wsgiref.simple_server

import watchdog.events
import watchdog.observers
import watchdog.observers.polling


class _LoggerAdapter(logging.LoggerAdapter):
Expand All @@ -35,7 +34,7 @@ def __init__(
port,
root,
mount_path="/",
build_delay=0.25,
polling_interval=0.5,
shutdown_delay=0.25,
**kwargs,
):
Expand All @@ -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
Expand All @@ -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."""
Expand All @@ -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()
Expand Down
14 changes: 7 additions & 7 deletions mkdocs/tests/livereload_tests.py
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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"})
Expand Down Expand Up @@ -465,15 +465,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()
Expand Down Expand Up @@ -512,7 +512,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")
Expand Down

0 comments on commit cdb1f71

Please sign in to comment.