- Installer: one `cargo install maestro` instead of two; ExecStart becomes `~/.cargo/bin/maestro daemon`; user unit renamed `maestrod.service` → `maestro.service`; configure step is `maestro daemon configure`. - E2E harness: both `Daemon` and `Registry` subprocess wrappers shell out to the unified `maestro` binary with the appropriate subcommand. - State dir: `~/.local/state/maestrod` → `~/.local/state/maestro`. - Docs: CLAUDE.md repo layout, design spec repo layout + "Daemon (`maestro daemon`)" heading, INSTALL.md (full rewrite of CLI references + new thin-builds section).
96 lines
2.8 KiB
Rust
96 lines
2.8 KiB
Rust
//! Subprocess wrapper around `maestro-registry`.
|
|
|
|
use std::net::{Ipv4Addr, SocketAddr};
|
|
use std::path::PathBuf;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::time::Duration;
|
|
|
|
use tempfile::TempDir;
|
|
|
|
use crate::build::workspace_target_bin;
|
|
use crate::util::{free_port, wait_for_async};
|
|
|
|
/// A `maestro-registry` running in a child process. The state directory is
|
|
/// kept as a `TempDir` so it survives the `Registry`'s lifetime and gets
|
|
/// cleaned up on drop.
|
|
#[derive(Debug)]
|
|
pub struct Registry {
|
|
pub addr: SocketAddr,
|
|
pub token: String,
|
|
child: Option<Child>,
|
|
_home: TempDir,
|
|
}
|
|
|
|
impl Registry {
|
|
pub async fn spawn(token: &str) -> Self {
|
|
let port = free_port();
|
|
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
|
|
let home = TempDir::new().expect("home tmpdir");
|
|
|
|
let bin = workspace_target_bin("maestro", "maestro");
|
|
let child = Command::new(&bin)
|
|
.arg("registry")
|
|
.env_clear()
|
|
// PATH so tmux etc. resolve. We need a few essentials.
|
|
.env(
|
|
"PATH",
|
|
std::env::var("PATH").unwrap_or_else(|_| "/usr/bin:/bin".into()),
|
|
)
|
|
.env("HOME", home.path())
|
|
.env("XDG_CONFIG_HOME", home.path().join(".config"))
|
|
.env("XDG_STATE_HOME", home.path().join(".local/state"))
|
|
.env("MAESTRO_TOKEN", token)
|
|
.env("MAESTRO_REGISTRY_BIND_ADDR", addr.to_string())
|
|
.env("RUST_LOG", "warn")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::inherit())
|
|
.spawn()
|
|
.unwrap_or_else(|e| panic!("spawn {} failed: {e}", bin.display()));
|
|
|
|
let url = format!("http://{addr}/api/daemons");
|
|
let http = reqwest::Client::builder()
|
|
.timeout(Duration::from_secs(2))
|
|
.build()
|
|
.expect("reqwest client");
|
|
wait_for_async("registry to bind", Duration::from_secs(5), || {
|
|
let http = http.clone();
|
|
let url = url.clone();
|
|
async move { http.get(&url).send().await.is_ok() }
|
|
})
|
|
.await;
|
|
|
|
Self {
|
|
addr,
|
|
token: token.to_string(),
|
|
child: Some(child),
|
|
_home: home,
|
|
}
|
|
}
|
|
|
|
pub fn http_base(&self) -> String {
|
|
format!("http://{}", self.addr)
|
|
}
|
|
|
|
pub fn ws_base(&self) -> String {
|
|
format!("ws://{}", self.addr)
|
|
}
|
|
|
|
/// URL daemons dial.
|
|
pub fn daemon_ws_url(&self) -> String {
|
|
format!("ws://{}/daemon", self.addr)
|
|
}
|
|
|
|
pub fn state_dir(&self) -> PathBuf {
|
|
self._home.path().to_path_buf()
|
|
}
|
|
}
|
|
|
|
impl Drop for Registry {
|
|
fn drop(&mut self) {
|
|
if let Some(mut c) = self.child.take() {
|
|
let _ = c.kill();
|
|
let _ = c.wait();
|
|
}
|
|
}
|
|
}
|