Compare commits
1 Commits
main-rfs-c
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
b4e370b668 |
22
service_manager/Cargo.toml
Normal file
22
service_manager/Cargo.toml
Normal file
@ -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"]
|
54
service_manager/README.md
Normal file
54
service_manager/README.md
Normal file
@ -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<dyn std::error::Error>> {
|
||||
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(())
|
||||
}
|
||||
```
|
399
service_manager/src/launchctl.rs
Normal file
399
service_manager/src/launchctl.rs
Normal file
@ -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<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, 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::<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)
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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> {
|
||||
// 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<ServiceStatus, 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);
|
||||
|
||||
// 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 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<Vec<String>, 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<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> {
|
||||
// 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(())
|
||||
})
|
||||
}
|
||||
}
|
112
service_manager/src/lib.rs
Normal file
112
service_manager/src/lib.rs
Normal file
@ -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<String>,
|
||||
pub working_directory: Option<String>,
|
||||
pub environment: HashMap<String, String>,
|
||||
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<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
|
||||
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<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;
|
||||
|
||||
#[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<dyn ServiceManager> {
|
||||
#[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")
|
||||
}
|
||||
}
|
42
service_manager/src/systemd.rs
Normal file
42
service_manager/src/systemd.rs
Normal file
@ -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<ServiceStatus, ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn logs(&self, _service_name: &str, _lines: Option<usize>) -> Result<String, ServiceManagerError> {
|
||||
Err(ServiceManagerError::Other("Systemd implementation not yet complete".to_string()))
|
||||
}
|
||||
|
||||
async fn list(&self) -> Result<Vec<String>, 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()))
|
||||
}
|
||||
}
|
122
service_manager/src/zinit.rs
Normal file
122
service_manager/src/zinit.rs
Normal file
@ -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<ZinitClientWrapper>,
|
||||
}
|
||||
|
||||
impl ZinitServiceManager {
|
||||
pub fn new(socket_path: &str) -> Result<Self, ServiceManagerError> {
|
||||
// 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<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> {
|
||||
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<ServiceStatus, ServiceManagerError> {
|
||||
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<usize>) -> Result<String, ServiceManagerError> {
|
||||
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<Vec<String>, 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()))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user