Files
tfm_ainventory/start_servers.py
Daniel Bedeleanu dee8941f15 feat(6): add background mode and lifecycle commands to standalone launcher
New commands:
  start_servers.py            # Foreground (interactive, Ctrl+C to stop)
  start_servers.py start      # Background mode (detached)
  start_servers.py stop       # Stop background servers
  start_servers.py restart    # Stop then start background servers
  start_servers.py status     # Show running servers and PIDs

Features:
  - Process IDs saved to .servers.pid for management
  - Status command shows all running servers and URLs
  - Full argument parsing with help and examples
  - Graceful process group handling for clean shutdown
2026-04-22 19:10:54 +03:00

458 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
"""
TFM aInventory Standalone Server Launcher
Starts both backend (FastAPI) and frontend (Next.js) servers
Manages processes, logs, and graceful shutdown
Usage:
python3 start_servers.py # Start in foreground (interactive)
python3 start_servers.py start # Start in background
python3 start_servers.py stop # Stop background servers
python3 start_servers.py restart # Restart background servers
python3 start_servers.py status # Show server status
"""
import os
import sys
import subprocess
import signal
import time
import argparse
import json
from pathlib import Path
class StandaloneServerManager:
def __init__(self):
self.root_dir = Path(__file__).parent
self.pid_file = self.root_dir / ".servers.pid"
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,
capture_output=True
)
# Build if needed
if not (frontend_dir / ".next" / "standalone").exists():
self.log("Building Next.js application...")
result = subprocess.run(
["npm", "run", "build"],
cwd=str(frontend_dir),
capture_output=False
)
if result.returncode != 0:
self.error(f"Frontend build failed with exit code {result.returncode}")
def start_backend(self):
"""Start FastAPI backend server using uvicorn from project root"""
self.log(f"Starting backend on port {self.backend_port}...")
backend_log = self.logs_dir / "backend.log"
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",
"backend.main:app",
"--host", "0.0.0.0",
"--port", str(self.backend_port),
"--log-level", self.log_level
],
cwd=str(self.root_dir),
stdout=log_f,
stderr=subprocess.STDOUT,
env=env,
preexec_fn=os.setsid
)
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)
env['NODE_ENV'] = 'production'
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,
preexec_fn=os.setsid
)
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})")
def save_pids(self):
"""Save process IDs to file for stop/restart commands"""
pids = {
'backend': self.backend_process.pid if self.backend_process else None,
'frontend': self.frontend_process.pid if self.frontend_process else None,
'timestamp': time.time()
}
with open(self.pid_file, 'w') as f:
json.dump(pids, f)
self.log(f"✓ Process IDs saved to {self.pid_file}")
def load_pids(self):
"""Load process IDs from file"""
if not self.pid_file.exists():
return None
try:
with open(self.pid_file) as f:
return json.load(f)
except Exception:
return None
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" Status: python3 start_servers.py status")
print("\nTo stop servers:")
print(f" python3 start_servers.py stop")
print("="*60 + "\n")
def run_foreground(self):
"""Run servers in foreground (interactive)"""
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()
self.log("✓ Servers are running! Press Ctrl+C to stop.")
while True:
if self.backend_process.poll() is not None:
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:
self.log(f"⚠ Frontend process exited with code {self.frontend_process.returncode}")
self.log(" Check logs/frontend.log for details")
time.sleep(5)
except KeyboardInterrupt:
self.log("Received interrupt signal, shutting down...")
self.cleanup()
except Exception as e:
self.error(str(e))
def run_background(self):
"""Run servers in background (detached)"""
try:
print("\n=== TFM aInventory Standalone Server Start (Background) ===\n")
self.check_ports()
self.setup_backend()
self.setup_frontend()
self.start_backend()
self.start_frontend()
self.save_pids()
self.wait_for_startup()
self.print_summary()
print(f"✓ Servers running in background")
print(f" Stop: python3 start_servers.py stop")
print(f" Status: python3 start_servers.py status\n")
except Exception as e:
self.error(str(e))
def stop_servers(self):
"""Stop background servers"""
pids = self.load_pids()
if not pids:
print("[ERROR] No running servers found")
return False
backend_pid = pids.get('backend')
frontend_pid = pids.get('frontend')
success = True
if backend_pid:
try:
os.killpg(os.getpgid(backend_pid), signal.SIGTERM)
print(f"[INFO] Backend stopped (PID {backend_pid})")
time.sleep(1)
except Exception as e:
print(f"[WARN] Failed to stop backend: {e}")
success = False
if frontend_pid:
try:
os.killpg(os.getpgid(frontend_pid), signal.SIGTERM)
print(f"[INFO] Frontend stopped (PID {frontend_pid})")
time.sleep(1)
except Exception as e:
print(f"[WARN] Failed to stop frontend: {e}")
success = False
if self.pid_file.exists():
self.pid_file.unlink()
print("[INFO] ✓ Servers stopped")
return success
def show_status(self):
"""Show status of running servers"""
pids = self.load_pids()
if not pids:
print("[INFO] No running servers")
return
backend_pid = pids.get('backend')
frontend_pid = pids.get('frontend')
print("\n" + "="*60)
print(" Server Status")
print("="*60 + "\n")
# Check backend
if backend_pid:
try:
os.kill(backend_pid, 0)
print(f"✓ Backend: RUNNING (PID {backend_pid})")
print(f" http://localhost:{self.backend_port}")
except ProcessLookupError:
print(f"✗ Backend: STOPPED (was PID {backend_pid})")
else:
print(f"✗ Backend: NOT FOUND")
# Check frontend
if frontend_pid:
try:
os.kill(frontend_pid, 0)
print(f"✓ Frontend: RUNNING (PID {frontend_pid})")
print(f" http://localhost:{self.frontend_port}")
except ProcessLookupError:
print(f"✗ Frontend: STOPPED (was PID {frontend_pid})")
else:
print(f"✗ Frontend: NOT FOUND")
print("\nLog files:")
print(f" {self.logs_dir / 'backend.log'}")
print(f" {self.logs_dir / 'frontend.log'}")
print("="*60 + "\n")
def cleanup(self):
"""Gracefully shutdown servers"""
if self.backend_process:
try:
os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM)
self.backend_process.wait(timeout=5)
except Exception:
try:
self.backend_process.kill()
except Exception:
pass
if self.frontend_process:
try:
os.killpg(os.getpgid(self.frontend_process.pid), signal.SIGTERM)
self.frontend_process.wait(timeout=5)
except Exception:
try:
self.frontend_process.kill()
except Exception:
pass
if self.pid_file.exists():
self.pid_file.unlink()
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)
def main():
parser = argparse.ArgumentParser(
description='TFM aInventory Standalone Server Launcher',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 start_servers.py # Start in foreground (interactive, Ctrl+C to stop)
python3 start_servers.py start # Start in background
python3 start_servers.py stop # Stop background servers
python3 start_servers.py restart # Restart background servers
python3 start_servers.py status # Show server status
"""
)
parser.add_argument(
'command',
nargs='?',
choices=['start', 'stop', 'restart', 'status'],
default='foreground',
help='Command to execute (default: foreground mode)'
)
args = parser.parse_args()
manager = StandaloneServerManager()
if args.command == 'start':
manager.run_background()
elif args.command == 'stop':
manager.stop_servers()
elif args.command == 'restart':
manager.stop_servers()
time.sleep(2)
manager.run_background()
elif args.command == 'status':
manager.show_status()
else:
# Foreground mode (default)
manager.run_foreground()
if __name__ == "__main__":
main()