use crate::error::*;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;
use std::path::Path;
const DAEMON_PORT: u16 = 19100;
#[derive(Debug, Serialize)]
struct DaemonRequest {
request: String,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct DaemonResponse {
pub ok: bool,
#[serde(default)]
pub error: Option<String>,
#[serde(default)]
pub port: Option<u16>,
#[serde(default)]
pub services: Option<Vec<ServiceStatus>>,
}
#[derive(Debug, Deserialize)]
pub struct ServiceStatus {
pub name: String,
pub port: u16,
pub status: String,
}
fn daemon_addr() -> String {
#[cfg(unix)]
{
format!("127.0.0.1:{}", DAEMON_PORT)
}
#[cfg(windows)]
{
format!("127.0.0.1:{}", DAEMON_PORT)
}
}
fn send_request(stream: &TcpStream, req: &DaemonRequest) -> Result<DaemonResponse> {
let mut stream = stream.try_clone().context("failed to clone daemon stream")?;
let json = serde_json::to_string(req).context("failed to serialize daemon request")?;
writeln!(stream, "{}", json).context("failed to write to daemon")?;
stream.flush().context("failed to flush daemon stream")?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader
.read_line(&mut line)
.context("failed to read daemon response")?;
serde_json::from_str(line.trim()).context("failed to parse daemon response")
}
pub fn connect() -> Option<TcpStream> {
TcpStream::connect(daemon_addr()).ok()
}
pub fn connect_or_start_daemon(home: &Path) -> Result<TcpStream> {
if let Some(stream) = connect() {
return Ok(stream);
}
start_daemon(home)?;
let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(5);
let interval = std::time::Duration::from_millis(50);
loop {
if let Some(stream) = connect() {
return Ok(stream);
}
if start.elapsed() > timeout {
bail!("failed to start daemon: timeout after 5 seconds");
}
std::thread::sleep(interval);
}
}
fn start_daemon(home: &Path) -> Result<()> {
let exe = std::env::current_exe().context("failed to determine current executable path")?;
let zacor_bin = if exe.file_stem().is_some_and(|s| s.to_string_lossy().starts_with("zr")) {
let parent = exe.parent().unwrap_or(Path::new("."));
let zacor_name = if cfg!(windows) { "zacor.exe" } else { "zacor" };
parent.join(zacor_name)
} else {
exe
};
std::process::Command::new(&zacor_bin)
.args(["daemon", "start"])
.env("ZR_HOME", home)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.with_context(|| format!("failed to spawn daemon: {}", zacor_bin.display()))?;
Ok(())
}
pub fn ping(stream: &TcpStream) -> Result<DaemonResponse> {
send_request(
stream,
&DaemonRequest {
request: "ping".into(),
name: None,
},
)
}
pub fn start_service(stream: &TcpStream, name: &str) -> Result<DaemonResponse> {
send_request(
stream,
&DaemonRequest {
request: "start-service".into(),
name: Some(name.into()),
},
)
}
pub fn stop_service(stream: &TcpStream, name: &str) -> Result<DaemonResponse> {
send_request(
stream,
&DaemonRequest {
request: "stop-service".into(),
name: Some(name.into()),
},
)
}
pub fn status(stream: &TcpStream) -> Result<DaemonResponse> {
send_request(
stream,
&DaemonRequest {
request: "status".into(),
name: None,
},
)
}
pub fn shutdown(stream: &TcpStream) -> Result<DaemonResponse> {
send_request(
stream,
&DaemonRequest {
request: "shutdown".into(),
name: None,
},
)
}