From c300a1a29182175d0d76c2c889ebf0aad035f3c8 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 2 Feb 2026 21:02:34 +0000 Subject: [PATCH] Simplified uvicorn running in multiprocess mode, avoiding a crash on Windows when it couldn't setup threads (running from a worker thread). --- template/scripts/devserver.py | 20 ++++++------------ template/scripts/fastapi-vue/devutil.py | 27 ++++++++++++++++--------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/template/scripts/devserver.py b/template/scripts/devserver.py index 9f33f89..531e772 100755 --- a/template/scripts/devserver.py +++ b/template/scripts/devserver.py @@ -4,13 +4,11 @@ import argparse import asyncio -import contextlib import os import sys +from contextlib import suppress from pathlib import Path -from fastapi_vue import server - # 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"))) from devutil import ( # type: ignore @@ -34,7 +32,7 @@ async def run_devserver(frontend: str, backend: str) -> None: raise SystemExit(1) 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 ) @@ -45,15 +43,8 @@ async def run_devserver(frontend: str, backend: str) -> None: async with ProcessGroup() as pg: npm_i = await pg.spawn(*npm_install, cwd=front) await check_ports_free(viteurl, backurl) - - # 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.spawn(*uvicorn) 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) @@ -75,7 +66,7 @@ def main(): help=f"FastAPI backend endpoint (default: localhost:{DEFAULT_DEV_PORT})", ) args = parser.parse_args() - with contextlib.suppress(KeyboardInterrupt): + with suppress(KeyboardInterrupt): 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 --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). """ diff --git a/template/scripts/fastapi-vue/devutil.py b/template/scripts/fastapi-vue/devutil.py index 1eec93e..2d2f20d 100644 --- a/template/scripts/fastapi-vue/devutil.py +++ b/template/scripts/fastapi-vue/devutil.py @@ -2,6 +2,7 @@ import asyncio import subprocess +import sys from collections.abc import Coroutine from contextlib import suppress from pathlib import Path @@ -159,10 +160,10 @@ def setup_vite( def setup_fastapi( endpoint: str, module: str, default_port: int = 8000 -) -> tuple[str, str, dict]: - """Parse backend endpoint and build server.run() config. +) -> tuple[str, list[str]]: + """Parse backend endpoint and build uvicorn command. - Returns (url, module, config_dict). + Returns (url, uvicorn_cmd). Raises SystemExit(1) on invalid config. """ endpoints = parse_endpoint(endpoint, default_port) @@ -173,11 +174,17 @@ def setup_fastapi( host = endpoints[0]["host"] port = endpoints[0]["port"] + reload_dir = module.split(".")[0] # Don't reload on frontend changes - config = { - "listen": f"{host}:{port}", - "reload": True, - "reload_dirs": [module.split(".")[0]], # Don't reload on frontend changes - "forwarded_allow_ips": "*", - } - return f"http://{host}:{port}", module, config + cmd = [ + sys.executable, + "-m", + "uvicorn", + module, + f"--host={host}", + f"--port={port}", + "--reload", + f"--reload-dir={reload_dir}", + "--forwarded-allow-ips=*", + ] + return f"http://{host}:{port}", cmd