302 lines
9.6 KiB
Rust
302 lines
9.6 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|