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

Reimplement livereload in a simpler and better way #2385

Merged
merged 36 commits into from May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f362309
Reimplement livereload in a simpler and better way
oprypin Apr 29, 2021
07e44a1
Support specifying different callbacks
oprypin May 3, 2021
f91b3bc
Merge remote-tracking branch 'origin/master' into HEAD
oprypin May 9, 2021
9002448
Add DeprecationWarning
oprypin May 6, 2021
e8d370d
Stall requests that happen in the middle of a site build
oprypin May 6, 2021
458772f
Don't need to populate the server ourselves
oprypin May 6, 2021
a231c72
Rework to use a single throttler for all callbacks
oprypin May 9, 2021
06b7b70
Fix <script> insertion with no body end tag
oprypin May 9, 2021
bd2153d
Add tests
oprypin May 9, 2021
0240655
Ensure correct MIME type for JavaScript
oprypin May 9, 2021
328714a
Python 3.6 compatiblity
oprypin May 9, 2021
286a267
Add timeouts in tests
oprypin May 9, 2021
099d5d9
Add release notes
oprypin May 9, 2021
afc08d2
Reword release notes
oprypin May 10, 2021
f4eb78d
Move all startup code into `serve` function
oprypin May 10, 2021
66a403a
Add tests for MIME types and clarify support
oprypin May 10, 2021
f24dae1
Relax MIME test
oprypin May 10, 2021
93c21cb
Exclude file edit events to help editors that write to the same dir
oprypin May 10, 2021
223d958
Skip edit events only on Linux
oprypin May 12, 2021
f342716
Silence BrokenPipeError
oprypin May 12, 2021
ae33a32
Add sleep to test, as it sometimes fails on Mac
oprypin May 12, 2021
c5c3761
Merge remote-tracking branch 'origin/master' into reload
oprypin May 13, 2021
b5871ca
Try to fix Content-length but now it breaks for use_directory_urls
oprypin May 13, 2021
6b242fc
Merge remote-tracking branch 'origin/master' into HEAD
oprypin May 13, 2021
c6ea44e
Un-break printing extra log content such as tracebacks
oprypin May 13, 2021
36daf2b
Re-implement as a WSGI server
oprypin May 13, 2021
368a29b
Increase "cooldown" before starting to build
oprypin May 13, 2021
7c0cc6d
Add timestamp to log messages
oprypin May 13, 2021
0015a15
Also do our own `guess_type`, otherwise it's too brittle
oprypin May 13, 2021
ac71ddf
Better solution for logging
oprypin May 14, 2021
b3a3651
Ignore directory events (there are misfires on Windows) and debug-log…
oprypin May 14, 2021
5607bd4
Add breaks when waiting for the next site rebuild
oprypin May 14, 2021
7f537cd
Don't log "browser connected" if it gets an instant response
oprypin May 14, 2021
367ae00
Merge remote-tracking branch 'origin/master' into reload
oprypin May 18, 2021
82652d0
Add another test
oprypin May 19, 2021
133193a
Improve tests' stability, by sleeping after a watch...
oprypin May 18, 2021
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
29 changes: 29 additions & 0 deletions docs/about/release-notes.md
Expand Up @@ -81,6 +81,25 @@ MkDocs can now benefit from recent bug fixes and new features, including the fol

[ghp-import]: https://github.com/c-w/ghp-import/

#### Rework auto-reload and HTTP server for `mkdocs serve` (#2385)

`mkdocs serve` now uses a new underlying server + file watcher implementation,
based on [http.server] from standard library and [watchdog]. It provides similar
functionality to the previously used [livereload] library (which is now dropped
from dependencies, along with [tornado]).

This makes reloads more responsive and consistent in terms of timing. Multiple
rapid file changes no longer cause the site to repeatedly rebuild (issue #2061).

Almost every aspect of the server is slightly different, but actual visible
changes are minor. The logging outputs are only *similar* to the old ones.
Degradations in behavior are not expected, and should be reported if found.

[http.server]: https://docs.python.org/3/library/http.server.html
[watchdog]: https://pypi.org/project/watchdog/
[livereload]: https://pypi.org/project/livereload/
[tornado]: https://pypi.org/project/tornado/

#### A `build_error` event was added (#2103)

Plugin developers can now use the `on_build_error` hook
Expand Down Expand Up @@ -148,6 +167,16 @@ configuration documentation for details.
To ensure any warnings get counted, simply log them to the `mkdocs` log (i.e:
`mkdocs.plugins.pluginname`).

* The `on_serve` event (which receives the `server` object and the `builder`
function) is affected by the server rewrite. `server` is now a
`mkdocs.livereload.LiveReloadServer` instead of `livereload.server.Server`.
The typical action that plugins can do with these is to call
`server.watch(some_dir, builder)`, which basically adds that directory to
watched directories, causing the site to be rebuilt on file changes. That
still works, but passing any other function to `watch` is deprecated and shows
a warning. This 2nd parameter is already optional, and will accept only this
exact `builder` function just for compatibility.

* The `python` method of the `plugins.search.prebuild_index` configuration
option is pending deprecation as of version 1.2. It is expected that in
version 1.3 it will raise a warning if used and in version 1.4 it will raise
Expand Down
8 changes: 3 additions & 5 deletions mkdocs/__main__.py
Expand Up @@ -33,18 +33,19 @@ class ColorFormatter(logging.Formatter):
)

