Simplified uvicorn running in multiprocess mode, avoiding a crash on Windows when it couldn't setup threads (running from a worker thread).

This commit is contained in:
2026-02-02 21:02:34 +00:00
parent bdfaa22b0c
commit c300a1a291
2 changed files with 23 additions and 24 deletions
+6 -14
View File
@@ -4,13 +4,11 @@
import argparse import argparse
import asyncio import asyncio
import contextlib
import os import os
import sys import sys
from contextlib import suppress
from pathlib import Path from pathlib import Path
from fastapi_vue import server
# Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path) # Import util.py from scripts/fastapi-vue (not a package, so we adjust sys.path)
sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue"))) sys.path.insert(0, str(Path(__file__).with_name("fastapi-vue")))
from devutil import ( # type: ignore from devutil import ( # type: ignore
@@ -34,7 +32,7 @@ async def run_devserver(frontend: str, backend: str) -> None:
raise SystemExit(1) raise SystemExit(1)
viteurl, npm_install, vite = setup_vite(frontend, DEFAULT_VITE_PORT) viteurl, npm_install, vite = setup_vite(frontend, DEFAULT_VITE_PORT)
backurl, module, backend_config = setup_fastapi( backurl, uvicorn = setup_fastapi(
backend, "MODULE_NAME.APP_MODULE:APP_VAR", DEFAULT_DEV_PORT backend, "MODULE_NAME.APP_MODULE:APP_VAR", DEFAULT_DEV_PORT
) )
@@ -45,15 +43,8 @@ async def run_devserver(frontend: str, backend: str) -> None:
async with ProcessGroup() as pg: async with ProcessGroup() as pg:
npm_i = await pg.spawn(*npm_install, cwd=front) npm_i = await pg.spawn(*npm_install, cwd=front)
await check_ports_free(viteurl, backurl) await check_ports_free(viteurl, backurl)
await pg.spawn(*uvicorn)
# Run backend in a thread (server.run is blocking)
loop = asyncio.get_event_loop()
loop.run_in_executor(None, lambda: server.run(module, **backend_config))
# Wait for both install and backend to be ready
await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py")) await pg.wait(npm_i, ready(backurl, path="/api/health?from=devserver.py"))
# Start Vite dev server (ProcessGroup waits for any exit, then terminates others)
await pg.spawn(*vite, cwd=front) await pg.spawn(*vite, cwd=front)
@@ -75,7 +66,7 @@ def main():
help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})", help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})",
) )
args = parser.parse_args() args = parser.parse_args()
with contextlib.suppress(KeyboardInterrupt): with suppress(KeyboardInterrupt):
asyncio.run(run_devserver(args.frontend, args.backend)) asyncio.run(run_devserver(args.frontend, args.backend))
@@ -84,7 +75,8 @@ HELP_EPILOG = """
scripts/devserver.py 3000 # Vite on localhost:3000 scripts/devserver.py 3000 # Vite on localhost:3000
scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000 scripts/devserver.py :3000 --backend 8000 # *:3000, localhost:8000
JS_RUNTIME environment variable can be used to select the JS runtime JS_RUNTIME environment variable can be used to select the JS runtime:
npm, deno, bun, or full path to the runtime executable (node maps to npm).
""" """
+17 -10
View File
@@ -2,6 +2,7 @@
import asyncio import asyncio
import subprocess import subprocess
import sys
from collections.abc import Coroutine from collections.abc import Coroutine
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
@@ -159,10 +160,10 @@ def setup_vite(
def setup_fastapi( def setup_fastapi(
endpoint: str, module: str, default_port: int = 8000 endpoint: str, module: str, default_port: int = 8000
) -> tuple[str, str, dict]: ) -> tuple[str, list[str]]:
"""Parse backend endpoint and build server.run() config. """Parse backend endpoint and build uvicorn command.
Returns (url, module, config_dict). Returns (url, uvicorn_cmd).
Raises SystemExit(1) on invalid config. Raises SystemExit(1) on invalid config.
""" """
endpoints = parse_endpoint(endpoint, default_port) endpoints = parse_endpoint(endpoint, default_port)
@@ -173,11 +174,17 @@ def setup_fastapi(
host = endpoints[0]["host"] host = endpoints[0]["host"]
port = endpoints[0]["port"] port = endpoints[0]["port"]
reload_dir = module.split(".")[0] # Don't reload on frontend changes
config = { cmd = [
"listen": f"{host}:{port}", sys.executable,
"reload": True, "-m",
"reload_dirs": [module.split(".")[0]], # Don't reload on frontend changes "uvicorn",
"forwarded_allow_ips": "*", module,
} f"--host={host}",
return f"http://{host}:{port}", module, config f"--port={port}",
"--reload",
f"--reload-dir={reload_dir}",
"--forwarded-allow-ips=*",
]
return f"http://{host}:{port}", cmd