Skip to main content

kaizen/daemon/
lifecycle.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Daemon process lifecycle.
3
4use crate::ipc::{DaemonRequest, DaemonResponse, DaemonStatus};
5use anyhow::{Result, anyhow};
6use std::path::{Path, PathBuf};
7
8const PID_FILE: &str = "daemon.pid";
9const SOCK_FILE: &str = "daemon.sock";
10const LOG_FILE: &str = "daemon.log";
11const TOKEN_FILE: &str = "web_token.hex";
12
13#[derive(Debug, Clone)]
14pub struct RuntimePaths {
15    pub dir: PathBuf,
16    pub pid: PathBuf,
17    pub sock: PathBuf,
18    pub log: PathBuf,
19    pub token: PathBuf,
20}
21
22#[derive(Debug, Clone)]
23pub enum DaemonStatusOutcome {
24    Running(DaemonStatus),
25    Stopped { socket: PathBuf },
26}
27
28pub fn enabled() -> bool {
29    if let Ok(value) = std::env::var("KAIZEN_DAEMON") {
30        return value != "0";
31    }
32    executable_name().is_some_and(|name| name == "kaizen")
33}
34
35fn executable_name() -> Option<String> {
36    std::env::args()
37        .next()
38        .and_then(|path| PathBuf::from(path).file_stem().map(|stem| stem.to_owned()))
39        .and_then(|stem| stem.to_str().map(str::to_string))
40}
41
42pub fn runtime_paths() -> Result<RuntimePaths> {
43    runtime_paths_for(&std::env::current_dir()?)
44}
45
46pub fn runtime_paths_for(workspace: &Path) -> Result<RuntimePaths> {
47    let dir = crate::core::home_paths::root(workspace)?;
48    let child = |name| crate::core::paths::descendant_path(&dir, Path::new(name));
49    Ok(RuntimePaths {
50        pid: child(PID_FILE)?,
51        sock: child(SOCK_FILE)?,
52        log: child(LOG_FILE)?,
53        token: child(TOKEN_FILE)?,
54        dir,
55    })
56}
57
58pub fn ensure_running() -> Result<()> {
59    ensure_running_for(&std::env::current_dir()?)
60}
61
62pub fn ensure_running_for(workspace: &Path) -> Result<()> {
63    runtime_paths_for(workspace)?;
64    if !enabled() || try_status().is_ok() {
65        return Ok(());
66    }
67    super::start_background_for(workspace).map(|_| ())
68}
69
70pub fn try_status() -> Result<DaemonStatus> {
71    let response = tokio::runtime::Runtime::new()?.block_on(
72        super::client::request_lifecycle_async(DaemonRequest::Status),
73    )?;
74    match response {
75        DaemonResponse::Status(status) => Ok(status),
76        DaemonResponse::Error { message, .. } => Err(anyhow!(message)),
77        _ => Err(anyhow!("unexpected daemon status response")),
78    }
79}
80
81pub fn status_outcome() -> Result<DaemonStatusOutcome> {
82    match try_status() {
83        Ok(status) => Ok(DaemonStatusOutcome::Running(status)),
84        Err(err) if is_daemon_unavailable(&err) => Ok(DaemonStatusOutcome::Stopped {
85            socket: runtime_paths()?.sock,
86        }),
87        Err(err) => Err(err),
88    }
89}
90
91fn is_daemon_unavailable(err: &anyhow::Error) -> bool {
92    err.chain().any(is_unavailable_cause)
93}
94
95fn is_unavailable_cause(cause: &(dyn std::error::Error + 'static)) -> bool {
96    if cause.to_string().starts_with("connect daemon socket:") {
97        return true;
98    }
99    cause.downcast_ref::<std::io::Error>().is_some_and(|error| {
100        matches!(
101            error.kind(),
102            std::io::ErrorKind::UnexpectedEof
103                | std::io::ErrorKind::ConnectionReset
104                | std::io::ErrorKind::BrokenPipe
105        )
106    })
107}
108
109pub fn start_foreground() -> Result<()> {
110    tokio::runtime::Builder::new_multi_thread()
111        .enable_all()
112        .build()?
113        .block_on(super::server::run_server())
114}
115
116pub fn stop() -> Result<String> {
117    let response = tokio::runtime::Runtime::new()?
118        .block_on(super::client::request_lifecycle_async(DaemonRequest::Stop))?;
119    match response {
120        DaemonResponse::Ack { message } => Ok(message),
121        DaemonResponse::Error { message, .. } => Err(anyhow!(message)),
122        _ => Err(anyhow!("unexpected daemon stop response")),
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn eof_after_shutdown_is_unavailable() {
132        let error = anyhow::Error::new(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
133        assert!(is_daemon_unavailable(&error));
134    }
135}