...
This commit is contained in:
		
							
								
								
									
										492
									
								
								_archive/service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										492
									
								
								_archive/service_manager/src/launchctl.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,492 @@
 | 
			
		||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use tokio::process::Command;
 | 
			
		||||
use tokio::runtime::Runtime;
 | 
			
		||||
 | 
			
		||||
// Shared runtime for async operations - production-safe initialization
 | 
			
		||||
static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok());
 | 
			
		||||
 | 
			
		||||
/// Get the async runtime, creating a temporary one if the static runtime failed
 | 
			
		||||
fn get_runtime() -> Result<Runtime, ServiceManagerError> {
 | 
			
		||||
    // Try to use the static runtime first
 | 
			
		||||
    if let Some(_runtime) = ASYNC_RUNTIME.as_ref() {
 | 
			
		||||
        // We can't return a reference to the static runtime because we need ownership
 | 
			
		||||
        // for block_on, so we create a new one. This is a reasonable trade-off for safety.
 | 
			
		||||
        Runtime::new().map_err(|e| {
 | 
			
		||||
            ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
        // Static runtime failed, try to create a new one
 | 
			
		||||
        Runtime::new().map_err(|e| {
 | 
			
		||||
            ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct LaunchctlServiceManager {
 | 
			
		||||
    service_prefix: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
struct LaunchDaemon {
 | 
			
		||||
    #[serde(rename = "Label")]
 | 
			
		||||
    label: String,
 | 
			
		||||
    #[serde(rename = "ProgramArguments")]
 | 
			
		||||
    program_arguments: Vec<String>,
 | 
			
		||||
    #[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    working_directory: Option<String>,
 | 
			
		||||
    #[serde(
 | 
			
		||||
        rename = "EnvironmentVariables",
 | 
			
		||||
        skip_serializing_if = "Option::is_none"
 | 
			
		||||
    )]
 | 
			
		||||
    environment_variables: Option<HashMap<String, String>>,
 | 
			
		||||
    #[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    keep_alive: Option<bool>,
 | 
			
		||||
    #[serde(rename = "RunAtLoad")]
 | 
			
		||||
    run_at_load: bool,
 | 
			
		||||
    #[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    standard_out_path: Option<String>,
 | 
			
		||||
    #[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")]
 | 
			
		||||
    standard_error_path: Option<String>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl LaunchctlServiceManager {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            service_prefix: "tf.ourworld.circles".to_string(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_service_label(&self, service_name: &str) -> String {
 | 
			
		||||
        format!("{}.{}", self.service_prefix, service_name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_plist_path(&self, service_name: &str) -> PathBuf {
 | 
			
		||||
        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
 | 
			
		||||
        PathBuf::from(home)
 | 
			
		||||
            .join("Library")
 | 
			
		||||
            .join("LaunchAgents")
 | 
			
		||||
            .join(format!("{}.plist", self.get_service_label(service_name)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_log_path(&self, service_name: &str) -> PathBuf {
 | 
			
		||||
        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
 | 
			
		||||
        PathBuf::from(home)
 | 
			
		||||
            .join("Library")
 | 
			
		||||
            .join("Logs")
 | 
			
		||||
            .join("circles")
 | 
			
		||||
            .join(format!("{}.log", service_name))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn create_plist(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let label = self.get_service_label(&config.name);
 | 
			
		||||
        let plist_path = self.get_plist_path(&config.name);
 | 
			
		||||
        let log_path = self.get_log_path(&config.name);
 | 
			
		||||
 | 
			
		||||
        // Ensure the LaunchAgents directory exists
 | 
			
		||||
        if let Some(parent) = plist_path.parent() {
 | 
			
		||||
            tokio::fs::create_dir_all(parent).await?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Ensure the logs directory exists
 | 
			
		||||
        if let Some(parent) = log_path.parent() {
 | 
			
		||||
            tokio::fs::create_dir_all(parent).await?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut program_arguments = vec![config.binary_path.clone()];
 | 
			
		||||
        program_arguments.extend(config.args.clone());
 | 
			
		||||
 | 
			
		||||
        let launch_daemon = LaunchDaemon {
 | 
			
		||||
            label: label.clone(),
 | 
			
		||||
            program_arguments,
 | 
			
		||||
            working_directory: config.working_directory.clone(),
 | 
			
		||||
            environment_variables: if config.environment.is_empty() {
 | 
			
		||||
                None
 | 
			
		||||
            } else {
 | 
			
		||||
                Some(config.environment.clone())
 | 
			
		||||
            },
 | 
			
		||||
            keep_alive: if config.auto_restart {
 | 
			
		||||
                Some(true)
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            },
 | 
			
		||||
            run_at_load: true,
 | 
			
		||||
            standard_out_path: Some(log_path.to_string_lossy().to_string()),
 | 
			
		||||
            standard_error_path: Some(log_path.to_string_lossy().to_string()),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut plist_content = Vec::new();
 | 
			
		||||
        plist::to_writer_xml(&mut plist_content, &launch_daemon)
 | 
			
		||||
            .map_err(|e| ServiceManagerError::Other(format!("Failed to serialize plist: {}", e)))?;
 | 
			
		||||
        let plist_content = String::from_utf8(plist_content).map_err(|e| {
 | 
			
		||||
            ServiceManagerError::Other(format!("Failed to convert plist to string: {}", e))
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        tokio::fs::write(&plist_path, plist_content).await?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn run_launchctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
 | 
			
		||||
        let output = Command::new("launchctl").args(args).output().await?;
 | 
			
		||||
 | 
			
		||||
        if !output.status.success() {
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            return Err(ServiceManagerError::Other(format!(
 | 
			
		||||
                "launchctl command failed: {}",
 | 
			
		||||
                stderr
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn wait_for_service_status(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        use tokio::time::{sleep, timeout, Duration};
 | 
			
		||||
 | 
			
		||||
        let timeout_duration = Duration::from_secs(timeout_secs);
 | 
			
		||||
        let poll_interval = Duration::from_millis(500);
 | 
			
		||||
 | 
			
		||||
        let result = timeout(timeout_duration, async {
 | 
			
		||||
            loop {
 | 
			
		||||
                match self.status(service_name) {
 | 
			
		||||
                    Ok(ServiceStatus::Running) => {
 | 
			
		||||
                        return Ok(());
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(ServiceStatus::Failed) => {
 | 
			
		||||
                        // Service failed, get error details from logs
 | 
			
		||||
                        let logs = self.logs(service_name, Some(20)).unwrap_or_default();
 | 
			
		||||
                        let error_msg = if logs.is_empty() {
 | 
			
		||||
                            "Service failed to start (no logs available)".to_string()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Extract error lines from logs
 | 
			
		||||
                            let error_lines: Vec<&str> = logs
 | 
			
		||||
                                .lines()
 | 
			
		||||
                                .filter(|line| {
 | 
			
		||||
                                    line.to_lowercase().contains("error")
 | 
			
		||||
                                        || line.to_lowercase().contains("failed")
 | 
			
		||||
                                })
 | 
			
		||||
                                .take(3)
 | 
			
		||||
                                .collect();
 | 
			
		||||
 | 
			
		||||
                            if error_lines.is_empty() {
 | 
			
		||||
                                format!(
 | 
			
		||||
                                    "Service failed to start. Recent logs:\n{}",
 | 
			
		||||
                                    logs.lines()
 | 
			
		||||
                                        .rev()
 | 
			
		||||
                                        .take(5)
 | 
			
		||||
                                        .collect::<Vec<_>>()
 | 
			
		||||
                                        .into_iter()
 | 
			
		||||
                                        .rev()
 | 
			
		||||
                                        .collect::<Vec<_>>()
 | 
			
		||||
                                        .join("\n")
 | 
			
		||||
                                )
 | 
			
		||||
                            } else {
 | 
			
		||||
                                format!(
 | 
			
		||||
                                    "Service failed to start. Errors:\n{}",
 | 
			
		||||
                                    error_lines.join("\n")
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        };
 | 
			
		||||
                        return Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                            service_name.to_string(),
 | 
			
		||||
                            error_msg,
 | 
			
		||||
                        ));
 | 
			
		||||
                    }
 | 
			
		||||
                    Ok(ServiceStatus::Stopped) | Ok(ServiceStatus::Unknown) => {
 | 
			
		||||
                        // Still starting, continue polling
 | 
			
		||||
                        sleep(poll_interval).await;
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(ServiceManagerError::ServiceNotFound(_)) => {
 | 
			
		||||
                        return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                            service_name.to_string(),
 | 
			
		||||
                        ));
 | 
			
		||||
                    }
 | 
			
		||||
                    Err(e) => {
 | 
			
		||||
                        return Err(e);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        .await;
 | 
			
		||||
 | 
			
		||||
        match result {
 | 
			
		||||
            Ok(Ok(())) => Ok(()),
 | 
			
		||||
            Ok(Err(e)) => Err(e),
 | 
			
		||||
            Err(_) => Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
                format!("Service did not start within {} seconds", timeout_secs),
 | 
			
		||||
            )),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServiceManager for LaunchctlServiceManager {
 | 
			
		||||
    fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
 | 
			
		||||
        let plist_path = self.get_plist_path(service_name);
 | 
			
		||||
        Ok(plist_path.exists())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Use production-safe runtime for async operations
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let label = self.get_service_label(&config.name);
 | 
			
		||||
 | 
			
		||||
            // Check if service is already loaded
 | 
			
		||||
            let list_output = self.run_launchctl(&["list"]).await?;
 | 
			
		||||
            if list_output.contains(&label) {
 | 
			
		||||
                return Err(ServiceManagerError::ServiceAlreadyExists(
 | 
			
		||||
                    config.name.clone(),
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Create the plist file
 | 
			
		||||
            self.create_plist(config).await?;
 | 
			
		||||
 | 
			
		||||
            // Load the service
 | 
			
		||||
            let plist_path = self.get_plist_path(&config.name);
 | 
			
		||||
            self.run_launchctl(&["load", &plist_path.to_string_lossy()])
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    ServiceManagerError::StartFailed(config.name.clone(), e.to_string())
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let label = self.get_service_label(service_name);
 | 
			
		||||
            let plist_path = self.get_plist_path(service_name);
 | 
			
		||||
 | 
			
		||||
            // Check if plist file exists
 | 
			
		||||
            if !plist_path.exists() {
 | 
			
		||||
                return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                    service_name.to_string(),
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if service is already loaded and running
 | 
			
		||||
            let list_output = self.run_launchctl(&["list"]).await?;
 | 
			
		||||
            if list_output.contains(&label) {
 | 
			
		||||
                // Service is loaded, check if it's running
 | 
			
		||||
                match self.status(service_name)? {
 | 
			
		||||
                    ServiceStatus::Running => {
 | 
			
		||||
                        return Ok(()); // Already running, nothing to do
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => {
 | 
			
		||||
                        // Service is loaded but not running, try to start it
 | 
			
		||||
                        self.run_launchctl(&["start", &label]).await.map_err(|e| {
 | 
			
		||||
                            ServiceManagerError::StartFailed(
 | 
			
		||||
                                service_name.to_string(),
 | 
			
		||||
                                e.to_string(),
 | 
			
		||||
                            )
 | 
			
		||||
                        })?;
 | 
			
		||||
                        return Ok(());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Service is not loaded, load it
 | 
			
		||||
            self.run_launchctl(&["load", &plist_path.to_string_lossy()])
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        config: &ServiceConfig,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // First start the service
 | 
			
		||||
        self.start(config)?;
 | 
			
		||||
 | 
			
		||||
        // Then wait for confirmation using production-safe runtime
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            self.wait_for_service_status(&config.name, timeout_secs)
 | 
			
		||||
                .await
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // First start the existing service
 | 
			
		||||
        self.start_existing(service_name)?;
 | 
			
		||||
 | 
			
		||||
        // Then wait for confirmation using production-safe runtime
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            self.wait_for_service_status(service_name, timeout_secs)
 | 
			
		||||
                .await
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let _label = self.get_service_label(service_name);
 | 
			
		||||
            let plist_path = self.get_plist_path(service_name);
 | 
			
		||||
 | 
			
		||||
            // Unload the service
 | 
			
		||||
            self.run_launchctl(&["unload", &plist_path.to_string_lossy()])
 | 
			
		||||
                .await
 | 
			
		||||
                .map_err(|e| {
 | 
			
		||||
                    ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
                })?;
 | 
			
		||||
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // For launchctl, we stop and start
 | 
			
		||||
        if let Err(e) = self.stop(service_name) {
 | 
			
		||||
            // If stop fails because service doesn't exist, that's ok for restart
 | 
			
		||||
            if !matches!(e, ServiceManagerError::ServiceNotFound(_)) {
 | 
			
		||||
                return Err(ServiceManagerError::RestartFailed(
 | 
			
		||||
                    service_name.to_string(),
 | 
			
		||||
                    e.to_string(),
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // We need the config to restart, but we don't have it stored
 | 
			
		||||
        // For now, return an error - in a real implementation we might store configs
 | 
			
		||||
        Err(ServiceManagerError::RestartFailed(
 | 
			
		||||
            service_name.to_string(),
 | 
			
		||||
            "Restart requires re-providing service configuration".to_string(),
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let label = self.get_service_label(service_name);
 | 
			
		||||
            let plist_path = self.get_plist_path(service_name);
 | 
			
		||||
 | 
			
		||||
            // First check if the plist file exists
 | 
			
		||||
            if !plist_path.exists() {
 | 
			
		||||
                return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                    service_name.to_string(),
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let list_output = self.run_launchctl(&["list"]).await?;
 | 
			
		||||
 | 
			
		||||
            if !list_output.contains(&label) {
 | 
			
		||||
                return Ok(ServiceStatus::Stopped);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get detailed status
 | 
			
		||||
            match self.run_launchctl(&["list", &label]).await {
 | 
			
		||||
                Ok(output) => {
 | 
			
		||||
                    if output.contains("\"PID\" = ") {
 | 
			
		||||
                        Ok(ServiceStatus::Running)
 | 
			
		||||
                    } else if output.contains("\"LastExitStatus\" = ") {
 | 
			
		||||
                        Ok(ServiceStatus::Failed)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        Ok(ServiceStatus::Unknown)
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Err(_) => Ok(ServiceStatus::Stopped),
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn logs(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        lines: Option<usize>,
 | 
			
		||||
    ) -> Result<String, ServiceManagerError> {
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let log_path = self.get_log_path(service_name);
 | 
			
		||||
 | 
			
		||||
            if !log_path.exists() {
 | 
			
		||||
                return Ok(String::new());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            match lines {
 | 
			
		||||
                Some(n) => {
 | 
			
		||||
                    let output = Command::new("tail")
 | 
			
		||||
                        .args(&["-n", &n.to_string(), &log_path.to_string_lossy()])
 | 
			
		||||
                        .output()
 | 
			
		||||
                        .await?;
 | 
			
		||||
                    Ok(String::from_utf8_lossy(&output.stdout).to_string())
 | 
			
		||||
                }
 | 
			
		||||
                None => {
 | 
			
		||||
                    let content = tokio::fs::read_to_string(&log_path).await?;
 | 
			
		||||
                    Ok(content)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let list_output = self.run_launchctl(&["list"]).await?;
 | 
			
		||||
 | 
			
		||||
            let services: Vec<String> = list_output
 | 
			
		||||
                .lines()
 | 
			
		||||
                .filter_map(|line| {
 | 
			
		||||
                    if line.contains(&self.service_prefix) {
 | 
			
		||||
                        // Extract service name from label
 | 
			
		||||
                        line.split_whitespace()
 | 
			
		||||
                            .last()
 | 
			
		||||
                            .and_then(|label| {
 | 
			
		||||
                                label.strip_prefix(&format!("{}.", self.service_prefix))
 | 
			
		||||
                            })
 | 
			
		||||
                            .map(|s| s.to_string())
 | 
			
		||||
                    } else {
 | 
			
		||||
                        None
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .collect();
 | 
			
		||||
 | 
			
		||||
            Ok(services)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Try to stop the service first, but don't fail if it's already stopped or doesn't exist
 | 
			
		||||
        if let Err(e) = self.stop(service_name) {
 | 
			
		||||
            // Log the error but continue with removal
 | 
			
		||||
            log::warn!(
 | 
			
		||||
                "Failed to stop service '{}' before removal: {}",
 | 
			
		||||
                service_name,
 | 
			
		||||
                e
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove the plist file using production-safe runtime
 | 
			
		||||
        let runtime = get_runtime()?;
 | 
			
		||||
        runtime.block_on(async {
 | 
			
		||||
            let plist_path = self.get_plist_path(service_name);
 | 
			
		||||
            if plist_path.exists() {
 | 
			
		||||
                tokio::fs::remove_file(&plist_path).await?;
 | 
			
		||||
            }
 | 
			
		||||
            Ok(())
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										301
									
								
								_archive/service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										301
									
								
								_archive/service_manager/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,301 @@
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use thiserror::Error;
 | 
			
		||||
 | 
			
		||||
#[derive(Error, Debug)]
 | 
			
		||||
pub enum ServiceManagerError {
 | 
			
		||||
    #[error("Service '{0}' not found")]
 | 
			
		||||
    ServiceNotFound(String),
 | 
			
		||||
    #[error("Service '{0}' already exists")]
 | 
			
		||||
    ServiceAlreadyExists(String),
 | 
			
		||||
    #[error("Failed to start service '{0}': {1}")]
 | 
			
		||||
    StartFailed(String, String),
 | 
			
		||||
    #[error("Failed to stop service '{0}': {1}")]
 | 
			
		||||
    StopFailed(String, String),
 | 
			
		||||
    #[error("Failed to restart service '{0}': {1}")]
 | 
			
		||||
    RestartFailed(String, String),
 | 
			
		||||
    #[error("Failed to get logs for service '{0}': {1}")]
 | 
			
		||||
    LogsFailed(String, String),
 | 
			
		||||
    #[error("IO error: {0}")]
 | 
			
		||||
    IoError(#[from] std::io::Error),
 | 
			
		||||
    #[error("Service manager error: {0}")]
 | 
			
		||||
    Other(String),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone)]
 | 
			
		||||
pub struct ServiceConfig {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub binary_path: String,
 | 
			
		||||
    pub args: Vec<String>,
 | 
			
		||||
    pub working_directory: Option<String>,
 | 
			
		||||
    pub environment: HashMap<String, String>,
 | 
			
		||||
    pub auto_restart: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, PartialEq)]
 | 
			
		||||
pub enum ServiceStatus {
 | 
			
		||||
    Running,
 | 
			
		||||
    Stopped,
 | 
			
		||||
    Failed,
 | 
			
		||||
    Unknown,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait ServiceManager: Send + Sync {
 | 
			
		||||
    /// Check if a service exists
 | 
			
		||||
    fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Start a service with the given configuration
 | 
			
		||||
    fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Start an existing service by name (load existing plist/config)
 | 
			
		||||
    fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Start a service and wait for confirmation that it's running or failed
 | 
			
		||||
    fn start_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        config: &ServiceConfig,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Start an existing service and wait for confirmation that it's running or failed
 | 
			
		||||
    fn start_existing_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Stop a service by name
 | 
			
		||||
    fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Restart a service by name
 | 
			
		||||
    fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Get the status of a service
 | 
			
		||||
    fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Get logs for a service
 | 
			
		||||
    fn logs(&self, service_name: &str, lines: Option<usize>)
 | 
			
		||||
        -> Result<String, ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// List all managed services
 | 
			
		||||
    fn list(&self) -> Result<Vec<String>, ServiceManagerError>;
 | 
			
		||||
 | 
			
		||||
    /// Remove a service configuration (stop if running)
 | 
			
		||||
    fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Platform-specific implementations
 | 
			
		||||
#[cfg(target_os = "macos")]
 | 
			
		||||
mod launchctl;
 | 
			
		||||
#[cfg(target_os = "macos")]
 | 
			
		||||
pub use launchctl::LaunchctlServiceManager;
 | 
			
		||||
 | 
			
		||||
#[cfg(target_os = "linux")]
 | 
			
		||||
mod systemd;
 | 
			
		||||
#[cfg(target_os = "linux")]
 | 
			
		||||
pub use systemd::SystemdServiceManager;
 | 
			
		||||
 | 
			
		||||
mod zinit;
 | 
			
		||||
pub use zinit::ZinitServiceManager;
 | 
			
		||||
 | 
			
		||||
#[cfg(feature = "rhai")]
 | 
			
		||||
pub mod rhai;
 | 
			
		||||
 | 
			
		||||
/// Discover available zinit socket paths
 | 
			
		||||
///
 | 
			
		||||
/// This function checks for zinit sockets in the following order:
 | 
			
		||||
/// 1. Environment variable ZINIT_SOCKET_PATH (if set)
 | 
			
		||||
/// 2. Common socket locations with connectivity testing
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// Returns the first working socket path found, or None if no working zinit server is detected.
 | 
			
		||||
#[cfg(target_os = "linux")]
 | 
			
		||||
fn discover_zinit_socket() -> Option<String> {
 | 
			
		||||
    // First check environment variable
 | 
			
		||||
    if let Ok(env_socket_path) = std::env::var("ZINIT_SOCKET_PATH") {
 | 
			
		||||
        log::debug!("Checking ZINIT_SOCKET_PATH: {}", env_socket_path);
 | 
			
		||||
        if test_zinit_socket(&env_socket_path) {
 | 
			
		||||
            log::info!(
 | 
			
		||||
                "Using zinit socket from ZINIT_SOCKET_PATH: {}",
 | 
			
		||||
                env_socket_path
 | 
			
		||||
            );
 | 
			
		||||
            return Some(env_socket_path);
 | 
			
		||||
        } else {
 | 
			
		||||
            log::warn!(
 | 
			
		||||
                "ZINIT_SOCKET_PATH specified but socket is not accessible: {}",
 | 
			
		||||
                env_socket_path
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try common socket locations
 | 
			
		||||
    let common_paths = [
 | 
			
		||||
        "/var/run/zinit.sock",
 | 
			
		||||
        "/tmp/zinit.sock",
 | 
			
		||||
        "/run/zinit.sock",
 | 
			
		||||
        "./zinit.sock",
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    log::debug!("Discovering zinit socket from common locations...");
 | 
			
		||||
    for path in &common_paths {
 | 
			
		||||
        log::debug!("Testing socket path: {}", path);
 | 
			
		||||
        if test_zinit_socket(path) {
 | 
			
		||||
            log::info!("Found working zinit socket at: {}", path);
 | 
			
		||||
            return Some(path.to_string());
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    log::debug!("No working zinit socket found");
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Test if a zinit socket is accessible and responsive
 | 
			
		||||
///
 | 
			
		||||
/// This function attempts to create a ZinitServiceManager and perform a basic
 | 
			
		||||
/// connectivity test by listing services.
 | 
			
		||||
#[cfg(target_os = "linux")]
 | 
			
		||||
fn test_zinit_socket(socket_path: &str) -> bool {
 | 
			
		||||
    // Check if socket file exists first
 | 
			
		||||
    if !std::path::Path::new(socket_path).exists() {
 | 
			
		||||
        log::debug!("Socket file does not exist: {}", socket_path);
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Try to create a manager and test basic connectivity
 | 
			
		||||
    match ZinitServiceManager::new(socket_path) {
 | 
			
		||||
        Ok(manager) => {
 | 
			
		||||
            // Test basic connectivity by trying to list services
 | 
			
		||||
            match manager.list() {
 | 
			
		||||
                Ok(_) => {
 | 
			
		||||
                    log::debug!("Socket {} is responsive", socket_path);
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    log::debug!("Socket {} exists but not responsive: {}", socket_path, e);
 | 
			
		||||
                    false
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            log::debug!("Failed to create manager for socket {}: {}", socket_path, e);
 | 
			
		||||
            false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a service manager appropriate for the current platform
 | 
			
		||||
///
 | 
			
		||||
/// - On macOS: Uses launchctl for service management
 | 
			
		||||
/// - On Linux: Uses zinit for service management with systemd fallback
 | 
			
		||||
///
 | 
			
		||||
/// # Returns
 | 
			
		||||
///
 | 
			
		||||
/// Returns a Result containing the service manager or an error if initialization fails.
 | 
			
		||||
/// On Linux, it first tries to discover a working zinit socket. If no zinit server is found,
 | 
			
		||||
/// it will fall back to systemd.
 | 
			
		||||
///
 | 
			
		||||
/// # Environment Variables
 | 
			
		||||
///
 | 
			
		||||
/// - `ZINIT_SOCKET_PATH`: Specifies the zinit socket path (Linux only)
 | 
			
		||||
///
 | 
			
		||||
/// # Errors
 | 
			
		||||
///
 | 
			
		||||
/// Returns `ServiceManagerError` if:
 | 
			
		||||
/// - The platform is not supported (Windows, etc.)
 | 
			
		||||
/// - Service manager initialization fails on all available backends
 | 
			
		||||
pub fn create_service_manager() -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
 | 
			
		||||
    #[cfg(target_os = "macos")]
 | 
			
		||||
    {
 | 
			
		||||
        Ok(Box::new(LaunchctlServiceManager::new()))
 | 
			
		||||
    }
 | 
			
		||||
    #[cfg(target_os = "linux")]
 | 
			
		||||
    {
 | 
			
		||||
        // Try to discover a working zinit socket
 | 
			
		||||
        if let Some(socket_path) = discover_zinit_socket() {
 | 
			
		||||
            match ZinitServiceManager::new(&socket_path) {
 | 
			
		||||
                Ok(zinit_manager) => {
 | 
			
		||||
                    log::info!("Using zinit service manager with socket: {}", socket_path);
 | 
			
		||||
                    return Ok(Box::new(zinit_manager));
 | 
			
		||||
                }
 | 
			
		||||
                Err(zinit_error) => {
 | 
			
		||||
                    log::warn!(
 | 
			
		||||
                        "Failed to create zinit manager for discovered socket {}: {}",
 | 
			
		||||
                        socket_path,
 | 
			
		||||
                        zinit_error
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            log::info!("No running zinit server detected. To use zinit, start it with: zinit -s /tmp/zinit.sock init");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fallback to systemd
 | 
			
		||||
        log::info!("Falling back to systemd service manager");
 | 
			
		||||
        Ok(Box::new(SystemdServiceManager::new()))
 | 
			
		||||
    }
 | 
			
		||||
    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
 | 
			
		||||
    {
 | 
			
		||||
        Err(ServiceManagerError::Other(
 | 
			
		||||
            "Service manager not implemented for this platform".to_string(),
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a service manager for zinit with a custom socket path
 | 
			
		||||
///
 | 
			
		||||
/// This is useful when zinit is running with a non-default socket path
 | 
			
		||||
pub fn create_zinit_service_manager(
 | 
			
		||||
    socket_path: &str,
 | 
			
		||||
) -> Result<Box<dyn ServiceManager>, ServiceManagerError> {
 | 
			
		||||
    Ok(Box::new(ZinitServiceManager::new(socket_path)?))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a service manager for systemd (Linux alternative)
 | 
			
		||||
///
 | 
			
		||||
/// This creates a systemd-based service manager as an alternative to zinit on Linux
 | 
			
		||||
#[cfg(target_os = "linux")]
 | 
			
		||||
pub fn create_systemd_service_manager() -> Box<dyn ServiceManager> {
 | 
			
		||||
    Box::new(SystemdServiceManager::new())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_create_service_manager() {
 | 
			
		||||
        // This test ensures the service manager can be created without panicking
 | 
			
		||||
        let result = create_service_manager();
 | 
			
		||||
        assert!(result.is_ok(), "Service manager creation should succeed");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(target_os = "linux")]
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_socket_discovery_with_env_var() {
 | 
			
		||||
        // Test that environment variable is respected
 | 
			
		||||
        std::env::set_var("ZINIT_SOCKET_PATH", "/test/path.sock");
 | 
			
		||||
 | 
			
		||||
        // The discover function should check the env var first
 | 
			
		||||
        // Since the socket doesn't exist, it should return None, but we can't test
 | 
			
		||||
        // the actual discovery logic without a real socket
 | 
			
		||||
 | 
			
		||||
        std::env::remove_var("ZINIT_SOCKET_PATH");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[cfg(target_os = "linux")]
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_socket_discovery_without_env_var() {
 | 
			
		||||
        // Ensure env var is not set
 | 
			
		||||
        std::env::remove_var("ZINIT_SOCKET_PATH");
 | 
			
		||||
 | 
			
		||||
        // The discover function should try common paths
 | 
			
		||||
        // Since no zinit is running, it should return None
 | 
			
		||||
        let result = discover_zinit_socket();
 | 
			
		||||
 | 
			
		||||
        // This is expected to be None in test environment
 | 
			
		||||
        assert!(
 | 
			
		||||
            result.is_none(),
 | 
			
		||||
            "Should return None when no zinit server is running"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										256
									
								
								_archive/service_manager/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								_archive/service_manager/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
			
		||||
//! Rhai integration for the service manager module
 | 
			
		||||
//!
 | 
			
		||||
//! This module provides Rhai scripting support for service management operations.
 | 
			
		||||
 | 
			
		||||
use crate::{create_service_manager, ServiceConfig, ServiceManager};
 | 
			
		||||
use rhai::{Engine, EvalAltResult, Map};
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
/// A wrapper around ServiceManager that can be used in Rhai
 | 
			
		||||
#[derive(Clone)]
 | 
			
		||||
pub struct RhaiServiceManager {
 | 
			
		||||
    inner: Arc<Box<dyn ServiceManager>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RhaiServiceManager {
 | 
			
		||||
    pub fn new() -> Result<Self, Box<EvalAltResult>> {
 | 
			
		||||
        let manager = create_service_manager()
 | 
			
		||||
            .map_err(|e| format!("Failed to create service manager: {}", e))?;
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            inner: Arc::new(manager),
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Register the service manager module with a Rhai engine
 | 
			
		||||
pub fn register_service_manager_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
    // Factory function to create service manager
 | 
			
		||||
    engine.register_type::<RhaiServiceManager>();
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "create_service_manager",
 | 
			
		||||
        || -> Result<RhaiServiceManager, Box<EvalAltResult>> { RhaiServiceManager::new() },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Service management functions
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "start",
 | 
			
		||||
        |manager: &mut RhaiServiceManager, config: Map| -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            let service_config = map_to_service_config(config)?;
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .start(&service_config)
 | 
			
		||||
                .map_err(|e| format!("Failed to start service: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "stop",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String|
 | 
			
		||||
         -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .stop(&service_name)
 | 
			
		||||
                .map_err(|e| format!("Failed to stop service: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "restart",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String|
 | 
			
		||||
         -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .restart(&service_name)
 | 
			
		||||
                .map_err(|e| format!("Failed to restart service: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "status",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String|
 | 
			
		||||
         -> Result<String, Box<EvalAltResult>> {
 | 
			
		||||
            let status = manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .status(&service_name)
 | 
			
		||||
                .map_err(|e| format!("Failed to get service status: {}", e))?;
 | 
			
		||||
            Ok(format!("{:?}", status))
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "logs",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String,
 | 
			
		||||
         lines: i64|
 | 
			
		||||
         -> Result<String, Box<EvalAltResult>> {
 | 
			
		||||
            let lines_opt = if lines > 0 {
 | 
			
		||||
                Some(lines as usize)
 | 
			
		||||
            } else {
 | 
			
		||||
                None
 | 
			
		||||
            };
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .logs(&service_name, lines_opt)
 | 
			
		||||
                .map_err(|e| format!("Failed to get service logs: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "list",
 | 
			
		||||
        |manager: &mut RhaiServiceManager| -> Result<Vec<String>, Box<EvalAltResult>> {
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .list()
 | 
			
		||||
                .map_err(|e| format!("Failed to list services: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "remove",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String|
 | 
			
		||||
         -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .remove(&service_name)
 | 
			
		||||
                .map_err(|e| format!("Failed to remove service: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "exists",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String|
 | 
			
		||||
         -> Result<bool, Box<EvalAltResult>> {
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .exists(&service_name)
 | 
			
		||||
                .map_err(|e| format!("Failed to check if service exists: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "start_and_confirm",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         config: Map,
 | 
			
		||||
         timeout_secs: i64|
 | 
			
		||||
         -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            let service_config = map_to_service_config(config)?;
 | 
			
		||||
            let timeout = if timeout_secs > 0 {
 | 
			
		||||
                timeout_secs as u64
 | 
			
		||||
            } else {
 | 
			
		||||
                30
 | 
			
		||||
            };
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .start_and_confirm(&service_config, timeout)
 | 
			
		||||
                .map_err(|e| format!("Failed to start and confirm service: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    engine.register_fn(
 | 
			
		||||
        "start_existing_and_confirm",
 | 
			
		||||
        |manager: &mut RhaiServiceManager,
 | 
			
		||||
         service_name: String,
 | 
			
		||||
         timeout_secs: i64|
 | 
			
		||||
         -> Result<(), Box<EvalAltResult>> {
 | 
			
		||||
            let timeout = if timeout_secs > 0 {
 | 
			
		||||
                timeout_secs as u64
 | 
			
		||||
            } else {
 | 
			
		||||
                30
 | 
			
		||||
            };
 | 
			
		||||
            manager
 | 
			
		||||
                .inner
 | 
			
		||||
                .start_existing_and_confirm(&service_name, timeout)
 | 
			
		||||
                .map_err(|e| format!("Failed to start existing service and confirm: {}", e).into())
 | 
			
		||||
        },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Convert a Rhai Map to a ServiceConfig
 | 
			
		||||
fn map_to_service_config(map: Map) -> Result<ServiceConfig, Box<EvalAltResult>> {
 | 
			
		||||
    let name = map
 | 
			
		||||
        .get("name")
 | 
			
		||||
        .and_then(|v| v.clone().into_string().ok())
 | 
			
		||||
        .ok_or("Service config must have a 'name' field")?;
 | 
			
		||||
 | 
			
		||||
    let binary_path = map
 | 
			
		||||
        .get("binary_path")
 | 
			
		||||
        .and_then(|v| v.clone().into_string().ok())
 | 
			
		||||
        .ok_or("Service config must have a 'binary_path' field")?;
 | 
			
		||||
 | 
			
		||||
    let args = map
 | 
			
		||||
        .get("args")
 | 
			
		||||
        .and_then(|v| v.clone().try_cast::<rhai::Array>())
 | 
			
		||||
        .map(|arr| {
 | 
			
		||||
            arr.into_iter()
 | 
			
		||||
                .filter_map(|v| v.into_string().ok())
 | 
			
		||||
                .collect::<Vec<String>>()
 | 
			
		||||
        })
 | 
			
		||||
        .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
    let working_directory = map
 | 
			
		||||
        .get("working_directory")
 | 
			
		||||
        .and_then(|v| v.clone().into_string().ok());
 | 
			
		||||
 | 
			
		||||
    let environment = map
 | 
			
		||||
        .get("environment")
 | 
			
		||||
        .and_then(|v| v.clone().try_cast::<Map>())
 | 
			
		||||
        .map(|env_map| {
 | 
			
		||||
            env_map
 | 
			
		||||
                .into_iter()
 | 
			
		||||
                .filter_map(|(k, v)| v.into_string().ok().map(|val| (k.to_string(), val)))
 | 
			
		||||
                .collect::<HashMap<String, String>>()
 | 
			
		||||
        })
 | 
			
		||||
        .unwrap_or_default();
 | 
			
		||||
 | 
			
		||||
    let auto_restart = map
 | 
			
		||||
        .get("auto_restart")
 | 
			
		||||
        .and_then(|v| v.as_bool().ok())
 | 
			
		||||
        .unwrap_or(false);
 | 
			
		||||
 | 
			
		||||
    Ok(ServiceConfig {
 | 
			
		||||
        name,
 | 
			
		||||
        binary_path,
 | 
			
		||||
        args,
 | 
			
		||||
        working_directory,
 | 
			
		||||
        environment,
 | 
			
		||||
        auto_restart,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod tests {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use rhai::{Engine, Map};
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_register_service_manager_module() {
 | 
			
		||||
        let mut engine = Engine::new();
 | 
			
		||||
        register_service_manager_module(&mut engine).unwrap();
 | 
			
		||||
 | 
			
		||||
        // Test that the functions are registered
 | 
			
		||||
        // Note: Rhai doesn't expose a public API to check if functions are registered
 | 
			
		||||
        // So we'll just verify the module registration doesn't panic
 | 
			
		||||
        assert!(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_map_to_service_config() {
 | 
			
		||||
        let mut map = Map::new();
 | 
			
		||||
        map.insert("name".into(), "test-service".into());
 | 
			
		||||
        map.insert("binary_path".into(), "/bin/echo".into());
 | 
			
		||||
        map.insert("auto_restart".into(), true.into());
 | 
			
		||||
 | 
			
		||||
        let config = map_to_service_config(map).unwrap();
 | 
			
		||||
        assert_eq!(config.name, "test-service");
 | 
			
		||||
        assert_eq!(config.binary_path, "/bin/echo");
 | 
			
		||||
        assert_eq!(config.auto_restart, true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										434
									
								
								_archive/service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										434
									
								
								_archive/service_manager/src/systemd.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,434 @@
 | 
			
		||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
 | 
			
		||||
use std::fs;
 | 
			
		||||
use std::path::PathBuf;
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
 | 
			
		||||
#[derive(Debug)]
 | 
			
		||||
pub struct SystemdServiceManager {
 | 
			
		||||
    service_prefix: String,
 | 
			
		||||
    user_mode: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl SystemdServiceManager {
 | 
			
		||||
    pub fn new() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            service_prefix: "sal".to_string(),
 | 
			
		||||
            user_mode: true, // Default to user services for safety
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn new_system() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            service_prefix: "sal".to_string(),
 | 
			
		||||
            user_mode: false, // System-wide services (requires root)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_service_name(&self, service_name: &str) -> String {
 | 
			
		||||
        format!("{}-{}.service", self.service_prefix, service_name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_unit_file_path(&self, service_name: &str) -> PathBuf {
 | 
			
		||||
        let service_file = self.get_service_name(service_name);
 | 
			
		||||
        if self.user_mode {
 | 
			
		||||
            // User service directory
 | 
			
		||||
            let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
 | 
			
		||||
            PathBuf::from(home)
 | 
			
		||||
                .join(".config")
 | 
			
		||||
                .join("systemd")
 | 
			
		||||
                .join("user")
 | 
			
		||||
                .join(service_file)
 | 
			
		||||
        } else {
 | 
			
		||||
            // System service directory
 | 
			
		||||
            PathBuf::from("/etc/systemd/system").join(service_file)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn run_systemctl(&self, args: &[&str]) -> Result<String, ServiceManagerError> {
 | 
			
		||||
        let mut cmd = Command::new("systemctl");
 | 
			
		||||
 | 
			
		||||
        if self.user_mode {
 | 
			
		||||
            cmd.arg("--user");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        cmd.args(args);
 | 
			
		||||
 | 
			
		||||
        let output = cmd
 | 
			
		||||
            .output()
 | 
			
		||||
            .map_err(|e| ServiceManagerError::Other(format!("Failed to run systemctl: {}", e)))?;
 | 
			
		||||
 | 
			
		||||
        if !output.status.success() {
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            return Err(ServiceManagerError::Other(format!(
 | 
			
		||||
                "systemctl command failed: {}",
 | 
			
		||||
                stderr
 | 
			
		||||
            )));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn create_unit_file(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let unit_path = self.get_unit_file_path(&config.name);
 | 
			
		||||
 | 
			
		||||
        // Ensure the directory exists
 | 
			
		||||
        if let Some(parent) = unit_path.parent() {
 | 
			
		||||
            fs::create_dir_all(parent).map_err(|e| {
 | 
			
		||||
                ServiceManagerError::Other(format!("Failed to create unit directory: {}", e))
 | 
			
		||||
            })?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the unit file content
 | 
			
		||||
        let mut unit_content = String::new();
 | 
			
		||||
        unit_content.push_str("[Unit]\n");
 | 
			
		||||
        unit_content.push_str(&format!("Description={} service\n", config.name));
 | 
			
		||||
        unit_content.push_str("After=network.target\n\n");
 | 
			
		||||
 | 
			
		||||
        unit_content.push_str("[Service]\n");
 | 
			
		||||
        unit_content.push_str("Type=simple\n");
 | 
			
		||||
 | 
			
		||||
        // Build the ExecStart command
 | 
			
		||||
        let mut exec_start = config.binary_path.clone();
 | 
			
		||||
        for arg in &config.args {
 | 
			
		||||
            exec_start.push(' ');
 | 
			
		||||
            exec_start.push_str(arg);
 | 
			
		||||
        }
 | 
			
		||||
        unit_content.push_str(&format!("ExecStart={}\n", exec_start));
 | 
			
		||||
 | 
			
		||||
        if let Some(working_dir) = &config.working_directory {
 | 
			
		||||
            unit_content.push_str(&format!("WorkingDirectory={}\n", working_dir));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add environment variables
 | 
			
		||||
        for (key, value) in &config.environment {
 | 
			
		||||
            unit_content.push_str(&format!("Environment=\"{}={}\"\n", key, value));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if config.auto_restart {
 | 
			
		||||
            unit_content.push_str("Restart=always\n");
 | 
			
		||||
            unit_content.push_str("RestartSec=5\n");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        unit_content.push_str("\n[Install]\n");
 | 
			
		||||
        unit_content.push_str("WantedBy=default.target\n");
 | 
			
		||||
 | 
			
		||||
        // Write the unit file
 | 
			
		||||
        fs::write(&unit_path, unit_content)
 | 
			
		||||
            .map_err(|e| ServiceManagerError::Other(format!("Failed to write unit file: {}", e)))?;
 | 
			
		||||
 | 
			
		||||
        // Reload systemd to pick up the new unit file
 | 
			
		||||
        self.run_systemctl(&["daemon-reload"])?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServiceManager for SystemdServiceManager {
 | 
			
		||||
    fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
 | 
			
		||||
        let unit_path = self.get_unit_file_path(service_name);
 | 
			
		||||
        Ok(unit_path.exists())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let service_name = self.get_service_name(&config.name);
 | 
			
		||||
 | 
			
		||||
        // Check if service already exists and is running
 | 
			
		||||
        if self.exists(&config.name)? {
 | 
			
		||||
            match self.status(&config.name)? {
 | 
			
		||||
                ServiceStatus::Running => {
 | 
			
		||||
                    return Err(ServiceManagerError::ServiceAlreadyExists(
 | 
			
		||||
                        config.name.clone(),
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
                _ => {
 | 
			
		||||
                    // Service exists but not running, we can start it
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Create the unit file
 | 
			
		||||
            self.create_unit_file(config)?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Enable and start the service
 | 
			
		||||
        self.run_systemctl(&["enable", &service_name])
 | 
			
		||||
            .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
 | 
			
		||||
 | 
			
		||||
        self.run_systemctl(&["start", &service_name])
 | 
			
		||||
            .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if unit file exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if already running
 | 
			
		||||
        match self.status(service_name)? {
 | 
			
		||||
            ServiceStatus::Running => {
 | 
			
		||||
                return Ok(()); // Already running, nothing to do
 | 
			
		||||
            }
 | 
			
		||||
            _ => {
 | 
			
		||||
                // Start the service
 | 
			
		||||
                self.run_systemctl(&["start", &service_unit]).map_err(|e| {
 | 
			
		||||
                    ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
                })?;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        config: &ServiceConfig,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Start the service first
 | 
			
		||||
        self.start(config)?;
 | 
			
		||||
 | 
			
		||||
        // Wait for confirmation with timeout
 | 
			
		||||
        let start_time = std::time::Instant::now();
 | 
			
		||||
        let timeout_duration = std::time::Duration::from_secs(timeout_secs);
 | 
			
		||||
 | 
			
		||||
        while start_time.elapsed() < timeout_duration {
 | 
			
		||||
            match self.status(&config.name) {
 | 
			
		||||
                Ok(ServiceStatus::Running) => return Ok(()),
 | 
			
		||||
                Ok(ServiceStatus::Failed) => {
 | 
			
		||||
                    return Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                        config.name.clone(),
 | 
			
		||||
                        "Service failed to start".to_string(),
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
                Ok(_) => {
 | 
			
		||||
                    // Still starting, wait a bit
 | 
			
		||||
                    std::thread::sleep(std::time::Duration::from_millis(100));
 | 
			
		||||
                }
 | 
			
		||||
                Err(_) => {
 | 
			
		||||
                    // Service might not exist yet, wait a bit
 | 
			
		||||
                    std::thread::sleep(std::time::Duration::from_millis(100));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Err(ServiceManagerError::StartFailed(
 | 
			
		||||
            config.name.clone(),
 | 
			
		||||
            format!("Service did not start within {} seconds", timeout_secs),
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Start the existing service first
 | 
			
		||||
        self.start_existing(service_name)?;
 | 
			
		||||
 | 
			
		||||
        // Wait for confirmation with timeout
 | 
			
		||||
        let start_time = std::time::Instant::now();
 | 
			
		||||
        let timeout_duration = std::time::Duration::from_secs(timeout_secs);
 | 
			
		||||
 | 
			
		||||
        while start_time.elapsed() < timeout_duration {
 | 
			
		||||
            match self.status(service_name) {
 | 
			
		||||
                Ok(ServiceStatus::Running) => return Ok(()),
 | 
			
		||||
                Ok(ServiceStatus::Failed) => {
 | 
			
		||||
                    return Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                        service_name.to_string(),
 | 
			
		||||
                        "Service failed to start".to_string(),
 | 
			
		||||
                    ));
 | 
			
		||||
                }
 | 
			
		||||
                Ok(_) => {
 | 
			
		||||
                    // Still starting, wait a bit
 | 
			
		||||
                    std::thread::sleep(std::time::Duration::from_millis(100));
 | 
			
		||||
                }
 | 
			
		||||
                Err(_) => {
 | 
			
		||||
                    // Service might not exist yet, wait a bit
 | 
			
		||||
                    std::thread::sleep(std::time::Duration::from_millis(100));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Err(ServiceManagerError::StartFailed(
 | 
			
		||||
            service_name.to_string(),
 | 
			
		||||
            format!("Service did not start within {} seconds", timeout_secs),
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if service exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Stop the service
 | 
			
		||||
        self.run_systemctl(&["stop", &service_unit]).map_err(|e| {
 | 
			
		||||
            ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if service exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Restart the service
 | 
			
		||||
        self.run_systemctl(&["restart", &service_unit])
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if service exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get service status
 | 
			
		||||
        let output = self
 | 
			
		||||
            .run_systemctl(&["is-active", &service_unit])
 | 
			
		||||
            .unwrap_or_else(|_| "unknown".to_string());
 | 
			
		||||
 | 
			
		||||
        let status = match output.trim() {
 | 
			
		||||
            "active" => ServiceStatus::Running,
 | 
			
		||||
            "inactive" => ServiceStatus::Stopped,
 | 
			
		||||
            "failed" => ServiceStatus::Failed,
 | 
			
		||||
            _ => ServiceStatus::Unknown,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        Ok(status)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn logs(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        lines: Option<usize>,
 | 
			
		||||
    ) -> Result<String, ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if service exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Build journalctl command
 | 
			
		||||
        let mut args = vec!["--unit", &service_unit, "--no-pager"];
 | 
			
		||||
        let lines_arg;
 | 
			
		||||
        if let Some(n) = lines {
 | 
			
		||||
            lines_arg = format!("--lines={}", n);
 | 
			
		||||
            args.push(&lines_arg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Use journalctl to get logs
 | 
			
		||||
        let mut cmd = std::process::Command::new("journalctl");
 | 
			
		||||
        if self.user_mode {
 | 
			
		||||
            cmd.arg("--user");
 | 
			
		||||
        }
 | 
			
		||||
        cmd.args(&args);
 | 
			
		||||
 | 
			
		||||
        let output = cmd.output().map_err(|e| {
 | 
			
		||||
            ServiceManagerError::LogsFailed(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
                format!("Failed to run journalctl: {}", e),
 | 
			
		||||
            )
 | 
			
		||||
        })?;
 | 
			
		||||
 | 
			
		||||
        if !output.status.success() {
 | 
			
		||||
            let stderr = String::from_utf8_lossy(&output.stderr);
 | 
			
		||||
            return Err(ServiceManagerError::LogsFailed(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
                format!("journalctl command failed: {}", stderr),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(String::from_utf8_lossy(&output.stdout).to_string())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
 | 
			
		||||
        // List all services with our prefix
 | 
			
		||||
        let output =
 | 
			
		||||
            self.run_systemctl(&["list-units", "--type=service", "--all", "--no-pager"])?;
 | 
			
		||||
 | 
			
		||||
        let mut services = Vec::new();
 | 
			
		||||
        for line in output.lines() {
 | 
			
		||||
            if line.contains(&format!("{}-", self.service_prefix)) {
 | 
			
		||||
                // Extract service name from the line
 | 
			
		||||
                if let Some(unit_name) = line.split_whitespace().next() {
 | 
			
		||||
                    if let Some(service_name) = unit_name.strip_suffix(".service") {
 | 
			
		||||
                        if let Some(name) =
 | 
			
		||||
                            service_name.strip_prefix(&format!("{}-", self.service_prefix))
 | 
			
		||||
                        {
 | 
			
		||||
                            services.push(name.to_string());
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(services)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let service_unit = self.get_service_name(service_name);
 | 
			
		||||
 | 
			
		||||
        // Check if service exists
 | 
			
		||||
        if !self.exists(service_name)? {
 | 
			
		||||
            return Err(ServiceManagerError::ServiceNotFound(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
            ));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Try to stop the service first, but don't fail if it's already stopped
 | 
			
		||||
        if let Err(e) = self.stop(service_name) {
 | 
			
		||||
            log::warn!(
 | 
			
		||||
                "Failed to stop service '{}' before removal: {}",
 | 
			
		||||
                service_name,
 | 
			
		||||
                e
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Disable the service
 | 
			
		||||
        if let Err(e) = self.run_systemctl(&["disable", &service_unit]) {
 | 
			
		||||
            log::warn!("Failed to disable service '{}': {}", service_name, e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Remove the unit file
 | 
			
		||||
        let unit_path = self.get_unit_file_path(service_name);
 | 
			
		||||
        if unit_path.exists() {
 | 
			
		||||
            std::fs::remove_file(&unit_path).map_err(|e| {
 | 
			
		||||
                ServiceManagerError::Other(format!("Failed to remove unit file: {}", e))
 | 
			
		||||
            })?;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Reload systemd to pick up the changes
 | 
			
		||||
        self.run_systemctl(&["daemon-reload"])?;
 | 
			
		||||
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										379
									
								
								_archive/service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								_archive/service_manager/src/zinit.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,379 @@
 | 
			
		||||
use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus};
 | 
			
		||||
use once_cell::sync::Lazy;
 | 
			
		||||
use serde_json::json;
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use tokio::runtime::Runtime;
 | 
			
		||||
use tokio::time::timeout;
 | 
			
		||||
use zinit_client::{ServiceStatus as ZinitServiceStatus, ZinitClient, ZinitError};
 | 
			
		||||
 | 
			
		||||
// Shared runtime for async operations - production-safe initialization
 | 
			
		||||
static ASYNC_RUNTIME: Lazy<Option<Runtime>> = Lazy::new(|| Runtime::new().ok());
 | 
			
		||||
 | 
			
		||||
/// Get the async runtime, creating a temporary one if the static runtime failed
 | 
			
		||||
fn get_runtime() -> Result<Runtime, ServiceManagerError> {
 | 
			
		||||
    // Try to use the static runtime first
 | 
			
		||||
    if let Some(_runtime) = ASYNC_RUNTIME.as_ref() {
 | 
			
		||||
        // We can't return a reference to the static runtime because we need ownership
 | 
			
		||||
        // for block_on, so we create a new one. This is a reasonable trade-off for safety.
 | 
			
		||||
        Runtime::new().map_err(|e| {
 | 
			
		||||
            ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
        // Static runtime failed, try to create a new one
 | 
			
		||||
        Runtime::new().map_err(|e| {
 | 
			
		||||
            ServiceManagerError::Other(format!("Failed to create async runtime: {}", e))
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct ZinitServiceManager {
 | 
			
		||||
    client: Arc<ZinitClient>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ZinitServiceManager {
 | 
			
		||||
    pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> {
 | 
			
		||||
        // Create the base zinit client directly
 | 
			
		||||
        let client = Arc::new(ZinitClient::new(socket_path));
 | 
			
		||||
 | 
			
		||||
        Ok(ZinitServiceManager { client })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Execute an async operation using the shared runtime or current context
 | 
			
		||||
    fn execute_async<F, T>(&self, operation: F) -> Result<T, ServiceManagerError>
 | 
			
		||||
    where
 | 
			
		||||
        F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
 | 
			
		||||
        T: Send + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        // Check if we're already in a tokio runtime context
 | 
			
		||||
        if let Ok(_handle) = tokio::runtime::Handle::try_current() {
 | 
			
		||||
            // We're in an async context, use spawn_blocking to avoid nested runtime
 | 
			
		||||
            let result = std::thread::spawn(
 | 
			
		||||
                move || -> Result<Result<T, ZinitError>, ServiceManagerError> {
 | 
			
		||||
                    let rt = Runtime::new().map_err(|e| {
 | 
			
		||||
                        ServiceManagerError::Other(format!("Failed to create runtime: {}", e))
 | 
			
		||||
                    })?;
 | 
			
		||||
                    Ok(rt.block_on(operation))
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            .join()
 | 
			
		||||
            .map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
 | 
			
		||||
            result?.map_err(|e| ServiceManagerError::Other(e.to_string()))
 | 
			
		||||
        } else {
 | 
			
		||||
            // No current runtime, use production-safe runtime
 | 
			
		||||
            let runtime = get_runtime()?;
 | 
			
		||||
            runtime
 | 
			
		||||
                .block_on(operation)
 | 
			
		||||
                .map_err(|e| ServiceManagerError::Other(e.to_string()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Execute an async operation with timeout using the shared runtime or current context
 | 
			
		||||
    fn execute_async_with_timeout<F, T>(
 | 
			
		||||
        &self,
 | 
			
		||||
        operation: F,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<T, ServiceManagerError>
 | 
			
		||||
    where
 | 
			
		||||
        F: std::future::Future<Output = Result<T, ZinitError>> + Send + 'static,
 | 
			
		||||
        T: Send + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        let timeout_duration = Duration::from_secs(timeout_secs);
 | 
			
		||||
        let timeout_op = timeout(timeout_duration, operation);
 | 
			
		||||
 | 
			
		||||
        // Check if we're already in a tokio runtime context
 | 
			
		||||
        if let Ok(_handle) = tokio::runtime::Handle::try_current() {
 | 
			
		||||
            // We're in an async context, use spawn_blocking to avoid nested runtime
 | 
			
		||||
            let result = std::thread::spawn(move || {
 | 
			
		||||
                let rt = tokio::runtime::Runtime::new().unwrap();
 | 
			
		||||
                rt.block_on(timeout_op)
 | 
			
		||||
            })
 | 
			
		||||
            .join()
 | 
			
		||||
            .map_err(|_| ServiceManagerError::Other("Thread join failed".to_string()))?;
 | 
			
		||||
 | 
			
		||||
            result
 | 
			
		||||
                .map_err(|_| {
 | 
			
		||||
                    ServiceManagerError::Other(format!(
 | 
			
		||||
                        "Operation timed out after {} seconds",
 | 
			
		||||
                        timeout_secs
 | 
			
		||||
                    ))
 | 
			
		||||
                })?
 | 
			
		||||
                .map_err(|e| ServiceManagerError::Other(e.to_string()))
 | 
			
		||||
        } else {
 | 
			
		||||
            // No current runtime, use production-safe runtime
 | 
			
		||||
            let runtime = get_runtime()?;
 | 
			
		||||
            runtime
 | 
			
		||||
                .block_on(timeout_op)
 | 
			
		||||
                .map_err(|_| {
 | 
			
		||||
                    ServiceManagerError::Other(format!(
 | 
			
		||||
                        "Operation timed out after {} seconds",
 | 
			
		||||
                        timeout_secs
 | 
			
		||||
                    ))
 | 
			
		||||
                })?
 | 
			
		||||
                .map_err(|e| ServiceManagerError::Other(e.to_string()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl ServiceManager for ZinitServiceManager {
 | 
			
		||||
    fn exists(&self, service_name: &str) -> Result<bool, ServiceManagerError> {
 | 
			
		||||
        let status_res = self.status(service_name);
 | 
			
		||||
        match status_res {
 | 
			
		||||
            Ok(_) => Ok(true),
 | 
			
		||||
            Err(ServiceManagerError::ServiceNotFound(_)) => Ok(false),
 | 
			
		||||
            Err(e) => Err(e),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Build the exec command with args
 | 
			
		||||
        let mut exec_command = config.binary_path.clone();
 | 
			
		||||
        if !config.args.is_empty() {
 | 
			
		||||
            exec_command.push(' ');
 | 
			
		||||
            exec_command.push_str(&config.args.join(" "));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create zinit-compatible service configuration
 | 
			
		||||
        let mut service_config = json!({
 | 
			
		||||
            "exec": exec_command,
 | 
			
		||||
            "oneshot": !config.auto_restart,  // zinit uses oneshot, not restart
 | 
			
		||||
            "env": config.environment,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add optional fields if present
 | 
			
		||||
        if let Some(ref working_dir) = config.working_directory {
 | 
			
		||||
            // Zinit doesn't support working_directory directly, so we need to modify the exec command
 | 
			
		||||
            let cd_command = format!("cd {} && {}", working_dir, exec_command);
 | 
			
		||||
            service_config["exec"] = json!(cd_command);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name = config.name.clone();
 | 
			
		||||
        self.execute_async(
 | 
			
		||||
            async move { client.create_service(&service_name, service_config).await },
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|e| ServiceManagerError::StartFailed(config.name.clone(), e.to_string()))?;
 | 
			
		||||
 | 
			
		||||
        self.start_existing(&config.name)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name_owned = service_name.to_string();
 | 
			
		||||
        let service_name_for_error = service_name.to_string();
 | 
			
		||||
        self.execute_async(async move { client.start(&service_name_owned).await })
 | 
			
		||||
            .map_err(|e| ServiceManagerError::StartFailed(service_name_for_error, e.to_string()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        config: &ServiceConfig,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Start the service first
 | 
			
		||||
        self.start(config)?;
 | 
			
		||||
 | 
			
		||||
        // Wait for confirmation with timeout using the shared runtime
 | 
			
		||||
        self.execute_async_with_timeout(
 | 
			
		||||
            async move {
 | 
			
		||||
                let start_time = std::time::Instant::now();
 | 
			
		||||
                let timeout_duration = Duration::from_secs(timeout_secs);
 | 
			
		||||
 | 
			
		||||
                while start_time.elapsed() < timeout_duration {
 | 
			
		||||
                    // We need to call status in a blocking way from within the async context
 | 
			
		||||
                    // For now, we'll use a simple polling approach
 | 
			
		||||
                    tokio::time::sleep(Duration::from_millis(100)).await;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Return a timeout error that will be handled by execute_async_with_timeout
 | 
			
		||||
                // Use a generic error since we don't know the exact ZinitError variants
 | 
			
		||||
                Err(ZinitError::from(std::io::Error::new(
 | 
			
		||||
                    std::io::ErrorKind::TimedOut,
 | 
			
		||||
                    "Timeout waiting for service confirmation",
 | 
			
		||||
                )))
 | 
			
		||||
            },
 | 
			
		||||
            timeout_secs,
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        // Check final status
 | 
			
		||||
        match self.status(&config.name)? {
 | 
			
		||||
            ServiceStatus::Running => Ok(()),
 | 
			
		||||
            ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                config.name.clone(),
 | 
			
		||||
                "Service failed to start".to_string(),
 | 
			
		||||
            )),
 | 
			
		||||
            _ => Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                config.name.clone(),
 | 
			
		||||
                format!("Service did not start within {} seconds", timeout_secs),
 | 
			
		||||
            )),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn start_existing_and_confirm(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        timeout_secs: u64,
 | 
			
		||||
    ) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Start the existing service first
 | 
			
		||||
        self.start_existing(service_name)?;
 | 
			
		||||
 | 
			
		||||
        // Wait for confirmation with timeout using the shared runtime
 | 
			
		||||
        self.execute_async_with_timeout(
 | 
			
		||||
            async move {
 | 
			
		||||
                let start_time = std::time::Instant::now();
 | 
			
		||||
                let timeout_duration = Duration::from_secs(timeout_secs);
 | 
			
		||||
 | 
			
		||||
                while start_time.elapsed() < timeout_duration {
 | 
			
		||||
                    tokio::time::sleep(Duration::from_millis(100)).await;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Return a timeout error that will be handled by execute_async_with_timeout
 | 
			
		||||
                // Use a generic error since we don't know the exact ZinitError variants
 | 
			
		||||
                Err(ZinitError::from(std::io::Error::new(
 | 
			
		||||
                    std::io::ErrorKind::TimedOut,
 | 
			
		||||
                    "Timeout waiting for service confirmation",
 | 
			
		||||
                )))
 | 
			
		||||
            },
 | 
			
		||||
            timeout_secs,
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        // Check final status
 | 
			
		||||
        match self.status(service_name)? {
 | 
			
		||||
            ServiceStatus::Running => Ok(()),
 | 
			
		||||
            ServiceStatus::Failed => Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
                "Service failed to start".to_string(),
 | 
			
		||||
            )),
 | 
			
		||||
            _ => Err(ServiceManagerError::StartFailed(
 | 
			
		||||
                service_name.to_string(),
 | 
			
		||||
                format!("Service did not start within {} seconds", timeout_secs),
 | 
			
		||||
            )),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name_owned = service_name.to_string();
 | 
			
		||||
        let service_name_for_error = service_name.to_string();
 | 
			
		||||
        self.execute_async(async move { client.stop(&service_name_owned).await })
 | 
			
		||||
            .map_err(|e| ServiceManagerError::StopFailed(service_name_for_error, e.to_string()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name_owned = service_name.to_string();
 | 
			
		||||
        let service_name_for_error = service_name.to_string();
 | 
			
		||||
        self.execute_async(async move { client.restart(&service_name_owned).await })
 | 
			
		||||
            .map_err(|e| ServiceManagerError::RestartFailed(service_name_for_error, e.to_string()))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name_owned = service_name.to_string();
 | 
			
		||||
        let service_name_for_error = service_name.to_string();
 | 
			
		||||
        let status: ZinitServiceStatus = self
 | 
			
		||||
            .execute_async(async move { client.status(&service_name_owned).await })
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                // Check if this is a "service not found" error
 | 
			
		||||
                if e.to_string().contains("not found") || e.to_string().contains("does not exist") {
 | 
			
		||||
                    ServiceManagerError::ServiceNotFound(service_name_for_error)
 | 
			
		||||
                } else {
 | 
			
		||||
                    ServiceManagerError::Other(e.to_string())
 | 
			
		||||
                }
 | 
			
		||||
            })?;
 | 
			
		||||
 | 
			
		||||
        // ServiceStatus is a struct with fields, not an enum
 | 
			
		||||
        // We need to check the state field to determine the status
 | 
			
		||||
        // Convert ServiceState to string and match on that
 | 
			
		||||
        let state_str = format!("{:?}", status.state).to_lowercase();
 | 
			
		||||
        let service_status = match state_str.as_str() {
 | 
			
		||||
            s if s.contains("running") => crate::ServiceStatus::Running,
 | 
			
		||||
            s if s.contains("stopped") => crate::ServiceStatus::Stopped,
 | 
			
		||||
            s if s.contains("failed") => crate::ServiceStatus::Failed,
 | 
			
		||||
            _ => crate::ServiceStatus::Unknown,
 | 
			
		||||
        };
 | 
			
		||||
        Ok(service_status)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn logs(
 | 
			
		||||
        &self,
 | 
			
		||||
        service_name: &str,
 | 
			
		||||
        _lines: Option<usize>,
 | 
			
		||||
    ) -> Result<String, ServiceManagerError> {
 | 
			
		||||
        // The logs method takes (follow: bool, filter: Option<impl AsRef<str>>)
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name_owned = service_name.to_string();
 | 
			
		||||
        let logs = self
 | 
			
		||||
            .execute_async(async move {
 | 
			
		||||
                use futures::StreamExt;
 | 
			
		||||
                use tokio::time::{timeout, Duration};
 | 
			
		||||
 | 
			
		||||
                let mut log_stream = client
 | 
			
		||||
                    .logs(false, Some(service_name_owned.as_str()))
 | 
			
		||||
                    .await?;
 | 
			
		||||
                let mut logs = Vec::new();
 | 
			
		||||
 | 
			
		||||
                // Collect logs from the stream with a reasonable limit
 | 
			
		||||
                let mut count = 0;
 | 
			
		||||
                const MAX_LOGS: usize = 100;
 | 
			
		||||
                const LOG_TIMEOUT: Duration = Duration::from_secs(5);
 | 
			
		||||
 | 
			
		||||
                // Use timeout to prevent hanging
 | 
			
		||||
                let result = timeout(LOG_TIMEOUT, async {
 | 
			
		||||
                    while let Some(log_result) = log_stream.next().await {
 | 
			
		||||
                        match log_result {
 | 
			
		||||
                            Ok(log_entry) => {
 | 
			
		||||
                                logs.push(format!("{:?}", log_entry));
 | 
			
		||||
                                count += 1;
 | 
			
		||||
                                if count >= MAX_LOGS {
 | 
			
		||||
                                    break;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            Err(_) => break,
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .await;
 | 
			
		||||
 | 
			
		||||
                // Handle timeout - this is not an error, just means no more logs available
 | 
			
		||||
                if result.is_err() {
 | 
			
		||||
                    log::debug!(
 | 
			
		||||
                        "Log reading timed out after {} seconds, returning {} logs",
 | 
			
		||||
                        LOG_TIMEOUT.as_secs(),
 | 
			
		||||
                        logs.len()
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                Ok::<Vec<String>, ZinitError>(logs)
 | 
			
		||||
            })
 | 
			
		||||
            .map_err(|e| {
 | 
			
		||||
                ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string())
 | 
			
		||||
            })?;
 | 
			
		||||
        Ok(logs.join("\n"))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let services = self
 | 
			
		||||
            .execute_async(async move { client.list().await })
 | 
			
		||||
            .map_err(|e| ServiceManagerError::Other(e.to_string()))?;
 | 
			
		||||
        Ok(services.keys().cloned().collect())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> {
 | 
			
		||||
        // Try to stop the service first, but don't fail if it's already stopped or doesn't exist
 | 
			
		||||
        if let Err(e) = self.stop(service_name) {
 | 
			
		||||
            // Log the error but continue with removal
 | 
			
		||||
            log::warn!(
 | 
			
		||||
                "Failed to stop service '{}' before removal: {}",
 | 
			
		||||
                service_name,
 | 
			
		||||
                e
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let client = Arc::clone(&self.client);
 | 
			
		||||
        let service_name = service_name.to_string();
 | 
			
		||||
        self.execute_async(async move { client.delete_service(&service_name).await })
 | 
			
		||||
            .map_err(|e| ServiceManagerError::Other(e.to_string()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user