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:
@@ -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?;
|
||||
|
@@ -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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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!(
|
||||
|
Reference in New Issue
Block a user