kaizen/daemon/
background.rs1use 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) {}