Open simonw opened 4 years ago
This is a bit tricky to fix. This change to uvicorn is relevant: https://github.com/encode/uvicorn/commit/a75fe1381f6b1f78901691c71894f3cf487b5d30
Within uvicorn it does this:
if port == 0:
port = server.sockets[0].getsockname()[1]
That server
variable is later stashed here:
self.servers = [server]
Where self
is the instance of class Server
- which is the class that Uvicorn instantiates and calls .run()
on when we do uvicorn.run()
here: https://github.com/simonw/datasette/blob/35aee82c60b2c9a0185b934db5528c8bd11830f2/datasette/cli.py#L409
The challenge is... can we run our own custom code after that line has executed that has access to server
and can hence access server.servers[0].sockets[0].getsockname()[1]
to find the port?
This line is interesting: is this a hook I can attach to somehow?
await self.lifespan.startup()
From https://github.com/encode/uvicorn/blob/a75fe1381f6b1f78901691c71894f3cf487b5d30/uvicorn/main.py#L475
Uvicorn's lifespan stuff isn't easy to figure out, but this test suite holds some clues: https://github.com/encode/uvicorn/blob/master/tests/test_lifespan.py
Can I detect the port the server is running on from within the regular Datasette ASGI code? If so I could use that ability and maybe output the magic --root
link a second after the server starts up somehow.
On startup Datasette currently outputs:
INFO: Waiting for application startup.
INFO: ASGI 'lifespan' protocol appears unsupported.
INFO: Application startup complete.
So the ASGI lifespan protocol is almost certainly the right way to solve this.
I already have a AsgiLifespan
class:
https://github.com/simonw/datasette/blob/35aee82c60b2c9a0185b934db5528c8bd11830f2/datasette/app.py#L896-L905
It runs this function: https://github.com/simonw/datasette/blob/35aee82c60b2c9a0185b934db5528c8bd11830f2/datasette/app.py#L890-L894
Could that startup function also output the --root
login URL, if needed?
I'm not convinced that function is ever actually being called - I added a print()
statement to it and it's not executing. I don't think the tests cover it.
Aha! Yes it's not being called, and the reason is this: https://github.com/encode/starlette/issues/486
Short version: by default an exception raised during that phase is silently swallowed! You can avoid the swallowing by adding lifespan="on"
to the call to uvicorn.run()
.
When I did that here:
uvicorn.run(ds.app(), host=host, port=port, log_level="info", lifespan="on")
The server failed to start with this error:
INFO: Started server process [68849]
INFO: Waiting for application startup.
ERROR: Exception in 'lifespan' protocol
Traceback (most recent call last):
File ".../uvicorn/lifespan/on.py", line 48, in main
await app(scope, self.receive, self.send)
File ".../uvicorn/middleware/proxy_headers.py", line 45, in __call__
return await self.app(scope, receive, send)
File ".../datasette_debug_asgi.py", line 9, in wrapped_app
if scope["path"] == "/-/asgi-scope":
KeyError: 'path'
Uninstalling datasette-debug-asgi
caused the server to startup correctly again.
I'm having real trouble figuring out how to gain access to the port that was used to start the server. I'm treating this as a very low priority - it only affects the exact -p 0 --root
combination which isn't going to affect many people at all.
Asked about this on the encode gitter here: https://gitter.im/encode/community?at=5f3c0dcaa8c17801765940c0
Maybe I can do this with ASGI after all. Here's the output of /-/asgi-scope
with datasette-debug-asgi
installed:
{'asgi': {'spec_version': '2.1', 'version': '3.0'},
'client': ('127.0.0.1', 62035),
'headers': [(b'host', b'127.0.0.1:62029'),
(b'user-agent',
b'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:79.0) Gecko'
b'/20100101 Firefox/79.0'),
(b'accept',
b'text/html,application/xhtml+xml,application/xml;q=0.9,image/'
b'webp,*/*;q=0.8'),
(b'accept-language', b'en-US,en;q=0.5'),
(b'accept-encoding', b'gzip, deflate'),
(b'dnt', b'1'),
(b'connection', b'keep-alive'),
(b'upgrade-insecure-requests', b'1'),
(b'cache-control', b'max-age=0')],
'http_version': '1.1',
'method': 'GET',
'path': '/-/asgi-scope',
'query_string': b'',
'raw_path': b'/-/asgi-scope',
'root_path': '',
'scheme': 'http',
'server': ('127.0.0.1', 62029),
'type': 'http'}
That 'server': ('127.0.0.1', 62029)
bit has the correct port. Question is, can I access that programmatically on server startup?
The port is incorrect.