File size: 3,168 Bytes
56dc7f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | """
Single-command dev launcher: starts the FastAPI backend (main.py) and the
Vite frontend dev server together, both streaming to this terminal.
Ctrl+C stops both.
Usage:
python run_dev.py
"""
from __future__ import annotations
import os
import platform
import signal
import subprocess
import sys
import time
ROOT = os.path.dirname(os.path.abspath(__file__))
FRONTEND = os.path.join(ROOT, "frontend")
IS_WINDOWS = platform.system() == "Windows"
def _venv_python() -> str:
"""Prefer this repo's own .venv interpreter over whatever `python`
happens to be on PATH, so backend deps resolve the same way they do
when running `python main.py` directly."""
candidate = (
os.path.join(ROOT, ".venv", "Scripts", "python.exe")
if IS_WINDOWS
else os.path.join(ROOT, ".venv", "bin", "python")
)
return candidate if os.path.exists(candidate) else sys.executable
def _popen_frontend() -> subprocess.Popen:
"""`npm run dev` needs shell=True to resolve npm.cmd/npm on PATH,
which makes the Popen object track a shell wrapper, not the actual
vite/node process underneath it - terminating just the wrapper
leaves vite running as an orphan (observed: it kept the dev port
bound after this script had already exited). Put it in its own
process group/session so _kill_tree can take out the whole tree."""
if IS_WINDOWS:
return subprocess.Popen(
"npm run dev", cwd=FRONTEND, shell=True,
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
)
return subprocess.Popen(
"npm run dev", cwd=FRONTEND, shell=True, preexec_fn=os.setsid,
)
def _kill_tree(proc: subprocess.Popen) -> None:
if proc.poll() is not None:
return
if IS_WINDOWS:
subprocess.run(
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
else:
try:
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
except ProcessLookupError:
pass
def main() -> int:
backend_cmd = [_venv_python(), "main.py"]
print(f"[dev] backend: {' '.join(backend_cmd)} (cwd={ROOT})")
backend = subprocess.Popen(backend_cmd, cwd=ROOT)
print(f"[dev] frontend: npm run dev (cwd={FRONTEND})")
frontend = _popen_frontend()
exit_code = 0
try:
while True:
if backend.poll() is not None:
exit_code = backend.returncode
print(f"[dev] backend exited ({exit_code}) - stopping frontend...")
_kill_tree(frontend)
break
if frontend.poll() is not None:
exit_code = frontend.returncode
print(f"[dev] frontend exited ({exit_code}) - stopping backend...")
_kill_tree(backend)
break
time.sleep(0.5)
except KeyboardInterrupt:
print("\n[dev] stopping backend and frontend...")
_kill_tree(backend)
_kill_tree(frontend)
finally:
backend.wait()
frontend.wait()
return exit_code
if __name__ == "__main__":
sys.exit(main())
|