From b4e370b668beb7645ba15113c656995b141d0c0c Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Tue, 1 Jul 2025 09:11:45 +0200 Subject: [PATCH] add service manager sal --- service_manager/Cargo.toml | 22 ++ service_manager/README.md | 54 +++++ service_manager/src/launchctl.rs | 399 +++++++++++++++++++++++++++++++ service_manager/src/lib.rs | 112 +++++++++ service_manager/src/systemd.rs | 42 ++++ service_manager/src/zinit.rs | 122 ++++++++++ 6 files changed, 751 insertions(+) create mode 100644 service_manager/Cargo.toml create mode 100644 service_manager/README.md create mode 100644 service_manager/src/launchctl.rs create mode 100644 service_manager/src/lib.rs create mode 100644 service_manager/src/systemd.rs create mode 100644 service_manager/src/zinit.rs diff --git a/service_manager/Cargo.toml b/service_manager/Cargo.toml new file mode 100644 index 0000000..79c5503 --- /dev/null +++ b/service_manager/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "sal-service-manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = "0.1" +thiserror = "1.0" +tokio = { workspace = true } +log = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true, optional = true } + +zinit_client = { package = "sal-zinit-client", path = "../zinit_client", optional = true } + +[target.'cfg(target_os = "macos")'.dependencies] +# macOS-specific dependencies for launchctl +plist = "1.6" + +[features] +default = [] +zinit = ["dep:zinit_client", "dep:serde_json"] \ No newline at end of file diff --git a/service_manager/README.md b/service_manager/README.md new file mode 100644 index 0000000..b7c45fb --- /dev/null +++ b/service_manager/README.md @@ -0,0 +1,54 @@ +# Service Manager + +This crate provides a unified interface for managing system services across different platforms. +It abstracts the underlying service management system (like `launchctl` on macOS or `systemd` on Linux), +allowing you to start, stop, and monitor services with a consistent API. + +## Features + +- A `ServiceManager` trait defining a common interface for service operations. +- Platform-specific implementations for: + - macOS (`launchctl`) + - Linux (`systemd`) +- A factory function `create_service_manager` that returns the appropriate manager for the current platform. + +## Usage + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +service_manager = { path = "../service_manager" } +``` + +Here is an example of how to use the `ServiceManager`: + +```rust,no_run +use service_manager::{create_service_manager, ServiceConfig}; +use std::collections::HashMap; + +fn main() -> Result<(), Box> { + let service_manager = create_service_manager(); + + let config = ServiceConfig { + name: "my-service".to_string(), + binary_path: "/usr/local/bin/my-service-executable".to_string(), + args: vec!["--config".to_string(), "/etc/my-service.conf".to_string()], + working_directory: Some("/var/tmp".to_string()), + environment: HashMap::new(), + auto_restart: true, + }; + + // Start a new service + service_manager.start(&config)?; + + // Get the status of the service + let status = service_manager.status("my-service")?; + println!("Service status: {:?}", status); + + // Stop the service + service_manager.stop("my-service")?; + + Ok(()) +} +``` diff --git a/service_manager/src/launchctl.rs b/service_manager/src/launchctl.rs new file mode 100644 index 0000000..bb89d13 --- /dev/null +++ b/service_manager/src/launchctl.rs @@ -0,0 +1,399 @@ +use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use tokio::process::Command; + +#[derive(Debug)] +pub struct LaunchctlServiceManager { + service_prefix: String, +} + +#[derive(Serialize, Deserialize)] +struct LaunchDaemon { + #[serde(rename = "Label")] + label: String, + #[serde(rename = "ProgramArguments")] + program_arguments: Vec, + #[serde(rename = "WorkingDirectory", skip_serializing_if = "Option::is_none")] + working_directory: Option, + #[serde(rename = "EnvironmentVariables", skip_serializing_if = "Option::is_none")] + environment_variables: Option>, + #[serde(rename = "KeepAlive", skip_serializing_if = "Option::is_none")] + keep_alive: Option, + #[serde(rename = "RunAtLoad")] + run_at_load: bool, + #[serde(rename = "StandardOutPath", skip_serializing_if = "Option::is_none")] + standard_out_path: Option, + #[serde(rename = "StandardErrorPath", skip_serializing_if = "Option::is_none")] + standard_error_path: Option, +} + +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 { + 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, Duration, timeout}; + + 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::>().into_iter().rev().collect::>().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) + )), + } + } +} + +#[async_trait] +impl ServiceManager for LaunchctlServiceManager { + fn exists(&self, service_name: &str) -> Result { + let plist_path = self.get_plist_path(service_name); + Ok(plist_path.exists()) + } + + fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> { + // For synchronous version, we'll use blocking operations + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.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 rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.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(()) + }) + } + + async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { + // First start the service + self.start(config)?; + + // Then wait for confirmation + self.wait_for_service_status(&config.name, timeout_secs).await + } + + async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError> { + self.start_and_confirm(config, timeout_secs).await + } + + async 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 + self.wait_for_service_status(service_name, timeout_secs).await + } + + fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.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 { + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.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) -> Result { + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.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, ServiceManagerError> { + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.block_on(async { + let list_output = self.run_launchctl(&["list"]).await?; + + let services: Vec = 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> { + // Stop the service first + let _ = self.stop(service_name); + + // Remove the plist file + let rt = tokio::runtime::Runtime::new().map_err(|e| ServiceManagerError::Other(e.to_string()))?; + rt.block_on(async { + let plist_path = self.get_plist_path(service_name); + if plist_path.exists() { + tokio::fs::remove_file(&plist_path).await?; + } + Ok(()) + }) + } +} \ No newline at end of file diff --git a/service_manager/src/lib.rs b/service_manager/src/lib.rs new file mode 100644 index 0000000..63b1891 --- /dev/null +++ b/service_manager/src/lib.rs @@ -0,0 +1,112 @@ +use async_trait::async_trait; +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, + pub working_directory: Option, + pub environment: HashMap, + pub auto_restart: bool, +} + +#[derive(Debug, Clone)] +pub enum ServiceStatus { + Running, + Stopped, + Failed, + Unknown, +} + +#[async_trait] +pub trait ServiceManager: Send + Sync { + /// Check if a service exists + fn exists(&self, service_name: &str) -> Result; + + /// 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 + async fn start_and_confirm(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; + + /// Start a service and wait for confirmation that it's running or failed + async fn run(&self, config: &ServiceConfig, timeout_secs: u64) -> Result<(), ServiceManagerError>; + + /// Start an existing service and wait for confirmation that it's running or failed + async 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; + + /// Get logs for a service + fn logs(&self, service_name: &str, lines: Option) -> Result; + + /// List all managed services + fn list(&self) -> Result, 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; + +#[cfg(feature = "zinit")] +mod zinit; +#[cfg(feature = "zinit")] +pub use zinit::ZinitServiceManager; + +// Factory function to create the appropriate service manager for the platform +pub fn create_service_manager() -> Box { + #[cfg(target_os = "macos")] + { + Box::new(LaunchctlServiceManager::new()) + } + #[cfg(target_os = "linux")] + { + Box::new(SystemdServiceManager::new()) + } + #[cfg(not(any(target_os = "macos", target_os = "linux")))] + { + compile_error!("Service manager not implemented for this platform") + } +} \ No newline at end of file diff --git a/service_manager/src/systemd.rs b/service_manager/src/systemd.rs new file mode 100644 index 0000000..83f2c13 --- /dev/null +++ b/service_manager/src/systemd.rs @@ -0,0 +1,42 @@ +use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; +use async_trait::async_trait; + +#[derive(Debug)] +pub struct SystemdServiceManager; + +impl SystemdServiceManager { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl ServiceManager for SystemdServiceManager { + async fn start(&self, _config: &ServiceConfig) -> Result<(), ServiceManagerError> { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn stop(&self, _service_name: &str) -> Result<(), ServiceManagerError> { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn restart(&self, _service_name: &str) -> Result<(), ServiceManagerError> { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn status(&self, _service_name: &str) -> Result { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn logs(&self, _service_name: &str, _lines: Option) -> Result { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn list(&self) -> Result, ServiceManagerError> { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } + + async fn remove(&self, _service_name: &str) -> Result<(), ServiceManagerError> { + Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string())) + } +} \ No newline at end of file diff --git a/service_manager/src/zinit.rs b/service_manager/src/zinit.rs new file mode 100644 index 0000000..69e85b1 --- /dev/null +++ b/service_manager/src/zinit.rs @@ -0,0 +1,122 @@ +use crate::{ServiceConfig, ServiceManager, ServiceManagerError, ServiceStatus}; +use async_trait::async_trait; +use serde_json::json; +use std::sync::Arc; +use zinit_client::{get_zinit_client, ServiceStatus as ZinitServiceStatus, ZinitClientWrapper}; + +pub struct ZinitServiceManager { + client: Arc, +} + +impl ZinitServiceManager { + pub fn new(socket_path: &str) -> Result { + // This is a blocking call to get the async client. + // We might want to make this async in the future if the constructor can be async. + let client = tokio::runtime::Runtime::new() + .unwrap() + .block_on(get_zinit_client(socket_path)) + .map_err(|e| ServiceManagerError::Other(e.to_string()))?; + Ok(ZinitServiceManager { client }) + } +} + +#[async_trait] +impl ServiceManager for ZinitServiceManager { + fn exists(&self, service_name: &str) -> Result { + 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> { + let service_config = json!({ + "exec": config.binary_path, + "args": config.args, + "working_directory": config.working_directory, + "env": config.environment, + "restart": config.auto_restart, + }); + + tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.create_service(&config.name, service_config)) + .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> { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.start(service_name)) + .map_err(|e| ServiceManagerError::StartFailed(service_name.to_string(), e.to_string())) + } + + async fn start_and_confirm(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { + self.start(config) + } + + async fn run(&self, config: &ServiceConfig, _timeout_secs: u64) -> Result<(), ServiceManagerError> { + self.start(config) + } + + async fn start_existing_and_confirm(&self, service_name: &str, _timeout_secs: u64) -> Result<(), ServiceManagerError> { + self.start_existing(service_name) + } + + fn stop(&self, service_name: &str) -> Result<(), ServiceManagerError> { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.stop(service_name)) + .map_err(|e| ServiceManagerError::StopFailed(service_name.to_string(), e.to_string())) + } + + fn restart(&self, service_name: &str) -> Result<(), ServiceManagerError> { + tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.restart(service_name)) + .map_err(|e| ServiceManagerError::RestartFailed(service_name.to_string(), e.to_string())) + } + + fn status(&self, service_name: &str) -> Result { + let status: ZinitServiceStatus = tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.status(service_name)) + .map_err(|e| ServiceManagerError::Other(e.to_string()))?; + + let service_status = match status { + ZinitServiceStatus::Running(_) => crate::ServiceStatus::Running, + ZinitServiceStatus::Stopped => crate::ServiceStatus::Stopped, + ZinitServiceStatus::Failed(_) => crate::ServiceStatus::Failed, + ZinitServiceStatus::Waiting(_) => crate::ServiceStatus::Unknown, + }; + Ok(service_status) + } + + fn logs(&self, service_name: &str, _lines: Option) -> Result { + let logs = tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.logs(Some(service_name.to_string()))) + .map_err(|e| ServiceManagerError::LogsFailed(service_name.to_string(), e.to_string()))?; + Ok(logs.join("\n")) + } + + fn list(&self) -> Result, ServiceManagerError> { + let services = tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.list()) + .map_err(|e| ServiceManagerError::Other(e.to_string()))?; + Ok(services.keys().cloned().collect()) + } + + fn remove(&self, service_name: &str) -> Result<(), ServiceManagerError> { + let _ = self.stop(service_name); // Best effort to stop before removing + tokio::runtime::Runtime::new() + .unwrap() + .block_on(self.client.delete_service(service_name)) + .map_err(|e| ServiceManagerError::Other(e.to_string())) + } +}