1use std::io::{BufRead, BufReader, Write};
8use std::os::unix::net::UnixStream;
9use std::os::unix::process::CommandExt;
10use std::path::Path;
11use std::time::{Duration, Instant};
12
13use stackless_core::fault::{Fault, codes};
14use stackless_core::paths::Paths;
15use stackless_core::types::{ProtocolVersion, TcpPort};
16
17use crate::binary::{ResolveSource, resolve_daemon_bin, should_replace_daemon};
18use crate::proxy;
19use crate::rpc::{Envelope, Request, Response, ResponseBody, build_version};
20use crate::server::{DaemonRole, socket_path_for};
21
22#[derive(Debug, thiserror::Error)]
23pub enum DaemonError {
24 #[error("cannot reach the stackless daemon: {detail}")]
25 Unreachable { detail: String },
26
27 #[error("daemon request failed: {error}")]
28 Request { error: String },
29
30 #[error("daemon spawn failed: {detail}")]
31 Spawn { detail: String },
32
33 #[error("stackless CLI binary not found: {detail}")]
34 BinaryNotFound { detail: String },
35}
36
37impl Fault for DaemonError {
38 fn code(&self) -> &'static str {
39 match self {
40 Self::Unreachable { .. } => codes::DAEMON_UNREACHABLE,
41 Self::Request { .. } => codes::DAEMON_REQUEST_FAILED,
42 Self::Spawn { .. } => codes::DAEMON_SPAWN_FAILED,
43 Self::BinaryNotFound { .. } => codes::DAEMON_BINARY_NOT_FOUND,
44 }
45 }
46
47 fn remediation(&self) -> String {
48 match self {
49 Self::BinaryNotFound { .. } => {
50 "install the stackless CLI, ensure `stackless` is on PATH, or set STACKLESS_BIN \
51 to the CLI absolute path; for hermetic tests use Client::builder().paths(...) \
52 / TestContext (feature test-support)"
53 .into()
54 }
55 Self::Unreachable { .. } | Self::Spawn { .. } => {
56 "check `stackless daemon run` starts in a terminal and that the state dir is \
57 writable; if using the SDK from another binary, install the CLI or set \
58 STACKLESS_BIN"
59 .into()
60 }
61 Self::Request { .. } => {
62 "re-run the command; the daemon may have been restarting".into()
63 }
64 }
65 }
66}
67
68#[derive(Debug)]
69pub struct DaemonClient {
70 stream: UnixStream,
71}
72
73impl DaemonClient {
74 pub fn ensure() -> Result<Self, DaemonError> {
78 let paths = Paths::from_env();
79 Self::ensure_resolved(&paths, proxy::proxy_port(), DaemonRole::Operator)
80 }
81
82 pub fn ensure_resolved(
87 paths: &Paths,
88 proxy_port: TcpPort,
89 role: DaemonRole,
90 ) -> Result<Self, DaemonError> {
91 let client = match Self::connect_with(paths) {
92 Ok(client) => client,
93 Err(_) => {
94 let (exe, _source) = resolve_daemon_bin()?;
95 spawn_daemon(paths, &exe, proxy_port, role)?;
96 Self::wait_for_socket(paths, Duration::from_secs(5))?
97 }
98 };
99 Self::cli_self_upgrade_if_needed(client, paths, proxy_port, role)
103 }
104
105 fn cli_self_upgrade_if_needed(
108 mut client: Self,
109 paths: &Paths,
110 proxy_port: TcpPort,
111 role: DaemonRole,
112 ) -> Result<Self, DaemonError> {
113 let daemon_version = client.ping()?;
114 if !crate::is_cli_process() || daemon_version == build_version() {
115 return Ok(client);
116 }
117 let exe = std::env::current_exe().map_err(|err| DaemonError::Spawn {
118 detail: format!("cannot resolve current executable: {err}"),
119 })?;
120 let _ = client.call(Request::Shutdown);
121 std::thread::sleep(Duration::from_millis(200));
122 spawn_daemon(paths, &exe, proxy_port, role)?;
123 client = Self::wait_for_socket(paths, Duration::from_secs(5))?;
124 client.ping()?;
125 Ok(client)
126 }
127
128 pub fn ensure_with(
137 paths: &Paths,
138 executable: &Path,
139 proxy_port: TcpPort,
140 role: DaemonRole,
141 ) -> Result<Self, DaemonError> {
142 Self::ensure_with_source(paths, executable, proxy_port, role, ResolveSource::Explicit)
143 }
144
145 fn ensure_with_source(
146 paths: &Paths,
147 executable: &Path,
148 proxy_port: TcpPort,
149 role: DaemonRole,
150 source: ResolveSource,
151 ) -> Result<Self, DaemonError> {
152 let mut client = match Self::connect_with(paths) {
153 Ok(client) => client,
154 Err(_) => {
155 spawn_daemon(paths, executable, proxy_port, role)?;
156 Self::wait_for_socket(paths, Duration::from_secs(5))?
157 }
158 };
159 let daemon_version = client.ping()?;
160 if should_replace_daemon(&daemon_version, build_version(), source) {
161 let _ = client.call(Request::Shutdown);
162 std::thread::sleep(Duration::from_millis(200));
163 spawn_daemon(paths, executable, proxy_port, role)?;
164 client = Self::wait_for_socket(paths, Duration::from_secs(5))?;
165 client.ping()?;
167 }
168 Ok(client)
169 }
170
171 pub fn connect() -> Result<Self, DaemonError> {
172 Self::connect_with(&Paths::from_env())
173 }
174
175 pub fn connect_with(paths: &Paths) -> Result<Self, DaemonError> {
176 let stream = UnixStream::connect(socket_path_for(paths)).map_err(|err| {
177 DaemonError::Unreachable {
178 detail: err.to_string(),
179 }
180 })?;
181 stream.set_read_timeout(Some(Duration::from_secs(10))).ok();
182 Ok(Self { stream })
183 }
184
185 fn wait_for_socket(paths: &Paths, budget: Duration) -> Result<Self, DaemonError> {
186 let deadline = Instant::now() + budget;
187 loop {
188 match Self::connect_with(paths) {
189 Ok(client) => return Ok(client),
190 Err(err) if Instant::now() > deadline => return Err(err),
191 Err(_) => std::thread::sleep(Duration::from_millis(50)),
192 }
193 }
194 }
195
196 pub fn ping(&mut self) -> Result<String, DaemonError> {
198 let (version, _body) = self.call_versioned(Request::Ping)?;
199 Ok(version)
200 }
201
202 pub fn call(&mut self, request: Request) -> Result<ResponseBody, DaemonError> {
203 self.call_versioned(request).map(|(_, body)| body)
204 }
205
206 fn call_versioned(&mut self, request: Request) -> Result<(String, ResponseBody), DaemonError> {
207 let envelope = Envelope {
208 protocol: ProtocolVersion::V1,
209 version: build_version().to_owned(),
210 body: request,
211 };
212 let mut line = serde_json::to_string(&envelope).map_err(|err| DaemonError::Request {
213 error: err.to_string(),
214 })?;
215 line.push('\n');
216 self.stream
217 .write_all(line.as_bytes())
218 .map_err(|err| DaemonError::Unreachable {
219 detail: err.to_string(),
220 })?;
221 let mut reader = BufReader::new(&self.stream);
222 let mut response_line = String::new();
223 reader
224 .read_line(&mut response_line)
225 .map_err(|err| DaemonError::Unreachable {
226 detail: err.to_string(),
227 })?;
228 let envelope: Envelope<Response> =
229 serde_json::from_str(&response_line).map_err(|err| DaemonError::Request {
230 error: format!("unparseable response: {err}"),
231 })?;
232 match envelope.body {
233 Response::Ok(body) => Ok((envelope.version, body)),
234 Response::Err { error } => Err(DaemonError::Request { error }),
235 }
236 }
237}
238
239fn spawn_daemon(
259 paths: &Paths,
260 executable: &Path,
261 proxy_port: TcpPort,
262 role: DaemonRole,
263) -> Result<(), DaemonError> {
264 let lock_path = paths.spawn_lock();
265 if let Some(dir) = lock_path.parent() {
266 std::fs::create_dir_all(dir).map_err(|err| DaemonError::Spawn {
267 detail: err.to_string(),
268 })?;
269 }
270 let _lock = match stackless_core::lockfile::FileLock::try_acquire(&lock_path) {
271 Ok(lock) => lock,
272 Err(_) => return Ok(()),
274 };
275 let default_proxy = proxy::proxy_port();
279 if role == DaemonRole::Operator
280 && proxy_port == default_proxy
281 && crate::launchd::kickstart_if_supervised()
282 {
283 return Ok(());
284 }
285 let log = std::fs::OpenOptions::new()
286 .create(true)
287 .append(true)
288 .open(paths.daemon_log())
289 .map_err(|err| DaemonError::Spawn {
290 detail: err.to_string(),
291 })?;
292 let log_err = log.try_clone().map_err(|err| DaemonError::Spawn {
293 detail: err.to_string(),
294 })?;
295 let mut command = std::process::Command::new(executable);
296 command
297 .args(["daemon", "run"])
298 .arg("--state-dir")
299 .arg(paths.state_dir())
300 .arg("--proxy-port")
301 .arg(proxy_port.get().to_string());
302 if role == DaemonRole::Embedded {
303 command.arg("--embedded");
304 }
305 command
306 .stdin(std::process::Stdio::null())
307 .stdout(log)
308 .stderr(log_err)
309 .process_group(0)
310 .spawn()
311 .map_err(|err| DaemonError::Spawn {
312 detail: err.to_string(),
313 })?;
314 Ok(())
315}