Proper error messages on npm and port check failures, avoid leaking asyncio tasks.
This commit is contained in:
@@ -1212,7 +1212,9 @@ def ensure_python_project(project_dir: Path, *, dry: bool = False) -> bool:
|
|||||||
return True
|
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."""
|
"""Ensure frontend directory exists with a Vue project, run create-vue if needed."""
|
||||||
frontend_dir = project_dir / "frontend"
|
frontend_dir = project_dir / "frontend"
|
||||||
package_json = frontend_dir / "package.json"
|
package_json = frontend_dir / "package.json"
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ async def run_devserver(
|
|||||||
|
|
||||||
viteurl, npm_install, vite = setup_vite(listen, DEFAULT_VITE_PORT)
|
viteurl, npm_install, vite = setup_vite(listen, DEFAULT_VITE_PORT)
|
||||||
backurl, MODULE_NAME = setup_cli("PROJECT_CLI", backend, DEFAULT_DEV_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)
|
# Tell everyone via environment (vite proxy and backend devmode use these)
|
||||||
os.environ["ENVPREFIX_VITE_URL"] = viteurl
|
os.environ["ENVPREFIX_VITE_URL"] = viteurl
|
||||||
@@ -49,6 +48,7 @@ async def run_devserver(
|
|||||||
os.environ["ENVPREFIX_DEV"] = "1"
|
os.environ["ENVPREFIX_DEV"] = "1"
|
||||||
|
|
||||||
async with ProcessGroup() as pg:
|
async with ProcessGroup() as pg:
|
||||||
|
pg.create_task(check_ports_free(viteurl, backurl))
|
||||||
npm_i = await pg.spawn(*npm_install, cwd=front)
|
npm_i = await pg.spawn(*npm_install, cwd=front)
|
||||||
await pg.spawn(*MODULE_NAME, *(extra_args or []), vital=True)
|
await pg.spawn(*MODULE_NAME, *(extra_args or []), vital=True)
|
||||||
await pg.wait(npm_i, ready(backurl, path=HEALTH))
|
await pg.wait(npm_i, ready(backurl, path=HEALTH))
|
||||||
@@ -78,9 +78,9 @@ def main() -> None:
|
|||||||
try:
|
try:
|
||||||
asyncio.run(run_devserver(args.listen, args.backend, extra_args))
|
asyncio.run(run_devserver(args.listen, args.backend, extra_args))
|
||||||
except* KeyboardInterrupt:
|
except* KeyboardInterrupt:
|
||||||
pass # user stopped the devserver: exit 0
|
pass # user stopped the devserver: normal exit
|
||||||
except* subprocess.SubprocessError:
|
except* (subprocess.SubprocessError, RuntimeError):
|
||||||
raise SystemExit(1) from None # error already logged; exit 1
|
raise SystemExit(1) from None # logged in devutil already; exit 1
|
||||||
|
|
||||||
|
|
||||||
HELP_EPILOG = """
|
HELP_EPILOG = """
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ class ProcessGroup(asyncio.TaskGroup):
|
|||||||
if not isinstance(w, Process):
|
if not isinstance(w, Process):
|
||||||
return await w
|
return await w
|
||||||
if retcode := await w.wait():
|
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
|
return retcode
|
||||||
|
|
||||||
async with asyncio.TaskGroup() as group:
|
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:
|
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:
|
Meant to run as a task inside a TaskGroup. Logs the conflict and raises
|
||||||
server = await http_get_server(url, timeout=0.1)
|
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:
|
if server is not None:
|
||||||
logger.warning("Conflicting %s already running at %s", server or "server", url)
|
logger.error("Conflicting %s already running at %s", server or "server", url)
|
||||||
raise SystemExit(1)
|
raise RuntimeError(url)
|
||||||
|
|
||||||
await asyncio.gather(*[check(url) for url in urls])
|
|
||||||
|
|
||||||
|
|
||||||
async def ready(url: str, path: str = "", max_attempts: int = 50) -> None:
|
async def ready(url: str, path: str = "", max_attempts: int = 50) -> None:
|
||||||
"""Wait for the server to be ready by polling an endpoint.
|
"""Wait for the server to be ready by polling an endpoint.
|
||||||
|
|
||||||
Use empty path to disable the check and make this return immediately.
|
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:
|
if not path:
|
||||||
return
|
return
|
||||||
@@ -129,8 +132,7 @@ async def ready(url: str, path: str = "", max_attempts: int = 50) -> None:
|
|||||||
logger.info("✓ Backend ready!")
|
logger.info("✓ Backend ready!")
|
||||||
return
|
return
|
||||||
if attempt == max_attempts - 1:
|
if attempt == max_attempts - 1:
|
||||||
logger.warning("Backend didn't start in time")
|
raise TimeoutError(f"Backend at {url} didn't start in time") # noqa: EM102, TRY003
|
||||||
raise SystemExit(1)
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user