use std::collections::HashMap;
use std::path::PathBuf;
use crate::client::client::{
DependencyDef, LifecycleDef, LoggingDef, RestartPolicy, ServiceClass, ServiceConfig,
ServiceDef, SocketAddr, State, Status, XinetConfig, XinetStatus,
};
use crate::client::{XinetStatusFull, ZinitHandle};
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: &ZinitHandle) -> 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: &ZinitHandle, 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: &ZinitHandle, 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: &ZinitHandle, name: &str) -> Result<(), String> {
client.stop(name).map_err(|e| e.to_string())?;
println!("Stopped: {}", name);
Ok(())
}
pub fn cmd_restart(client: &ZinitHandle, name: &str) -> Result<(), String> {
client.restart(name).map_err(|e| e.to_string())?;
println!("Restarted: {}", name);
Ok(())
}
pub fn cmd_kill(client: &ZinitHandle, 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: &ZinitHandle, 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(", "));
}
if let Some(port_msg) = &why.port_conflict {
println!("Port conflict: {}", port_msg);
}
Ok(())
}
pub fn cmd_tree(client: &ZinitHandle) -> Result<(), String> {
let tree = client.tree().map_err(|e| e.to_string())?;
println!("{}", tree);
Ok(())
}
pub fn cmd_remove(client: &ZinitHandle, name: &str) -> Result<(), String> {
client.service_delete(name).map_err(|e| e.to_string())?;
println!("Removed: {}", name);
Ok(())
}
pub fn cmd_logs(
client: &ZinitHandle,
name: &str,
lines: usize,
follow: bool,
) -> Result<(), String> {
if follow {
return Err("--follow is not yet implemented".to_string());
}
let logs = client
.logs(Some(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: &ZinitHandle) -> Result<(), String> {
let response = client.ping().map_err(|e| e.to_string())?;
println!("zinit daemon v{} is running", response.version);
Ok(())
}
pub fn cmd_shutdown(client: &ZinitHandle) -> Result<(), String> {
println!("Shutting down zinit daemon...");
client.shutdown().map_err(|e| e.to_string())?;
println!("zinit daemon stopped");
Ok(())
}
pub fn cmd_poweroff(client: &ZinitHandle) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
println!("macOS detected - stopping zinit daemon (not system poweroff)");
return cmd_shutdown(client);
}
#[cfg(not(target_os = "macos"))]
{
let zinit_is_pid1 = std::process::id() == 1 || {
std::fs::read_link("/proc/1/exe")
.map(|p| p.to_string_lossy().contains("zinit"))
.unwrap_or(false)
};
if zinit_is_pid1 {
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()
))
}
} else {
println!("zinit is not running as PID 1 - stopping daemon only");
cmd_shutdown(client)
}
}
}
pub fn cmd_reboot(client: &ZinitHandle) -> Result<(), String> {
#[cfg(target_os = "macos")]
{
println!("macOS detected - stopping zinit daemon (not system reboot)");
return cmd_shutdown(client);
}
#[cfg(not(target_os = "macos"))]
{
let zinit_is_pid1 = std::process::id() == 1 || {
std::fs::read_link("/proc/1/exe")
.map(|p| p.to_string_lossy().contains("zinit"))
.unwrap_or(false)
};
if zinit_is_pid1 {
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()
))
}
} else {
println!("zinit is not running as PID 1 - stopping daemon only");
cmd_shutdown(client)
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_add_service(
client: &ZinitHandle,
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: Status::default(),
class: ServiceClass::default(),
critical: false,
ports: Vec::new(),
kill_others: false,
process_filters: Vec::new(),
},
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.service_set(config).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: &ZinitHandle) -> Result<(), String> {
let output = client.debug_state().map_err(|e| e.to_string())?;
println!("{}", output);
Ok(())
}
pub fn cmd_debug_procs(client: &ZinitHandle, name: &str) -> Result<(), String> {
let output = client.debug_process_tree(name).map_err(|e| e.to_string())?;
println!("{}", output);
Ok(())
}
fn parse_socket_addr(s: &str) -> Result<SocketAddr, String> {
if let Some(path) = s.strip_prefix("unix:") {
Ok(SocketAddr::Unix(path.into()))
} else if let Some(addr) = s.strip_prefix("tcp:") {
Ok(SocketAddr::Tcp(addr.to_string()))
} else {
Ok(SocketAddr::Tcp(s.to_string()))
}
}
#[allow(clippy::too_many_arguments)]
pub fn cmd_xinet_register(
client: &ZinitHandle,
name: String,
listen: Vec<String>,
backend: String,
service: String,
connect_timeout: u64,
idle_timeout: u64,
single: bool,
) -> Result<(), String> {
let listen_addrs: Vec<SocketAddr> = 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_set(config).map_err(|e| e.to_string())?;
println!("Set xinet proxy '{}'", name);
Ok(())
}
pub fn cmd_xinet_unregister(client: &ZinitHandle, name: &str) -> Result<(), String> {
client.xinet_delete(name).map_err(|e| e.to_string())?;
println!("Unregistered xinet proxy '{}'", name);
Ok(())
}
pub fn cmd_xinet_list(client: &ZinitHandle) -> 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: &ZinitHandle, 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_full(status);
}
}
}
}
Ok(())
}
fn print_proxy_status(status: &XinetStatus) {
println!("Proxy: {}", status.name);
println!(
" Status: {}",
if status.running { "running" } else { "stopped" }
);
println!(" Connections: {} active", status.active_connections);
}
fn print_proxy_status_full(status: &XinetStatusFull) {
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
);
}
#[cfg(feature = "rhai")]
pub fn cmd_rhai(path: &PathBuf) -> Result<(), String> {
use std::fs;
use std::io::Read;
let path_metadata = fs::metadata(path).map_err(|e| format!("Failed to access path: {}", e))?;
let script_files = if path_metadata.is_dir() {
let mut files: Vec<PathBuf> = fs::read_dir(path)
.map_err(|e| format!("Failed to read directory: {}", e))?
.filter_map(|entry| {
entry.ok().and_then(|e| {
let p = e.path();
if p.extension().map_or(false, |ext| ext == "rhai") {
Some(p)
} else {
None
}
})
})
.collect();
files.sort();
files
} else if path.extension().map_or(false, |ext| ext == "rhai") {
vec![path.clone()]
} else {
return Err("Path must be a .rhai file or directory containing .rhai files".to_string());
};
if script_files.is_empty() {
return Err("No .rhai scripts found".to_string());
}
for script_path in script_files {
let mut script_content = String::new();
fs::File::open(&script_path)
.map_err(|e| format!("Failed to open script {}: {}", script_path.display(), e))?
.read_to_string(&mut script_content)
.map_err(|e| format!("Failed to read script {}: {}", script_path.display(), e))?;
let script_content = if script_content.starts_with("#!") {
script_content
.lines()
.skip(1)
.collect::<Vec<_>>()
.join("\n")
} else {
script_content
};
println!("Running {}", script_path.display());
#[cfg(feature = "rhai")]
{
use crate::client::handle::ZinitHandle;
use crate::client::rhai;
let handle = ZinitHandle::new().map_err(|e| format!("Failed to connect: {}", e))?;
rhai::set_global_handle(handle);
match rhai::execute_script(&script_content) {
Ok(_) => println!("✓ {} completed", script_path.display()),
Err(e) => {
eprintln!("✗ {} failed: {}", script_path.display(), e);
return Err(format!("Script execution failed: {}", e));
}
}
}
}
Ok(())
}