Compare commits
No commits in common. "46ad848e7e19a4bbd43d618b7d32a7669e865f34" and "ef8cc74d2b53b849e8c4722de3285c5d16d9a6cf" have entirely different histories.
46ad848e7e
...
ef8cc74d2b
@ -1,22 +0,0 @@
|
|||||||
[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"]
|
|
@ -1,54 +0,0 @@
|
|||||||
# 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(())
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,399 +0,0 @@
|
|||||||
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(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
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