From 73d010efc7469fdf8a66d65c7f0e2c36d103bb82 Mon Sep 17 00:00:00 2001 From: cyrus Date: Wed, 25 Feb 2026 16:31:51 +0530 Subject: [PATCH] Fix daemon service management and health communication issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This comprehensive fix resolves two critical daemon-related issues: 1. **Launchctl "Input/output error" fix**: - Add intelligent state checking before service operations - Only load LaunchAgent if not already loaded - Treat "already running" as success, not failure - Add helper functions for cross-platform service state detection - Implement idempotent service start operations 2. **Daemon health communication fix**: - Add ALPHAHUMAN_DAEMON_INTERNAL environment variable to external service plist - Force external daemon to use file-based communication (daemon_state.json) - Implement file watching bridge in main app to emit Tauri events - Ensure frontend receives proper health updates from external daemon **Key Changes**: - Enhanced `start()` function with state checking across all platforms - Added `is_service_loaded_macos()`, `is_service_enabled_linux()`, `is_task_exists_windows()` - Modified macOS plist to include `ALPHAHUMAN_DAEMON_INTERNAL=false` - Added `watch_daemon_health_file()` function for external daemon communication - Updated daemon mode detection logic for cross-platform consistency - Added comprehensive logging for service management operations **Expected Results**: - No more "Input/output error" when clicking daemon start button - External daemon status shows "connected" instead of "disconnected" - Idempotent service operations (safe to run multiple times) - Single daemon process prevents resource conflicts - Cross-platform daemon service reliability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src-tauri/src/alphahuman/service/mod.rs | 112 ++++++++++++++++++++- src-tauri/src/lib.rs | 126 +++++++++++++++++------- 2 files changed, 198 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/alphahuman/service/mod.rs b/src-tauri/src/alphahuman/service/mod.rs index ed97e1690..ddf54470b 100644 --- a/src-tauri/src/alphahuman/service/mod.rs +++ b/src-tauri/src/alphahuman/service/mod.rs @@ -49,20 +49,82 @@ pub fn install(config: &Config) -> Result { pub fn start(config: &Config) -> Result { if cfg!(target_os = "macos") { let plist = macos_service_file()?; - run_checked(Command::new("launchctl").arg("load").arg("-w").arg(&plist))?; - run_checked(Command::new("launchctl").arg("start").arg(SERVICE_LABEL))?; + + // Check if service is already loaded to avoid "Input/output error" + if !is_service_loaded_macos()? { + log::info!("[service] Loading macOS LaunchAgent service"); + run_checked(Command::new("launchctl").arg("load").arg("-w").arg(&plist))?; + } else { + log::info!("[service] LaunchAgent service already loaded, skipping load step"); + } + + // Always try to start - this is safe even if already running + log::info!("[service] Starting macOS LaunchAgent service"); + let start_result = run_checked(Command::new("launchctl").arg("start").arg(SERVICE_LABEL)); + if let Err(e) = start_result { + // Check if it's already running - that's not an error for us + let status_check = status(config)?; + if matches!(status_check.state, ServiceState::Running) { + log::info!("[service] Service was already running - operation successful"); + } else { + return Err(e); + } + } return status(config); } if cfg!(target_os = "linux") { + // Check if service is enabled before trying to start + if !is_service_enabled_linux()? { + log::info!("[service] Enabling systemd service"); + let _ = run_checked(Command::new("systemctl").args(["--user", "enable", "alphahuman.service"])); + } else { + log::info!("[service] Systemd service already enabled"); + } + run_checked(Command::new("systemctl").args(["--user", "daemon-reload"]))?; - run_checked(Command::new("systemctl").args(["--user", "start", "alphahuman.service"]))?; + + // Try to start - systemctl start is idempotent + log::info!("[service] Starting systemd service"); + let start_result = run_checked(Command::new("systemctl").args(["--user", "start", "alphahuman.service"])); + if let Err(e) = start_result { + // Check if it's already active - that's success for us + let status_check = status(config)?; + if matches!(status_check.state, ServiceState::Running) { + log::info!("[service] Service was already running - operation successful"); + } else { + return Err(e); + } + } return status(config); } if cfg!(target_os = "windows") { - let _ = config; - run_checked(Command::new("schtasks").args(["/Run", "/TN", windows_task_name()]))?; + let task_name = windows_task_name(); + + // Check if task exists before trying to run + if !is_task_exists_windows(task_name)? { + log::warn!("[service] Windows scheduled task does not exist, please install first"); + return Ok(ServiceStatus { + state: ServiceState::NotInstalled, + unit_path: None, + label: task_name.to_string(), + details: Some("Task not installed".to_string()), + }); + } + + // Try to run task - this may fail if already running, which is OK + log::info!("[service] Starting Windows scheduled task"); + let run_result = run_checked(Command::new("schtasks").args(["/Run", "/TN", task_name])); + if let Err(e) = run_result { + // Check if it's already running - that's success for us + let status_check = status(config)?; + if matches!(status_check.state, ServiceState::Running) { + log::info!("[service] Task was already running - operation successful"); + } else { + return Err(e); + } + } return status(config); } @@ -263,6 +325,11 @@ fn install_macos(config: &Config) -> Result<()> { {stdout} StandardErrorPath {stderr} + EnvironmentVariables + + ALPHAHUMAN_DAEMON_INTERNAL + false + "#, @@ -391,6 +458,41 @@ fn run_capture(cmd: &mut Command) -> Result { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } +/// Check if the macOS LaunchAgent service is loaded (regardless of running state) +fn is_service_loaded_macos() -> Result { + let out = run_capture(Command::new("launchctl").arg("list"))?; + Ok(out.lines().any(|line| { + line.contains(SERVICE_LABEL) || line.contains(LEGACY_SERVICE_LABEL) + })) +} + +/// Check if the Linux systemd service is enabled +fn is_service_enabled_linux() -> Result { + let result = Command::new("systemctl") + .args(["--user", "is-enabled", "alphahuman.service"]) + .output(); + + match result { + Ok(output) => { + let status_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(status_str == "enabled") + } + Err(_) => Ok(false), // Service not found or other error means not enabled + } +} + +/// Check if the Windows scheduled task exists +fn is_task_exists_windows(task_name: &str) -> Result { + let result = Command::new("schtasks") + .args(["/Query", "/TN", task_name]) + .output(); + + match result { + Ok(output) => Ok(output.status.success()), + Err(_) => Ok(false), // Command failed means task doesn't exist + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ebc52254c..6c43f8cd1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,7 +20,9 @@ mod utils; use ai::*; use commands::*; use services::socket_service::SOCKET_SERVICE; -use tauri::{AppHandle, Manager, RunEvent}; +use std::path::PathBuf; +use tauri::{AppHandle, Manager, RunEvent, Emitter}; +use tokio::{fs, time::{interval, Duration}}; #[cfg(desktop)] use tauri::{ @@ -220,6 +222,49 @@ fn setup_tray(app: &AppHandle) -> Result<(), Box> { Ok(()) } +/// Watch daemon health file and bridge changes to frontend Tauri events +async fn watch_daemon_health_file(app_handle: AppHandle, data_dir: PathBuf) { + let state_file = data_dir.join("daemon_state.json"); + let mut interval = interval(Duration::from_secs(2)); + let mut last_modified: Option = None; + + log::info!("[alphahuman] Watching daemon health file: {}", state_file.display()); + + loop { + interval.tick().await; + + // Check if file exists and was modified + if let Ok(metadata) = fs::metadata(&state_file).await { + if let Ok(modified) = metadata.modified() { + if last_modified.map_or(true, |last| modified > last) { + last_modified = Some(modified); + + // Read and parse health data + if let Ok(content) = fs::read_to_string(&state_file).await { + if let Ok(json_value) = serde_json::from_str::(&content) { + log::debug!("[alphahuman] Broadcasting health event from file: {:?}", json_value); + + // Emit Tauri event to frontend (same as internal daemon) + if let Err(e) = app_handle.emit("alphahuman:health", &json_value) { + log::error!("[alphahuman] Failed to emit health event from file: {}", e); + } else { + log::debug!("[alphahuman] Health event emitted successfully from file"); + } + } else { + log::debug!("[alphahuman] Failed to parse health file as JSON: {}", state_file.display()); + } + } else { + log::debug!("[alphahuman] Failed to read health file: {}", state_file.display()); + } + } + } + } else { + // File doesn't exist yet - external daemon may not be writing yet + log::debug!("[alphahuman] Health file not found yet: {}", state_file.display()); + } + } +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { if let Err(err) = rustls::crypto::ring::default_provider().install_default() { @@ -502,42 +547,20 @@ pub fn run() { }; app.manage(daemon_handle); - if cfg!(target_os = "macos") - && !daemon_mode - && !daemon_foreground_requested() - && !cfg!(debug_assertions) // Always use internal supervisor in debug builds - && std::env::var("ALPHAHUMAN_DAEMON_INTERNAL").unwrap_or("false".to_string()) != "true" // Allow override via env var - { - // On macOS, start external LaunchAgent service for background daemon - tauri::async_runtime::spawn(async move { - match alphahuman::config::Config::load_or_init().await { - Ok(config) => { - if let Err(e) = alphahuman::service::install(&config) { - log::warn!( - "[alphahuman] LaunchAgent install failed: {e}" - ); - } - if let Err(e) = alphahuman::service::start(&config) { - log::warn!( - "[alphahuman] LaunchAgent start failed: {e}" - ); - } - } - Err(e) => { - log::warn!( - "[alphahuman] Failed to load config for LaunchAgent: {e}" - ); - } - } - }); - } else { + // Determine daemon mode: internal supervisor vs external platform service + let use_internal_daemon = daemon_mode + || daemon_foreground_requested() + || cfg!(debug_assertions) // Always use internal supervisor in debug builds + || std::env::var("ALPHAHUMAN_DAEMON_INTERNAL").unwrap_or("false".to_string()) == "true"; // Cross-platform override via env var + + if use_internal_daemon { // Run internal daemon supervisor with health event emission // This path is taken when: - // - Not macOS, OR - // - macOS with daemon mode enabled, OR - // - macOS with foreground daemon requested, OR - // - macOS debug build (for easier development), OR - // - macOS with ALPHAHUMAN_DAEMON_INTERNAL=true env var + // - Daemon mode enabled, OR + // - Foreground daemon requested, OR + // - Debug build (for easier development), OR + // - ALPHAHUMAN_DAEMON_INTERNAL=true env var (any platform) + log::info!("[alphahuman] Using internal daemon supervisor (ALPHAHUMAN_DAEMON_INTERNAL=true or debug build)"); let app_handle_for_daemon = app.handle().clone(); tauri::async_runtime::spawn(async move { log::info!("[alphahuman] Starting daemon supervisor with health monitoring"); @@ -551,6 +574,39 @@ pub fn run() { log::error!("[alphahuman] Daemon supervisor error: {e}"); } }); + } else { + // Start external platform-specific service for background daemon + // This path is taken on all platforms when ALPHAHUMAN_DAEMON_INTERNAL=false/unset + // and not in daemon mode, foreground mode, or debug build + log::info!("[alphahuman] Using external daemon service (ALPHAHUMAN_DAEMON_INTERNAL=false/unset)"); + + // Setup file watching to bridge external daemon health events to frontend + let app_handle_for_watcher = app.handle().clone(); + let data_dir_clone = data_dir.clone(); + tauri::async_runtime::spawn(async move { + watch_daemon_health_file(app_handle_for_watcher, data_dir_clone).await; + }); + + // Start the external platform service + tauri::async_runtime::spawn(async move { + match alphahuman::config::Config::load_or_init().await { + Ok(config) => { + match alphahuman::service::install(&config) { + Ok(status) => log::info!("[alphahuman] External daemon service installed: {:?}", status), + Err(e) => log::error!("[alphahuman] Failed to install external daemon service: {e}"), + } + match alphahuman::service::start(&config) { + Ok(status) => log::info!("[alphahuman] External daemon service started: {:?}", status), + Err(e) => log::error!("[alphahuman] Failed to start external daemon service: {e}"), + } + } + Err(e) => { + log::error!( + "[alphahuman] Failed to load config for external service: {e}" + ); + } + } + }); } }