use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use tokio::process::Command;
use crate::server::error::ServerError;
pub const DEFAULT_BIND: &str = "127.0.0.1:7777";
pub const LAUNCHD_LABEL: &str = "com.mse.server";
const POLL_TOTAL: Duration = Duration::from_secs(30);
const POLL_STEP: Duration = Duration::from_millis(500);
const HEALTHZ_TIMEOUT: Duration = Duration::from_millis(500);
const SHUTDOWN_POLL_TOTAL: Duration = Duration::from_secs(10);
pub const TEMPLATE: &str = include_str!("./plist.template");
pub async fn healthz_ok(bind: &str) -> bool {
let url = format!("http://{bind}/v1/healthz");
let client = match reqwest::Client::builder().timeout(HEALTHZ_TIMEOUT).build() {
Ok(c) => c,
Err(_) => return false,
};
match client.get(&url).send().await {
Ok(r) if r.status().is_success() => {
r.text().await.map(|t| t.trim() == "ok").unwrap_or(false)
}
_ => false,
}
}
pub async fn occupancy(bind: &str) -> Result<Occupancy, ServerError> {
let url = format!("http://{bind}/v1/status");
let client = reqwest::Client::builder()
.timeout(HEALTHZ_TIMEOUT)
.build()
.map_err(|e| occupancy_io_err(format!("client build failed: {e}")))?;
let resp = client
.get(&url)
.send()
.await
.map_err(|e| occupancy_io_err(format!("request failed: {e}")))?;
if !resp.status().is_success() {
return Err(occupancy_io_err(format!(
"non-success status {}",
resp.status()
)));
}
resp.json::<Occupancy>()
.await
.map_err(|e| occupancy_io_err(format!("decode failed: {e}")))
}
fn occupancy_io_err(msg: String) -> ServerError {
ServerError::Io(std::io::Error::other(format!("occupancy: {msg}")))
}
fn current_uid() -> u32 {
#[cfg(unix)]
{
nix::unistd::Uid::current().as_raw()
}
#[cfg(not(unix))]
{
0
}
}
fn domain_target() -> String {
format!("gui/{}/{}", current_uid(), LAUNCHD_LABEL)
}
#[allow(dead_code)]
pub fn install_hint() -> String {
format!(
"launchd job '{label}' not found. Install it first:\n mse server install",
label = LAUNCHD_LABEL,
)
}
fn home_path() -> Result<PathBuf, ServerError> {
std::env::var_os("HOME").map(PathBuf::from).ok_or_else(|| {
ServerError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"HOME env not set",
))
})
}
pub fn installed_plist_path() -> Result<PathBuf, ServerError> {
let home = home_path()?;
Ok(home.join("Library/LaunchAgents/com.mse.server.plist"))
}
async fn run_launchctl(args: &[&str]) -> Result<std::process::Output, ServerError> {
Command::new("launchctl")
.args(args)
.output()
.await
.map_err(ServerError::LaunchctlExec)
}
fn combined_output_text(stdout: &[u8], stderr: &[u8]) -> String {
let stdout = String::from_utf8_lossy(stdout).trim().to_string();
let stderr = String::from_utf8_lossy(stderr).trim().to_string();
match (stdout.is_empty(), stderr.is_empty()) {
(true, true) => String::new(),
(false, true) => stdout,
(true, false) => stderr,
(false, false) => format!("{stdout}\n{stderr}"),
}
}
fn looks_like_missing_job(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("could not find")
|| lower.contains("no such process")
|| lower.contains("service target specification is invalid")
|| lower.contains("not find service")
}
fn looks_like_already_loaded(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("already loaded")
|| lower.contains("already bootstrapped")
|| lower.contains("service is already")
|| lower.contains("service already loaded")
|| lower.contains("already exists")
}
fn looks_like_missing_plist(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("no such file")
|| lower.contains("path not specified")
|| lower.contains("could not find specified service")
}
fn looks_like_bootstrap_eio(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("bootstrap failed: 5") || lower.contains("input/output error")
}
fn launchctl_print_body_indicates_loaded(text: &str, target: &str) -> bool {
let prefix = format!("{target} = {{");
text.trim_start().starts_with(&prefix)
}
async fn probe_already_loaded_via_print(target: &str) -> bool {
let out = match run_launchctl(&["print", target]).await {
Ok(o) => o,
Err(_) => return false,
};
if !out.status.success() {
return false;
}
let stdout = String::from_utf8_lossy(&out.stdout);
launchctl_print_body_indicates_loaded(&stdout, target)
}
async fn poll_healthz_until_up(bind: &str, total: Duration, step: Duration) -> bool {
let deadline = Instant::now() + total;
while Instant::now() < deadline {
if healthz_ok(bind).await {
return true;
}
tokio::time::sleep(step).await;
}
false
}
pub fn render(home: &Path, cargo_bin: &Path, project_root: &Path) -> Result<String, ServerError> {
render_impl(TEMPLATE, home, cargo_bin, project_root)
}
fn render_impl(
template: &str,
home: &Path,
cargo_bin: &Path,
project_root: &Path,
) -> Result<String, ServerError> {
let home_s = home
.to_str()
.ok_or_else(|| ServerError::Render("non-utf8 home".into()))?;
let cargo_bin_s = cargo_bin
.to_str()
.ok_or_else(|| ServerError::Render("non-utf8 cargo_bin".into()))?;
let project_root_s = project_root
.to_str()
.ok_or_else(|| ServerError::Render("non-utf8 project_root".into()))?;
let out = template
.replace("{{HOME}}", home_s)
.replace("{{CARGO_BIN}}", cargo_bin_s)
.replace("{{PROJECT_ROOT}}", project_root_s);
if let Some(start) = out.find("{{") {
let end_off = out[start..]
.find("}}")
.map(|e| start + e + 2)
.unwrap_or_else(|| out.len().min(start + 40));
let placeholder = &out[start..end_off];
return Err(ServerError::Render(format!(
"unresolved placeholder: {placeholder}"
)));
}
Ok(out)
}
pub async fn start(bind: &str) -> Result<StartOutcome, ServerError> {
if healthz_ok(bind).await {
return Ok(StartOutcome::AlreadyRunning { bind: bind.into() });
}
let target = domain_target();
let out = run_launchctl(&["kickstart", &target]).await?;
if !out.status.success() {
let text = combined_output_text(&out.stdout, &out.stderr);
if looks_like_missing_job(&text) {
bootstrap().await?;
let retry = run_launchctl(&["kickstart", &target]).await?;
if !retry.status.success() {
let retry_text = combined_output_text(&retry.stdout, &retry.stderr);
return Err(if looks_like_missing_job(&retry_text) {
ServerError::MissingJob {
label: LAUNCHD_LABEL.into(),
}
} else {
ServerError::LaunchctlFailed {
op: "kickstart",
stderr: retry_text,
}
});
}
} else {
return Err(ServerError::LaunchctlFailed {
op: "kickstart",
stderr: text,
});
}
}
if poll_healthz_until_up(bind, POLL_TOTAL, POLL_STEP).await {
Ok(StartOutcome::Started { bind: bind.into() })
} else {
Err(ServerError::HealthzTimeout {
op: "kickstart",
duration: POLL_TOTAL,
})
}
}
pub async fn shutdown(bind: &str) -> Result<StopOutcome, ServerError> {
let target = domain_target();
let out = run_launchctl(&["bootout", &target]).await?;
if !out.status.success() {
let text = combined_output_text(&out.stdout, &out.stderr);
if !looks_like_missing_job(&text) {
return Err(ServerError::LaunchctlFailed {
op: "bootout",
stderr: text,
});
}
}
let deadline = Instant::now() + SHUTDOWN_POLL_TOTAL;
while Instant::now() < deadline {
if !healthz_ok(bind).await {
return Ok(StopOutcome {
bind: bind.into(),
stopped: true,
});
}
tokio::time::sleep(POLL_STEP).await;
}
Ok(StopOutcome {
bind: bind.into(),
stopped: false,
})
}
#[cfg(target_os = "macos")]
pub async fn bootout(bind: &str) -> Result<StopOutcome, ServerError> {
shutdown(bind).await
}
pub async fn restart(bind: &str) -> Result<StartOutcome, ServerError> {
let target = domain_target();
let out = run_launchctl(&["kickstart", "-k", &target]).await?;
if !out.status.success() {
let text = combined_output_text(&out.stdout, &out.stderr);
if looks_like_missing_job(&text) {
bootstrap().await?;
let retry = run_launchctl(&["kickstart", &target]).await?;
if !retry.status.success() {
let retry_text = combined_output_text(&retry.stdout, &retry.stderr);
return Err(if looks_like_missing_job(&retry_text) {
ServerError::MissingJob {
label: LAUNCHD_LABEL.into(),
}
} else {
ServerError::LaunchctlFailed {
op: "kickstart",
stderr: retry_text,
}
});
}
} else {
return Err(ServerError::LaunchctlFailed {
op: "kickstart -k",
stderr: text,
});
}
}
if poll_healthz_until_up(bind, POLL_TOTAL, POLL_STEP).await {
Ok(StartOutcome::Started { bind: bind.into() })
} else {
Err(ServerError::HealthzTimeout {
op: "kickstart -k",
duration: POLL_TOTAL,
})
}
}
pub async fn status(bind: &str) -> StatusOutcome {
let up = healthz_ok(bind).await;
let target = domain_target();
let print_out = run_launchctl(&["print", &target]).await.ok();
let (state, pid, last_exit_code) = match &print_out {
Some(out) if out.status.success() => {
parse_launchctl_print(&String::from_utf8_lossy(&out.stdout))
}
_ => (None, None, None),
};
StatusOutcome {
bind: bind.into(),
up,
launchd_state: state,
launchd_pid: pid,
launchd_last_exit_code: last_exit_code,
}
}
pub async fn bootstrap() -> Result<BootstrapOutcome, ServerError> {
let plist_path = installed_plist_path()?;
let plist_str = plist_path
.to_str()
.ok_or_else(|| ServerError::Render("non-utf8 plist path".into()))?;
let domain = format!("gui/{}", current_uid());
let out = run_launchctl(&["bootstrap", &domain, plist_str]).await?;
if out.status.success() {
return Ok(BootstrapOutcome::Bootstrapped { plist_path });
}
let text = combined_output_text(&out.stdout, &out.stderr);
if looks_like_already_loaded(&text) {
return Ok(BootstrapOutcome::AlreadyLoaded { plist_path });
}
if looks_like_bootstrap_eio(&text) {
let target = domain_target();
if probe_already_loaded_via_print(&target).await {
return Ok(BootstrapOutcome::AlreadyLoaded { plist_path });
}
}
if looks_like_missing_plist(&text) {
return Err(ServerError::MissingJob {
label: LAUNCHD_LABEL.into(),
});
}
Err(ServerError::LaunchctlFailed {
op: "bootstrap",
stderr: text,
})
}
pub async fn install(
cargo_bin: Option<&Path>,
project_root: Option<&Path>,
) -> Result<InstallOutcome, ServerError> {
let home = home_path()?;
let cargo_bin_pb = cargo_bin.map(|p| p.to_path_buf()).unwrap_or_else(|| {
std::env::var_os("CARGO_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".cargo/bin"))
});
let project_root_pb = project_root.map(|p| p.to_path_buf()).unwrap_or_else(|| {
std::env::var_os("PWD")
.map(PathBuf::from)
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default())
});
let rendered = render(&home, &cargo_bin_pb, &project_root_pb)?;
let plist_path = installed_plist_path()?;
let launch_agents_dir = plist_path.parent().ok_or_else(|| {
ServerError::Render("installed plist path has no parent directory".into())
})?;
tokio::fs::create_dir_all(launch_agents_dir)
.await
.map_err(ServerError::Io)?;
let target = domain_target();
let print_out = run_launchctl(&["print", &target]).await?;
if print_out.status.success() {
let _ = run_launchctl(&["bootout", &target]).await?;
}
tokio::fs::write(&plist_path, rendered.as_bytes())
.await
.map_err(ServerError::Io)?;
let bootstrap_outcome = bootstrap().await?;
Ok(InstallOutcome {
plist_path,
bootstrap: bootstrap_outcome,
})
}
pub async fn uninstall() -> Result<UninstallOutcome, ServerError> {
let plist_path = installed_plist_path()?;
let target = domain_target();
let out = run_launchctl(&["bootout", &target]).await?;
if !out.status.success() {
let text = combined_output_text(&out.stdout, &out.stderr);
if !looks_like_missing_job(&text) {
return Err(ServerError::LaunchctlFailed {
op: "bootout",
stderr: text,
});
}
}
match tokio::fs::remove_file(&plist_path).await {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => return Err(ServerError::Io(e)),
}
Ok(UninstallOutcome { plist_path })
}
#[cfg(target_os = "macos")]
pub async fn logs(tail: Option<usize>) -> Result<LogsOutcome, ServerError> {
let stdout_path = PathBuf::from("/tmp/mse-server.stdout");
let stderr_path = PathBuf::from("/tmp/mse-server.stderr");
let n = tail.unwrap_or(20);
let stdout_tail = read_tail(&stdout_path, n).await;
let stderr_tail = read_tail(&stderr_path, n).await;
Ok(LogsOutcome {
stdout_path,
stderr_path,
stdout_tail,
stderr_tail,
})
}
#[cfg(target_os = "macos")]
async fn read_tail(path: &Path, n: usize) -> Vec<String> {
match tokio::fs::read_to_string(path).await {
Ok(text) => {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(n);
lines[start..].iter().map(|s| (*s).to_string()).collect()
}
Err(_) => Vec::new(),
}
}
fn parse_launchctl_print(text: &str) -> (Option<String>, Option<i64>, Option<i64>) {
let mut state = None;
let mut pid = None;
let mut last_exit_code = None;
for line in text.lines() {
let line = line.trim();
if let Some(v) = line.strip_prefix("state = ") {
state = Some(v.trim().to_string());
} else if let Some(v) = line.strip_prefix("pid = ") {
pid = v.trim().parse::<i64>().ok();
} else if let Some(v) = line.strip_prefix("last exit code = ") {
last_exit_code = v.trim().parse::<i64>().ok();
}
}
(state, pid, last_exit_code)
}
#[derive(serde::Serialize)]
#[serde(tag = "status")]
pub enum StartOutcome {
#[serde(rename = "already_running")]
AlreadyRunning {
bind: String,
},
#[serde(rename = "started")]
Started {
bind: String,
},
}
#[derive(serde::Serialize)]
pub struct StopOutcome {
pub bind: String,
pub stopped: bool,
}
#[derive(serde::Serialize)]
pub struct StatusOutcome {
pub bind: String,
pub up: bool,
pub launchd_state: Option<String>,
pub launchd_pid: Option<i64>,
pub launchd_last_exit_code: Option<i64>,
}
#[derive(serde::Serialize)]
#[serde(tag = "status")]
pub enum BootstrapOutcome {
#[serde(rename = "bootstrapped")]
Bootstrapped {
plist_path: PathBuf,
},
#[serde(rename = "already_loaded")]
AlreadyLoaded {
plist_path: PathBuf,
},
}
#[derive(serde::Serialize)]
pub struct InstallOutcome {
pub plist_path: PathBuf,
pub bootstrap: BootstrapOutcome,
}
#[derive(serde::Serialize)]
pub struct UninstallOutcome {
pub plist_path: PathBuf,
}
#[cfg(target_os = "macos")]
#[derive(serde::Serialize)]
pub struct LogsOutcome {
pub stdout_path: PathBuf,
pub stderr_path: PathBuf,
pub stdout_tail: Vec<String>,
pub stderr_tail: Vec<String>,
}
#[derive(serde::Deserialize)]
pub struct Occupancy {
pub running_runs: usize,
pub attached_operators: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_launchctl_print_extracts_state_pid_exit_code() {
let sample = "\
com.mse.server = {
\tactive count = 1
\tpath = $HOME/Library/LaunchAgents/com.mse.server.plist
\ttype = LaunchAgent
\tstate = running
\tprogram = $HOME/.cargo/bin/mse serve
\targuments = {
\t\t$HOME/.cargo/bin/mse serve
\t\t--config
\t\t$HOME/.mse/config.toml
\t}
\tpid = 12345
\tlast exit code = 0
}";
let (state, pid, code) = parse_launchctl_print(sample);
assert_eq!(state.as_deref(), Some("running"));
assert_eq!(pid, Some(12345));
assert_eq!(code, Some(0));
}
#[test]
fn parse_launchctl_print_missing_fields_are_none() {
let (state, pid, code) = parse_launchctl_print("not a plist dump\njust noise");
assert_eq!(state, None);
assert_eq!(pid, None);
assert_eq!(code, None);
}
#[test]
fn looks_like_missing_job_detects_common_launchctl_errors() {
assert!(looks_like_missing_job(
"Could not find service \"com.mse.server\" in domain for port"
));
assert!(!looks_like_missing_job("Operation now in progress"));
}
#[test]
fn combined_output_text_joins_stdout_and_stderr() {
assert_eq!(
combined_output_text(b"out-line", b"err-line"),
"out-line\nerr-line"
);
assert_eq!(combined_output_text(b"only-out", b""), "only-out");
assert_eq!(combined_output_text(b"", b"only-err"), "only-err");
assert_eq!(combined_output_text(b"", b""), "");
}
#[test]
fn domain_target_embeds_uid_and_label() {
let target = domain_target();
assert!(target.starts_with("gui/"));
assert!(target.ends_with(LAUNCHD_LABEL));
}
#[tokio::test]
async fn occupancy_parses_status_response() {
let engine = mlua_swarm::Engine::new(mlua_swarm::EngineCfg::default());
let router = mlua_swarm_server::build_router_full(
engine,
mlua_swarm_server::default_registry(),
None,
None,
None,
None,
None,
None,
None,
300,
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind ephemeral port");
let addr = listener.local_addr().expect("local addr");
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
let bind = addr.to_string();
let occ = occupancy(&bind).await.expect("occupancy() must succeed");
assert_eq!(occ.running_runs, 0);
assert_eq!(occ.attached_operators, 0);
}
#[test]
fn render_substitutes_placeholders() {
let home = Path::new("/Users/alice");
let cargo_bin = Path::new("/Users/alice/.cargo/bin");
let project_root = Path::new("/Users/alice/projects/mlua-swarm");
let rendered = render(home, cargo_bin, project_root).expect("render succeeds");
assert!(!rendered.contains("{{HOME}}"), "HOME placeholder leaked");
assert!(
!rendered.contains("{{CARGO_BIN}}"),
"CARGO_BIN placeholder leaked"
);
assert!(
!rendered.contains("{{PROJECT_ROOT}}"),
"PROJECT_ROOT placeholder leaked"
);
assert!(!rendered.contains("{{"), "unresolved `{{{{` in output");
assert!(rendered.contains("/Users/alice/.cargo/bin/mse"));
assert!(rendered.contains("/Users/alice/.mse/config.toml"));
assert!(rendered.contains("/Users/alice/projects/mlua-swarm"));
}
#[test]
fn render_rejects_unresolved_placeholder() {
let extended = format!("{TEMPLATE}\n<key>Future</key><string>{{{{FUTURE}}}}</string>");
let err = render_impl(&extended, Path::new("/H"), Path::new("/C"), Path::new("/P"))
.expect_err("unresolved placeholder must be rejected");
match err {
ServerError::Render(msg) => {
assert!(
msg.contains("unresolved placeholder"),
"message missing 'unresolved placeholder': {msg}"
);
assert!(
msg.contains("{{FUTURE}}"),
"message missing the leaked placeholder literal: {msg}"
);
}
other => panic!("expected ServerError::Render, got {other:?}"),
}
}
#[test]
fn looks_like_already_loaded_detects_common_launchctl_errors() {
assert!(looks_like_already_loaded(
"Bootstrap failed: Service is already loaded"
));
assert!(looks_like_already_loaded("SERVICE ALREADY LOADED"));
assert!(looks_like_already_loaded(
"com.mse.server: already bootstrapped"
));
assert!(looks_like_already_loaded(
"The service already exists in this domain"
));
assert!(looks_like_already_loaded(
"service is already registered in domain"
));
assert!(!looks_like_already_loaded("Operation now in progress"));
assert!(!looks_like_already_loaded(
"Could not find service in domain"
));
}
#[test]
fn probe_already_loaded_recognises_running_body() {
let body = "gui/501/com.mse.server = {\n\
\tactive count = 1\n\
\tstate = running\n\
\tpid = 12345\n\
}";
assert!(launchctl_print_body_indicates_loaded(
body,
"gui/501/com.mse.server"
));
}
#[test]
fn probe_already_loaded_rejects_missing_body() {
let bad_request = "Bad request.\n";
assert!(!launchctl_print_body_indicates_loaded(
bad_request,
"gui/501/com.mse.server"
));
let not_found = "Could not find service \"com.mse.server\" in domain for port\n";
assert!(!launchctl_print_body_indicates_loaded(
not_found,
"gui/501/com.mse.server"
));
assert!(!launchctl_print_body_indicates_loaded(
"",
"gui/501/com.mse.server"
));
}
#[test]
fn looks_like_bootstrap_eio_matches_sequoia_signature() {
let sequoia = "Bootstrap failed: 5: Input/output error\n\
Try re-running the command as root for richer errors.";
assert!(looks_like_bootstrap_eio(sequoia));
assert!(looks_like_bootstrap_eio("bootstrap failed: 5"));
assert!(looks_like_bootstrap_eio("INPUT/OUTPUT ERROR"));
assert!(!looks_like_bootstrap_eio(
"Bootstrap failed: 37: Service is already loaded"
));
assert!(!looks_like_bootstrap_eio("Operation now in progress"));
}
#[test]
fn classical_heuristic_does_not_cover_sequoia_signature() {
let sequoia = "Bootstrap failed: 5: Input/output error";
assert!(
!looks_like_already_loaded(sequoia),
"classical heuristic must stay narrow; Sequoia goes through \
the EIO-post-check branch, not this one"
);
}
#[test]
fn install_hint_points_to_mse_server_install() {
let hint = install_hint();
assert!(
hint.contains("mse server install"),
"install_hint missing `mse server install` literal: {hint}"
);
let legacy = format!("{}/{}/{}", "scripts", "launchd", "install.sh");
assert!(
!hint.contains(&legacy),
"install_hint still references the legacy shell installer: {hint}"
);
}
}