Skip to main content

kaizen/daemon/
background.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Background daemon process startup.
3
4use super::lifecycle::{
5    DaemonStatusOutcome, RuntimePaths, runtime_paths, runtime_paths_for, status_outcome,
6};
7use crate::ipc::{DaemonStatus, WebEndpoint};
8use anyhow::{Context, Result, anyhow};
9use std::path::Path;
10use std::process::{Child, Command, Stdio};
11use std::time::{Duration, Instant};
12
13// Restore runs migrations, backfills, first scans before readiness. Keep slow startup bounded.
14const START_WAIT_MS: u64 = 30_000;
15const STOP_WAIT_MS: u64 = 2_000;
16
17#[derive(Debug, Clone)]
18pub struct BackgroundStart {
19    pub pid: u32,
20    pub paths: RuntimePaths,
21    pub already_running: bool,
22    pub web: Option<WebEndpoint>,
23}
24
25pub fn start_background() -> Result<BackgroundStart> {
26    start_background_at(runtime_paths()?)
27}
28
29pub fn start_background_for(workspace: &Path) -> Result<BackgroundStart> {
30    start_background_at(runtime_paths_for(workspace)?)
31}
32
33pub fn restart_background() -> Result<BackgroundStart> {
34    if !daemon_stopped()? {
35        super::stop()?;
36        wait_until_stopped()?;
37    }
38    start_background()
39}
40
41fn wait_until_stopped() -> Result<()> {
42    let deadline = Instant::now() + Duration::from_millis(STOP_WAIT_MS);
43    while Instant::now() < deadline {
44        if daemon_stopped()? {
45            return Ok(());
46        }
47        std::thread::sleep(Duration::from_millis(25));
48    }
49    Err(anyhow!("daemon did not stop within {STOP_WAIT_MS}ms"))
50}
51
52fn daemon_stopped() -> Result<bool> {
53    Ok(matches!(
54        super::status_outcome()?,
55        super::DaemonStatusOutcome::Stopped { .. }
56    ))
57}
58
59fn start_background_at(paths: RuntimePaths) -> Result<BackgroundStart> {
60    if let Some(start) = status_start(&paths, true)? {
61        return Ok(start);
62    }
63    std::fs::create_dir_all(&paths.dir)?;
64    let mut child = spawn_background(&paths)?;
65    wait_until_ready(paths, &mut child)
66}
67
68fn status_start(paths: &RuntimePaths, already_running: bool) -> Result<Option<BackgroundStart>> {
69    match status_outcome()? {
70        DaemonStatusOutcome::Running(status) => Ok(Some(background_start(
71            status,
72            paths.clone(),
73            already_running,
74        ))),
75        DaemonStatusOutcome::Stopped { .. } => Ok(None),
76    }
77}
78
79fn spawn_background(paths: &RuntimePaths) -> Result<Child> {
80    let log = open_log(&paths.log)?;
81    let err = log.try_clone()?;
82    background_command(log, err)?
83        .spawn()
84        .context("spawn kaizen daemon")
85}
86
87fn open_log(path: &Path) -> Result<std::fs::File> {
88    crate::core::safe_fs::append(path)
89        .with_context(|| format!("open daemon log: {}", path.display()))
90}
91
92fn wait_until_ready(paths: RuntimePaths, child: &mut Child) -> Result<BackgroundStart> {
93    let deadline = Instant::now() + Duration::from_millis(START_WAIT_MS);
94    while Instant::now() < deadline {
95        if let Some(start) = poll_start(child, &paths)? {
96            return Ok(start);
97        }
98        std::thread::sleep(Duration::from_millis(25));
99    }
100    Err(start_timeout(&paths))
101}
102
103fn poll_start(child: &mut Child, paths: &RuntimePaths) -> Result<Option<BackgroundStart>> {
104    if let Some(status) = child.try_wait().context("poll daemon child")? {
105        return Err(early_exit(status, paths));
106    }
107    status_start(paths, false)
108}
109
110fn background_start(
111    status: DaemonStatus,
112    paths: RuntimePaths,
113    already_running: bool,
114) -> BackgroundStart {
115    BackgroundStart {
116        pid: status.pid,
117        paths,
118        already_running,
119        web: status.web,
120    }
121}
122
123fn early_exit(status: std::process::ExitStatus, paths: &RuntimePaths) -> anyhow::Error {
124    anyhow!(
125        "daemon exited before ready with status {status}; see {}",
126        paths.log.display()
127    )
128}
129
130fn start_timeout(paths: &RuntimePaths) -> anyhow::Error {
131    anyhow!(
132        "daemon did not become ready at {}; see {}",
133        paths.sock.display(),
134        paths.log.display()
135    )
136}
137
138fn background_command(log: std::fs::File, err: std::fs::File) -> Result<Command> {
139    let mut command = Command::new(std::env::current_exe()?);
140    command
141        .args(["daemon", "start"])
142        .stdin(Stdio::null())
143        .stdout(Stdio::from(log))
144        .stderr(Stdio::from(err));
145    detach_background(&mut command);
146    Ok(command)
147}
148
149#[cfg(unix)]
150fn detach_background(command: &mut Command) {
151    use std::os::unix::process::CommandExt;
152    unsafe {
153        command.pre_exec(|| {
154            (libc::setsid() != -1)
155                .then_some(())
156                .ok_or_else(std::io::Error::last_os_error)
157        });
158    }
159}
160
161#[cfg(not(unix))]
162fn detach_background(_command: &mut Command) {}