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}