fix(6): standalone launcher improvements

- Use uvicorn CLI directly (not python -c workaround)
- Add process group handling for cleaner shutdown
- Silent npm output during setup
- Better error messages and logging
- Support graceful SIGTERM/SIGINT handling
This commit is contained in:
2026-04-22 18:44:18 +03:00
parent 5f9c6aee5a
commit bd39f6f1b7

View File

@@ -10,7 +10,6 @@ import sys
import subprocess import subprocess
import signal import signal
import time import time
import json
from pathlib import Path from pathlib import Path
@@ -106,32 +105,36 @@ class StandaloneServerManager:
subprocess.run( subprocess.run(
["npm", "ci", "--silent"], ["npm", "ci", "--silent"],
cwd=str(frontend_dir), cwd=str(frontend_dir),
check=True check=True,
capture_output=True
) )
# Build if needed # Build if needed
if not (frontend_dir / ".next").exists(): if not (frontend_dir / ".next" / "standalone").exists():
self.log("Building Next.js application...") self.log("Building Next.js application...")
subprocess.run( subprocess.run(
["npm", "run", "build"], ["npm", "run", "build"],
cwd=str(frontend_dir), cwd=str(frontend_dir),
check=True check=True,
capture_output=True
) )
def start_backend(self): def start_backend(self):
"""Start FastAPI backend server""" """Start FastAPI backend server using uvicorn directly from project root"""
self.log(f"Starting backend on port {self.backend_port}...") self.log(f"Starting backend on port {self.backend_port}...")
backend_log = self.logs_dir / "backend.log" backend_log = self.logs_dir / "backend.log"
backend_dir = self.root_dir / "backend" backend_dir = self.root_dir / "backend"
env = os.environ.copy() env = os.environ.copy()
env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}" env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}"
# Use uvicorn CLI from venv to run the app
with open(backend_log, 'w') as log_f: with open(backend_log, 'w') as log_f:
self.backend_process = subprocess.Popen( self.backend_process = subprocess.Popen(
[ [
str(self.python_exe), str(self.python_exe),
"-m", "uvicorn", "-m",
"uvicorn",
"main:app", "main:app",
"--host", "0.0.0.0", "--host", "0.0.0.0",
"--port", str(self.backend_port), "--port", str(self.backend_port),
@@ -140,7 +143,8 @@ class StandaloneServerManager:
cwd=str(backend_dir), cwd=str(backend_dir),
stdout=log_f, stdout=log_f,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
env=env env=env,
preexec_fn=os.setsid # Create new process group to avoid signal issues
) )
self.log(f"✓ Backend started (PID: {self.backend_process.pid})") self.log(f"✓ Backend started (PID: {self.backend_process.pid})")
@@ -153,6 +157,7 @@ class StandaloneServerManager:
env = os.environ.copy() env = os.environ.copy()
env['PORT'] = str(self.frontend_port) env['PORT'] = str(self.frontend_port)
env['NODE_ENV'] = 'production'
with open(frontend_log, 'w') as log_f: with open(frontend_log, 'w') as log_f:
self.frontend_process = subprocess.Popen( self.frontend_process = subprocess.Popen(
@@ -160,7 +165,8 @@ class StandaloneServerManager:
cwd=str(frontend_dir), cwd=str(frontend_dir),
stdout=log_f, stdout=log_f,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
env=env env=env,
preexec_fn=os.setsid # Create new process group
) )
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})") self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})")
@@ -205,8 +211,7 @@ class StandaloneServerManager:
print(f" Frontend: tail -f {self.logs_dir / 'frontend.log'}") print(f" Frontend: tail -f {self.logs_dir / 'frontend.log'}")
print(f" All logs: tail -f {self.logs_dir}/*.log") print(f" All logs: tail -f {self.logs_dir}/*.log")
print("\nTo stop servers:") print("\nTo stop servers:")
print(f" kill {self.backend_process.pid} {self.frontend_process.pid}") print(f" Press Ctrl+C in this terminal")
print("\nPress Ctrl+C to stop both servers")
print("="*60 + "\n") print("="*60 + "\n")
def run(self): def run(self):
@@ -225,14 +230,16 @@ class StandaloneServerManager:
self.print_summary() self.print_summary()
# Monitor processes # Monitor processes
self.log("✓ Servers are running!") self.log("✓ Servers are running! Press Ctrl+C to stop.")
while True: while True:
# Check if processes are alive # Check if processes are alive
if self.backend_process.poll() is not None: if self.backend_process.poll() is not None:
self.log(f"⚠ Backend process exited with code {self.backend_process.returncode}") self.log(f"⚠ Backend process exited with code {self.backend_process.returncode}")
self.log(" Check logs/backend.log for details")
if self.frontend_process.poll() is not None: if self.frontend_process.poll() is not None:
self.log(f"⚠ Frontend process exited with code {self.frontend_process.returncode}") self.log(f"⚠ Frontend process exited with code {self.frontend_process.returncode}")
self.log(" Check logs/frontend.log for details")
time.sleep(5) time.sleep(5)
@@ -244,19 +251,27 @@ class StandaloneServerManager:
def cleanup(self): def cleanup(self):
"""Gracefully shutdown servers""" """Gracefully shutdown servers"""
import signal
if self.backend_process: if self.backend_process:
try: try:
self.backend_process.terminate() os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM)
self.backend_process.wait(timeout=5) self.backend_process.wait(timeout=5)
except subprocess.TimeoutExpired: except Exception:
self.backend_process.kill() try:
self.backend_process.kill()
except Exception:
pass
if self.frontend_process: if self.frontend_process:
try: try:
self.frontend_process.terminate() os.killpg(os.getpgid(self.frontend_process.pid), signal.SIGTERM)
self.frontend_process.wait(timeout=5) self.frontend_process.wait(timeout=5)
except subprocess.TimeoutExpired: except Exception:
self.frontend_process.kill() try:
self.frontend_process.kill()
except Exception:
pass
self.log("✓ Servers stopped") self.log("✓ Servers stopped")
sys.exit(0) sys.exit(0)