diff --git a/start_servers.py b/start_servers.py index f3c61bb0..f133b16f 100755 --- a/start_servers.py +++ b/start_servers.py @@ -3,6 +3,13 @@ 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 @@ -10,12 +17,15 @@ 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() @@ -128,7 +138,6 @@ class StandaloneServerManager: env = os.environ.copy() env['PATH'] = f"{self.python_exe.parent}:{env['PATH']}" - # Use uvicorn CLI from project root with full module path (backend.main:app) with open(backend_log, 'w') as log_f: self.backend_process = subprocess.Popen( [ @@ -144,7 +153,7 @@ class StandaloneServerManager: stdout=log_f, stderr=subprocess.STDOUT, env=env, - preexec_fn=os.setsid # Create new process group to avoid signal issues + preexec_fn=os.setsid ) self.log(f"✓ Backend started (PID: {self.backend_process.pid})") @@ -166,11 +175,32 @@ class StandaloneServerManager: stdout=log_f, stderr=subprocess.STDOUT, env=env, - preexec_fn=os.setsid # Create new process group + 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) @@ -209,13 +239,13 @@ class StandaloneServerManager: 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(f" Status: python3 start_servers.py status") print("\nTo stop servers:") - print(f" Press Ctrl+C in this terminal") + print(f" python3 start_servers.py stop") print("="*60 + "\n") - def run(self): - """Run the server manager""" + def run_foreground(self): + """Run servers in foreground (interactive)""" try: print("\n=== TFM aInventory Standalone Server Start ===\n") @@ -229,10 +259,8 @@ class StandaloneServerManager: self.wait_for_startup() self.print_summary() - # Monitor processes self.log("✓ Servers are running! Press Ctrl+C to stop.") 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}") self.log(" Check logs/backend.log for details") @@ -249,10 +277,107 @@ class StandaloneServerManager: 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""" - import signal - if self.backend_process: try: os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM) @@ -273,6 +398,9 @@ class StandaloneServerManager: except Exception: pass + if self.pid_file.exists(): + self.pid_file.unlink() + self.log("✓ Servers stopped") sys.exit(0) @@ -286,6 +414,44 @@ class StandaloneServerManager: sys.exit(1) -if __name__ == "__main__": +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() - manager.run() + + 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()