def format(self, record):
message = super().format(record)
prefix = f'{record.levelname:<8} - '
if record.levelname in self.colors:
prefix = click.style(prefix, fg=self.colors[record.levelname])
if self.text_wrapper.width:
# Only wrap text if a terminal width was detected
msg = '\n'.join(
self.text_wrapper.fill(line)
for line in record.getMessage().splitlines()
for line in message.splitlines()
)
# Prepend prefix after wrapping so that color codes don't affect length
return prefix + msg[12:]
return prefix + record.getMessage()
return prefix + message


class State:
Expand Down Expand Up @@ -169,9 +170,6 @@ def cli():
@common_options
def serve_command(dev_addr, livereload, **kwargs):
"""Run the builtin development server"""

logging.getLogger('tornado').setLevel(logging.WARNING)

serve.serve(dev_addr=dev_addr, livereload=livereload, **kwargs)


Expand Down
126 changes: 29 additions & 97 deletions mkdocs/commands/serve.py
@@ -1,109 +1,16 @@
import logging
import shutil
import tempfile
import sys

from os.path import isfile, join
from mkdocs.commands.build import build
from mkdocs.config import load_config
from mkdocs.exceptions import Abort
from mkdocs.livereload import LiveReloadServer

log = logging.getLogger(__name__)


def _init_asyncio_patch():
"""
Select compatible event loop for Tornado 5+.

As of Python 3.8, the default event loop on Windows is `proactor`,
however Tornado requires the old default "selector" event loop.
As Tornado has decided to leave this to users to set, MkDocs needs
to set it. See https://github.com/tornadoweb/tornado/issues/2608.
"""
if sys.platform.startswith("win") and sys.version_info >= (3, 8):
import asyncio
try:
from asyncio import WindowsSelectorEventLoopPolicy
except ImportError:
pass # Can't assign a policy which doesn't exist.
else:
if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())


def _get_handler(site_dir, StaticFileHandler):

from tornado.template import Loader

class WebHandler(StaticFileHandler):

def write_error(self, status_code, **kwargs):

if status_code in (404, 500):
error_page = f'{status_code}.html'
if isfile(join(site_dir, error_page)):
self.write(Loader(site_dir).load(error_page).generate())
else:
super().write_error(status_code, **kwargs)

return WebHandler


def _livereload(host, port, config, builder, site_dir, watch_theme):

# We are importing here for anyone that has issues with livereload. Even if
# this fails, the --no-livereload alternative should still work.
_init_asyncio_patch()
from livereload import Server
import livereload.handlers

class LiveReloadServer(Server):

def get_web_handlers(self, script):
handlers = super().get_web_handlers(script)
# replace livereload handler
return [(handlers[0][0], _get_handler(site_dir, livereload.handlers.StaticFileHandler), handlers[0][2],)]

server = LiveReloadServer()

# Watch the documentation files, the config file and the theme files.
server.watch(config['docs_dir'], builder)
server.watch(config['config_file_path'], builder)

if watch_theme:
for d in config['theme'].dirs:
server.watch(d, builder)

# Run `serve` plugin events.
server = config['plugins'].run_event('serve', server, config=config, builder=builder)

server.serve(root=site_dir, host=host, port=port, restart_delay=0)


def _static_server(host, port, site_dir):

# Importing here to separate the code paths from the --livereload
# alternative.
_init_asyncio_patch()
from tornado import ioloop
from tornado import web

application = web.Application([
(r"/(.*)", _get_handler(site_dir, web.StaticFileHandler), {
"path": site_dir,
"default_filename": "index.html"
}),
])
application.listen(port=port, address=host)

log.info(f'Running at: http://{host}:{port}/')
log.info('Hold ctrl+c to quit.')
try:
ioloop.IOLoop.instance().start()
except KeyboardInterrupt:
log.info('Stopping server...')


def serve(config_file=None, dev_addr=None, strict=None, theme=None,
theme_dir=None, livereload='livereload', watch_theme=False, **kwargs):
"""
Expand Down Expand Up @@ -144,10 +51,35 @@ def builder():

host, port = config['dev_addr']

server = LiveReloadServer(builder=builder, host=host, port=port, root=site_dir)

def error_handler(code):
if code in (404, 500):
error_page = join(site_dir, f'{code}.html')
if isfile(error_page):
with open(error_page, 'rb') as f:
return f.read()

server.error_handler = error_handler

if livereload in ['livereload', 'dirty']:
_livereload(host, port, config, builder, site_dir, watch_theme)
else:
_static_server(host, port, site_dir)
# Watch the documentation files, the config file and the theme files.
server.watch(config['docs_dir'])
server.watch(config['config_file_path'])

if watch_theme:
for d in config['theme'].dirs:
server.watch(d)

# Run `serve` plugin events.
server = config['plugins'].run_event('serve', server, config=config, builder=builder)

try:
server.serve()
except KeyboardInterrupt:
log.info("Shutting down...")
finally:
server.shutdown()
except OSError as e: # pragma: no cover
# Avoid ugly, unhelpful traceback
raise Abort(str(e))
Expand Down