feat: Add zinit_client package to workspace
Some checks are pending
Rhai Tests / Run Rhai Tests (push) Waiting to run

- Add `zinit_client` package to the workspace, enabling its use
  in the SAL monorepo.  This allows for better organization and
  dependency management.
- Update `MONOREPO_CONVERSION_PLAN.md` to reflect the addition
  of `zinit_client` and its status.  This ensures the conversion
  plan stays up-to-date.
- Move `src/zinit_client/` directory to `zinit_client/` for better
   organization.  This improves the overall structure of the
   project.
- Update references to `zinit_client` to use the new path.  This
  ensures the codebase correctly links to the `zinit_client`
  package.
This commit is contained in:
Mahmoud-Emad
2025-06-22 10:59:19 +03:00
parent 74217364fa
commit 511729c477
17 changed files with 2681 additions and 455 deletions

363
zinit_client/src/lib.rs Normal file
View File

@@ -0,0 +1,363 @@
//! SAL Zinit Client
//!
//! This crate provides a Rust interface for interacting with a Zinit process supervisor daemon.
//! Zinit is a process and service manager for Linux systems, designed for simplicity and robustness.
//!
//! # Features
//!
//! - Async operations using tokio
//! - Unix socket communication with Zinit daemon
//! - Global client instance management
//! - Comprehensive service management (start, stop, restart, monitor, etc.)
//! - Service configuration management (create, delete, get)
//! - Log retrieval from Zinit
//! - Rhai scripting integration
//!
//! # Example
//!
//! ```rust,no_run
//! use sal_zinit_client::{list, status};
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let socket_path = "/var/run/zinit.sock";
//!
//! // List all services
//! let services = list(socket_path).await?;
//! println!("Services: {:?}", services);
//!
//! // Get status of a specific service
//! if let Some(service_name) = services.keys().next() {
//! let status = status(socket_path, service_name).await?;
//! println!("Status: {:?}", status);
//! }
//!
//! Ok(())
//! }
//! ```
pub mod rhai;
use lazy_static::lazy_static;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use zinit_client::{ServiceState, ServiceStatus as Status, ZinitClient, ZinitError};
// Global Zinit client instance using lazy_static
lazy_static! {
static ref ZINIT_CLIENT: Mutex<Option<Arc<ZinitClientWrapper>>> = Mutex::new(None);
}
// Wrapper for Zinit client to handle connection
pub struct ZinitClientWrapper {
client: ZinitClient,
initialized: AtomicBool,
}
impl ZinitClientWrapper {
// Create a new Zinit client wrapper
fn new(client: ZinitClient) -> Self {
ZinitClientWrapper {
client,
initialized: AtomicBool::new(false),
}
}
// Initialize the client
async fn initialize(&self) -> Result<(), ZinitError> {
if self.initialized.load(Ordering::Relaxed) {
return Ok(());
}
// Try to list services to check if the connection works
let _ = self.client.list().await.map_err(|e| {
log::error!("Failed to initialize Zinit client: {}", e);
e
})?;
self.initialized.store(true, Ordering::Relaxed);
Ok(())
}
// List all services
pub async fn list(&self) -> Result<HashMap<String, ServiceState>, ZinitError> {
self.client.list().await
}
// Get status of a service
pub async fn status(&self, name: &str) -> Result<Status, ZinitError> {
self.client.status(name).await
}
// Start a service
pub async fn start(&self, name: &str) -> Result<(), ZinitError> {
self.client.start(name).await
}
// Stop a service
pub async fn stop(&self, name: &str) -> Result<(), ZinitError> {
self.client.stop(name).await
}
// Restart a service
pub async fn restart(&self, name: &str) -> Result<(), ZinitError> {
self.client.restart(name).await
}
// Monitor a service
pub async fn monitor(&self, name: &str) -> Result<(), ZinitError> {
self.client.monitor(name).await
}
// Forget a service (stop monitoring)
pub async fn forget(&self, name: &str) -> Result<(), ZinitError> {
self.client.forget(name).await
}
// Kill a service
pub async fn kill(&self, name: &str, signal: Option<&str>) -> Result<(), ZinitError> {
let signal_str = signal.unwrap_or("TERM");
self.client.kill(name, signal_str).await
}
// Create a service
pub async fn create_service(
&self,
name: &str,
service_config: Value,
) -> Result<(), ZinitError> {
self.client.create_service(name, service_config).await
}
// Delete a service
pub async fn delete_service(&self, name: &str) -> Result<(), ZinitError> {
self.client.delete_service(name).await
}
// Get service configuration
pub async fn get_service(&self, name: &str) -> Result<Value, ZinitError> {
self.client.get_service(name).await
}
// Reboot the system
pub async fn reboot(&self) -> Result<(), ZinitError> {
self.client.reboot().await
}
// Get logs with real implementation
pub async fn logs(&self, filter: Option<String>) -> Result<Vec<String>, ZinitError> {
use futures::StreamExt;
// The logs method requires a follow parameter and filter
let follow = false; // Don't follow logs, just get existing ones
let mut log_stream = self.client.logs(follow, filter).await?;
let mut logs = Vec::new();
// Collect logs from the stream with a reasonable limit
let mut count = 0;
const MAX_LOGS: usize = 1000;
while let Some(log_result) = log_stream.next().await {
match log_result {
Ok(log_entry) => {
// Convert LogEntry to String using Debug formatting
logs.push(format!("{:?}", log_entry));
count += 1;
if count >= MAX_LOGS {
break;
}
}
Err(e) => {
log::warn!("Error reading log entry: {}", e);
break;
}
}
}
Ok(logs)
}
}
// Get the Zinit client instance
pub async fn get_zinit_client(socket_path: &str) -> Result<Arc<ZinitClientWrapper>, ZinitError> {
// Check if we already have a client
{
let guard = ZINIT_CLIENT.lock().unwrap();
if let Some(ref client) = &*guard {
return Ok(Arc::clone(client));
}
}
// Create a new client
let client = create_zinit_client(socket_path).await?;
// Store the client globally
{
let mut guard = ZINIT_CLIENT.lock().unwrap();
*guard = Some(Arc::clone(&client));
}
Ok(client)
}
// Create a new Zinit client
async fn create_zinit_client(socket_path: &str) -> Result<Arc<ZinitClientWrapper>, ZinitError> {
// Connect via Unix socket
let client = ZinitClient::new(socket_path);
let wrapper = Arc::new(ZinitClientWrapper::new(client));
// Initialize the client
wrapper.initialize().await?;
Ok(wrapper)
}
// Reset the Zinit client
pub async fn reset(socket_path: &str) -> Result<(), ZinitError> {
// Clear the existing client
{
let mut client_guard = ZINIT_CLIENT.lock().unwrap();
*client_guard = None;
}
// Create a new client, only return error if it fails
get_zinit_client(socket_path).await?;
Ok(())
}
// Convenience functions for common operations
// List all services - convert ServiceState to String for compatibility
pub async fn list(socket_path: &str) -> Result<HashMap<String, String>, ZinitError> {
let client = get_zinit_client(socket_path).await?;
let services = client.list().await?;
// Convert HashMap<String, ServiceState> to HashMap<String, String>
let mut result = HashMap::new();
for (name, state) in services {
result.insert(name, format!("{:?}", state));
}
Ok(result)
}
// Get status of a service
pub async fn status(socket_path: &str, name: &str) -> Result<Status, ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.status(name).await
}
// Start a service
pub async fn start(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.start(name).await
}
// Stop a service
pub async fn stop(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.stop(name).await
}
// Restart a service
pub async fn restart(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.restart(name).await
}
// Monitor a service
pub async fn monitor(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.monitor(name).await
}
// Forget a service (stop monitoring)
pub async fn forget(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.forget(name).await
}
// Kill a service
pub async fn kill(socket_path: &str, name: &str, signal: Option<&str>) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.kill(name, signal).await
}
// Create a service with simplified parameters
pub async fn create_service(
socket_path: &str,
name: &str,
exec: &str,
oneshot: bool,
) -> Result<(), ZinitError> {
use serde_json::json;
let service_config = json!({
"exec": exec,
"oneshot": oneshot
});
let client = get_zinit_client(socket_path).await?;
client.create_service(name, service_config).await
}
// Create a service with full parameters
pub async fn create_service_full(
socket_path: &str,
name: &str,
exec: &str,
oneshot: bool,
after: Option<Vec<String>>,
env: Option<HashMap<String, String>>,
log: Option<String>,
test: Option<String>,
) -> Result<(), ZinitError> {
use serde_json::json;
let mut service_config = json!({
"exec": exec,
"oneshot": oneshot
});
if let Some(after_deps) = after {
service_config["after"] = json!(after_deps);
}
if let Some(environment) = env {
service_config["env"] = json!(environment);
}
if let Some(log_path) = log {
service_config["log"] = json!(log_path);
}
if let Some(test_cmd) = test {
service_config["test"] = json!(test_cmd);
}
let client = get_zinit_client(socket_path).await?;
client.create_service(name, service_config).await
}
// Delete a service
pub async fn delete_service(socket_path: &str, name: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.delete_service(name).await
}
// Get service configuration
pub async fn get_service(socket_path: &str, name: &str) -> Result<Value, ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.get_service(name).await
}
// Reboot the system
pub async fn reboot(socket_path: &str) -> Result<(), ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.reboot().await
}
// Get logs
pub async fn logs(socket_path: &str, filter: Option<String>) -> Result<Vec<String>, ZinitError> {
let client = get_zinit_client(socket_path).await?;
client.logs(filter).await
}

