mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 15:03:57 +00:00
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:
@@ -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
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user