feat(07-03): convert run_standalone.sh to Python with YAML config parsing
This commit is contained in:
216
scripts/run_standalone.py
Executable file
216
scripts/run_standalone.py
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TFM aInventory - Standalone Launcher (v1.12.0)
|
||||
Converted from run_standalone.sh to Python per Decision D-05.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import argparse
|
||||
import subprocess
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import socket
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
# Color codes
|
||||
class Colors:
|
||||
RED = '\033[0;31m'
|
||||
GREEN = '\033[0;32m'
|
||||
YELLOW = '\033[1;33m'
|
||||
BLUE = '\033[0;34m'
|
||||
MAGENTA = '\033[0;35m'
|
||||
CYAN = '\033[0;36m'
|
||||
NC = '\033[0m'
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO, format=f"{Colors.BLUE}[INFO]{Colors.NC} %(message)s")
|
||||
logger = logging.getLogger("run_standalone")
|
||||
|
||||
processes: List[subprocess.Popen] = []
|
||||
shutdown_event = threading.Event()
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
logger.info("Shutdown signal received. Stopping processes...")
|
||||
shutdown_event.set()
|
||||
for p in processes:
|
||||
try:
|
||||
p.terminate()
|
||||
except:
|
||||
pass
|
||||
# Give them a moment to terminate gracefully
|
||||
time.sleep(1)
|
||||
for p in processes:
|
||||
try:
|
||||
p.kill()
|
||||
except:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
def load_yaml(file_path: str) -> Dict[str, Any]:
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Config file {file_path} not found. Using defaults.")
|
||||
return {}
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse {file_path}: {e}")
|
||||
return {}
|
||||
|
||||
def is_port_in_use(port: int) -> bool:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(('localhost', port)) == 0
|
||||
|
||||
def log_stream(stream, prefix, color):
|
||||
for line in iter(stream.readline, ''):
|
||||
if shutdown_event.is_set():
|
||||
break
|
||||
if line:
|
||||
print(f"{color}[{prefix}]{Colors.NC} {line.strip()}")
|
||||
stream.close()
|
||||
|
||||
def start_backend(backend_port: int, backend_cfg: Dict[str, Any]):
|
||||
logger.info(f"Starting Backend on port {backend_port}...")
|
||||
|
||||
app_cfg = backend_cfg.get("application", {})
|
||||
env = os.environ.copy()
|
||||
env["DATA_DIR"] = app_cfg.get("data_dir", "./data")
|
||||
env["LOGS_DIR"] = app_cfg.get("logs_dir", "./logs")
|
||||
env["LOG_LEVEL"] = backend_cfg.get("logging", {}).get("log_level", "INFO")
|
||||
|
||||
# Ensure directories exist
|
||||
os.makedirs(env["DATA_DIR"], exist_ok=True)
|
||||
os.makedirs(env["LOGS_DIR"], exist_ok=True)
|
||||
|
||||
cmd = [
|
||||
sys.executable, "-m", "uvicorn",
|
||||
"backend.main:app",
|
||||
"--host", "0.0.0.0",
|
||||
"--port", str(backend_port)
|
||||
]
|
||||
|
||||
# Add --reload if in dev mode (detectable via environment or flag,
|
||||
# but the script is for 'run_standalone' which might be used for prod too)
|
||||
# For now, let's keep it simple.
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
)
|
||||
processes.append(p)
|
||||
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "BACKEND", Colors.GREEN), daemon=True).start()
|
||||
|
||||
return p
|
||||
except FileNotFoundError:
|
||||
logger.error("uvicorn not found. Please install it with: pip install uvicorn")
|
||||
sys.exit(1)
|
||||
|
||||
def start_frontend(frontend_port: int, frontend_cfg: Dict[str, Any]):
|
||||
logger.info(f"Starting Frontend on port {frontend_port}...")
|
||||
|
||||
api_cfg = frontend_cfg.get("api", {})
|
||||
env = os.environ.copy()
|
||||
env["NEXT_PUBLIC_API_URL"] = api_cfg.get("backend_url", "http://localhost:8916")
|
||||
env["PORT"] = str(frontend_port)
|
||||
|
||||
# Check if we should use 'npm run dev' or 'node server.js'
|
||||
# For standalone, we might prefer 'npm run start' if built, or 'npm run dev'
|
||||
# Bash script used 'npm run start'
|
||||
|
||||
cmd = ["npm", "run", "dev"]
|
||||
if os.path.exists("frontend/server.js"):
|
||||
# If built for production
|
||||
cmd = ["node", "frontend/server.js"]
|
||||
|
||||
try:
|
||||
p = subprocess.Popen(
|
||||
cmd,
|
||||
cwd="frontend" if os.path.isdir("frontend") else None,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
env=env,
|
||||
bufsize=1,
|
||||
universal_newlines=True
|
||||
)
|
||||
processes.append(p)
|
||||
|
||||
threading.Thread(target=log_stream, args=(p.stdout, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
threading.Thread(target=log_stream, args=(p.stderr, "FRONTEND", Colors.MAGENTA), daemon=True).start()
|
||||
|
||||
return p
|
||||
except FileNotFoundError:
|
||||
logger.error("npm or node not found. Please install Node.js")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="TFM aInventory Standalone Launcher")
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--backend-only", action="store_true", help="Start only the backend")
|
||||
group.add_argument("--frontend-only", action="store_true", help="Start only the frontend")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load all configs to find ports and settings
|
||||
backend_cfg = load_yaml("config/backend.yaml")
|
||||
frontend_cfg = load_yaml("config/frontend.yaml")
|
||||
network_cfg = load_yaml("config/network.yaml")
|
||||
|
||||
ports_cfg = network_cfg.get("ports", {})
|
||||
backend_port = ports_cfg.get("backend_port", 8916)
|
||||
frontend_port = ports_cfg.get("frontend_port", 8917)
|
||||
|
||||
if not args.frontend_only:
|
||||
if is_port_in_use(backend_port):
|
||||
logger.error(f"Port {backend_port} is already in use. Cannot start backend.")
|
||||
if not args.backend_only:
|
||||
# If we are supposed to start both, we fail.
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_backend(backend_port, backend_cfg)
|
||||
|
||||
if not args.backend_only:
|
||||
if is_port_in_use(frontend_port):
|
||||
logger.error(f"Port {frontend_port} is already in use. Cannot start frontend.")
|
||||
if not args.frontend_only:
|
||||
sys.exit(1)
|
||||
else:
|
||||
start_frontend(frontend_port, frontend_cfg)
|
||||
|
||||
if not processes:
|
||||
logger.error("No processes started. Check configuration and port availability.")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("All services started. Press Ctrl+C to stop.")
|
||||
|
||||
# Monitor processes
|
||||
try:
|
||||
while not shutdown_event.is_set():
|
||||
time.sleep(1)
|
||||
for p in processes:
|
||||
if p.poll() is not None:
|
||||
logger.error(f"Process {p.pid} exited with code {p.returncode}")
|
||||
shutdown_event.set()
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
signal_handler(None, None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user