We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
mkdocs 1.2.x livereload no longer working
mkdocs==1.2 and latest no longer works
mkdocs==1.1.2 works

MacOS 11.4
Docker version 20.10.7, build f0df350
python:3.8-alpine (3.9.5 and 10-rc same behavior)

I checked the Docker container and the markdown is updating but the generated html is not. This seems to be associated with the 1.2 change with watcher.

LMK what logs/config/diag I can provide to help RCA the issue.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `mkdocs/livereload/__init__.py`
Content:
```
1 import functools
2 import io
3 import logging
4 import mimetypes
5 import os
6 import os.path
7 import pathlib
8 import re
9 import socketserver
10 import threading
11 import time
12 import warnings
13 import wsgiref.simple_server
14 
15 import watchdog.events
16 import watchdog.observers
17 
18 
19 class _LoggerAdapter(logging.LoggerAdapter):
20     def process(self, msg, kwargs):
21         return time.strftime("[%H:%M:%S] ") + msg, kwargs
22 
23 
24 log = _LoggerAdapter(logging.getLogger(__name__), {})
25 
26 
27 class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGIServer):
28     daemon_threads = True
29     poll_response_timeout = 60
30 
31     def __init__(
32         self,
33         builder,
34         host,
35         port,
36         root,
37         mount_path="/",
38         build_delay=0.25,
39         shutdown_delay=0.25,
40         **kwargs,
41     ):
42         self.builder = builder
43         self.server_name = host
44         self.server_port = port
45         self.root = os.path.abspath(root)
46         self.mount_path = ("/" + mount_path.lstrip("/")).rstrip("/") + "/"
47         self.url = f"http://{self.server_name}:{self.server_port}{self.mount_path}"
48         self.build_delay = build_delay
49         self.shutdown_delay = shutdown_delay
50         # To allow custom error pages.
51         self.error_handler = lambda code: None
52 
53         super().__init__((host, port), _Handler, **kwargs)
54         self.set_app(self.serve_request)
55 
56         self._wanted_epoch = _timestamp()  # The version of the site that started building.
57         self._visible_epoch = self._wanted_epoch  # Latest fully built version of the site.
58         self._epoch_cond = threading.Condition()  # Must be held when accessing _visible_epoch.
59 
60         self._to_rebuild = {}  # Used as an ordered set of functions to call.
61         self._rebuild_cond = threading.Condition()  # Must be held when accessing _to_rebuild.
62 
63         self._shutdown = False
64         self.serve_thread = threading.Thread(target=lambda: self.serve_forever(shutdown_delay))
65         self.observer = watchdog.observers.Observer(timeout=shutdown_delay)
66 
67     def watch(self, path, func=None, recursive=True):
68         """Add the 'path' to watched paths, call the function and reload when any file changes under it."""
69         path = os.path.abspath(path)
70         if func in (None, self.builder):
71             func = self.builder
72         else:
73             warnings.warn(
74                 "Plugins should not pass the 'func' parameter of watch(). "
75                 "The ability to execute custom callbacks will be removed soon.",
76                 DeprecationWarning,
77                 stacklevel=2,
78             )
79 
80         def callback(event, allowed_path=None):
81             if isinstance(event, watchdog.events.DirCreatedEvent):
82                 return
83             if allowed_path is not None and event.src_path != allowed_path:
84                 return
85             # Text editors always cause a "file close" event in addition to "modified" when saving
86             # a file. Some editors also have "swap" functionality that keeps writing into another
87             # file that's never closed. Prevent such write events from causing a rebuild.
88             if isinstance(event, watchdog.events.FileModifiedEvent):
89                 # But FileClosedEvent is implemented only on Linux, otherwise we mustn't skip this:
90                 if type(self.observer).__name__ == "InotifyObserver":
91                     return
92             log.debug(str(event))
93             with self._rebuild_cond:
94                 self._to_rebuild[func] = True
95                 self._rebuild_cond.notify_all()
96 
97         dir_handler = watchdog.events.FileSystemEventHandler()
98         dir_handler.on_any_event = callback
99 
100         seen = set()
101 
102         def schedule(path):
103             seen.add(path)
104             if path.is_file():
105                 # Watchdog doesn't support watching files, so watch its directory and filter by path
106                 handler = watchdog.events.FileSystemEventHandler()
107                 handler.on_any_event = lambda event: callback(event, allowed_path=os.fspath(path))
108 
109                 parent = path.parent
110                 log.debug(f"Watching file '{path}' through directory '{parent}'")
111                 self.observer.schedule(handler, parent)
112             else:
113                 log.debug(f"Watching directory '{path}'")
114                 self.observer.schedule(dir_handler, path, recursive=recursive)
115 
116         schedule(pathlib.Path(path).resolve())
117 
118         def watch_symlink_targets(path_obj):  # path is os.DirEntry or pathlib.Path
119             if path_obj.is_symlink():
120                 path_obj = pathlib.Path(path_obj).resolve()
121                 if path_obj in seen or not path_obj.exists():
122                     return
123                 schedule(path_obj)
124 
125             if path_obj.is_dir() and recursive:
126                 with os.scandir(os.fspath(path_obj)) as scan:
127                     for entry in scan:
128                         watch_symlink_targets(entry)
129 
130         watch_symlink_targets(pathlib.Path(path))
131 
132     def serve(self):
133         self.observer.start()
134 
135         log.info(f"Serving on {self.url}")
136         self.serve_thread.start()
137 
138         self._build_loop()
139 
140     def _build_loop(self):
141         while True:
142             with self._rebuild_cond:
143                 while not self._rebuild_cond.wait_for(
144                     lambda: self._to_rebuild or self._shutdown, timeout=self.shutdown_delay
145                 ):
146                     # We could have used just one wait instead of a loop + timeout, but we need
147                     # occasional breaks, otherwise on Windows we can't receive KeyboardInterrupt.
148                     pass
149                 if self._shutdown:
150                     break
151                 log.info("Detected file changes")
152                 while self._rebuild_cond.wait(timeout=self.build_delay):
153                     log.debug("Waiting for file changes to stop happening")
154 
155                 self._wanted_epoch = _timestamp()
156                 funcs = list(self._to_rebuild)
157                 self._to_rebuild.clear()
158 
159             for func in funcs:
160                 func()
161 
162             with self._epoch_cond:
163                 log.info("Reloading browsers")
164                 self._visible_epoch = self._wanted_epoch
165                 self._epoch_cond.notify_all()
166 
167     def shutdown(self):
168         self.observer.stop()
169         with self._rebuild_cond:
170             self._shutdown = True
171             self._rebuild_cond.notify_all()
172 
173         if self.serve_thread.is_alive():
174             super().shutdown()
175             self.serve_thread.join()
176             self.observer.join()
177 
178     def serve_request(self, environ, start_response):
179         try:
180             result = self._serve_request(environ, start_response)
181         except Exception:
182             code = 500
183             msg = "500 Internal Server Error"
184             log.exception(msg)
185         else:
186             if result is not None:
187                 return result
188             code = 404
189             msg = "404 Not Found"
190 
191         error_content = None
192         try:
193             error_content = self.error_handler(code)
194         except Exception:
195             log.exception("Failed to render an error message!")
196         if error_content is None:
197             error_content = msg.encode()
198 
199         start_response(msg, [("Content-Type", "text/html")])
200         return [error_content]
201 
202     def _serve_request(self, environ, start_response):
203         path = environ["PATH_INFO"]
204 
205         m = re.fullmatch(r"/livereload/([0-9]+)/[0-9]+", path)
206         if m:
207             epoch = int(m[1])
208             start_response("200 OK", [("Content-Type", "text/plain")])
209 
210             def condition():
211                 return self._visible_epoch > epoch
212 
213             with self._epoch_cond:
214                 if not condition():
215                     # Stall the browser, respond as soon as there's something new.
216                     # If there's not, respond anyway after a minute.
217                     self._log_poll_request(environ.get("HTTP_REFERER"), request_id=path)
218                     self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)
219                 return [b"%d" % self._visible_epoch]
220 
221         if path == "/js/livereload.js":
222             file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "livereload.js")
223         elif path.startswith(self.mount_path):
224             if path.endswith("/"):
225                 path += "index.html"
226             path = path[len(self.mount_path):]
227             file_path = os.path.join(self.root, path.lstrip("/"))
228         elif path == "/":
229             start_response("302 Found", [("Location", self.mount_path)])
230             return []
231         else:
232             return None  # Not found
233 
234         # Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site.
235         with self._epoch_cond:
236             self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)
237             epoch = self._visible_epoch
238 
239         try:
240             file = open(file_path, "rb")
241         except OSError:
242             return None  # Not found
243 
244         if path.endswith(".html"):
245             with file:
246                 content = file.read()
247             content = self._inject_js_into_html(content, epoch)
248             file = io.BytesIO(content)
249             content_length = len(content)
250         else:
251             content_length = os.path.getsize(file_path)
252 
253         content_type = self._guess_type(file_path)
254         start_response(
255             "200 OK", [("Content-Type", content_type), ("Content-Length", str(content_length))]
256         )
257         return wsgiref.util.FileWrapper(file)
258 
259     @classmethod
260     def _inject_js_into_html(cls, content, epoch):
261         try:
262             body_end = content.rindex(b"</body>")
263         except ValueError:
264             body_end = len(content)
265         # The page will reload if the livereload poller returns a newer epoch than what it knows.
266         # The other timestamp becomes just a unique identifier for the initiating page.
267         return (
268             b'%b<script src="/js/livereload.js"></script><script>livereload(%d, %d);</script>%b'
269             % (content[:body_end], epoch, _timestamp(), content[body_end:])
270         )
271 
272     @classmethod
273     @functools.lru_cache()  # "Cache" to not repeat the same message for the same browser tab.
274     def _log_poll_request(cls, url, request_id):
275         log.info(f"Browser connected: {url}")
276 
277     def _guess_type(cls, path):
278         # MkDocs only ensures a few common types (as seen in livereload_tests.py::test_mime_types).
279         # Other uncommon types will not be accepted.
280         if path.endswith((".js", ".JS")):
281             return "application/javascript"
282         if path.endswith(".gz"):
283             return "application/gzip"
284 
285         guess, _ = mimetypes.guess_type(path)
286         if guess:
287             return guess
288         return "application/octet-stream"
289 
290 
291 class _Handler(wsgiref.simple_server.WSGIRequestHandler):
292     def log_request(self, code="-", size="-"):
293         level = logging.DEBUG if str(code) == "200" else logging.WARNING
294         log.log(level, f'"{self.requestline}" code {code}')
295 
296     def log_message(self, format, *args):
297         log.debug(format, *args)
298 
299 
300 def _timestamp():
301     return round(time.monotonic() * 1000)
302
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py
--- 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 @@
         port,
         root,
         mount_path="/",
-        build_delay=0.25,
+        polling_interval=0.5,
         shutdown_delay=0.25,
         **kwargs,
     ):
@@ -45,7 +44,7 @@
         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 @@
 
         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 @@
                 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()

{"golden_diff": "diff --git a/mkdocs/livereload/__init__.py b/mkdocs/livereload/__init__.py\n--- a/mkdocs/livereload/__init__.py\n+++ b/mkdocs/livereload/__init__.py\n@@ -4,7 +4,6 @@\n import mimetypes\n import os\n import os.path\n-import pathlib\n import re\n import socketserver\n import threading\n@@ -13,7 +12,7 @@\n import wsgiref.simple_server\n \n import watchdog.events\n-import watchdog.observers\n+import watchdog.observers.polling\n \n \n class _LoggerAdapter(logging.LoggerAdapter):\n@@ -35,7 +34,7 @@\n         port,\n         root,\n         mount_path=\"/\",\n-        build_delay=0.25,\n+        polling_interval=0.5,\n         shutdown_delay=0.25,\n         **kwargs,\n     ):\n@@ -45,7 +44,7 @@\n         self.root = os.path.abspath(root)\n         self.mount_path = (\"/\" + mount_path.lstrip(\"/\")).rstrip(\"/\") + \"/\"\n         self.url = f\"http://{self.server_name}:{self.server_port}{self.mount_path}\"\n-        self.build_delay = build_delay\n+        self.build_delay = 0.1\n         self.shutdown_delay = shutdown_delay\n         # To allow custom error pages.\n         self.error_handler = lambda code: None\n@@ -62,7 +61,7 @@\n \n         self._shutdown = False\n         self.serve_thread = threading.Thread(target=lambda: self.serve_forever(shutdown_delay))\n-        self.observer = watchdog.observers.Observer(timeout=shutdown_delay)\n+        self.observer = watchdog.observers.polling.PollingObserver(timeout=polling_interval)\n \n     def watch(self, path, func=None, recursive=True):\n         \"\"\"Add the 'path' to watched paths, call the function and reload when any file changes under it.\"\"\"\n@@ -77,57 +76,18 @@\n                 stacklevel=2,\n             )\n \n-        def callback(event, allowed_path=None):\n-            if isinstance(event, watchdog.events.DirCreatedEvent):\n+        def callback(event):\n+            if event.is_directory:\n                 return\n-            if allowed_path is not None and event.src_path != allowed_path:\n-                return\n-            # Text editors always cause a \"file close\" event in addition to \"modified\" when saving\n-            # a file. Some editors also have \"swap\" functionality that keeps writing into another\n-            # file that's never closed. Prevent such write events from causing a rebuild.\n-            if isinstance(event, watchdog.events.FileModifiedEvent):\n-                # But FileClosedEvent is implemented only on Linux, otherwise we mustn't skip this:\n-                if type(self.observer).__name__ == \"InotifyObserver\":\n-                    return\n             log.debug(str(event))\n             with self._rebuild_cond:\n                 self._to_rebuild[func] = True\n                 self._rebuild_cond.notify_all()\n \n-        dir_handler = watchdog.events.FileSystemEventHandler()\n-        dir_handler.on_any_event = callback\n-\n-        seen = set()\n-\n-        def schedule(path):\n-            seen.add(path)\n-            if path.is_file():\n-                # Watchdog doesn't support watching files, so watch its directory and filter by path\n-                handler = watchdog.events.FileSystemEventHandler()\n-                handler.on_any_event = lambda event: callback(event, allowed_path=os.fspath(path))\n-\n-                parent = path.parent\n-                log.debug(f\"Watching file '{path}' through directory '{parent}'\")\n-                self.observer.schedule(handler, parent)\n-            else:\n-                log.debug(f\"Watching directory '{path}'\")\n-                self.observer.schedule(dir_handler, path, recursive=recursive)\n-\n-        schedule(pathlib.Path(path).resolve())\n-\n-        def watch_symlink_targets(path_obj):  # path is os.DirEntry or pathlib.Path\n-            if path_obj.is_symlink():\n-                path_obj = pathlib.Path(path_obj).resolve()\n-                if path_obj in seen or not path_obj.exists():\n-                    return\n-                schedule(path_obj)\n-\n-            if path_obj.is_dir() and recursive:\n-                with os.scandir(os.fspath(path_obj)) as scan:\n-                    for entry in scan:\n-                        watch_symlink_targets(entry)\n-\n-        watch_symlink_targets(pathlib.Path(path))\n+        handler = watchdog.events.FileSystemEventHandler()\n+        handler.on_any_event = callback\n+        log.debug(f\"Watching '{path}'\")\n+        self.observer.schedule(handler, path, recursive=recursive)\n \n     def serve(self):\n         self.observer.start()\n", "issue": "mkdocs 1.2.x livereload no longer working\nmkdocs==1.2 and latest no longer works\r\nmkdocs==1.1.2 works\r\n\r\nMacOS 11.4\r\nDocker version 20.10.7, build f0df350\r\npython:3.8-alpine (3.9.5 and 10-rc same behavior)\r\n\r\nI checked the Docker container and the markdown is updating but the generated html is not. This seems to be associated with the 1.2 change with watcher.\r\n\r\nLMK what logs/config/diag I can provide to help RCA the issue.\n", "before_files": [{"content": "import functools\nimport io\nimport logging\nimport mimetypes\nimport os\nimport os.path\nimport pathlib\nimport re\nimport socketserver\nimport threading\nimport time\nimport warnings\nimport wsgiref.simple_server\n\nimport watchdog.events\nimport watchdog.observers\n\n\nclass _LoggerAdapter(logging.LoggerAdapter):\n    def process(self, msg, kwargs):\n        return time.strftime(\"[%H:%M:%S] \") + msg, kwargs\n\n\nlog = _LoggerAdapter(logging.getLogger(__name__), {})\n\n\nclass LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGIServer):\n    daemon_threads = True\n    poll_response_timeout = 60\n\n    def __init__(\n        self,\n        builder,\n        host,\n        port,\n        root,\n        mount_path=\"/\",\n        build_delay=0.25,\n        shutdown_delay=0.25,\n        **kwargs,\n    ):\n        self.builder = builder\n        self.server_name = host\n        self.server_port = port\n        self.root = os.path.abspath(root)\n        self.mount_path = (\"/\" + mount_path.lstrip(\"/\")).rstrip(\"/\") + \"/\"\n        self.url = f\"http://{self.server_name}:{self.server_port}{self.mount_path}\"\n        self.build_delay = build_delay\n        self.shutdown_delay = shutdown_delay\n        # To allow custom error pages.\n        self.error_handler = lambda code: None\n\n        super().__init__((host, port), _Handler, **kwargs)\n        self.set_app(self.serve_request)\n\n        self._wanted_epoch = _timestamp()  # The version of the site that started building.\n        self._visible_epoch = self._wanted_epoch  # Latest fully built version of the site.\n        self._epoch_cond = threading.Condition()  # Must be held when accessing _visible_epoch.\n\n        self._to_rebuild = {}  # Used as an ordered set of functions to call.\n        self._rebuild_cond = threading.Condition()  # Must be held when accessing _to_rebuild.\n\n        self._shutdown = False\n        self.serve_thread = threading.Thread(target=lambda: self.serve_forever(shutdown_delay))\n        self.observer = watchdog.observers.Observer(timeout=shutdown_delay)\n\n    def watch(self, path, func=None, recursive=True):\n        \"\"\"Add the 'path' to watched paths, call the function and reload when any file changes under it.\"\"\"\n        path = os.path.abspath(path)\n        if func in (None, self.builder):\n            func = self.builder\n        else:\n            warnings.warn(\n                \"Plugins should not pass the 'func' parameter of watch(). \"\n                \"The ability to execute custom callbacks will be removed soon.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        def callback(event, allowed_path=None):\n            if isinstance(event, watchdog.events.DirCreatedEvent):\n                return\n            if allowed_path is not None and event.src_path != allowed_path:\n                return\n            # Text editors always cause a \"file close\" event in addition to \"modified\" when saving\n            # a file. Some editors also have \"swap\" functionality that keeps writing into another\n            # file that's never closed. Prevent such write events from causing a rebuild.\n            if isinstance(event, watchdog.events.FileModifiedEvent):\n                # But FileClosedEvent is implemented only on Linux, otherwise we mustn't skip this:\n                if type(self.observer).__name__ == \"InotifyObserver\":\n                    return\n            log.debug(str(event))\n            with self._rebuild_cond:\n                self._to_rebuild[func] = True\n                self._rebuild_cond.notify_all()\n\n        dir_handler = watchdog.events.FileSystemEventHandler()\n        dir_handler.on_any_event = callback\n\n        seen = set()\n\n        def schedule(path):\n            seen.add(path)\n            if path.is_file():\n                # Watchdog doesn't support watching files, so watch its directory and filter by path\n                handler = watchdog.events.FileSystemEventHandler()\n                handler.on_any_event = lambda event: callback(event, allowed_path=os.fspath(path))\n\n                parent = path.parent\n                log.debug(f\"Watching file '{path}' through directory '{parent}'\")\n                self.observer.schedule(handler, parent)\n            else:\n                log.debug(f\"Watching directory '{path}'\")\n                self.observer.schedule(dir_handler, path, recursive=recursive)\n\n        schedule(pathlib.Path(path).resolve())\n\n        def watch_symlink_targets(path_obj):  # path is os.DirEntry or pathlib.Path\n            if path_obj.is_symlink():\n                path_obj = pathlib.Path(path_obj).resolve()\n                if path_obj in seen or not path_obj.exists():\n                    return\n                schedule(path_obj)\n\n            if path_obj.is_dir() and recursive:\n                with os.scandir(os.fspath(path_obj)) as scan:\n                    for entry in scan:\n                        watch_symlink_targets(entry)\n\n        watch_symlink_targets(pathlib.Path(path))\n\n    def serve(self):\n        self.observer.start()\n\n        log.info(f\"Serving on {self.url}\")\n        self.serve_thread.start()\n\n        self._build_loop()\n\n    def _build_loop(self):\n        while True:\n            with self._rebuild_cond:\n                while not self._rebuild_cond.wait_for(\n                    lambda: self._to_rebuild or self._shutdown, timeout=self.shutdown_delay\n                ):\n                    # We could have used just one wait instead of a loop + timeout, but we need\n                    # occasional breaks, otherwise on Windows we can't receive KeyboardInterrupt.\n                    pass\n                if self._shutdown:\n                    break\n                log.info(\"Detected file changes\")\n                while self._rebuild_cond.wait(timeout=self.build_delay):\n                    log.debug(\"Waiting for file changes to stop happening\")\n\n                self._wanted_epoch = _timestamp()\n                funcs = list(self._to_rebuild)\n                self._to_rebuild.clear()\n\n            for func in funcs:\n                func()\n\n            with self._epoch_cond:\n                log.info(\"Reloading browsers\")\n                self._visible_epoch = self._wanted_epoch\n                self._epoch_cond.notify_all()\n\n    def shutdown(self):\n        self.observer.stop()\n        with self._rebuild_cond:\n            self._shutdown = True\n            self._rebuild_cond.notify_all()\n\n        if self.serve_thread.is_alive():\n            super().shutdown()\n            self.serve_thread.join()\n            self.observer.join()\n\n    def serve_request(self, environ, start_response):\n        try:\n            result = self._serve_request(environ, start_response)\n        except Exception:\n            code = 500\n            msg = \"500 Internal Server Error\"\n            log.exception(msg)\n        else:\n            if result is not None:\n                return result\n            code = 404\n            msg = \"404 Not Found\"\n\n        error_content = None\n        try:\n            error_content = self.error_handler(code)\n        except Exception:\n            log.exception(\"Failed to render an error message!\")\n        if error_content is None:\n            error_content = msg.encode()\n\n        start_response(msg, [(\"Content-Type\", \"text/html\")])\n        return [error_content]\n\n    def _serve_request(self, environ, start_response):\n        path = environ[\"PATH_INFO\"]\n\n        m = re.fullmatch(r\"/livereload/([0-9]+)/[0-9]+\", path)\n        if m:\n            epoch = int(m[1])\n            start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n\n            def condition():\n                return self._visible_epoch > epoch\n\n            with self._epoch_cond:\n                if not condition():\n                    # Stall the browser, respond as soon as there's something new.\n                    # If there's not, respond anyway after a minute.\n                    self._log_poll_request(environ.get(\"HTTP_REFERER\"), request_id=path)\n                    self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)\n                return [b\"%d\" % self._visible_epoch]\n\n        if path == \"/js/livereload.js\":\n            file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"livereload.js\")\n        elif path.startswith(self.mount_path):\n            if path.endswith(\"/\"):\n                path += \"index.html\"\n            path = path[len(self.mount_path):]\n            file_path = os.path.join(self.root, path.lstrip(\"/\"))\n        elif path == \"/\":\n            start_response(\"302 Found\", [(\"Location\", self.mount_path)])\n            return []\n        else:\n            return None  # Not found\n\n        # Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site.\n        with self._epoch_cond:\n            self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)\n            epoch = self._visible_epoch\n\n        try:\n            file = open(file_path, \"rb\")\n        except OSError:\n            return None  # Not found\n\n        if path.endswith(\".html\"):\n            with file:\n                content = file.read()\n            content = self._inject_js_into_html(content, epoch)\n            file = io.BytesIO(content)\n            content_length = len(content)\n        else:\n            content_length = os.path.getsize(file_path)\n\n        content_type = self._guess_type(file_path)\n        start_response(\n            \"200 OK\", [(\"Content-Type\", content_type), (\"Content-Length\", str(content_length))]\n        )\n        return wsgiref.util.FileWrapper(file)\n\n    @classmethod\n    def _inject_js_into_html(cls, content, epoch):\n        try:\n            body_end = content.rindex(b\"</body>\")\n        except ValueError:\n            body_end = len(content)\n        # The page will reload if the livereload poller returns a newer epoch than what it knows.\n        # The other timestamp becomes just a unique identifier for the initiating page.\n        return (\n            b'%b<script src=\"/js/livereload.js\"></script><script>livereload(%d, %d);</script>%b'\n            % (content[:body_end], epoch, _timestamp(), content[body_end:])\n        )\n\n    @classmethod\n    @functools.lru_cache()  # \"Cache\" to not repeat the same message for the same browser tab.\n    def _log_poll_request(cls, url, request_id):\n        log.info(f\"Browser connected: {url}\")\n\n    def _guess_type(cls, path):\n        # MkDocs only ensures a few common types (as seen in livereload_tests.py::test_mime_types).\n        # Other uncommon types will not be accepted.\n        if path.endswith((\".js\", \".JS\")):\n            return \"application/javascript\"\n        if path.endswith(\".gz\"):\n            return \"application/gzip\"\n\n        guess, _ = mimetypes.guess_type(path)\n        if guess:\n            return guess\n        return \"application/octet-stream\"\n\n\nclass _Handler(wsgiref.simple_server.WSGIRequestHandler):\n    def log_request(self, code=\"-\", size=\"-\"):\n        level = logging.DEBUG if str(code) == \"200\" else logging.WARNING\n        log.log(level, f'\"{self.requestline}\" code {code}')\n\n    def log_message(self, format, *args):\n        log.debug(format, *args)\n\n\ndef _timestamp():\n    return round(time.monotonic() * 1000)\n", "path": "mkdocs/livereload/__init__.py"}], "after_files": [{"content": "import functools\nimport io\nimport logging\nimport mimetypes\nimport os\nimport os.path\nimport re\nimport socketserver\nimport threading\nimport time\nimport warnings\nimport wsgiref.simple_server\n\nimport watchdog.events\nimport watchdog.observers.polling\n\n\nclass _LoggerAdapter(logging.LoggerAdapter):\n    def process(self, msg, kwargs):\n        return time.strftime(\"[%H:%M:%S] \") + msg, kwargs\n\n\nlog = _LoggerAdapter(logging.getLogger(__name__), {})\n\n\nclass LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGIServer):\n    daemon_threads = True\n    poll_response_timeout = 60\n\n    def __init__(\n        self,\n        builder,\n        host,\n        port,\n        root,\n        mount_path=\"/\",\n        polling_interval=0.5,\n        shutdown_delay=0.25,\n        **kwargs,\n    ):\n        self.builder = builder\n        self.server_name = host\n        self.server_port = port\n        self.root = os.path.abspath(root)\n        self.mount_path = (\"/\" + mount_path.lstrip(\"/\")).rstrip(\"/\") + \"/\"\n        self.url = f\"http://{self.server_name}:{self.server_port}{self.mount_path}\"\n        self.build_delay = 0.1\n        self.shutdown_delay = shutdown_delay\n        # To allow custom error pages.\n        self.error_handler = lambda code: None\n\n        super().__init__((host, port), _Handler, **kwargs)\n        self.set_app(self.serve_request)\n\n        self._wanted_epoch = _timestamp()  # The version of the site that started building.\n        self._visible_epoch = self._wanted_epoch  # Latest fully built version of the site.\n        self._epoch_cond = threading.Condition()  # Must be held when accessing _visible_epoch.\n\n        self._to_rebuild = {}  # Used as an ordered set of functions to call.\n        self._rebuild_cond = threading.Condition()  # Must be held when accessing _to_rebuild.\n\n        self._shutdown = False\n        self.serve_thread = threading.Thread(target=lambda: self.serve_forever(shutdown_delay))\n        self.observer = watchdog.observers.polling.PollingObserver(timeout=polling_interval)\n\n    def watch(self, path, func=None, recursive=True):\n        \"\"\"Add the 'path' to watched paths, call the function and reload when any file changes under it.\"\"\"\n        path = os.path.abspath(path)\n        if func in (None, self.builder):\n            func = self.builder\n        else:\n            warnings.warn(\n                \"Plugins should not pass the 'func' parameter of watch(). \"\n                \"The ability to execute custom callbacks will be removed soon.\",\n                DeprecationWarning,\n                stacklevel=2,\n            )\n\n        def callback(event):\n            if event.is_directory:\n                return\n            log.debug(str(event))\n            with self._rebuild_cond:\n                self._to_rebuild[func] = True\n                self._rebuild_cond.notify_all()\n\n        handler = watchdog.events.FileSystemEventHandler()\n        handler.on_any_event = callback\n        log.debug(f\"Watching '{path}'\")\n        self.observer.schedule(handler, path, recursive=recursive)\n\n    def serve(self):\n        self.observer.start()\n\n        log.info(f\"Serving on {self.url}\")\n        self.serve_thread.start()\n\n        self._build_loop()\n\n    def _build_loop(self):\n        while True:\n            with self._rebuild_cond:\n                while not self._rebuild_cond.wait_for(\n                    lambda: self._to_rebuild or self._shutdown, timeout=self.shutdown_delay\n                ):\n                    # We could have used just one wait instead of a loop + timeout, but we need\n                    # occasional breaks, otherwise on Windows we can't receive KeyboardInterrupt.\n                    pass\n                if self._shutdown:\n                    break\n                log.info(\"Detected file changes\")\n                while self._rebuild_cond.wait(timeout=self.build_delay):\n                    log.debug(\"Waiting for file changes to stop happening\")\n\n                self._wanted_epoch = _timestamp()\n                funcs = list(self._to_rebuild)\n                self._to_rebuild.clear()\n\n            for func in funcs:\n                func()\n\n            with self._epoch_cond:\n                log.info(\"Reloading browsers\")\n                self._visible_epoch = self._wanted_epoch\n                self._epoch_cond.notify_all()\n\n    def shutdown(self):\n        self.observer.stop()\n        with self._rebuild_cond:\n            self._shutdown = True\n            self._rebuild_cond.notify_all()\n\n        if self.serve_thread.is_alive():\n            super().shutdown()\n            self.serve_thread.join()\n            self.observer.join()\n\n    def serve_request(self, environ, start_response):\n        try:\n            result = self._serve_request(environ, start_response)\n        except Exception:\n            code = 500\n            msg = \"500 Internal Server Error\"\n            log.exception(msg)\n        else:\n            if result is not None:\n                return result\n            code = 404\n            msg = \"404 Not Found\"\n\n        error_content = None\n        try:\n            error_content = self.error_handler(code)\n        except Exception:\n            log.exception(\"Failed to render an error message!\")\n        if error_content is None:\n            error_content = msg.encode()\n\n        start_response(msg, [(\"Content-Type\", \"text/html\")])\n        return [error_content]\n\n    def _serve_request(self, environ, start_response):\n        path = environ[\"PATH_INFO\"]\n\n        m = re.fullmatch(r\"/livereload/([0-9]+)/[0-9]+\", path)\n        if m:\n            epoch = int(m[1])\n            start_response(\"200 OK\", [(\"Content-Type\", \"text/plain\")])\n\n            def condition():\n                return self._visible_epoch > epoch\n\n            with self._epoch_cond:\n                if not condition():\n                    # Stall the browser, respond as soon as there's something new.\n                    # If there's not, respond anyway after a minute.\n                    self._log_poll_request(environ.get(\"HTTP_REFERER\"), request_id=path)\n                    self._epoch_cond.wait_for(condition, timeout=self.poll_response_timeout)\n                return [b\"%d\" % self._visible_epoch]\n\n        if path == \"/js/livereload.js\":\n            file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"livereload.js\")\n        elif path.startswith(self.mount_path):\n            if path.endswith(\"/\"):\n                path += \"index.html\"\n            path = path[len(self.mount_path):]\n            file_path = os.path.join(self.root, path.lstrip(\"/\"))\n        elif path == \"/\":\n            start_response(\"302 Found\", [(\"Location\", self.mount_path)])\n            return []\n        else:\n            return None  # Not found\n\n        # Wait until the ongoing rebuild (if any) finishes, so we're not serving a half-built site.\n        with self._epoch_cond:\n            self._epoch_cond.wait_for(lambda: self._visible_epoch == self._wanted_epoch)\n            epoch = self._visible_epoch\n\n        try:\n            file = open(file_path, \"rb\")\n        except OSError:\n            return None  # Not found\n\n        if path.endswith(\".html\"):\n            with file:\n                content = file.read()\n            content = self._inject_js_into_html(content, epoch)\n            file = io.BytesIO(content)\n            content_length = len(content)\n        else:\n            content_length = os.path.getsize(file_path)\n\n        content_type = self._guess_type(file_path)\n        start_response(\n            \"200 OK\", [(\"Content-Type\", content_type), (\"Content-Length\", str(content_length))]\n        )\n        return wsgiref.util.FileWrapper(file)\n\n    @classmethod\n    def _inject_js_into_html(cls, content, epoch):\n        try:\n            body_end = content.rindex(b\"</body>\")\n        except ValueError:\n            body_end = len(content)\n        # The page will reload if the livereload poller returns a newer epoch than what it knows.\n        # The other timestamp becomes just a unique identifier for the initiating page.\n        return (\n            b'%b<script src=\"/js/livereload.js\"></script><script>livereload(%d, %d);</script>%b'\n            % (content[:body_end], epoch, _timestamp(), content[body_end:])\n        )\n\n    @classmethod\n    @functools.lru_cache()  # \"Cache\" to not repeat the same message for the same browser tab.\n    def _log_poll_request(cls, url, request_id):\n        log.info(f\"Browser connected: {url}\")\n\n    def _guess_type(cls, path):\n        # MkDocs only ensures a few common types (as seen in livereload_tests.py::test_mime_types).\n        # Other uncommon types will not be accepted.\n        if path.endswith((\".js\", \".JS\")):\n            return \"application/javascript\"\n        if path.endswith(\".gz\"):\n            return \"application/gzip\"\n\n        guess, _ = mimetypes.guess_type(path)\n        if guess:\n            return guess\n        return \"application/octet-stream\"\n\n\nclass _Handler(wsgiref.simple_server.WSGIRequestHandler):\n    def log_request(self, code=\"-\", size=\"-\"):\n        level = logging.DEBUG if str(code) == \"200\" else logging.WARNING\n        log.log(level, f'\"{self.requestline}\" code {code}')\n\n    def log_message(self, format, *args):\n        log.debug(format, *args)\n\n\ndef _timestamp():\n    return round(time.monotonic() * 1000)\n", "path": "mkdocs/livereload/__init__.py"}]}