307
zinit_client/src/rhai.rs Normal file
View File

@@ -0,0 +1,307 @@
//! Rhai wrappers for Zinit client module functions
//!
//! This module provides Rhai wrappers for the functions in the Zinit client module.
use crate::{self as client};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map};
use serde_json::Value;
use std::path::Path;
use tokio::runtime::Runtime;
/// A trait for converting a Result to a Rhai-compatible error
pub trait ToRhaiError<T> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>>;
}
impl<T, E: std::error::Error> ToRhaiError<T> for Result<T, E> {
fn to_rhai_error(self) -> Result<T, Box<EvalAltResult>> {
self.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.to_string().into(),
rhai::Position::NONE,
))
})
}
}
/// Register Zinit module functions with the Rhai engine
///
/// # Arguments
///
/// * `engine` - The Rhai engine to register the functions with
///
/// # Returns
///
/// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise
pub fn register_zinit_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> {
// Register Zinit client functions
engine.register_fn("zinit_list", zinit_list);
engine.register_fn("zinit_status", zinit_status);
engine.register_fn("zinit_start", zinit_start);
engine.register_fn("zinit_stop", zinit_stop);
engine.register_fn("zinit_restart", zinit_restart);
engine.register_fn("zinit_monitor", zinit_monitor);
engine.register_fn("zinit_forget", zinit_forget);
engine.register_fn("zinit_kill", zinit_kill);
engine.register_fn("zinit_create_service", zinit_create_service);
engine.register_fn("zinit_delete_service", zinit_delete_service);
engine.register_fn("zinit_get_service", zinit_get_service);
engine.register_fn("zinit_logs", zinit_logs);
engine.register_fn("zinit_logs_all", zinit_logs_all);
Ok(())
}
// Helper function to get a runtime
fn get_runtime() -> Result<Runtime, Box<EvalAltResult>> {
tokio::runtime::Runtime::new().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to create Tokio runtime: {}", e).into(),
rhai::Position::NONE,
))
})
}
//
// Zinit Client Function Wrappers
//
/// Wrapper for zinit_client::list
///
/// Lists all services managed by Zinit.
pub fn zinit_list(socket_path: &str) -> Result<Map, Box<EvalAltResult>> {
if !Path::new(socket_path).exists() {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Zinit socket not found at '{}'", socket_path).into(),
rhai::Position::NONE,
)));
}
let rt = get_runtime()?;
let result = rt.block_on(async { client::list(socket_path).await });
let services = result.to_rhai_error()?;
// Convert HashMap<String, String> to Rhai Map
let mut map = Map::new();
for (name, state) in services {
map.insert(name.into(), Dynamic::from(state));
}
Ok(map)
}
/// Wrapper for zinit_client::status
///
/// Gets the status of a specific service.
pub fn zinit_status(socket_path: &str, name: &str) -> Result<Map, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::status(socket_path, name).await });
let status = result.to_rhai_error()?;
// Convert Status to Rhai Map
let mut map = Map::new();
map.insert("name".into(), Dynamic::from(status.name));
map.insert("pid".into(), Dynamic::from(status.pid));
map.insert("state".into(), Dynamic::from(status.state));
map.insert("target".into(), Dynamic::from(status.target));
// Convert dependencies
let mut deps_map = Map::new();
for (dep, state) in status.after {
deps_map.insert(dep.into(), Dynamic::from(state));
}
map.insert("after".into(), Dynamic::from_map(deps_map));
Ok(map)
}
/// Wrapper for zinit_client::start
///
/// Starts a service.
pub fn zinit_start(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::start(socket_path, name).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::stop
///
/// Stops a service.
pub fn zinit_stop(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::stop(socket_path, name).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::restart
///
/// Starts a service.
pub fn zinit_restart(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::restart(socket_path, name).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::monitor
///
/// Starts monitoring a service.
pub fn zinit_monitor(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::monitor(socket_path, name).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::forget
///
/// Stops monitoring a service.
pub fn zinit_forget(socket_path: &str, name: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::forget(socket_path, name).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::kill
///
/// Sends a signal to a service.
pub fn zinit_kill(socket_path: &str, name: &str, signal: &str) -> Result<bool, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::kill(socket_path, name, Some(signal)).await });
result.to_rhai_error()?;
Ok(true)
}
/// Wrapper for zinit_client::create_service
///
/// Creates a new service.
pub fn zinit_create_service(
socket_path: &str,
name: &str,
exec: &str,
oneshot: bool,
) -> Result<String, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result =
rt.block_on(async { client::create_service(socket_path, name, exec, oneshot).await });
result.to_rhai_error()?;
Ok(format!("Service '{}' created successfully", name))
}
/// Wrapper for zinit_client::delete_service
///
/// Deletes a service.
pub fn zinit_delete_service(socket_path: &str, name: &str) -> Result<String, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::delete_service(socket_path, name).await });
result.to_rhai_error()?;
Ok(format!("Service '{}' deleted successfully", name))
}
/// Wrapper for zinit_client::get_service
///
/// Gets a service configuration.
pub fn zinit_get_service(socket_path: &str, name: &str) -> Result<Dynamic, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::get_service(socket_path, name).await });
let value = result.to_rhai_error()?;
// Convert Value to Dynamic
Ok(value_to_dynamic(value))
}
/// Wrapper for zinit_client::logs with a filter
///
/// Gets logs for a specific service.
pub fn zinit_logs(socket_path: &str, filter: &str) -> Result<Array, Box<EvalAltResult>> {
let rt = get_runtime()?;
let filter_string = Some(filter.to_string());
let result = rt.block_on(async { client::logs(socket_path, filter_string).await });
let logs = result.to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for log in logs {
array.push(Dynamic::from(log));
}
Ok(array)
}
/// Wrapper for zinit_client::logs without a filter
///
/// Gets all logs.
pub fn zinit_logs_all(socket_path: &str) -> Result<Array, Box<EvalAltResult>> {
let rt = get_runtime()?;
let result = rt.block_on(async { client::logs(socket_path, None).await });
let logs = result.to_rhai_error()?;
// Convert Vec<String> to Rhai Array
let mut array = Array::new();
for log in logs {
array.push(Dynamic::from(log));
}
Ok(array)
}
// Helper function to convert serde_json::Value to rhai::Dynamic
fn value_to_dynamic(value: Value) -> Dynamic {
match value {
Value::Null => Dynamic::UNIT,
Value::Bool(b) => Dynamic::from(b),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Dynamic::from(i)
} else if let Some(f) = n.as_f64() {
Dynamic::from(f)
} else {
Dynamic::from(n.to_string())
}
}
Value::String(s) => Dynamic::from(s),
Value::Array(arr) => {
let mut rhai_arr = Array::new();
for item in arr {
rhai_arr.push(value_to_dynamic(item));
}
Dynamic::from(rhai_arr)
}
Value::Object(map) => {
let mut rhai_map = Map::new();
for (k, v) in map {
rhai_map.insert(k.into(), value_to_dynamic(v));
}
Dynamic::from_map(rhai_map)
}
}
}