use anyhow::{Context, Result};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpStream, UnixStream};
use tokio::time::timeout;
#[derive(Debug, Serialize)]
struct RpcRequest {
jsonrpc: &'static str,
method: String,
params: Value,
id: u64,
}
#[derive(Debug, Deserialize)]
struct RpcResponse {
#[allow(dead_code)]
jsonrpc: String,
result: Option<Value>,
error: Option<RpcError>,
#[allow(dead_code)]
id: Value,
}
#[derive(Debug, Deserialize)]
struct RpcError {
#[allow(dead_code)]
code: i32,
message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum State {
Inactive,
Blocked,
Starting,
Running,
Stopping,
Exited,
Failed,
}
impl State {
pub fn is_running(&self) -> bool {
matches!(self, State::Running)
}
pub fn is_active(&self) -> bool {
matches!(self, State::Starting | State::Running | State::Stopping)
}
}
impl std::fmt::Display for State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
State::Inactive => write!(f, "inactive"),
State::Blocked => write!(f, "blocked"),
State::Starting => write!(f, "starting"),
State::Running => write!(f, "running"),
State::Stopping => write!(f, "stopping"),
State::Exited => write!(f, "exited"),
State::Failed => write!(f, "failed"),
}
}
}
impl Default for State {
fn default() -> Self {
State::Inactive
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceConfig {
pub service: ServiceDef,
#[serde(default)]
pub dependencies: DependencyDef,
#[serde(default)]
pub lifecycle: LifecycleDef,
#[serde(default)]
pub health: Option<HealthDef>,
#[serde(default)]
pub logging: LoggingDef,
}
impl ServiceConfig {
pub fn parse(content: &str) -> Result<Self> {
toml::from_str(content).context("Failed to parse service config")
}
pub fn to_toml(&self) -> Result<String> {
toml::to_string_pretty(self).context("Failed to serialize service config")
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceDef {
pub name: String,
pub exec: String,
#[serde(default)]
pub dir: Option<String>,
#[serde(default)]
pub oneshot: bool,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub status: Status,
#[serde(default)]
pub class: ServiceClass,
#[serde(default)]
pub critical: bool,
#[serde(default)]
pub ports: Vec<u16>,
#[serde(default)]
pub kill_others: bool,
#[serde(default)]
pub process_filters: Vec<String>,
}
impl Default for ServiceDef {
fn default() -> Self {
Self {
name: String::new(),
exec: String::new(),
dir: None,
oneshot: false,
env: HashMap::new(),
status: Status::default(),
class: ServiceClass::default(),
critical: false,
ports: Vec::new(),
kill_others: false,
process_filters: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
#[default]
Start,
Stop,
Ignore,
}
impl Status {
pub fn should_autostart(&self) -> bool {
matches!(self, Status::Start)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ServiceClass {
#[default]
User,
System,
}
impl ServiceClass {
pub fn is_system(&self) -> bool {
matches!(self, ServiceClass::System)
}
}
#[derive(Debug, Clone)]
pub struct ServiceConfigBuilder {
service: ServiceDef,
dependencies: DependencyDef,
lifecycle: LifecycleDef,
health: Option<HealthDef>,
logging: LoggingDef,
}
impl ServiceConfigBuilder {
pub fn new(name: impl Into<String>) -> Self {
Self {
service: ServiceDef {
name: name.into(),
exec: String::new(),
dir: None,
oneshot: false,
env: HashMap::new(),
status: Status::Start,
class: ServiceClass::default(),
critical: false,
ports: Vec::new(),
kill_others: false,
process_filters: Vec::new(),
},
dependencies: DependencyDef::default(),
lifecycle: LifecycleDef::default(),
health: None,
logging: LoggingDef::default(),
}
}
pub fn exec(mut self, exec: impl Into<String>) -> Self {
self.service.exec = exec.into();
self
}
pub fn dir(mut self, dir: impl Into<String>) -> Self {
self.service.dir = Some(dir.into());
self
}
pub fn oneshot(mut self, oneshot: bool) -> Self {
self.service.oneshot = oneshot;
self
}
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.service.env.insert(key.into(), value.into());
self
}
pub fn status(mut self, status: Status) -> Self {
self.service.status = status;
self
}
pub fn class(mut self, class: ServiceClass) -> Self {
self.service.class = class;
self
}
pub fn critical(mut self, critical: bool) -> Self {
self.service.critical = critical;
self
}
pub fn after(mut self, service: impl Into<String>) -> Self {
self.dependencies.after.push(service.into());
self
}
pub fn requires(mut self, service: impl Into<String>) -> Self {
self.dependencies.requires.push(service.into());
self
}
pub fn wants(mut self, service: impl Into<String>) -> Self {
self.dependencies.wants.push(service.into());
self
}
pub fn conflicts(mut self, service: impl Into<String>) -> Self {
self.dependencies.conflicts.push(service.into());
self
}
pub fn restart(mut self, policy: impl Into<String>) -> Self {
let policy_str = policy.into().to_lowercase();
self.lifecycle.restart = match policy_str.as_str() {
"always" => RestartPolicy::Always,
"never" => RestartPolicy::Never,
_ => RestartPolicy::OnFailure,
};
self
}
pub fn restart_delay_ms(mut self, ms: u64) -> Self {
self.lifecycle.restart_delay_ms = ms;
self
}
pub fn max_restarts(mut self, count: u32) -> Self {
self.lifecycle.max_restarts = count;
self
}
pub fn health_tcp(mut self, target: impl Into<String>) -> Self {
self.health = Some(HealthDef::Tcp {
target: target.into(),
common: Default::default(),
});
self
}
pub fn health_http(mut self, target: impl Into<String>) -> Self {
self.health = Some(HealthDef::Http {
target: target.into(),
expect_status: 200,
common: Default::default(),
});
self
}
pub fn health_interval_ms(self, _ms: u64) -> Self {
self
}
pub fn log_buffer_lines(mut self, lines: usize) -> Self {
self.logging.buffer_lines = lines;
self
}
pub fn log_file(mut self, path: impl Into<String>) -> Self {
self.logging.file = Some(path.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.service.ports.push(port);
self
}
pub fn kill_others(mut self) -> Self {
self.service.kill_others = true;
self
}
pub fn process_filter(mut self, filter: impl Into<String>) -> Self {
self.service.process_filters.push(filter.into());
self
}
pub fn build(self) -> ServiceConfig {
ServiceConfig {
service: self.service,
dependencies: self.dependencies,
lifecycle: self.lifecycle,
health: self.health,
logging: self.logging,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct DependencyDef {
#[serde(default)]
pub after: Vec<String>,
#[serde(default)]
pub requires: Vec<String>,
#[serde(default)]
pub wants: Vec<String>,
#[serde(default)]
pub conflicts: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LifecycleDef {
#[serde(default = "default_restart_policy")]
pub restart: RestartPolicy,
#[serde(default = "default_restart_delay_ms")]
pub restart_delay_ms: u64,
#[serde(default = "default_restart_delay_max_ms")]
pub restart_delay_max_ms: u64,
#[serde(default = "default_max_restarts")]
pub max_restarts: u32,
#[serde(default = "default_stability_period_ms")]
pub stability_period_ms: u64,
#[serde(default = "default_start_timeout_ms")]
pub start_timeout_ms: u64,
#[serde(default = "default_stop_timeout_ms")]
pub stop_timeout_ms: u64,
#[serde(default = "default_stop_signal")]
pub stop_signal: String,
}
impl Default for LifecycleDef {
fn default() -> Self {
Self {
restart: default_restart_policy(),
restart_delay_ms: default_restart_delay_ms(),
restart_delay_max_ms: default_restart_delay_max_ms(),
max_restarts: default_max_restarts(),
stability_period_ms: default_stability_period_ms(),
start_timeout_ms: default_start_timeout_ms(),
stop_timeout_ms: default_stop_timeout_ms(),
stop_signal: default_stop_signal(),
}
}
}
fn default_restart_policy() -> RestartPolicy {
RestartPolicy::OnFailure
}
fn default_restart_delay_ms() -> u64 {
1000
}
fn default_restart_delay_max_ms() -> u64 {
300000
}
fn default_max_restarts() -> u32 {
10
}
fn default_stability_period_ms() -> u64 {
30000
}
fn default_start_timeout_ms() -> u64 {
30000
}
fn default_stop_timeout_ms() -> u64 {
10000
}
fn default_stop_signal() -> String {
"SIGTERM".to_string()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RestartPolicy {
Always,
#[default]
OnFailure,
Never,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum HealthDef {
Tcp {
target: String,
#[serde(flatten)]
common: HealthCommon,
},
Http {
target: String,
#[serde(default = "default_http_status")]
expect_status: u16,
#[serde(flatten)]
common: HealthCommon,
},
Exec {
target: String,
#[serde(flatten)]
common: HealthCommon,
},
}
fn default_http_status() -> u16 {
200
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HealthCommon {
#[serde(default = "default_health_interval_ms")]
pub interval_ms: u64,
#[serde(default = "default_health_timeout_ms")]
pub timeout_ms: u64,
#[serde(default = "default_health_retries")]
pub retries: u32,
#[serde(default = "default_health_start_period_ms")]
pub start_period_ms: u64,
}
impl Default for HealthCommon {
fn default() -> Self {
Self {
interval_ms: default_health_interval_ms(),
timeout_ms: default_health_timeout_ms(),
retries: default_health_retries(),
start_period_ms: default_health_start_period_ms(),
}
}
}
fn default_health_interval_ms() -> u64 {
10000
}
fn default_health_timeout_ms() -> u64 {
5000
}
fn default_health_retries() -> u32 {
3
}
fn default_health_start_period_ms() -> u64 {
0
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LoggingDef {
#[serde(default = "default_buffer_lines")]
pub buffer_lines: usize,
#[serde(default)]
pub file: Option<String>,
#[serde(default)]
pub forward: Option<String>,
}
impl Default for LoggingDef {
fn default() -> Self {
Self {
buffer_lines: default_buffer_lines(),
file: None,
forward: None,
}
}
}
fn default_buffer_lines() -> usize {
1000
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceStatus {
pub name: String,
pub state: State,
#[serde(default)]
pub pid: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub exit_code: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceStats {
pub pid: u32,
pub memory_bytes: u64,
pub cpu_percent: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PingResponse {
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WhyBlocked {
pub name: String,
pub blocked: bool,
pub waiting_on: Vec<String>,
pub conflicts_with: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub port_conflict: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub process_conflict: Option<ProcessConflictDetails>,
pub ascii: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessConflictDetails {
pub filter: String,
pub processes: Vec<ProcessConflictInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProcessConflictInfo {
pub pid: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cmdline: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TreeResponse {
pub ascii: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PrepareRestartResult {
pub state_path: String,
pub ready: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChildProcessInfo {
pub pid: u32,
pub name: String,
pub memory_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChildrenResponse {
pub children: Vec<ChildProcessInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LogLine {
pub timestamp_ms: u64,
pub service: String,
pub stream: String,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DebugOutput {
pub output: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceInfo {
pub name: String,
pub state: ServiceState,
pub is_target: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "lowercase")]
pub enum ServiceState {
Inactive,
Blocked {
#[serde(default)]
waiting_on: Vec<String>,
},
Starting {
pid: u32,
},
Running {
pid: u32,
},
Stopping {
pid: u32,
},
Exited {
#[serde(default)]
exit_code: Option<i32>,
},
Failed {
reason: FailureReason,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum FailureReason {
ExitCode { code: i32 },
Signal { signal: i32 },
StartTimeout,
StopTimeout,
HealthCheckFailed { attempts: u32 },
DependencyFailed { service: String },
SpawnError { message: String },
MissingDependency { dependency: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DependencyInfo {
pub name: String,
pub dep_type: String,
pub state: ServiceState,
pub satisfied: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ServiceStatusFull {
pub name: String,
pub state: ServiceState,
pub is_target: bool,
pub dependencies: Vec<DependencyInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uptime_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XinetDef {
pub name: String,
pub listen: Vec<SocketAddr>,
pub backend: SocketAddr,
pub service: String,
#[serde(default = "default_connect_timeout")]
pub connect_timeout: u64,
#[serde(default)]
pub idle_timeout: u64,
#[serde(default)]
pub single_connection: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XinetStatus {
pub name: String,
pub running: bool,
pub active_connections: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XinetStatusFull {
pub name: String,
pub listen: String,
pub backend: String,
pub service: String,
pub running: bool,
pub total_connections: u64,
pub active_connections: usize,
pub bytes_to_backend: u64,
pub bytes_from_backend: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AddServiceResult {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(default)]
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum SocketAddr {
Unix(PathBuf),
Tcp(String),
}
impl SocketAddr {
pub fn unix<P: Into<PathBuf>>(path: P) -> Self {
SocketAddr::Unix(path.into())
}
pub fn tcp<S: Into<String>>(addr: S) -> Self {
SocketAddr::Tcp(addr.into())
}
pub fn is_unix(&self) -> bool {
matches!(self, SocketAddr::Unix(_))
}
pub fn is_tcp(&self) -> bool {
matches!(self, SocketAddr::Tcp(_))
}
}
impl std::fmt::Display for SocketAddr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SocketAddr::Unix(p) => write!(f, "unix:{}", p.display()),
SocketAddr::Tcp(a) => write!(f, "tcp:{}", a),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct XinetConfig {
pub name: String,
pub listen: Vec<SocketAddr>,
pub backend: SocketAddr,
pub service: String,
#[serde(default = "default_connect_timeout")]
pub connect_timeout: u64,
#[serde(default)]
pub idle_timeout: u64,
#[serde(default)]
pub single_connection: bool,
}
fn default_connect_timeout() -> u64 {
30
}
impl XinetConfig {
pub fn new<S: Into<String>>(
name: S,
listen: SocketAddr,
backend: SocketAddr,
service: S,
) -> Self {
Self {
name: name.into(),
listen: vec![listen],
backend,
service: service.into(),
connect_timeout: default_connect_timeout(),
idle_timeout: 0,
single_connection: false,
}
}
pub fn add_listen(mut self, addr: SocketAddr) -> Self {
self.listen.push(addr);
self
}
pub fn with_connect_timeout(mut self, seconds: u64) -> Self {
self.connect_timeout = seconds;
self
}
pub fn with_idle_timeout(mut self, seconds: u64) -> Self {
self.idle_timeout = seconds;
self
}
pub fn with_single_connection(mut self, single: bool) -> Self {
self.single_connection = single;
self
}
pub fn listen_addrs_string(&self) -> String {
self.listen
.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ")
}
}
const SYSTEM_SOCKET: &str = "/run/zinit.sock";
const USER_SOCKET_SUFFIX: &str = "hero/var/zinit.sock";
pub fn get_socket_path() -> Result<PathBuf> {
let system = PathBuf::from(SYSTEM_SOCKET);
if system.exists() {
return Ok(system);
}
let home = dirs::home_dir().context("Could not determine home directory")?;
Ok(home.join(USER_SOCKET_SUFFIX))
}
pub fn get_config_dir() -> PathBuf {
if let Some(home) = dirs::home_dir() {
home.join("hero/cfg/zinit")
} else {
PathBuf::from("/tmp/zinit/services")
}
}
#[derive(Debug, Clone)]
pub struct ZinitClientBuilder {
socket_path: Option<String>,
log_level: u32,
}
impl ZinitClientBuilder {
pub fn new() -> Self {
Self {
socket_path: None,
log_level: 1,
}
}
pub fn socket(mut self, path: &str) -> Self {
self.socket_path = Some(path.to_string());
self
}
pub fn log_level(mut self, level: u32) -> Self {
self.log_level = level.min(3);
self
}
fn log(&self, message: &str, min_level: u32) {
if self.log_level >= min_level {
println!("{}", message);
}
}
pub fn build(self) -> Result<ZinitClient> {
let client = if let Some(path) = &self.socket_path {
self.log(&format!("✓ Connecting to socket: {}", path), 1);
ZinitClient::unix(path)
} else {
self.log("✓ Connecting to default socket", 1);
ZinitClient::try_default()?
};
self.log("✓ Connected to zinit server", 1);
Ok(client)
}
}
impl Default for ZinitClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Silent = 0,
Minimal = 1,
Normal = 2,
Verbose = 3,
}
impl LogLevel {
pub fn log(&self, min_level: LogLevel, message: &str) {
if *self >= min_level {
println!("{}", message);
}
}
}
#[derive(Clone)]
pub struct ZinitClient {
addr: String,
log_level: LogLevel,
}
impl ZinitClient {
pub fn unix<P: AsRef<Path>>(path: P) -> Self {
Self {
addr: format!("unix:{}", path.as_ref().display()),
log_level: LogLevel::Normal,
}
}
pub fn tcp(addr: &str) -> Self {
Self {
addr: format!("tcp:{}", addr),
log_level: LogLevel::Normal,
}
}
pub fn try_default() -> Result<Self> {
let socket_path = get_socket_path()?;
Ok(Self::unix(socket_path))
}
pub fn with_log_level(mut self, level: LogLevel) -> Self {
self.log_level = level;
self
}
pub fn log_level(&self) -> LogLevel {
self.log_level
}
async fn call<T: DeserializeOwned>(&self, method: &str, params: Value) -> Result<T> {
let request = RpcRequest {
jsonrpc: "2.0",
method: method.to_string(),
params,
id: 1,
};
let request_json = serde_json::to_string(&request)? + "\n";
let connect_timeout = Duration::from_secs(3);
let response_json = if self.addr.starts_with("unix:") {
let path = self.addr.trim_start_matches("unix:");
let mut stream = timeout(connect_timeout, UnixStream::connect(path))
.await
.context("Connection to zinit server timed out (server not running?)")?
.context("Failed to connect to Unix socket")?;
stream.write_all(request_json.as_bytes()).await?;
stream.flush().await?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await?;
line
} else {
let addr = self.addr.trim_start_matches("tcp:");
let mut stream = timeout(connect_timeout, TcpStream::connect(addr))
.await
.context("Connection to zinit server timed out (server not running?)")?
.context("Failed to connect to TCP")?;
stream.write_all(request_json.as_bytes()).await?;
stream.flush().await?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await?;
line
};
let response: RpcResponse =
serde_json::from_str(&response_json).context("Failed to parse response")?;
if let Some(error) = response.error {
anyhow::bail!("{}", error.message);
}
let result = response.result.unwrap_or(Value::Null);
serde_json::from_value(result).context("Failed to parse result")
}
pub async fn discover(&self) -> Result<Value> {
self.call("rpc.discover", Value::Null).await
}
pub async fn ping(&self) -> Result<PingResponse> {
self.call("system.ping", Value::Null).await
}
pub async fn test_connection(&self) -> Result<String> {
self.ping()
.await
.map(|resp| resp.version)
.map_err(|e| anyhow::anyhow!("Failed to connect to zinit server: {}", e))
}
pub async fn shutdown(&self) -> Result<()> {
let _: Value = self.call("system.shutdown", Value::Null).await?;
Ok(())
}
pub async fn reboot(&self) -> Result<()> {
let _: Value = self.call("system.reboot", Value::Null).await?;
Ok(())
}
pub async fn prepare_restart(&self) -> Result<PrepareRestartResult> {
self.call("system.prepare_restart", Value::Null).await
}
pub async fn service_set(&self, config: &ServiceConfig) -> Result<AddServiceResult> {
self.call("service.set", json!({ "config": config })).await
}
pub async fn service_get(&self, name: &str) -> Result<ServiceConfig> {
self.call("service.get", json!({ "name": name })).await
}
pub async fn service_delete(&self, name: &str) -> Result<()> {
let _: Value = self.call("service.delete", json!({ "name": name })).await?;
Ok(())
}
pub async fn list(&self) -> Result<Vec<String>> {
self.call("service.list", Value::Null).await
}
pub async fn list_full(&self) -> Result<Vec<ServiceInfo>> {
self.call("service.list_full", Value::Null).await
}
pub async fn start(&self, name: &str) -> Result<()> {
let _: Value = self.call("service.start", json!({ "name": name })).await?;
Ok(())
}
pub async fn stop(&self, name: &str) -> Result<()> {
let _: Value = self.call("service.stop", json!({ "name": name })).await?;
Ok(())
}
pub async fn restart(&self, name: &str) -> Result<()> {
let _: Value = self
.call("service.restart", json!({ "name": name }))
.await?;
Ok(())
}
pub async fn kill(&self, name: &str, signal: Option<&str>) -> Result<()> {
let params = match signal {
Some(sig) => json!({ "name": name, "signal": sig }),
None => json!({ "name": name }),
};
let _: Value = self.call("service.kill", params).await?;
Ok(())
}
pub async fn status(&self, name: &str) -> Result<ServiceStatus> {
self.call("service.status", json!({ "name": name })).await
}
pub async fn status_full(&self, name: &str) -> Result<ServiceStatusFull> {
self.call("service.status_full", json!({ "name": name }))
.await
}
pub async fn stats(&self, name: &str) -> Result<ServiceStats> {
self.call("service.stats", json!({ "name": name })).await
}
pub async fn children(&self, name: &str) -> Result<ChildrenResponse> {
self.call("service.children", json!({ "name": name })).await
}
pub async fn is_running(&self, name: &str) -> Result<bool> {
self.call("service.is_running", json!({ "name": name }))
.await
}
pub async fn why(&self, name: &str) -> Result<WhyBlocked> {
self.call("service.why", json!({ "name": name })).await
}
pub async fn tree(&self) -> Result<String> {
let tree: TreeResponse = self.call("service.tree", Value::Null).await?;
Ok(tree.ascii)
}
pub async fn logs(&self, name: Option<&str>, lines: Option<usize>) -> Result<Vec<String>> {
let mut params = json!({});
if let Some(n) = name {
params["name"] = json!(n);
}
if let Some(l) = lines {
params["lines"] = json!(l);
}
self.call("logs.get", params).await
}
pub async fn logs_tail(
&self,
name: Option<&str>,
lines: Option<usize>,
) -> Result<Vec<LogLine>> {
let mut params = json!({});
if let Some(n) = name {
params["name"] = json!(n);
}
if let Some(l) = lines {
params["lines"] = json!(l);
}
self.call("logs.tail", params).await
}
pub async fn logs_filter(
&self,
name: Option<&str>,
stream: Option<&str>,
since: Option<u64>,
lines: Option<usize>,
) -> Result<Vec<LogLine>> {
let mut params = json!({});
if let Some(n) = name {
params["name"] = json!(n);
}
if let Some(s) = stream {
params["stream"] = json!(s);
}
if let Some(s) = since {
params["since"] = json!(s);
}
if let Some(l) = lines {
params["lines"] = json!(l);
}
self.call("logs.filter", params).await
}
pub async fn debug_state(&self) -> Result<String> {
let output: DebugOutput = self.call("debug.state", Value::Null).await?;
Ok(output.output)
}
pub async fn debug_process_tree(&self, name: &str) -> Result<String> {
let output: DebugOutput = self
.call("debug.process_tree", json!({ "name": name }))
.await?;
Ok(output.output)
}
pub async fn xinet_set(&self, config: &XinetConfig) -> Result<()> {
let _: Value = self
.call(
"xinet.set",
json!({ "config": serde_json::to_value(config)? }),
)
.await?;
Ok(())
}
pub async fn xinet_delete(&self, name: &str) -> Result<()> {
let _: Value = self.call("xinet.delete", json!({ "name": name })).await?;
Ok(())
}
pub async fn xinet_list(&self) -> Result<Vec<String>> {
self.call("xinet.list", Value::Null).await
}
pub async fn xinet_status(&self, name: &str) -> Result<XinetStatus> {
self.call("xinet.status", json!({ "name": name })).await
}
pub async fn xinet_status_all(&self) -> Result<Vec<XinetStatusFull>> {
self.call("xinet.status_all", Value::Null).await
}
}