feat(6): create python standalone server launcher
- Replaces bash script with robust Python implementation - Simpler process management using subprocess module - Better error handling and logging - Manages venv, dependencies, and both services - Handles graceful shutdown on Ctrl+C
This commit is contained in:
276
start_servers.py
Executable file
276
start_servers.py
Executable file
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TFM aInventory Standalone Server Launcher
|
||||
Starts both backend (FastAPI) and frontend (Next.js) servers
|
||||
Manages processes, logs, and graceful shutdown
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
import time
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class StandaloneServerManager:
|
||||
def __init__(self):
|
||||
self.root_dir = Path(__file__).parent
|
||||
self.backend_process = None
|
||||
self.frontend_process = None
|
||||
self.load_config()
|
||||
self.setup_directories()
|
||||
|
||||
def load_config(self):
|
||||
"""Load configuration from inventory.env"""
|
||||
env_file = self.root_dir / "inventory.env"
|
||||
if not env_file.exists():
|
||||
self.error(f"inventory.env not found at {env_file}")
|
||||
|
||||
self.config = {}
|
||||
with open(env_file) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
self.config[key.strip()] = value.strip().strip('"').strip("'")
|
||||
|
||||
self.backend_port = int(self.config.get('BACKEND_PORT', 8000))
|
||||
self.frontend_port = int(self.config.get('FRONTEND_PORT', 3000))
|
||||
self.data_dir = self.root_dir / self.config.get('DATA_DIR', 'data')
|
||||
self.logs_dir = self.root_dir / self.config.get('LOGS_DIR', 'logs')
|
||||
self.log_level = self.config.get('LOG_LEVEL', 'INFO').lower()
|
||||
|
||||
self.log(f"✓ Configuration loaded (Backend: {self.backend_port}, Frontend: {self.frontend_port})")
|
||||
|
||||
def setup_directories(self):
|
||||
"""Create necessary directories"""
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
||||
(self.data_dir / "temp").mkdir(parents=True, exist_ok=True)
|
||||
self.log(f"✓ Directories ready")
|
||||
|
||||
def check_ports(self):
|
||||
"""Check if required ports are available"""
|
||||
import socket
|
||||
for port in [self.backend_port, self.frontend_port]:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex(('127.0.0.1', port))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
self.error(f"Port {port} is already in use")
|
||||
except Exception as e:
|
||||
self.log(f"⚠ Could not check port {port}: {e}")
|
||||
self.log(f"✓ Required ports available")
|
||||
|
||||
def setup_backend(self):
|
||||
"""Setup Python virtual environment and dependencies"""
|
||||
venv_dir = self.root_dir / ".venv"
|
||||
backend_dir = self.root_dir / "backend"
|
||||
|
||||
# Create venv if needed
|
||||
if not venv_dir.exists():
|
||||
self.log("Creating Python virtual environment...")
|
||||
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
|
||||
|
||||
# Install dependencies
|
||||
pip_exe = venv_dir / "bin" / "pip"
|
||||
python_exe = venv_dir / "bin" / "python"
|
||||
requirements = backend_dir / "requirements.txt"
|
||||
|
||||
if not python_exe.exists():
|
||||
self.error(f"Virtual environment failed - python not found at {python_exe}")
|
||||
|
||||
# Check if fastapi is installed
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(python_exe), "-c", "import fastapi"],
|
||||
check=True,
|
||||
capture_output=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
self.log("Installing Python dependencies...")
|
||||
subprocess.run([str(pip_exe), "install", "-q", "-r", str(requirements)], check=True)
|
||||
|
||||
self.python_exe = python_exe
|
||||
|
||||
def setup_frontend(self):
|
||||
"""Setup frontend dependencies"""
|
||||
frontend_dir = self.root_dir / "frontend"
|
||||
|
||||
# Install dependencies if needed
|
||||
if not (frontend_dir / "node_modules").exists():
|
||||
self.log("Installing npm dependencies...")
|
||||
subprocess.run(
|
||||
["npm", "ci", "--silent"],
|
||||
cwd=str(frontend_dir),
|
||||
check=True
|
||||
)
|
||||
|
||||
# Build if needed
|
||||
if not (frontend_dir / ".next").exists():
|
||||
self.log("Building Next.js application...")
|
||||
subprocess.run(
|
||||
["npm", "run", "build"],
|
||||
cwd=str(frontend_dir),
|
||||
check=True
|
||||
)
|
||||
|
||||
def start_backend(self):
|
||||
"""Start FastAPI backend server"""
|
||||
self.log(f"Starting backend on port {self.backend_port}...")
|
||||
backend_log = self.logs_dir / "backend.log"
|
||||
|
||||
backend_dir = self.root_dir / "backend"
|
||||
env = os.environ.copy()
|
||||
env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}"
|
||||
|
||||
with open(backend_log, 'w') as log_f:
|
||||
self.backend_process = subprocess.Popen(
|
||||
[
|
||||
str(self.python_exe),
|
||||
"-m", "uvicorn",
|
||||
"main:app",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", str(self.backend_port),
|
||||
"--log-level", self.log_level
|
||||
],
|
||||
cwd=str(backend_dir),
|
||||
stdout=log_f,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env
|
||||
)
|
||||
|
||||
self.log(f"✓ Backend started (PID: {self.backend_process.pid})")
|
||||
|
||||
def start_frontend(self):
|
||||
"""Start Next.js frontend server"""
|
||||
self.log(f"Starting frontend on port {self.frontend_port}...")
|
||||
frontend_log = self.logs_dir / "frontend.log"
|
||||
frontend_dir = self.root_dir / "frontend"
|
||||
|
||||
env = os.environ.copy()
|
||||
env['PORT'] = str(self.frontend_port)
|
||||
|
||||
with open(frontend_log, 'w') as log_f:
|
||||
self.frontend_process = subprocess.Popen(
|
||||
["node", ".next/standalone/server.js"],
|
||||
cwd=str(frontend_dir),
|
||||
stdout=log_f,
|
||||
stderr=subprocess.STDOUT,
|
||||
env=env
|
||||
)
|
||||
|
||||
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})")
|
||||
|
||||
def wait_for_startup(self):
|
||||
"""Wait a bit and check if servers started"""
|
||||
time.sleep(3)
|
||||
|
||||
# Check backend
|
||||
try:
|
||||
import urllib.request
|
||||
urllib.request.urlopen(f"http://localhost:{self.backend_port}/health", timeout=2)
|
||||
self.log(f"✓ Backend responding at http://localhost:{self.backend_port}/health")
|
||||
except Exception:
|
||||
self.log(f"⚠ Backend not responding yet (may still be initializing)")
|
||||
|
||||
# Check frontend
|
||||
try:
|
||||
import urllib.request
|
||||
urllib.request.urlopen(f"http://localhost:{self.frontend_port}/", timeout=2)
|
||||
self.log(f"✓ Frontend responding at http://localhost:{self.frontend_port}")
|
||||
except Exception:
|
||||
self.log(f"⚠ Frontend not responding yet (may still be initializing)")
|
||||
|
||||
def print_summary(self):
|
||||
"""Print summary of running servers"""
|
||||
print("\n" + "="*60)
|
||||
print(" ✓ Standalone servers started successfully!")
|
||||
print("="*60 + "\n")
|
||||
print("Access points:")
|
||||
print(f" Frontend: http://localhost:{self.frontend_port}")
|
||||
print(f" Backend: http://localhost:{self.backend_port}")
|
||||
print(f" API Docs: http://localhost:{self.backend_port}/docs")
|
||||
print("\nProcess IDs:")
|
||||
print(f" Backend: {self.backend_process.pid}")
|
||||
print(f" Frontend: {self.frontend_process.pid}")
|
||||
print("\nLog files:")
|
||||
print(f" Backend: {self.logs_dir / 'backend.log'}")
|
||||
print(f" Frontend: {self.logs_dir / 'frontend.log'}")
|
||||
print("\nMonitoring:")
|
||||
print(f" Backend: tail -f {self.logs_dir / 'backend.log'}")
|
||||
print(f" Frontend: tail -f {self.logs_dir / 'frontend.log'}")
|
||||
print(f" All logs: tail -f {self.logs_dir}/*.log")
|
||||
print("\nTo stop servers:")
|
||||
print(f" kill {self.backend_process.pid} {self.frontend_process.pid}")
|
||||
print("\nPress Ctrl+C to stop both servers")
|
||||
print("="*60 + "\n")
|
||||
|
||||
def run(self):
|
||||
"""Run the server manager"""
|
||||
try:
|
||||
print("\n=== TFM aInventory Standalone Server Start ===\n")
|
||||
|
||||
self.check_ports()
|
||||
self.setup_backend()
|
||||
self.setup_frontend()
|
||||
|
||||
self.start_backend()
|
||||
self.start_frontend()
|
||||
|
||||
self.wait_for_startup()
|
||||
self.print_summary()
|
||||
|
||||
# Monitor processes
|
||||
self.log("✓ Servers are running!")
|
||||
while True:
|
||||
# Check if processes are alive
|
||||
if self.backend_process.poll() is not None:
|
||||
self.log(f"⚠ Backend process exited with code {self.backend_process.returncode}")
|
||||
|
||||
if self.frontend_process.poll() is not None:
|
||||
self.log(f"⚠ Frontend process exited with code {self.frontend_process.returncode}")
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.log("Received interrupt signal, shutting down...")
|
||||
self.cleanup()
|
||||
except Exception as e:
|
||||
self.error(str(e))
|
||||
|
||||
def cleanup(self):
|
||||
"""Gracefully shutdown servers"""
|
||||
if self.backend_process:
|
||||
try:
|
||||
self.backend_process.terminate()
|
||||
self.backend_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.backend_process.kill()
|
||||
|
||||
if self.frontend_process:
|
||||
try:
|
||||
self.frontend_process.terminate()
|
||||
self.frontend_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.frontend_process.kill()
|
||||
|
||||
self.log("✓ Servers stopped")
|
||||
sys.exit(0)
|
||||
|
||||
def log(self, message):
|
||||
"""Print log message"""
|
||||
print(f"[INFO] {message}")
|
||||
|
||||
def error(self, message):
|
||||
"""Print error and exit"""
|
||||
print(f"[ERROR] {message}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager = StandaloneServerManager()
|
||||
manager.run()
|
||||
Reference in New Issue
Block a user