feat: Improve service manager testing and error handling

- Add comprehensive testing instructions to README.
- Improve error handling in examples to prevent crashes.
- Enhance launchctl error handling for production safety.
- Improve zinit error handling for production safety.
- Remove obsolete plan_to_fix.md file.
- Update Rhai integration tests for improved robustness.
- Improve service manager creation on Linux with systemd fallback.
This commit is contained in:
Mahmoud-Emad
2025-07-02 12:05:03 +03:00
parent a63cbe2bd9
commit 95122dffee
12 changed files with 473 additions and 422 deletions

View File

@@ -6,10 +6,25 @@ use std::path::PathBuf;
use tokio::process::Command;
use tokio::runtime::Runtime;
// Shared runtime for async operations
static ASYNC_RUNTIME: Lazy<Runtime> = Lazy::new(|| {
Runtime::new().expect("Failed to create async runtime for LaunchctlServiceManager")
});
// 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 {
@@ -221,8 +236,9 @@ impl ServiceManager for LaunchctlServiceManager {
}
fn start(&self, config: &ServiceConfig) -> Result<(), ServiceManagerError> {
// Use the shared runtime for async operations
ASYNC_RUNTIME.block_on(async {
// 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
@@ -249,7 +265,8 @@ impl ServiceManager for LaunchctlServiceManager {
}
fn start_existing(&self, service_name: &str) -> Result<(), ServiceManagerError> {
ASYNC_RUNTIME.block_on(async {
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);
@@ -300,8 +317,9 @@ impl ServiceManager for LaunchctlServiceManager {
// First start the service
self.start(config)?;
// Then wait for confirmation using the shared runtime
ASYNC_RUNTIME.block_on(async {
// 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
})
@@ -315,15 +333,17 @@ impl ServiceManager for LaunchctlServiceManager {
// First start the existing service
self.start_existing(service_name)?;
// Then wait for confirmation using the shared runtime
ASYNC_RUNTIME.block_on(async {
// 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> {
ASYNC_RUNTIME.block_on(async {
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);
@@ -359,7 +379,8 @@ impl ServiceManager for LaunchctlServiceManager {
}
fn status(&self, service_name: &str) -> Result<ServiceStatus, ServiceManagerError> {
ASYNC_RUNTIME.block_on(async {
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);
@@ -397,7 +418,8 @@ impl ServiceManager for LaunchctlServiceManager {
service_name: &str,
lines: Option<usize>,
) -> Result<String, ServiceManagerError> {
ASYNC_RUNTIME.block_on(async {
let runtime = get_runtime()?;
runtime.block_on(async {
let log_path = self.get_log_path(service_name);
if !log_path.exists() {
@@ -421,7 +443,8 @@ impl ServiceManager for LaunchctlServiceManager {
}
fn list(&self) -> Result<Vec<String>, ServiceManagerError> {
ASYNC_RUNTIME.block_on(async {
let runtime = get_runtime()?;
runtime.block_on(async {
let list_output = self.run_launchctl(&["list"]).await?;
let services: Vec<String> = list_output
@@ -456,8 +479,9 @@ impl ServiceManager for LaunchctlServiceManager {
);
}
// Remove the plist file using the shared runtime
ASYNC_RUNTIME.block_on(async {
// 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?;

View File

@@ -103,28 +103,47 @@ pub mod rhai;
/// Create a service manager appropriate for the current platform
///
/// - On macOS: Uses launchctl for service management
/// - On Linux: Uses zinit for service management (requires zinit to be installed and running)
/// - On Linux: Uses zinit for service management with systemd fallback
///
/// # Panics
/// # Returns
///
/// Panics on unsupported platforms (Windows, etc.)
pub fn create_service_manager() -> Box<dyn ServiceManager> {
/// Returns a Result containing the service manager or an error if initialization fails.
/// On Linux, if zinit is not available, it will automatically fall back to systemd.
///
/// # 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")]
{
Box::new(LaunchctlServiceManager::new())
Ok(Box::new(LaunchctlServiceManager::new()))
}
#[cfg(target_os = "linux")]
{
// Use zinit as the default service manager on Linux
// Default socket path for zinit
// Try zinit first (preferred), fall back to systemd if it fails
let socket_path = "/tmp/zinit.sock";
Box::new(
ZinitServiceManager::new(socket_path).expect("Failed to create ZinitServiceManager"),
)
match ZinitServiceManager::new(socket_path) {
Ok(zinit_manager) => {
log::debug!("Using zinit service manager");
Ok(Box::new(zinit_manager))
}
Err(zinit_error) => {
log::warn!(
"Zinit service manager failed, falling back to systemd: {}",
zinit_error
);
// Fallback to systemd
Ok(Box::new(SystemdServiceManager::new()))
}
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
compile_error!("Service manager not implemented for this platform")
Err(ServiceManagerError::Other(
"Service manager not implemented for this platform".to_string(),
))
}
}

View File

@@ -14,10 +14,12 @@ pub struct RhaiServiceManager {
}
impl RhaiServiceManager {
pub fn new() -> Self {
Self {
inner: Arc::new(create_service_manager()),
}
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),
})
}
}
@@ -25,7 +27,10 @@ impl RhaiServiceManager {
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", RhaiServiceManager::new);
engine.register_fn(
"create_service_manager",
|| -> Result<RhaiServiceManager, Box<EvalAltResult>> { RhaiServiceManager::new() },
);
// Service management functions
engine.register_fn(

View File

@@ -7,9 +7,25 @@ use tokio::runtime::Runtime;
use tokio::time::timeout;
use zinit_client::{ServiceStatus as ZinitServiceStatus, ZinitClient, ZinitError};
// Shared runtime for async operations
static ASYNC_RUNTIME: Lazy<Runtime> =
Lazy::new(|| Runtime::new().expect("Failed to create async runtime for ZinitServiceManager"));
// 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>,
@@ -32,16 +48,21 @@ impl ZinitServiceManager {
// 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(operation)
})
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()))
result?.map_err(|e| ServiceManagerError::Other(e.to_string()))
} else {
// No current runtime, use the shared runtime
ASYNC_RUNTIME
// No current runtime, use production-safe runtime
let runtime = get_runtime()?;
runtime
.block_on(operation)
.map_err(|e| ServiceManagerError::Other(e.to_string()))
}
@@ -79,8 +100,9 @@ impl ZinitServiceManager {
})?
.map_err(|e| ServiceManagerError::Other(e.to_string()))
} else {
// No current runtime, use the shared runtime
ASYNC_RUNTIME
// No current runtime, use production-safe runtime
let runtime = get_runtime()?;
runtime
.block_on(timeout_op)
.map_err(|_| {
ServiceManagerError::Other(format!(