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