#!/usr/bin/env python3 """ TFM aInventory Standalone Server Launcher with SSL Support Starts backend (FastAPI), frontend (Next.js), and Caddy reverse proxy Full production-equivalent to Docker Compose deployment 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.caddyfile = self.root_dir / "Caddyfile.standalone" self.backend_process = None self.frontend_process = None self.caddy_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.backend_ssl_port = int(self.config.get('BACKEND_SSL_PORT', 8918)) self.frontend_ssl_port = int(self.config.get('FRONTEND_SSL_PORT', 8919)) 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") self.log(f" Backend HTTP: {self.backend_port}, HTTPS: {self.backend_ssl_port}") self.log(f" Frontend HTTP: {self.frontend_port}, HTTPS: {self.frontend_ssl_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.logs_dir / "caddy").mkdir(parents=True, exist_ok=True) self.log(f"✓ Directories ready") def check_caddy(self): """Check if Caddy is installed""" try: subprocess.run( ["caddy", "version"], capture_output=True, check=True, timeout=5 ) self.log("✓ Caddy is installed") return True except (subprocess.CalledProcessError, FileNotFoundError): self.log("⚠ Caddy not found - installing...") return self.install_caddy() def install_caddy(self): """Install Caddy using system package manager""" try: # Try apt (Debian/Ubuntu) subprocess.run( ["apt-get", "update"], capture_output=True, timeout=60 ) subprocess.run( ["apt-get", "install", "-y", "caddy"], capture_output=True, timeout=120, check=True ) self.log("✓ Caddy installed successfully") return True except Exception as e: self.log(f"⚠ Failed to install Caddy: {e}") self.log(" Install Caddy manually: https://caddyserver.com/docs/install") return False def check_ports(self): """Check if all required ports are available""" import socket ports = [ (self.backend_port, "Backend HTTP"), (self.frontend_port, "Frontend HTTP"), (self.backend_ssl_port, "Backend HTTPS"), (self.frontend_ssl_port, "Frontend HTTPS") ] for port, name in ports: 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} ({name}) 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" if not venv_dir.exists(): self.log("Creating Python virtual environment...") subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True) 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}") 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" 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 ) if not (frontend_dir / ".next" / "standalone").exists(): # Generate CSS variables from colors.mjs before building self.log("Generating CSS variables from design system...") result = subprocess.run( ["node", "scripts/generate-css-vars.mjs"], cwd=str(frontend_dir), capture_output=True ) if result.returncode != 0: self.log(f"⚠ CSS variable generation warning: {result.stderr.decode()}") 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}") # Copy static files to standalone directory (required for Next.js standalone mode) static_src = frontend_dir / ".next" / "static" static_dst = frontend_dir / ".next" / "standalone" / ".next" / "static" if static_src.exists() and not static_dst.exists(): self.log("Copying static files to standalone build...") import shutil shutil.copytree(static_src, static_dst) 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" 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 start_caddy(self): """Start Caddy reverse proxy with SSL""" if not self.caddyfile.exists(): self.error(f"Caddyfile not found: {self.caddyfile}") self.log(f"Starting Caddy reverse proxy...") caddy_log = self.logs_dir / "caddy.log" with open(caddy_log, 'w') as log_f: self.caddy_process = subprocess.Popen( [ "caddy", "run", "--config", str(self.caddyfile), "--adapter", "caddyfile" ], stdout=log_f, stderr=subprocess.STDOUT, preexec_fn=os.setsid ) self.log(f"✓ Caddy started (PID: {self.caddy_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, 'caddy': self.caddy_process.pid if self.caddy_process else None, 'timestamp': time.time() } with open(self.pid_file, 'w') as f: json.dump(pids, f) self.log(f"✓ Process IDs saved") 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 and check if servers started""" time.sleep(3) try: import urllib.request urllib.request.urlopen(f"http://localhost:{self.backend_port}/health", timeout=2) self.log(f"✓ Backend responding") except Exception: self.log(f"⚠ Backend not responding yet") try: import urllib.request urllib.request.urlopen(f"http://localhost:{self.frontend_port}/", timeout=2) self.log(f"✓ Frontend responding") except Exception: self.log(f"⚠ Frontend not responding yet") def print_summary(self): """Print summary of running servers""" print("\n" + "="*70) print(" ✓ TFM aInventory Standalone Deployment Started Successfully!") print("="*70 + "\n") print("Access points:") print(f" Frontend HTTP: http://localhost:{self.frontend_port}") print(f" Frontend HTTPS: https://localhost:{self.frontend_ssl_port} (self-signed)") print(f" Backend HTTP: http://localhost:{self.backend_port}") print(f" Backend HTTPS: https://localhost:{self.backend_ssl_port} (self-signed)") 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(f" Caddy (SSL): {self.caddy_process.pid}") print("\nLog files:") print(f" {self.logs_dir / 'backend.log'}") print(f" {self.logs_dir / 'frontend.log'}") print(f" {self.logs_dir / 'caddy.log'}") print("\nMonitoring:") print(f" Status: python3 start_servers.py status") print(f" Logs: tail -f {self.logs_dir}/*.log") print("\nTo stop servers:") print(f" python3 start_servers.py stop") print("="*70 + "\n") def run_foreground(self): """Run servers in foreground (interactive)""" try: print("\n=== TFM aInventory Standalone Deployment (Foreground) ===\n") self.check_ports() self.setup_backend() self.setup_frontend() if not self.check_caddy(): self.log("⚠ Caddy not available - HTTPS disabled") self.start_backend() self.start_frontend() if self.caddy_process is None: self.start_caddy() self.wait_for_startup() self.print_summary() self.log("✓ All servers running! Press Ctrl+C to stop.") while True: if self.backend_process and self.backend_process.poll() is not None: self.log(f"⚠ Backend exited (code {self.backend_process.returncode})") if self.frontend_process and self.frontend_process.poll() is not None: self.log(f"⚠ Frontend exited (code {self.frontend_process.returncode})") if self.caddy_process and self.caddy_process.poll() is not None: self.log(f"⚠ Caddy exited (code {self.caddy_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 run_background(self): """Run servers in background (detached)""" try: print("\n=== TFM aInventory Standalone Deployment (Background) ===\n") self.check_ports() self.setup_backend() self.setup_frontend() if not self.check_caddy(): self.log("⚠ Caddy not available - HTTPS disabled") self.start_backend() self.start_frontend() if self.caddy_process is None: self.start_caddy() 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 success = True for service, pid in [('Backend', pids.get('backend')), ('Frontend', pids.get('frontend')), ('Caddy', pids.get('caddy'))]: if pid: try: os.killpg(os.getpgid(pid), signal.SIGTERM) print(f"[INFO] {service} stopped (PID {pid})") time.sleep(1) except Exception as e: print(f"[WARN] Failed to stop {service}: {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 print("\n" + "="*70) print(" Server Status") print("="*70 + "\n") for service, pid, port, ssl_port in [ ('Backend', pids.get('backend'), self.backend_port, self.backend_ssl_port), ('Frontend', pids.get('frontend'), self.frontend_port, self.frontend_ssl_port), ('Caddy SSL', pids.get('caddy'), None, None) ]: if pid: try: os.kill(pid, 0) status = "✓ RUNNING" if port: print(f"{status}: {service} (PID {pid})") print(f" HTTP: http://localhost:{port}") print(f" HTTPS: https://localhost:{ssl_port}") else: print(f"{status}: {service} (PID {pid})") except ProcessLookupError: print(f"✗ STOPPED: {service} (was PID {pid})") else: print(f"✗ NOT FOUND: {service}") print("\nLog files:") print(f" {self.logs_dir / 'backend.log'}") print(f" {self.logs_dir / 'frontend.log'}") print(f" {self.logs_dir / 'caddy.log'}") print("="*70 + "\n") def cleanup(self): """Gracefully shutdown servers""" for process in [self.backend_process, self.frontend_process, self.caddy_process]: if process: try: os.killpg(os.getpgid(process.pid), signal.SIGTERM) process.wait(timeout=5) except Exception: try: 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 Deployment with SSL Support', 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 Features: - Backend + Frontend + Caddy reverse proxy with SSL - HTTP and HTTPS access - Process management with PID file - Self-signed certificates (development-friendly) """ ) parser.add_argument( 'command', nargs='?', choices=['start', 'stop', 'restart', 'status'], default=None, 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() elif args.command is None: manager.run_foreground() else: manager.run_foreground() if __name__ == "__main__": main()