Skip to content

Commit 2fcd89f

Browse files
Kill Uvicorn server via gc.get_objects() in ui.run_with (zauberzeug#5686)
### Motivation This PR fixes zauberzeug#3253, how `app.shutdown` does not work when we do `ui.run_with` as `Server.instance` was never set. ### Analysis Note that upstream is extremely unlikely to be able to supply it to us: - FastAPI CLI simply calls `uvicorn.run` https://github.com/fastapi/fastapi-cli/blob/e62b61d6c56b77d39310a1c49e8eb7abdad2785a/src/fastapi_cli/cli.py#L218-L233 - `uvicorn.run` doesn't even return `server` https://github.com/Kludex/uvicorn/blob/918dae6ef91917a5824ab48c46026e07b7c80beb/uvicorn/main.py#L579-L602 ### Implementation (Copilot) **Improvements to shutdown handling:** * Updated the `shutdown` method in `nicegui/app/app.py` to use garbage collection to find and set the `should_exit` flag on the correct `uvicorn.Server` instance if the standard reference is unavailable, increasing reliability of server shutdown. * Added the `gc` and `uvicorn` imports to support the improved shutdown logic in `nicegui/app/app.py`. [[1]](diffhunk://#diff-adae3337464d5c5adf411c1b2e4b52034747030db2de3f87cfc83fea39763f96R2) [[2]](diffhunk://#diff-adae3337464d5c5adf411c1b2e4b52034747030db2de3f87cfc83fea39763f96R13) * **Human note: Server instance is always unavailable when using `ui.run_with`, and always available when using `ui.run`** **Application instance management:** * Introduced a new global variable `fastapi_app` in `nicegui/core.py` to hold a reference to the FastAPI application, and ensured it is set during application startup in `nicegui/ui_run_with.py`. [[1]](diffhunk://#diff-5ed898650c8cdc47c39575a2e752cfe07edba4c755c1c2390460373640983081R6) [[2]](diffhunk://#diff-5ed898650c8cdc47c39575a2e752cfe07edba4c755c1c2390460373640983081R15) [[3]](diffhunk://#diff-8af9e19f881b73ad9d426d4e341acfbc54b5f68e57e68d8eb2e7866fdde76f24R95-R96) * **Human note: Prevents killing the wrong server, in case there are multiple!** ### Progress - [x] I chose a meaningful title that completes the sentence: "If applied, this PR will..." - [x] The implementation is complete. - [x] If this PR addresses a security issue, it has been coordinated via the [security advisory](https://github.com/zauberzeug/nicegui/security/advisories/new) process. - [x] Pytests is out-of-scope. - [x] Documentation is not necessary for a capability expansion of an already-there function. --------- Co-authored-by: Falko Schindler <falko@zauberzeug.com>
1 parent 6fa0b51 commit 2fcd89f

File tree

3 files changed

+15
-2
lines changed

3 files changed

+15
-2
lines changed

nicegui/app/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def shutdown(self) -> None:
189189
"""
190190
if self.native.main_window:
191191
self.native.main_window.destroy()
192-
if self.config.reload:
192+
if self.config.reload or Server.instance.config.should_reload:
193193
os.kill(os.getppid(), getattr(signal, 'CTRL_C_EVENT' if platform.system() == 'Windows' else 'SIGINT'))
194194
else:
195195
Server.instance.should_exit = True

nicegui/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class CustomServerConfig(uvicorn.Config):
1818

1919

2020
class Server(uvicorn.Server):
21-
instance: Server
21+
instance: uvicorn.Server
2222

2323
@classmethod
2424
def create_singleton(cls, config: CustomServerConfig) -> None:

nicegui/ui_run_with.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import gc
12
from contextlib import asynccontextmanager
23
from pathlib import Path
34
from typing import Any, Callable, Literal, Optional, Union
45

6+
import uvicorn
57
from fastapi import FastAPI
68
from fastapi.middleware.gzip import GZipMiddleware
79
from starlette.types import ASGIApp
@@ -11,6 +13,7 @@
1113
from .language import Language
1214
from .middlewares import RedirectWithPrefixMiddleware, SetCacheControlMiddleware
1315
from .nicegui import _shutdown, _startup
16+
from .server import Server
1417

1518

1619
def run_with(
@@ -86,6 +89,16 @@ def run_with(
8689

8790
@asynccontextmanager
8891
async def lifespan_wrapper(app):
92+
def _get_server_instance() -> Optional[uvicorn.Server]:
93+
for server in (obj for obj in gc.get_objects() if isinstance(obj, uvicorn.Server)):
94+
wrapped = server.config.loaded_app
95+
while wrapped is not None:
96+
if wrapped is app:
97+
return server
98+
wrapped = getattr(wrapped, 'app', None)
99+
return None
100+
if (instance := _get_server_instance()) is not None:
101+
Server.instance = instance
89102
await _startup()
90103
async with main_app_lifespan(app) as state:
91104
yield state

0 commit comments

Comments
 (0)