1mod server;
5mod worker;
6
7use crate::core::paths::kaizen_dir;
8use crate::ipc::{
9 ClientHello, ClientKind, DaemonRequest, DaemonResponse, DaemonStatus, PROTO_VERSION,
10 ServerHello, read_frame, write_frame,
11};
12use anyhow::{Context, Result, anyhow};
13use std::path::PathBuf;
14use std::process::{Command, Stdio};
15use std::time::{Duration, Instant};
16use tokio::net::UnixStream;
17
18const PID_FILE: &str = "daemon.pid";
19const SOCK_FILE: &str = "daemon.sock";
20const LOG_FILE: &str = "daemon.log";
21const START_WAIT_MS: u64 = 2_000;
22
23#[derive(Debug, Clone)]
24pub struct RuntimePaths {
25 pub dir: PathBuf,
26 pub pid: PathBuf,
27 pub sock: PathBuf,
28 pub log: PathBuf,
29}
30
31pub fn enabled() -> bool {
32 if let Ok(v) = std::env::var("KAIZEN_DAEMON") {
33 return v != "0";
34 }
35 std::env::args()
36 .next()
37 .and_then(|p| PathBuf::from(p).file_stem().map(|s| s.to_owned()))
38 .and_then(|s| s.to_str().map(str::to_string))
39 .is_some_and(|name| name == "kaizen")
40}
41
42pub fn runtime_paths() -> Result<RuntimePaths> {
43 let dir = kaizen_dir().ok_or_else(|| anyhow!("KAIZEN_HOME/HOME not set"))?;
44 Ok(RuntimePaths {
45 pid: dir.join(PID_FILE),
46 sock: dir.join(SOCK_FILE),
47 log: dir.join(LOG_FILE),
48 dir,
49 })
50}
51
52pub fn ensure_running() -> Result<()> {
53 if !enabled() || try_status().is_ok() {
54 return Ok(());
55 }
56 let paths = runtime_paths()?;
57 std::fs::create_dir_all(&paths.dir)?;
58 let log = std::fs::OpenOptions::new()
59 .create(true)
60 .append(true)
61 .open(&paths.log)
62 .with_context(|| format!("open daemon log: {}", paths.log.display()))?;
63 let err = log.try_clone()?;
64 Command::new(std::env::current_exe()?)
65 .args(["daemon", "start", "--background"])
66 .stdin(Stdio::null())
67 .stdout(Stdio::from(log))
68 .stderr(Stdio::from(err))
69 .spawn()
70 .context("spawn kaizen daemon")?;
71 let deadline = Instant::now() + Duration::from_millis(START_WAIT_MS);
72 while Instant::now() < deadline {
73 if try_status().is_ok() {
74 return Ok(());
75 }
76 std::thread::sleep(Duration::from_millis(25));
77 }
78 Err(anyhow!(
79 "daemon did not become ready at {}",
80 paths.sock.display()
81 ))
82}
83
84pub fn request_blocking(request: DaemonRequest) -> Result<DaemonResponse> {
85 ensure_running()?;
86 tokio::runtime::Runtime::new()?.block_on(request_async(request))
87}
88
89pub fn try_status() -> Result<DaemonStatus> {
90 let response =
91 tokio::runtime::Runtime::new()?.block_on(request_async(DaemonRequest::Status))?;
92 match response {
93 DaemonResponse::Status(status) => Ok(status),
94 DaemonResponse::Error { message, .. } => Err(anyhow!(message)),
95 _ => Err(anyhow!("unexpected daemon status response")),
96 }
97}
98
99pub fn start_foreground() -> Result<()> {
100 tokio::runtime::Builder::new_multi_thread()
101 .enable_all()
102 .build()?
103 .block_on(server::run_server())
104}
105
106pub fn stop() -> Result<String> {
107 match tokio::runtime::Runtime::new()?.block_on(request_async(DaemonRequest::Stop))? {
108 DaemonResponse::Ack { message } => Ok(message),
109 DaemonResponse::Error { message, .. } => Err(anyhow!(message)),
110 _ => Err(anyhow!("unexpected daemon stop response")),
111 }
112}
113
114pub fn hello_blocking(client: ClientKind, workspace: Option<String>) -> Result<ServerHello> {
115 match request_blocking(DaemonRequest::Hello(ClientHello {
116 proto_version: PROTO_VERSION,
117 client,
118 workspace,
119 }))? {
120 DaemonResponse::Hello(hello) => Ok(hello),
121 DaemonResponse::Error { message, .. } => Err(anyhow!(message)),
122 _ => Err(anyhow!("unexpected daemon hello response")),
123 }
124}
125
126async fn request_async(request: DaemonRequest) -> Result<DaemonResponse> {
127 let paths = runtime_paths()?;
128 let mut stream = UnixStream::connect(&paths.sock)
129 .await
130 .with_context(|| format!("connect daemon socket: {}", paths.sock.display()))?;
131 write_frame(&mut stream, &request).await?;
132 read_frame(&mut stream).await
133}