#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::process::Command;
use std::time::Duration;
use omni_dev::daemon::client::DaemonClient;
async fn wait_for<F>(mut f: F) -> bool
where
F: FnMut() -> bool,
{
for _ in 0..200 {
if f() {
return true;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
false
}
#[tokio::test]
async fn daemon_run_status_stop_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("d.sock");
let mut child = Command::new(env!("CARGO_BIN_EXE_omni-dev"))
.arg("daemon")
.arg("run")
.arg("--socket")
.arg(&socket)
.arg("--bridge-control-port")
.arg("0")
.arg("--bridge-ws-port")
.arg("0")
.arg("--no-menu")
.spawn()
.expect("failed to spawn `omni-dev daemon run`");
let client = DaemonClient::new(&socket);
let mut ready = false;
for _ in 0..200 {
if client.ping().await.is_ok() {
ready = true;
break;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
assert!(ready, "daemon did not become ready");
let report = client.status().await.expect("status request failed");
assert!(
report
.services
.iter()
.any(|s| s.name == "browser-bridge" && s.healthy),
"expected a healthy browser-bridge service, got {report:?}"
);
client.shutdown().await.expect("shutdown request failed");
let exited = wait_for(|| matches!(child.try_wait(), Ok(Some(_)))).await;
if !exited {
let _ = child.kill();
}
assert!(exited, "daemon did not exit after shutdown");
let status = child.wait().unwrap();
assert!(status.success(), "daemon exited with failure: {status:?}");
}
#[cfg(unix)]
#[tokio::test]
async fn daemon_run_shuts_down_gracefully_on_sighup() {
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("d.sock");
let mut child = Command::new(env!("CARGO_BIN_EXE_omni-dev"))
.arg("daemon")
.arg("run")
.arg("--socket")
.arg(&socket)
.arg("--bridge-control-port")
.arg("0")
.arg("--bridge-ws-port")
.arg("0")
.arg("--no-menu")
.spawn()
.expect("failed to spawn `omni-dev daemon run`");
let client = DaemonClient::new(&socket);
let ready = {
let client = &client;
let mut ok = false;
for _ in 0..200 {
if client.ping().await.is_ok() {
ok = true;
break;
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
ok
};
if !ready {
let _ = child.kill();
}
assert!(ready, "daemon did not become ready");
kill(
Pid::from_raw(child.id().try_into().unwrap()),
Signal::SIGHUP,
)
.expect("failed to send SIGHUP");
let exited = wait_for(|| matches!(child.try_wait(), Ok(Some(_)))).await;
if !exited {
let _ = child.kill();
}
assert!(exited, "daemon did not exit after SIGHUP");
let status = child.wait().unwrap();
assert!(status.success(), "SIGHUP exit was not graceful: {status:?}");
assert!(!socket.exists(), "socket was not unlinked on shutdown");
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn daemon_start_detaches_into_its_own_session() {
use std::os::unix::fs::PermissionsExt;
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
fn session_id(pid: &str) -> i32 {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).unwrap();
let after_comm = &stat[stat.rfind(')').unwrap() + 2..];
after_comm
.split_whitespace()
.nth(3)
.unwrap()
.parse()
.unwrap()
}
let dir = tempfile::tempdir().unwrap();
let socket = dir.path().join("d.sock");
let launcher = Command::new(env!("CARGO_BIN_EXE_omni-dev"))
.arg("daemon")
.arg("start")
.arg("--socket")
.arg(&socket)
.env("OMNI_DEV_DAEMON_DISABLE_SYSTEMD", "1")
.status()
.expect("failed to run `omni-dev daemon start`");
assert!(launcher.success(), "daemon start failed: {launcher:?}");
let client = DaemonClient::new(&socket);
client.ping().await.expect("detached daemon is not alive");
let socket_str = socket.to_str().unwrap();
let daemon_pid = std::fs::read_dir("/proc")
.unwrap()
.filter_map(|e| {
let name = e.ok()?.file_name().into_string().ok()?;
name.chars().all(|c| c.is_ascii_digit()).then_some(name)
})
.find(|pid| {
std::fs::read(format!("/proc/{pid}/cmdline")).is_ok_and(|argv| {
let argv = String::from_utf8_lossy(&argv);
argv.contains("daemon\0run\0") && argv.contains(socket_str)
})
})
.expect("could not find the detached daemon process");
let daemon_sid = session_id(&daemon_pid);
assert_eq!(
daemon_sid,
daemon_pid.parse::<i32>().unwrap(),
"daemon is not a session leader"
);
assert_ne!(
daemon_sid,
session_id("self"),
"daemon shares the launcher's session"
);
let log = dir.path().join("daemon.log");
assert!(
log.is_file(),
"daemon.log was not created beside the socket"
);
assert_eq!(
std::fs::metadata(&log).unwrap().permissions().mode() & 0o777,
0o600,
"daemon.log is not 0600"
);
let stopped = client.shutdown().await;
let pid = Pid::from_raw(daemon_pid.parse().unwrap());
let gone = wait_for(|| kill(pid, None).is_err()).await;
if !gone {
let _ = kill(pid, Signal::SIGKILL);
}
stopped.expect("shutdown request failed");
assert!(gone, "daemon did not exit after shutdown");
}