Newer
Older
cortex-hub / agent-node / install_service.py
#!/usr/bin/env python3
"""
Cortex Agent Node - Daemon Installer
====================================
Configures the Cortex Agent to run automatically as a background daemon
on macOS (launchd) or Linux (systemd).

Usage:
    python3 install_service.py
"""

import os
import sys
import platform
import subprocess

def get_python_path():
    return sys.executable

def get_agent_main_path():
    return os.path.abspath(os.path.join(os.path.dirname(__file__), "src", "agent_node", "main.py"))

def get_working_dir():
    return os.path.abspath(os.path.dirname(__file__))

def install_mac_launchd():
    print("Installing macOS launchd service...")
    
    plist_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.jerxie.cortex.agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>{get_python_path()}</string>
        <string>{get_agent_main_path()}</string>
    </array>
    <key>WorkingDirectory</key>
    <string>{get_working_dir()}</string>
    <key>KeepAlive</key>
    <true/>
    <key>RunAtLoad</key>
    <true/>
    <key>StandardErrorPath</key>
    <string>{os.path.expanduser("~")}/.cortex/agent.err.log</string>
    <key>StandardOutPath</key>
    <string>{os.path.expanduser("~")}/.cortex/agent.out.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>GRPC_ENABLE_FORK_SUPPORT</key>
        <string>1</string>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    </dict>
</dict>
</plist>
"""
    agents_dir = os.path.expanduser("~/Library/LaunchAgents")
    os.makedirs(agents_dir, exist_ok=True)
    os.makedirs(os.path.expanduser("~/.cortex"), exist_ok=True)
    
    plist_path = os.path.join(agents_dir, "com.jerxie.cortex.agent.plist")
    
    with open(plist_path, "w") as f:
        f.write(plist_content)
        
    print(f"Created plist at {plist_path}")
    
    try:
        # Unload if exists
        subprocess.run(["launchctl", "unload", plist_path], capture_output=True)
        # Load new
        subprocess.run(["launchctl", "load", plist_path], check=True)
        print("✅ macOS Daemon successfully started!")
        print(f"Logs: ~/.cortex/agent.out.log and ~/.cortex/agent.err.log")
        print("Commands:")
        print(f"  Stop: launchctl unload {plist_path}")
        print(f"  Start: launchctl load {plist_path}")
    except subprocess.CalledProcessError as e:
        print(f"❌ Failed to load launchd service: {e}")

def _is_systemd_available():
    try:
        # Check if systemd is running (PID 1)
        with open("/proc/1/comm", "r") as f:
            if "systemd" in f.read():
                # Even if systemd is PID 1, --user might fail if no session D-Bus
                r = subprocess.run(["systemctl", "--user", "list-units"], capture_output=True)
                return r.returncode == 0
    except:
        pass
    return False

def install_linux_background_loop():
    print("Systemd not available or refusing connection. Falling back to background loop (nohup)...")
    
    # Create a small control script to manage the background process
    ctl_script = os.path.join(get_working_dir(), "cortex-ctl")
    
    script_content = f"""#!/bin/sh
# Cortex Agent Control Script (Background-loop mode)
PIDFILE="{get_working_dir()}/agent.pid"
LOGFILE="{os.path.expanduser("~")}/.cortex/agent.out.log"

case "$1" in
  start)
    if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
        echo "Agent is already running (PID $(cat $PIDFILE))"
        exit 0
    fi
    echo "Starting Cortex Agent..."
    mkdir -p "$(dirname "$LOGFILE")"
    cd "{get_working_dir()}"
    export GRPC_ENABLE_FORK_SUPPORT=1
    nohup {get_python_path()} {get_agent_main_path()} >> "$LOGFILE" 2>&1 &
    echo $! > "$PIDFILE"
    echo "Agent started (PID $!)"
    ;;
  stop)
    if [ -f "$PIDFILE" ]; then
        echo "Stopping Cortex Agent (PID $(cat $PIDFILE))..."
        kill $(cat "$PIDFILE")
        rm "$PIDFILE"
    else
        echo "Agent is not running"
    fi
    ;;
  status)
    if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
        echo "Agent is RUNNING (PID $(cat $PIDFILE))"
    else
        echo "Agent is STOPPED"
    fi
    ;;
  logs)
    tail -f "$LOGFILE"
    ;;
  *)
    echo "Usage: $0 {{start|stop|status|logs}}"
    exit 1
esac
"""
    with open(ctl_script, "w") as f:
        f.write(script_content)
    os.chmod(ctl_script, 0o755)
    
    # Start it
    try:
        subprocess.run([ctl_script, "start"], check=True)
        print("✅ Linux Background Service successfully started!")
        print(f"Management script created at: {ctl_script}")
        print(f"Use '{ctl_script} status' to check.")
    except Exception as e:
        print(f"❌ Failed to start background loop: {e}")

def install_linux_systemd():
    if not _is_systemd_available():
        install_linux_background_loop()
        return

    print("Installing Linux systemd user service...")
    
    service_content = f"""[Unit]
Description=Cortex Agent Node
After=network.target

[Service]
Type=simple
ExecStart={get_python_path()} {get_agent_main_path()}
WorkingDirectory={get_working_dir()}
Restart=always
RestartSec=5
Environment=GRPC_ENABLE_FORK_SUPPORT=1
StandardOutput=append:{os.path.expanduser("~")}/.cortex/agent.out.log
StandardError=append:{os.path.expanduser("~")}/.cortex/agent.err.log

[Install]
WantedBy=default.target
"""
    
    systemd_dir = os.path.expanduser("~/.config/systemd/user")
    os.makedirs(systemd_dir, exist_ok=True)
    os.makedirs(os.path.expanduser("~/.cortex"), exist_ok=True)
    
    service_path = os.path.join(systemd_dir, "cortex-agent.service")
    
    with open(service_path, "w") as f:
        f.write(service_content)
        
    print(f"Created systemd service at {service_path}")
    
    try:
        subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
        subprocess.run(["systemctl", "--user", "enable", "cortex-agent"], check=True)
        subprocess.run(["systemctl", "--user", "restart", "cortex-agent"], check=True)
        
        # Ensure user services run even when not logged in
        subprocess.run(["loginctl", "enable-linger", os.environ.get("USER", "root")], capture_output=True)
        
        print("✅ Linux Daemon successfully started!")
        print(f"Logs: ~/.cortex/agent.out.log and ~/.cortex/agent.err.log")
        print("Commands:")
        print("  Status: systemctl --user status cortex-agent")
        print("  Stop: systemctl --user stop cortex-agent")
        print("  Start: systemctl --user start cortex-agent")
        print("  Logs: journalctl --user -u cortex-agent -f")
    except subprocess.CalledProcessError as e:
        print(f"❌ Failed to configure systemd service: {e}")
        # Final fallback
        install_linux_background_loop()

def main():
    if not os.path.exists(get_agent_main_path()):
        print(f"❌ Error: Could not find main agent script at {get_agent_main_path()}")
        sys.exit(1)

    system = platform.system().lower()
    
    if system == "darwin":
        install_mac_launchd()
    elif system == "linux":
        install_linux_systemd()
    else:
        print(f"❌ Unsupported OS for automated daemon install: {system}")
        print("Please configure your system service manager manually.")

if __name__ == "__main__":
    main()