Proper error messages on npm and port check failures, avoid leaking asyncio tasks.
This commit is contained in:
@@ -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 = """
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user