Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Atom OS Daemon Manager - Background service management. | |
| Allows Atom OS to run as background daemon service for agent-to-agent execution. | |
| Supports PID file tracking, graceful shutdown, and status monitoring. | |
| Usage: | |
| atom-os daemon # Start as background service | |
| atom-os status # Check daemon status | |
| atom-os stop # Stop daemon | |
| atom-os execute <command> # Run on-demand | |
| """ | |
| import os | |
| import sys | |
| import signal | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Optional | |
| try: | |
| import psutil | |
| except ImportError: | |
| psutil = None | |
| print("Warning: psutil not installed. Daemon features limited.") | |
| print("Install with: pip install psutil>=6.0.0") | |
| # Daemon configuration | |
| PID_DIR = Path.home() / ".atom" / "pids" | |
| PID_FILE = PID_DIR / "atom-os.pid" | |
| LOG_DIR = Path.home() / ".atom" / "logs" | |
| LOG_FILE = LOG_DIR / "daemon.log" | |
| class DaemonManager: | |
| """Manage Atom OS as background daemon service.""" | |
| def get_pid() -> Optional[int]: | |
| """Get running daemon PID from PID file. | |
| Returns: | |
| PID if file exists and contains valid integer, None otherwise | |
| """ | |
| if PID_FILE.exists(): | |
| try: | |
| with open(PID_FILE, 'r') as f: | |
| return int(f.read().strip()) | |
| except (ValueError, IOError): | |
| return None | |
| return None | |
| def is_running() -> bool: | |
| """Check if daemon process is running. | |
| Returns: | |
| True if process is alive, False otherwise | |
| """ | |
| pid = DaemonManager.get_pid() | |
| if pid is None: | |
| return False | |
| if psutil is None: | |
| # Fallback: try sending signal 0 | |
| try: | |
| os.kill(pid, 0) | |
| return True | |
| except OSError: | |
| return False | |
| try: | |
| return psutil.pid_exists(pid) | |
| except Exception: | |
| return False | |
| def start_daemon( | |
| port: int = 8000, | |
| host: str = "0.0.0.0", | |
| workers: int = 1, | |
| host_mount: bool = False, | |
| dev: bool = False | |
| ) -> int: | |
| """Start Atom OS as background daemon. | |
| Creates background subprocess with PID file tracking. | |
| Detaches from terminal for long-running service. | |
| Args: | |
| port: Port for web server (default: 8000) | |
| host: Host to bind to (default: 0.0.0.0) | |
| workers: Number of worker processes (default: 1) | |
| host_mount: Enable host filesystem mount (default: False) | |
| dev: Enable development mode (default: False) | |
| Returns: | |
| Daemon process PID | |
| Raises: | |
| RuntimeError: If daemon is already running | |
| IOError: If PID file cannot be written | |
| """ | |
| if DaemonManager.is_running(): | |
| current_pid = DaemonManager.get_pid() | |
| raise RuntimeError(f"Atom OS is already running (PID: {current_pid})") | |
| # Ensure directories exist | |
| PID_DIR.mkdir(parents=True, exist_ok=True) | |
| LOG_DIR.mkdir(parents=True, exist_ok=True) | |
| # Prepare environment | |
| env = os.environ.copy() | |
| if host_mount: | |
| env["ATOM_HOST_MOUNT_ENABLED"] = "true" | |
| # Prepare command | |
| cmd = [ | |
| sys.executable, "-m", "uvicorn", | |
| "main_api_app:app", | |
| "--host", host, | |
| "--port", str(port), | |
| "--workers", str(workers) | |
| ] | |
| if dev: | |
| cmd.append("--reload") | |
| # Open log file | |
| try: | |
| log_file = open(LOG_FILE, 'a') | |
| except IOError as e: | |
| raise IOError(f"Cannot open log file {LOG_FILE}: {e}") | |
| # Start subprocess | |
| try: | |
| process = subprocess.Popen( | |
| cmd, | |
| env=env, | |
| stdout=log_file, | |
| stderr=subprocess.STDOUT, | |
| start_new_session=True # Detach from parent process | |
| ) | |
| except Exception as e: | |
| log_file.close() | |
| raise RuntimeError(f"Failed to start daemon: {e}") | |
| # Write PID file | |
| try: | |
| with open(PID_FILE, 'w') as f: | |
| f.write(str(process.pid)) | |
| except IOError as e: | |
| process.terminate() | |
| log_file.close() | |
| raise IOError(f"Cannot write PID file {PID_FILE}: {e}") | |
| log_file.close() | |
| return process.pid | |
| def stop_daemon() -> bool: | |
| """Stop daemon gracefully. | |
| Attempts graceful shutdown with SIGTERM, then SIGKILL after timeout. | |
| Returns: | |
| True if stopped, False if not running | |
| Raises: | |
| IOError: If PID file cannot be removed | |
| """ | |
| pid = DaemonManager.get_pid() | |
| if pid is None: | |
| return False | |
| try: | |
| # Try graceful shutdown first | |
| os.kill(pid, signal.SIGTERM) | |
| # Wait up to 10 seconds for graceful shutdown | |
| import time | |
| for _ in range(100): | |
| time.sleep(0.1) | |
| if not DaemonManager.is_running(): | |
| break | |
| # Force kill if still running | |
| if DaemonManager.is_running(): | |
| os.kill(pid, signal.SIGKILL) | |
| time.sleep(0.5) | |
| # Clean up PID file | |
| try: | |
| PID_FILE.unlink(missing_ok=True) | |
| except IOError as e: | |
| raise IOError(f"Cannot remove PID file {PID_FILE}: {e}") | |
| return True | |
| except ProcessLookupError: | |
| # Process already dead, clean up PID file | |
| try: | |
| PID_FILE.unlink(missing_ok=True) | |
| except IOError: | |
| pass | |
| return True | |
| def get_status() -> dict: | |
| """Get daemon status information. | |
| Returns: | |
| Dict with running status, PID, uptime, memory usage, CPU | |
| Example: | |
| { | |
| "running": True, | |
| "pid": 12345, | |
| "uptime_seconds": 3600, | |
| "memory_mb": 256.5, | |
| "cpu_percent": 5.2, | |
| "status": "running" | |
| } | |
| """ | |
| pid = DaemonManager.get_pid() | |
| if pid is None: | |
| return { | |
| "running": False, | |
| "pid": None, | |
| "uptime_seconds": None, | |
| "memory_mb": None, | |
| "cpu_percent": None, | |
| "status": "not_running" | |
| } | |
| if not DaemonManager.is_running(): | |
| return { | |
| "running": False, | |
| "pid": pid, | |
| "uptime_seconds": None, | |
| "memory_mb": None, | |
| "cpu_percent": None, | |
| "status": "stale_pid_file", | |
| "note": "Stale PID file" | |
| } | |
| if psutil is None: | |
| # Limited status without psutil | |
| return { | |
| "running": True, | |
| "pid": pid, | |
| "uptime_seconds": None, | |
| "memory_mb": None, | |
| "cpu_percent": None, | |
| "status": "running" | |
| } | |
| try: | |
| process = psutil.Process(pid) | |
| return { | |
| "running": True, | |
| "pid": pid, | |
| "uptime_seconds": process.cpu_times().system, | |
| "memory_mb": process.memory_info().rss / 1024 / 1024, | |
| "cpu_percent": process.cpu_percent(interval=0.1), | |
| "status": "running" | |
| } | |
| except psutil.NoSuchProcess: | |
| return { | |
| "running": False, | |
| "pid": pid, | |
| "uptime_seconds": None, | |
| "memory_mb": None, | |
| "cpu_percent": None, | |
| "status": "died_unexpectedly", | |
| "note": "Process died unexpectedly" | |
| } | |