Skip to main content

kaizen/daemon/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Local daemon client/lifecycle API.
3
4mod 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}