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 stream = UnixStream::connect(paths::socket()).await.map_err(|e| format!("no daemon on {}: {e}", paths::socket().display()))?;
18    let (mut sender, connection) =
19        hyper::client::conn::http1::handshake(TokioIo::new(stream)).await.map_err(|e| e.to_string())?;
20    tokio::spawn(connection);
21    let body = body.map(|value| Bytes::from(value.to_string())).unwrap_or_default();
22    let request = Request::builder()
23        .method(method)
24        .uri(path)
25        .header(hyper::header::HOST, "jevhooks")
26        .header(hyper::header::CONTENT_TYPE, "application/json")
27        .body(Full::new(body))
28        .map_err(|e| e.to_string())?;
29    let response = sender.send_request(request).await.map_err(|e| e.to_string())?;
30    let status = response.status();
31    let bytes = response.into_body().collect().await.map_err(|e| e.to_string())?.to_bytes();
32    let value = serde_json::from_slice(&bytes).map_err(|e| format!("the daemon's reply was not JSON: {e}"))?;
33    Ok((status, value))
34}
35
36/// The first `name` on `PATH`.
37fn on_path(name: &str) -> bool {
38    std::env::var_os("PATH").is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(name).is_file()))
39}
40
41/// Starts a daemon from this same executable, detached from the session that
42/// asked for it, with its stderr in the daemon log.
43///
44/// The key reaches it one of two ways: it is already in this process's
45/// environment and is inherited, or `op-env-run` puts it in the daemon's
46/// environment and nowhere else. With neither the daemon runs without a key
47/// and says so in `/status`.
48fn spawn() -> Result<(), String> {
49    use std::os::unix::process::CommandExt;
50    std::fs::create_dir_all(paths::state_dir()).map_err(|e| e.to_string())?;
51    let log = std::fs::OpenOptions::new()
52        .create(true)
53        .append(true)
54        .open(paths::daemon_log())
55        .map_err(|e| format!("opening {}: {e}", paths::daemon_log().display()))?;
56    let exe = std::env::current_exe().map_err(|e| e.to_string())?;
57    let has_key = std::env::var_os(jev_http::KEY).is_some_and(|key| !key.is_empty());
58    let mut command = if !has_key && on_path("op-env-run") {
59        let mut command = std::process::Command::new("op-env-run");
60        command.arg("--").arg(&exe);
61        command
62    } else {
63        std::process::Command::new(&exe)
64    };
65    command
66        .arg("serve")
67        .stdin(Stdio::null())
68        .stdout(Stdio::null())
69        .stderr(log)
70        // Its own process group, so the session's exit does not signal it.
71        .process_group(0)
72        .spawn()
73        .map(drop)
74        .map_err(|e| format!("starting the daemon: {e}"))
75}
76
77/// Makes sure a daemon of this build is serving: starts one if none answers,
78/// and replaces one that is a different build. Returns its status.
79pub async fn ensure() -> Result<Value, String> {
80    let mine = paths::identity();
81    if let Ok((_, status)) = call(Method::GET, "/status", None).await {
82        if status.get("identity").and_then(Value::as_str) == Some(mine.as_str()) {
83            return Ok(status);
84        }
85        // A daemon from another build: ask it to stop and wait for the socket to close.
86        call(Method::POST, "/shutdown", None).await.ok();
87        for _ in 0..50 {
88            if UnixStream::connect(paths::socket()).await.is_err() {
89                break;
90            }
91            tokio::time::sleep(Duration::from_millis(100)).await;
92        }
93    }
94    spawn()?;
95    // `op-env-run` fetching the key has been measured at 3 to 15 seconds.
96    for _ in 0..300 {
97        tokio::time::sleep(Duration::from_millis(100)).await;
98        if let Ok((_, status)) = call(Method::GET, "/status", None).await {
99            return Ok(status);
100        }
101    }
102    Err(format!("the daemon did not start within 30 s; see {}", paths::daemon_log().display()))
103}