1//! A client for the daemon's socket, and the one place a daemon is started.
2
3use std::process::Stdio;
4use std::time::Duration;
5
6use bytes::Bytes;
7use http_body_util::{BodyExt, Full};
8use hyper::{Method, Request, StatusCode};
9use hyper_util::rt::TokioIo;
10use serde_json::Value;
11use tokio::net::UnixStream;
12
13use crate::paths;
14
15/// One request to the daemon. An `Err` means it could not be reached.
16pub async fn call(method: Method, path: &str, body: Option<&Value>) -> Result<(StatusCode, Value), String> {
17    let socket = paths::socket()?;
18    let stream = UnixStream::connect(&socket).await.map_err(|e| format!("no daemon on {}: {e}", socket.display()))?;
19    let (mut sender, connection) =
20        hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.map_err(|e| e.to_string())?;
21    tokio::spawn(connection);
22    let body = body.map(|value| Bytes::from(value.to_string())).unwrap_or_default();
23    let request = Request::builder()
24        .method(method)
25        .uri(path)
26        .header(hyper::header::HOST, "jevhooks")
27        .header(hyper::header::CONTENT_TYPE, "application/json")
28        .body(Full::new(body))
29        .map_err(|e| e.to_string())?;
30    let response = sender.send_request(request).await.map_err(|e| e.to_string())?;
31    let status = response.status();
32    let bytes = response.into_body().collect().await.map_err(|e| e.to_string())?.to_bytes();
33    let value = serde_json::from_slice(&bytes).map_err(|e| format!("the daemon's reply was not JSON: {e}"))?;
34    Ok((status, value))
35}
36
37
38/// Starts a daemon from this same executable, detached from the session that
39/// asked for it, with its stderr in the daemon log.
40///
41/// The key reaches it one of two ways: it is already in this process's
42/// environment and is inherited, or the config file names a `secrets_command`
43/// and the daemon is started through that, which puts the key in the daemon's
44/// environment and nowhere else. With neither the daemon runs without a key
45/// and says so in `/status`. No particular secrets tool is assumed.
46fn spawn() -> Result<(), String> {
47    use std::os::unix::process::CommandExt;
48    std::fs::create_dir_all(paths::state_dir()).map_err(|e| e.to_string())?;
49    let log = std::fs::OpenOptions::new()
50        .create(true)
51        .append(true)
52        .open(paths::daemon_log())
53        .map_err(|e| format!("opening {}: {e}", paths::daemon_log().display()))?;
54    let exe = std::env::current_exe().map_err(|e| e.to_string())?;
55    let has_key = std::env::var_os(jev_http::KEY).is_some_and(|key| !key.is_empty());
56    // A config file that cannot be read starts the daemon plainly; the
57    // daemon reports the file's error in its own status.
58    let through = if has_key { None } else { crate::headroom::Config::load().ok().and_then(|c| c.secrets_command) };
59    let words: Vec<&str> = through.as_deref().map(|c| c.split_whitespace().collect()).unwrap_or_default();
60    let mut command = match words.split_first() {
61        Some((program, arguments)) => {
62            let mut command = std::process::Command::new(program);
63            command.args(arguments).arg(&exe);
64            command
65        }
66        None => std::process::Command::new(&exe),
67    };
68    command
69        .arg("serve")
70        .stdin(Stdio::null())
71        .stdout(Stdio::null())
72        .stderr(log)
73        // Its own process group, so the session's exit does not signal it.
74        .process_group(0)
75        .spawn()
76        .map(drop)
77        .map_err(|e| format!("starting the daemon: {e}"))
78}
79
80/// Makes sure a daemon of this build is serving: starts one if none answers,
81/// and replaces one that is a different build. Returns its status.
82pub async fn ensure() -> Result<Value, String> {
83    let mine = paths::identity();
84    if let Ok((_, status)) = call(Method::GET, "/status", None).await {
85        if status.get("identity").and_then(Value::as_str) == Some(mine.as_str()) {
86            return Ok(status);
87        }
88        // A daemon from another build: ask it to stop and wait for the socket to close.
89        call(Method::POST, "/shutdown", None).await.ok();
90        for _ in 0..50 {
91            let gone = match paths::socket() {
92                Ok(socket) => UnixStream::connect(socket).await.is_err(),
93                Err(_) => true,
94            };
95            if gone {
96                break;
97            }
98            tokio::time::sleep(Duration::from_millis(100)).await;
99        }
100    }
101    spawn()?;
102    // A secrets command fetching the key has been measured at 3 to 15 seconds.
103    for _ in 0..300 {
104        tokio::time::sleep(Duration::from_millis(100)).await;
105        if let Ok((_, status)) = call(Method::GET, "/status", None).await {
106            return Ok(status);
107        }
108    }
109    Err(format!("the daemon did not start within 30 s; see {}", paths::daemon_log().display()))
110}