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

Revert livereload file watching to use polling observer #2477

Merged
merged 1 commit into from Jul 8, 2021
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
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 @@ -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()
Expand Down Expand Up @@ -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")
Expand Down