Skip to content

Commit

Permalink
Work around signals on Windows (fixes aio-libs#548)
Browse files Browse the repository at this point in the history
As discussed in the issue above, this works around signals not working
properly on Windows by setting up an endpoint that shuts down the
server.
  • Loading branch information
haukex committed Apr 23, 2023
1 parent 7ecebe3 commit d253a34
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 4 deletions.
5 changes: 5 additions & 0 deletions aiohttp_devtools/runserver/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def __init__(self, *,
python_path: Optional[str] = None,
static_url: str = '/static/',
livereload: bool = True,
shutdown_endpoint: bool = sys.platform.startswith("win32"),
path_prefix: str = "/_devtools",
app_factory_name: Optional[str] = None,
host: str = INFER_HOST,
main_port: int = 8000,
Expand Down Expand Up @@ -66,6 +68,8 @@ def __init__(self, *,
self.static_path = self._resolve_path(static_path, "is_dir", "static-path") if static_path else None
self.static_url = static_url
self.livereload = livereload
self.shutdown_endpoint = shutdown_endpoint
self.path_prefix = path_prefix
self.app_factory_name = app_factory_name
self.infer_host = host == INFER_HOST
self.host = 'localhost' if self.infer_host else host
Expand Down Expand Up @@ -184,5 +188,6 @@ async def load_app(self, app_factory: AppFactory) -> web.Application:

def __str__(self) -> str:
fields = ('py_file', 'static_path', 'static_url', 'livereload',
'shutdown_endpoint', 'path_prefix',
'app_factory_name', 'host', 'main_port', 'aux_port')
return 'Config:\n' + '\n'.join(' {0}: {1!r}'.format(f, getattr(self, f)) for f in fields)
9 changes: 9 additions & 0 deletions aiohttp_devtools/runserver/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ async def static_middleware(request: web.Request, handler: Handler) -> web.Strea

app.middlewares.insert(0, static_middleware)

if config.shutdown_endpoint: # a workaround for singals not working on Windows
from aiohttp.web_runner import GracefulExit
async def get_shutdown(request):
request.app.logger.info('shutting down due to request at endpoint')
raise GracefulExit()
path = config.path_prefix+"/shutdown"
app.router.add_route("GET", path, get_shutdown, name="devtools.shutdown")
dft_logger.debug('set up shutdown endpoint at http://{}:{}{}'.format(config.host, config.main_port, path))

if config.static_path is not None:
static_url = 'http://{}:{}/{}'.format(config.host, config.aux_port, static_path)
dft_logger.debug('settings app static_root_url to "%s"', static_url)
Expand Down
31 changes: 27 additions & 4 deletions aiohttp_devtools/runserver/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from multiprocessing import Process
from pathlib import Path
from typing import AsyncIterator, Iterable, Optional, Tuple, Union
from contextlib import suppress

from aiohttp import ClientSession, web
from aiohttp.client_exceptions import ClientError, ServerDisconnectedError, ClientConnectorError
from watchfiles import awatch

from ..exceptions import AiohttpDevException
Expand Down Expand Up @@ -43,7 +45,6 @@ async def cleanup_ctx(self, app: web.Application) -> AsyncIterator[None]:
yield
await self.close(app)


class AppTask(WatchTask):
template_files = '.html', '.jinja', '.jinja2'

Expand Down Expand Up @@ -71,7 +72,7 @@ def is_static(changes: Iterable[Tuple[object, str]]) -> bool:
self._reloads += 1
if any(f.endswith('.py') for _, f in changes):
logger.debug('%d changes, restarting server', len(changes))
self._stop_dev_server()
await self._stop_dev_server()
self._start_dev_server()
await self._src_reload_when_live(live_checks)
elif len(changes) == 1 and is_static(changes):
Expand Down Expand Up @@ -119,10 +120,32 @@ def _start_dev_server(self) -> None:
self._process = Process(target=serve_main_app, args=(self._config, tty_path))
self._process.start()

def _stop_dev_server(self) -> None:
async def _stop_dev_server(self) -> None:
if self._process.is_alive():
logger.debug('stopping server process...')
if self._config.shutdown_endpoint: # a workaround for singals not working on Windows
url = 'http://localhost:{}{}/shutdown'.format(self._config.main_port, self._config.path_prefix)
logger.debug('attempting to stop process via shutdown endpoint {}'.format(url))
try:
# these errors are expected because the request kills the server *immediately*
with suppress(ServerDisconnectedError, ClientConnectorError):
async with self._session.get(url):
pass
except (ConnectionError, ClientError, asyncio.TimeoutError) as ex:
if self._process.is_alive():
logger.warning("shutdown endpoint caused an error (will try signals next): {}".format(ex))
else:
logger.warning("process stopped (despite error at shutdown endpoint: {})".format(ex))
return
else:
self._process.join(5)
if self._process.exitcode is None:
logger.warning('shutdown endpoint did not terminate process, trying signals')
else:
logger.debug('process stopped via shutdown endpoint')
return
if self._process.pid:
logger.debug('sending SIGINT')
os.kill(self._process.pid, signal.SIGINT)
self._process.join(5)
if self._process.exitcode is None:
Expand All @@ -136,7 +159,7 @@ def _stop_dev_server(self) -> None:

async def close(self, *args: object) -> None:
self.stopper.set()
self._stop_dev_server()
await self._stop_dev_server()
if self._session is None:
raise RuntimeError("Object not started correctly before calling .close()")
await asyncio.gather(super().close(), self._session.close())
Expand Down

0 comments on commit d253a34

Please sign in to comment.