add service manager sal

This commit is contained in:
Timur Gordon 2025-07-01 09:11:45 +02:00
parent 717cd7b16f
commit b4e370b668
6 changed files with 751 additions and 0 deletions

View 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
View 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(())
}
```

View 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
View 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")
}
}

View 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()))
}
}

View 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()))
}
}