Fix daemon service management and health communication issues

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 <noreply@anthropic.com>
This commit is contained in:
cyrus
2026-02-25 16:31:51 +05:30
co-authored by Claude
parent 4edf2f5b8c
commit 73d010efc7
2 changed files with 198 additions and 40 deletions
+107 -5
View File
@@ -49,20 +49,82 @@ pub fn install(config: &Config) -> Result<ServiceStatus> {
pub fn start(config: &Config) -> Result<ServiceStatus> {
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<()> {
<string>{stdout}</string>
<key>StandardErrorPath</key>
<string>{stderr}</string>
<key>EnvironmentVariables</key>
<dict>
<key>ALPHAHUMAN_DAEMON_INTERNAL</key>
<string>false</string>
</dict>
</dict>
</plist>
"#,
@@ -391,6 +458,41 @@ fn run_capture(cmd: &mut Command) -> Result<String> {
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<bool> {
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<bool> {
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<bool> {
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::*;
+91 -35
View File
@@ -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<dyn std::error::Error>> {
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<std::time::SystemTime> = 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::<serde_json::Value>(&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}"
);
}
}
});
}
}