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
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()