use anyhow::{bail, Context, Result};
use std::io::{BufRead, BufReader, Write};
use std::time::Duration;
use crate::config;
use crate::ipc::{Request, Response};
const STARTUP_TIMEOUT_SECS: u64 = 15;
pub fn is_alive() -> bool {
#[cfg(unix)]
{
use std::os::unix::net::UnixStream;
let sock_path = config::sock_path();
if !sock_path.exists() {
return false;
}
let mut stream = match UnixStream::connect(&sock_path) {
Ok(s) => s,
Err(_) => return false,
};
stream.set_read_timeout(Some(Duration::from_secs(2))).ok();
stream.set_write_timeout(Some(Duration::from_secs(2))).ok();
let req = serde_json::json!({"cmd": "ping"});
if write!(stream, "{}\n", req).is_err() {
return false;
}
let mut line = String::new();
let mut reader = BufReader::new(&stream);
if reader.read_line(&mut line).is_err() {
return false;
}
serde_json::from_str::<serde_json::Value>(&line)
.ok()
.and_then(|v| v.get("pong").and_then(|p| p.as_bool()))
.unwrap_or(false)
}
#[cfg(windows)]
{
let pipe_path = r"\\.\pipe\wx-cli-daemon";
use std::fs::OpenOptions;
OpenOptions::new().read(true).write(true).open(pipe_path).is_ok()
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
pub fn ensure_daemon() -> Result<()> {
if is_alive() {
return Ok(());
}
eprintln!("启动 wx-daemon...");
start_daemon()?;
Ok(())
}
fn start_daemon() -> Result<()> {
let exe = std::env::current_exe().context("无法获取当前可执行文件路径")?;
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new(&exe);
cmd.env("WX_DAEMON_MODE", "1")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
unsafe { cmd.pre_exec(|| { libc::setsid(); Ok(()) }); }
let _ = cmd.spawn().context("无法启动 daemon 进程")?;
}
#[cfg(windows)]
{
let _ = std::process::Command::new(&exe)
.env("WX_DAEMON_MODE", "1")
.creation_flags(0x00000008) .spawn()
.context("无法启动 daemon 进程")?;
}
let deadline = std::time::Instant::now() + Duration::from_secs(STARTUP_TIMEOUT_SECS);
while std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(300));
if is_alive() {
return Ok(());
}
}
bail!(
"wx-daemon 启动超时(>{}s)\n请查看日志: {}",
STARTUP_TIMEOUT_SECS,
config::log_path().display()
)
}
pub fn send(req: Request) -> Result<Response> {
ensure_daemon()?;
#[cfg(unix)]
{
send_unix(req)
}
#[cfg(windows)]
{
send_windows(req)
}
#[cfg(not(any(unix, windows)))]
{
bail!("不支持当前平台")
}
}
#[cfg(unix)]
fn send_unix(req: Request) -> Result<Response> {
use std::os::unix::net::UnixStream;
let sock_path = config::sock_path();
let mut stream = UnixStream::connect(&sock_path)
.context("连接 daemon socket 失败")?;
stream.set_read_timeout(Some(Duration::from_secs(120))).ok();
stream.set_write_timeout(Some(Duration::from_secs(120))).ok();
let req_str = serde_json::to_string(&req)? + "\n";
stream.write_all(req_str.as_bytes())?;
let mut line = String::new();
let mut reader = BufReader::new(&stream);
reader.read_line(&mut line)?;
let resp: Response = serde_json::from_str(&line)
.context("解析 daemon 响应失败")?;
if !resp.ok {
bail!("{}", resp.error.as_deref().unwrap_or("未知错误"));
}
Ok(resp)
}
#[cfg(windows)]
fn send_windows(req: Request) -> Result<Response> {
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
let pipe_path = r"\\.\pipe\wx-cli-daemon";
let mut pipe = OpenOptions::new()
.read(true)
.write(true)
.open(pipe_path)
.context("连接 daemon named pipe 失败")?;
let req_str = serde_json::to_string(&req)? + "\n";
pipe.write_all(req_str.as_bytes())?;
let mut line = String::new();
let mut reader = BufReader::new(pipe);
reader.read_line(&mut line)?;
let resp: Response = serde_json::from_str(&line)
.context("解析 daemon 响应失败")?;
if !resp.ok {
bail!("{}", resp.error.as_deref().unwrap_or("未知错误"));
}
Ok(resp)
}