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:
192
start_servers.py
192
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()
|
||||
|
||||
Reference in New Issue
Block a user