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
This commit is contained in:
2026-04-22 19:10:54 +03:00
parent ce9fd32f68
commit dee8941f15

View File

@@ -3,6 +3,13 @@
TFM aInventory Standalone Server Launcher TFM aInventory Standalone Server Launcher
Starts both backend (FastAPI) and frontend (Next.js) servers Starts both backend (FastAPI) and frontend (Next.js) servers
Manages processes, logs, and graceful shutdown 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 os
@@ -10,12 +17,15 @@ import sys
import subprocess import subprocess
import signal import signal
import time import time
import argparse
import json
from pathlib import Path from pathlib import Path
class StandaloneServerManager: class StandaloneServerManager:
def __init__(self): def __init__(self):
self.root_dir = Path(__file__).parent self.root_dir = Path(__file__).parent
self.pid_file = self.root_dir / ".servers.pid"
self.backend_process = None self.backend_process = None
self.frontend_process = None self.frontend_process = None
self.load_config() self.load_config()
@@ -128,7 +138,6 @@ class StandaloneServerManager:
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 project root with full module path (backend.main: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(
[ [
@@ -144,7 +153,7 @@ class StandaloneServerManager:
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 preexec_fn=os.setsid
) )
self.log(f"✓ Backend started (PID: {self.backend_process.pid})") self.log(f"✓ Backend started (PID: {self.backend_process.pid})")
@@ -166,11 +175,32 @@ class StandaloneServerManager:
stdout=log_f, stdout=log_f,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
env=env, env=env,
preexec_fn=os.setsid # Create new process group preexec_fn=os.setsid
) )
self.log(f"✓ Frontend started (PID: {self.frontend_process.pid})") 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): def wait_for_startup(self):
"""Wait a bit and check if servers started""" """Wait a bit and check if servers started"""
time.sleep(3) time.sleep(3)
@@ -209,13 +239,13 @@ class StandaloneServerManager:
print("\nMonitoring:") print("\nMonitoring:")
print(f" Backend: tail -f {self.logs_dir / 'backend.log'}") print(f" Backend: tail -f {self.logs_dir / 'backend.log'}")
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" Status: python3 start_servers.py status")
print("\nTo stop servers:") print("\nTo stop servers:")
print(f" Press Ctrl+C in this terminal") print(f" python3 start_servers.py stop")
print("="*60 + "\n") print("="*60 + "\n")
def run(self): def run_foreground(self):
"""Run the server manager""" """Run servers in foreground (interactive)"""
try: try:
print("\n=== TFM aInventory Standalone Server Start ===\n") print("\n=== TFM aInventory Standalone Server Start ===\n")
@@ -229,10 +259,8 @@ class StandaloneServerManager:
self.wait_for_startup() self.wait_for_startup()
self.print_summary() self.print_summary()
# Monitor processes
self.log("✓ Servers are running! Press Ctrl+C to stop.") self.log("✓ Servers are running! Press Ctrl+C to stop.")
while True: while True:
# 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") self.log(" Check logs/backend.log for details")
@@ -249,10 +277,107 @@ class StandaloneServerManager:
except Exception as e: except Exception as e:
self.error(str(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): def cleanup(self):
"""Gracefully shutdown servers""" """Gracefully shutdown servers"""
import signal
if self.backend_process: if self.backend_process:
try: try:
os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM) os.killpg(os.getpgid(self.backend_process.pid), signal.SIGTERM)
@@ -273,6 +398,9 @@ class StandaloneServerManager:
except Exception: except Exception:
pass pass
if self.pid_file.exists():
self.pid_file.unlink()
self.log("✓ Servers stopped") self.log("✓ Servers stopped")
sys.exit(0) sys.exit(0)
@@ -286,6 +414,44 @@ class StandaloneServerManager:
sys.exit(1) 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 = 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()