This commit is contained in:
2025-08-05 15:33:03 +02:00
parent 7856fc0a4e
commit 0c02d0e99f
326 changed files with 334 additions and 0 deletions

View 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(())
})
}
}

View 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"
);
}
}

View 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);
}
}

View 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(())
}
}

View 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()))
}
}