diff --git a/fastapi_vue_setup.py b/fastapi_vue_setup.py index 0a14d0a..218e861 100644 --- a/fastapi_vue_setup.py +++ b/fastapi_vue_setup.py @@ -1212,7 +1212,9 @@ def ensure_python_project(project_dir: Path, *, dry: bool = False) -> bool: return True -def ensure_frontend(project_dir: Path, *, vue_args: list[str] | None = None, dry: bool = False) -> bool: +def ensure_frontend( + project_dir: Path, *, vue_args: list[str] | None = None, dry: bool = False +) -> bool: """Ensure frontend directory exists with a Vue project, run create-vue if needed.""" frontend_dir = project_dir / "frontend" package_json = frontend_dir / "package.json" diff --git a/template/scripts/devserver.py b/template/scripts/devserver.py index 4ecd0d0..4904b55 100755 --- a/template/scripts/devserver.py +++ b/template/scripts/devserver.py @@ -41,7 +41,6 @@ async def run_devserver( viteurl, npm_install, vite = setup_vite(listen, DEFAULT_VITE_PORT) backurl, MODULE_NAME = setup_cli("PROJECT_CLI", backend, DEFAULT_DEV_PORT) - await check_ports_free(viteurl, backurl) # Tell everyone via environment (vite proxy and backend devmode use these) os.environ["ENVPREFIX_VITE_URL"] = viteurl @@ -49,6 +48,7 @@ async def run_devserver( os.environ["ENVPREFIX_DEV"] = "1" async with ProcessGroup() as pg: + pg.create_task(check_ports_free(viteurl, backurl)) npm_i = await pg.spawn(*npm_install, cwd=front) await pg.spawn(*MODULE_NAME, *(extra_args or []), vital=True) await pg.wait(npm_i, ready(backurl, path=HEALTH)) @@ -78,9 +78,9 @@ def main() -> None: try: asyncio.run(run_devserver(args.listen, args.backend, extra_args)) except* KeyboardInterrupt: - pass # user stopped the devserver: exit 0 - except* subprocess.SubprocessError: - raise SystemExit(1) from None # error already logged; exit 1 + pass # user stopped the devserver: normal exit + except* (subprocess.SubprocessError, RuntimeError): + raise SystemExit(1) from None # logged in devutil already; exit 1 HELP_EPILOG = """ diff --git a/template/scripts/fastapi-vue/devutil.py b/template/scripts/fastapi-vue/devutil.py index 974b206..98b5ff6 100644 --- a/template/scripts/fastapi-vue/devutil.py +++ b/template/scripts/fastapi-vue/devutil.py @@ -65,7 +65,9 @@ class ProcessGroup(asyncio.TaskGroup): if not isinstance(w, Process): return await w if retcode := await w.wait(): - raise CalledProcessError(retcode, self._cmds[w]) + cmd = self._cmds[w] + logger.warning("Process %s exited with status %d", Path(cmd[0]).stem, retcode) + raise CalledProcessError(retcode, cmd) return retcode async with asyncio.TaskGroup() as group: @@ -104,22 +106,23 @@ async def http_get_server(url: str, timeout: float) -> str | None: # noqa: ASYN async def check_ports_free(*urls: str) -> None: - """Verify URLs are not responding (ports are free). Raise SystemExit if any respond.""" + """Verify URLs are not responding (ports are free). - async def check(url: str) -> None: - server = await http_get_server(url, timeout=0.1) + Meant to run as a task inside a TaskGroup. Logs the conflict and raises + RuntimeError (handled like a failed process) if any URL responds. + """ + servers = await asyncio.gather(*(http_get_server(url, timeout=0.1) for url in urls)) + for url, server in zip(urls, servers, strict=True): if server is not None: - logger.warning("Conflicting %s already running at %s", server or "server", url) - raise SystemExit(1) - - await asyncio.gather(*[check(url) for url in urls]) + logger.error("Conflicting %s already running at %s", server or "server", url) + raise RuntimeError(url) async def ready(url: str, path: str = "", max_attempts: int = 50) -> None: """Wait for the server to be ready by polling an endpoint. Use empty path to disable the check and make this return immediately. - Raises SystemExit(1) if server doesn't start in time. + Raises TimeoutError if server doesn't start in time. """ if not path: return @@ -129,8 +132,7 @@ async def ready(url: str, path: str = "", max_attempts: int = 50) -> None: logger.info("✓ Backend ready!") return if attempt == max_attempts - 1: - logger.warning("Backend didn't start in time") - raise SystemExit(1) + raise TimeoutError(f"Backend at {url} didn't start in time") # noqa: EM102, TRY003 await asyncio.sleep(0.1)