import os
import platform
import abc
import subprocess
import sys
class BaseServiceManager(abc.ABC):
"""Abstract Base Class for Platform-Specific Service Management."""
@abc.abstractmethod
def install(self, python_path, script_path, working_dir) -> bool:
"""Installs the agent as a background service/daemon."""
pass
@abc.abstractmethod
def uninstall(self) -> bool:
"""Removes the agent service."""
pass
@abc.abstractmethod
def is_installed(self) -> bool:
"""Checks if the service is currently registered."""
pass
@abc.abstractmethod
def start(self) -> bool:
"""Starts the service."""
pass
@abc.abstractmethod
def stop(self) -> bool:
"""Stops the service."""
pass
class LaunchdManager(BaseServiceManager):
"""macOS implementation using launchd."""
LABEL = "com.jerxie.cortex.agent"
def _get_plist_path(self):
return os.path.expanduser(f"~/Library/LaunchAgents/{self.LABEL}.plist")
def install(self, python_path, script_path, working_dir) -> bool:
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>{self.LABEL}</string>
<key>ProgramArguments</key>
<array>
<string>{python_path}</string>
<string>{script_path}</string>
</array>
<key>WorkingDirectory</key>
<string>{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>
</dict>
</plist>
"""
path = self._get_plist_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(plist_content)
try:
subprocess.run(["launchctl", "load", path], check=True)
return True
except:
return False
def uninstall(self) -> bool:
path = self._get_plist_path()
if os.path.exists(path):
subprocess.run(["launchctl", "unload", path], capture_output=True)
os.remove(path)
return True
return False
def is_installed(self) -> bool:
return os.path.exists(self._get_plist_path())
def start(self) -> bool:
try:
subprocess.run(["launchctl", "load", self._get_plist_path()], check=True)
return True
except:
return False
def stop(self) -> bool:
try:
subprocess.run(["launchctl", "unload", self._get_plist_path()], check=True)
return True
except:
return False
class SystemdManager(BaseServiceManager):
"""Linux implementation using systemd user services."""
SERVICE_NAME = "cortex-agent"
def _get_service_path(self):
return os.path.expanduser(f"~/.config/systemd/user/{self.SERVICE_NAME}.service")
def install(self, python_path, script_path, working_dir) -> bool:
content = f"""[Unit]
Description=Cortex Agent Node
After=network.target
[Service]
Type=simple
ExecStart={python_path} {script_path}
WorkingDirectory={working_dir}
Restart=always
RestartSec=5
StandardOutput=append:{os.path.expanduser("~")}/.cortex/agent.out.log
StandardError=append:{os.path.expanduser("~")}/.cortex/agent.err.log
[Install]
WantedBy=default.target
"""
path = self._get_service_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(content)
try:
subprocess.run(["systemctl", "--user", "daemon-reload"], check=True)
subprocess.run(["systemctl", "--user", "enable", self.SERVICE_NAME], check=True)
subprocess.run(["systemctl", "--user", "start", self.SERVICE_NAME], check=True)
return True
except:
return False
def uninstall(self) -> bool:
try:
subprocess.run(["systemctl", "--user", "stop", self.SERVICE_NAME], capture_output=True)
subprocess.run(["systemctl", "--user", "disable", self.SERVICE_NAME], capture_output=True)
path = self._get_service_path()
if os.path.exists(path):
os.remove(path)
return True
except:
return False
def is_installed(self) -> bool:
return os.path.exists(self._get_service_path())
def start(self) -> bool:
return subprocess.run(["systemctl", "--user", "start", self.SERVICE_NAME]).returncode == 0
def stop(self) -> bool:
return subprocess.run(["systemctl", "--user", "stop", self.SERVICE_NAME]).returncode == 0
class SchtasksManager(BaseServiceManager):
"""Windows implementation using Task Scheduler (schtasks)."""
TASK_NAME = "CortexAgent"
def install(self, python_path, script_path, working_dir) -> bool:
# Create a scheduled task that runs on system start
# Use /SC ONSTART or /SC ONLOGON
# We'll use ONLOGON for now as it's more reliable for user-space agents
cmd = [
"schtasks", "/Create", "/TN", self.TASK_NAME,
"/TR", f'"{python_path}" "{script_path}"',
"/SC", "ONLOGON", "/F", "/RL", "HIGHEST"
]
try:
subprocess.run(cmd, check=True, capture_output=True)
# Start it immediately
subprocess.run(["schtasks", "/Run", "/TN", self.TASK_NAME], check=True, capture_output=True)
return True
except Exception as e:
print(f"Error installing Windows task: {e}")
return False
def uninstall(self) -> bool:
try:
subprocess.run(["schtasks", "/Delete", "/TN", self.TASK_NAME, "/F"], capture_output=True)
return True
except:
return False
def is_installed(self) -> bool:
r = subprocess.run(["schtasks", "/Query", "/TN", self.TASK_NAME], capture_output=True)
return r.returncode == 0
def start(self) -> bool:
return subprocess.run(["schtasks", "/Run", "/TN", self.TASK_NAME]).returncode == 0
def stop(self) -> bool:
return subprocess.run(["schtasks", "/End", "/TN", self.TASK_NAME]).returncode == 0
def get_service_manager() -> BaseServiceManager:
"""Factory to get the platform-appropriate service manager."""
sys_type = platform.system().lower()
if sys_type == "darwin":
return LaunchdManager()
elif sys_type == "windows":
return SchtasksManager()
else:
# Fallback to systemd for Linux
return SystemdManager()