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::{RuntimePaths, runtime_paths, runtime_paths_for, try_status};
5use crate::ipc::{DaemonStatus, WebEndpoint};
6use anyhow::{Context, Result, anyhow};
7use std::path::Path;
8use std::process::{Child, Command, Stdio};
9use std::time::{Duration, Instant};
10
11const START_WAIT_MS: u64 = 2_000;
12
13#[derive(Debug, Clone)]
14pub struct BackgroundStart {
15    pub pid: u32,
16    pub paths: RuntimePaths,
17    pub already_running: bool,
18    pub web: Option<WebEndpoint>,
19}
20
21pub fn start_background() -> Result<BackgroundStart> {
22    start_background_at(runtime_paths()?)
23}
24
25pub fn start_background_for(workspace: &Path) -> Result<BackgroundStart> {
26    start_background_at(runtime_paths_for(workspace)?)
27}
28
29fn start_background_at(paths: RuntimePaths) -> Result<BackgroundStart> {
30    if let Some(start) = running_start(&paths) {
31        return Ok(start);
32    }
33    std::fs::create_dir_all(&paths.dir)?;
34    let mut child = spawn_background(&paths)?;
35    wait_until_ready(paths, &mut child)
36}
37
38fn running_start(paths: &RuntimePaths) -> Option<BackgroundStart> {
39    try_status()
40        .ok()
41        .map(|status| background_start(status, paths.clone(), true))
42}
43
44fn spawn_background(paths: &RuntimePaths) -> Result<Child> {
45    let log = open_log(&paths.log)?;
46    let err = log.try_clone()?;
47    background_command(log, err)?
48        .spawn()
49        .context("spawn kaizen daemon")
50}
51
52fn open_log(path: &Path) -> Result<std::fs::File> {
53    crate::core::safe_fs::append(path)
54        .with_context(|| format!("open daemon log: {}", path.display()))
55}
56
57fn wait_until_ready(paths: RuntimePaths, child: &mut Child) -> Result<BackgroundStart> {
58    let deadline = Instant::now() + Duration::from_millis(START_WAIT_MS);
59    while Instant::now() < deadline {
60        if let Some(start) = poll_start(child, &paths)? {
61            return Ok(start);
62        }
63        std::thread::sleep(Duration::from_millis(25));
64    }
65    Err(start_timeout(&paths))
66}
67
68fn poll_start(child: &mut Child, paths: &RuntimePaths) -> Result<Option<BackgroundStart>> {
69    if let Some(status) = child.try_wait().context("poll daemon child")? {
70        return Err(early_exit(status, paths));
71    }
72    Ok(try_status()
73        .ok()
74        .map(|status| background_start(status, paths.clone(), false)))
75}
76
77fn background_start(
78    status: DaemonStatus,
79    paths: RuntimePaths,
80    already_running: bool,
81) -> BackgroundStart {
82    BackgroundStart {
83        pid: status.pid,
84        paths,
85        already_running,
86        web: status.web,
87    }
88}
89
90fn early_exit(status: std::process::ExitStatus, paths: &RuntimePaths) -> anyhow::Error {
91    anyhow!(
92        "daemon exited before ready with status {status}; see {}",
93        paths.log.display()
94    )
95}
96
97fn start_timeout(paths: &RuntimePaths) -> anyhow::Error {
98    anyhow!(
99        "daemon did not become ready at {}; see {}",
100        paths.sock.display(),
101        paths.log.display()
102    )
103}
104
105fn background_command(log: std::fs::File, err: std::fs::File) -> Result<Command> {
106    let mut command = Command::new(std::env::current_exe()?);
107    command
108        .args(["daemon", "start"])
109        .stdin(Stdio::null())
110        .stdout(Stdio::from(log))
111        .stderr(Stdio::from(err));
112    detach_background(&mut command);
113    Ok(command)
114}
115
116#[cfg(unix)]
117fn detach_background(command: &mut Command) {
118    use std::os::unix::process::CommandExt;
119    unsafe {
120        command.pre_exec(|| {
121            (libc::setsid() != -1)
122                .then_some(())
123                .ok_or_else(std::io::Error::last_os_error)
124        });
125    }
126}
127
128#[cfg(not(unix))]
129fn detach_background(_command: &mut Command) {}