use std::collections::HashMap;
use std::path::PathBuf;
use crate::sdk::{
DependencyDef, LifecycleDef, LoggingDef, RestartPolicy, ServiceConfig, ServiceDef, State,
ZinitClient,
xinet::{ProxyStatus, SocketAddr as XinetSocketAddr, XinetConfig},
};
fn state_symbol(state: State) -> &'static str {
match state {
State::Running => "●",
State::Starting => "◐",
State::Stopping => "◑",
State::Blocked => "○",
State::Inactive => "○",
State::Exited => "◌",
State::Failed => "✗",
}
}
pub fn cmd_list(client: &mut ZinitClient) -> Result<(), String> {
let names = client.list().map_err(|e| e.to_string())?;
if names.is_empty() {
println!("No services configured");
return Ok(());
}
let max_name_len = names.iter().map(|n| n.len()).max().unwrap_or(10);
for name in names {
if let Ok(status) = client.status(&name) {
let pid_str = if status.pid > 0 {
format!(" (pid: {})", status.pid)
} else {
String::new()
};
println!(
"{} {:<width$} {:?}{}",
state_symbol(status.state),
status.name,
status.state,
pid_str,
width = max_name_len
);
} else {
println!("? {:<width$} unknown", name, width = max_name_len);
}
}
Ok(())
}
pub fn cmd_status(client: &mut ZinitClient, name: &str) -> Result<(), String> {
let status = client.status(name).map_err(|e| e.to_string())?;
println!("Service: {}", status.name);
println!("State: {} {:?}", state_symbol(status.state), status.state);
if status.pid > 0 {
println!("PID: {}", status.pid);
}
if let Some(code) = status.exit_code {
println!("Exit: {}", code);
}
if let Some(ref err) = status.error {
println!("Error: {}", err);
}
Ok(())
}
pub fn cmd_start(client: &mut ZinitClient, name: &str, tree: bool) -> Result<(), String> {
if tree {
if let Ok(why) = client.why(name) {
for dep_name in &why.waiting_on {
println!("Starting dependency: {}", dep_name);
client.start(dep_name).map_err(|e| e.to_string())?;
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
client.start(name).map_err(|e| e.to_string())?;
println!("Started: {}", name);
std::thread::sleep(std::time::Duration::from_millis(100));
#[allow(clippy::collapsible_if)]
if let Ok(status) = client.status(name) {
if status.state == State::Blocked {
match status.error {
Some(err) => println!("\nService is blocked: {}", err),
None => println!("\nService is blocked"),
}
println!(
"\nTip: use 'zinit start {} --tree' to start dependencies too",
name
);
}
}
Ok(())
}
pub fn cmd_stop(client: &mut ZinitClient, name: &str) -> Result<(), String> {
client.stop(name).map_err(|e| e.to_string())?;
println!("Stopped: {}", name);
Ok(())
}
pub fn cmd_restart(client: &mut ZinitClient, name: &str) -> Result<(), String> {
client.restart(name).map_err(|e| e.to_string())?;
println!("Restarted: {}", name);
Ok(())
}
pub fn cmd_kill(client: &mut ZinitClient, name: &str, signal: Option<&str>) -> Result<(), String> {
client.kill(name, signal).map_err(|e| e.to_string())?;
let sig = signal.unwrap_or("SIGTERM");
println!("Sent {} to: {}", sig, name);
Ok(())
}
pub fn cmd_why(client: &mut ZinitClient, name: &str) -> Result<(), String> {
let why = client.why(name).map_err(|e| e.to_string())?;
if !why.blocked {
println!("{} is not blocked", name);
return Ok(());
}
println!("{}", why.ascii);
if !why.waiting_on.is_empty() {
println!("\nWaiting on: {}", why.waiting_on.join(", "));
}
if !why.conflicts_with.is_empty() {
println!("Conflicts with: {}", why.conflicts_with.join(", "));
}
Ok(())
}
pub fn cmd_tree(client: &mut ZinitClient) -> Result<(), String> {
let tree = client.tree().map_err(|e| e.to_string())?;
println!("{}", tree);
Ok(())
}
pub fn cmd_remove(client: &mut ZinitClient, name: &str) -> Result<(), String> {
client.remove(name).map_err(|e| e.to_string())?;
println!("Removed: {}", name);
Ok(())
}
pub fn cmd_reload(client: &mut ZinitClient) -> Result<(), String> {
let result = client.reload().map_err(|e| e.to_string())?;
if result.added.is_empty() && result.removed.is_empty() && result.changed.is_empty() {
println!("No changes detected");
return Ok(());
}
if !result.added.is_empty() {
println!("Added: {}", result.added.join(", "));
}
if !result.removed.is_empty() {
println!("Removed: {}", result.removed.join(", "));
}
if !result.changed.is_empty() {
println!("Changed: {}", result.changed.join(", "));
}
Ok(())
}
pub fn cmd_logs(
client: &mut ZinitClient,
name: &str,
lines: usize,
follow: bool,
) -> Result<(), String> {
if follow {
return Err("--follow is not yet implemented".to_string());
}
let logs = client.logs(name, Some(lines)).map_err(|e| e.to_string())?;
if logs.is_empty() {
println!("No logs available for {}", name);
return Ok(());
}
for log in logs {
println!("{}", log);
}
Ok(())
}
pub fn cmd_ping(client: &mut ZinitClient) -> Result<(), String> {
let version = client.ping().map_err(|e| e.to_string())?;
println!("zinit daemon v{} is running", version);
Ok(())
}
pub fn cmd_shutdown(client: &mut ZinitClient) -> Result<(), String> {
client.shutdown().map_err(|e| e.to_string())?;
println!("Shutdown requested");
Ok(())
}
pub fn cmd_poweroff() -> Result<(), String> {
let result = unsafe { libc::kill(1, libc::SIGTERM) };
if result == 0 {
println!("Poweroff signal sent to init");
Ok(())
} else {
Err(format!(
"Failed to signal init: {}",
std::io::Error::last_os_error()
))
}
}
pub fn cmd_reboot() -> Result<(), String> {
let result = unsafe { libc::kill(1, libc::SIGINT) };
if result == 0 {
println!("Reboot signal sent to init");
Ok(())
} else {
Err(format!(
"Failed to signal init: {}",
std::io::Error::last_os_error()
))
}
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_service(
client: &mut ZinitClient,
file: Option<PathBuf>,
name: Option<String>,
exec: Option<String>,
dir: String,
oneshot: bool,
envs: Vec<String>,
after: Vec<String>,
requires: Vec<String>,
wants: Vec<String>,
conflicts: Vec<String>,
restart: String,
restart_delay: u64,
restart_delay_max: u64,
max_restarts: u32,
persist: bool,
) -> Result<(), String> {
let config = if let Some(path) = file {
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
toml::from_str(&content)
.map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?
} else {
let name = name.ok_or("--name required when not using file")?;
let exec = exec.ok_or("--exec required when not using file")?;
let env: HashMap<String, String> = envs
.iter()
.map(|s| {
let (k, v) = s
.split_once('=')
.ok_or_else(|| format!("Invalid env format: {} (expected KEY=VALUE)", s))?;
Ok((k.to_string(), v.to_string()))
})
.collect::<Result<_, String>>()?;
let restart_policy = match restart.as_str() {
"always" => RestartPolicy::Always,
"on-failure" => RestartPolicy::OnFailure,
"never" => RestartPolicy::Never,
other => {
return Err(format!(
"Invalid restart policy: {} (use always, on-failure, or never)",
other
));
}
};
let dir_opt = if dir == "/" { None } else { Some(dir) };
ServiceConfig {
service: ServiceDef {
name,
exec,
dir: dir_opt,
oneshot,
env,
status: crate::sdk::Status::default(),
class: crate::sdk::ServiceClass::default(),
critical: false,
},
dependencies: DependencyDef {
after,
requires,
wants,
conflicts,
},
lifecycle: LifecycleDef {
restart: restart_policy,
restart_delay_ms: restart_delay,
restart_delay_max_ms: restart_delay_max,
max_restarts,
..Default::default()
},
health: None,
logging: LoggingDef::default(),
}
};
let service_name = config.service.name.clone();
let result = client
.add_service(&config, persist)
.map_err(|e| e.to_string())?;
let persist_msg = if let Some(path) = result.path {
format!(" (saved to {})", path)
} else {
" (ephemeral)".to_string()
};
println!("Service '{}' added{}", service_name, persist_msg);
if !result.warnings.is_empty() {
for warning in result.warnings {
println!("Warning: {}", warning);
}
}
Ok(())
}
pub fn cmd_debug_state(client: &mut ZinitClient) -> Result<(), String> {
let response = client
.call("debug.state", serde_json::json!({}))
.map_err(|e| e.to_string())?;
let result: serde_json::Value = response.into_result().map_err(|e| e.to_string())?;
if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
println!("{}", output);
}
Ok(())
}
pub fn cmd_debug_procs(client: &mut ZinitClient, name: &str) -> Result<(), String> {
let response = client
.call("debug.process_tree", serde_json::json!({ "name": name }))
.map_err(|e| e.to_string())?;
let result: serde_json::Value = response.into_result().map_err(|e| e.to_string())?;
if let Some(output) = result.get("output").and_then(|v| v.as_str()) {
println!("{}", output);
}
Ok(())
}
fn parse_socket_addr(s: &str) -> Result<XinetSocketAddr, String> {
if let Some(path) = s.strip_prefix("unix:") {
Ok(XinetSocketAddr::Unix(path.into()))
} else if let Some(addr) = s.strip_prefix("tcp:") {
Ok(XinetSocketAddr::Tcp(addr.to_string()))
} else {
Ok(XinetSocketAddr::Tcp(s.to_string()))
}
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_xinet_register(
client: &mut ZinitClient,
name: String,
listen: Vec<String>,
backend: String,
service: String,
connect_timeout: u64,
idle_timeout: u64,
single: bool,
) -> Result<(), String> {
let listen_addrs: Vec<XinetSocketAddr> = listen
.iter()
.map(|s| parse_socket_addr(s))
.collect::<Result<_, _>>()?;
let backend_addr = parse_socket_addr(&backend)?;
let config = XinetConfig {
name: name.clone(),
listen: listen_addrs,
backend: backend_addr,
service,
connect_timeout,
idle_timeout,
single_connection: single,
};
client.xinet_register(&config).map_err(|e| e.to_string())?;
println!("Registered xinet proxy '{}'", name);
Ok(())
}
pub fn cmd_xinet_unregister(client: &mut ZinitClient, name: &str) -> Result<(), String> {
client.xinet_unregister(name).map_err(|e| e.to_string())?;
println!("Unregistered xinet proxy '{}'", name);
Ok(())
}
pub fn cmd_xinet_list(client: &mut ZinitClient) -> Result<(), String> {
let proxies = client.xinet_list().map_err(|e| e.to_string())?;
if proxies.is_empty() {
println!("No xinet proxies registered");
} else {
println!("Registered xinet proxies:");
for name in proxies {
println!(" {}", name);
}
}
Ok(())
}
pub fn cmd_xinet_status(client: &mut ZinitClient, name: Option<&str>) -> Result<(), String> {
match name {
Some(n) => {
let status = client.xinet_status(n).map_err(|e| e.to_string())?;
print_proxy_status(&status);
}
None => {
let statuses = client.xinet_status_all().map_err(|e| e.to_string())?;
if statuses.is_empty() {
println!("No xinet proxies registered");
} else {
for (i, status) in statuses.iter().enumerate() {
if i > 0 {
println!();
}
print_proxy_status(status);
}
}
}
}
Ok(())
}
fn print_proxy_status(status: &ProxyStatus) {
println!("Proxy: {}", status.name);
println!(" Listen: {}", status.listen);
println!(" Backend: {}", status.backend);
println!(" Service: {}", status.service);
println!(
" Status: {}",
if status.running { "running" } else { "stopped" }
);
println!(
" Connections: {} active, {} total",
status.active_connections, status.total_connections
);
println!(
" Traffic: {} bytes in, {} bytes out",
status.bytes_to_backend, status.bytes_from_backend
);
}