use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use super::{bind, serve_until_shutdown, socket_path};
use crate::daemon::state::DaemonState;
const GENEROUS: Duration = Duration::from_secs(10);
async fn spawn_listener(
socket: &Path,
) -> (
tokio::sync::oneshot::Sender<()>,
tokio::task::JoinHandle<()>,
) {
let bound = bind(socket).await.expect("bind a fresh socket path");
serve_bound(bound, socket).await
}
async fn serve_bound(
bound: super::BoundSocket,
socket: &Path,
) -> (
tokio::sync::oneshot::Sender<()>,
tokio::task::JoinHandle<()>,
) {
let (stop_tx, stop_rx) = tokio::sync::oneshot::channel::<()>();
let state = DaemonState::shared();
let handle = tokio::spawn(async move {
serve_until_shutdown(bound, state, async {
let _ = stop_rx.await;
})
.await;
});
for _ in 0..200 {
if trusty_common::uds::socket_is_serving(socket, Duration::from_millis(50)).await {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
(stop_tx, handle)
}
struct RawExchange {
frame: serde_json::Value,
trailing: Vec<u8>,
}
async fn dial_once(socket: &Path, request: &serde_json::Value) -> RawExchange {
let stream = tokio::time::timeout(GENEROUS, UnixStream::connect(socket))
.await
.expect("connect must not hang")
.expect("a live socket must accept a connection");
let mut reader = BufReader::new(stream);
let mut body = serde_json::to_vec(request).expect("encode the request");
body.push(b'\n');
reader
.get_mut()
.write_all(&body)
.await
.expect("write the request frame");
let mut line = String::new();
let read = tokio::time::timeout(GENEROUS, reader.read_line(&mut line))
.await
.expect("the server must answer rather than hang")
.expect("read the response frame");
assert!(read > 0, "the server closed without writing a frame at all");
let mut trailing = Vec::new();
tokio::time::timeout(GENEROUS, reader.read_to_end(&mut trailing))
.await
.expect("the server must close rather than hold the connection open")
.expect("read to EOF");
RawExchange {
frame: serde_json::from_str(&line).expect("the answer must be one JSON frame"),
trailing,
}
}
#[tokio::test(flavor = "multi_thread")]
async fn unknown_method_gets_a_method_not_found_frame() {
let tmp = TempDir::new().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-mpm.sock");
let (stop, handle) = spawn_listener(&socket).await;
let exchange = dial_once(
&socket,
&serde_json::json!({
"jsonrpc": "2.0", "id": 7, "method": "mpm.no.such.method", "params": {},
}),
)
.await;
assert_eq!(exchange.frame["jsonrpc"], "2.0");
assert_eq!(exchange.frame["id"], 7);
assert!(
exchange.frame.get("result").is_none(),
"an unregistered method must not answer a result: {}",
exchange.frame
);
assert_eq!(
exchange.frame["error"]["code"],
serde_json::json!(trusty_common::uds::server::CODE_METHOD_NOT_FOUND),
"unknown methods answer method_not_found: {}",
exchange.frame
);
assert!(
exchange.trailing.is_empty(),
"the server must close cleanly after one frame, not send {} more bytes",
exchange.trailing.len()
);
let _ = stop.send(());
handle.await.expect("the serve task must not panic");
}
#[tokio::test(flavor = "multi_thread")]
async fn serve_unlinks_its_socket_on_shutdown() {
let tmp = TempDir::new().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-mpm.sock");
let (stop, handle) = spawn_listener(&socket).await;
assert!(socket.exists(), "the listener must have bound its socket");
let _ = stop.send(());
handle.await.expect("the serve task must not panic");
assert!(
!socket.exists(),
"the socket file must be unlinked on clean shutdown: {}",
socket.display()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn bind_refuses_a_socket_another_process_is_serving() {
let tmp = TempDir::new().expect("tempdir");
let socket = tmp.path().join("sockets").join("trusty-mpm.sock");
let (stop, handle) = spawn_listener(&socket).await;
let err = bind(&socket)
.await
.expect_err("a second bind against a live socket must fail, never degrade");
let rendered = format!("{err:#}");
assert!(
rendered.contains(&socket.display().to_string()),
"the error must name the path an operator has to act on: {rendered}"
);
assert!(
trusty_common::uds::socket_is_serving(&socket, GENEROUS).await,
"the incumbent must still be serving after the refused bind"
);
let _ = stop.send(());
handle.await.expect("the serve task must not panic");
}
#[tokio::test(flavor = "multi_thread")]
async fn bind_reclaims_a_stale_socket_file() {
let tmp = TempDir::new().expect("tempdir");
let dir = tmp.path().join("sockets");
std::fs::create_dir_all(&dir).expect("create the socket directory");
let socket = dir.join("trusty-mpm.sock");
let corpse = std::os::unix::net::UnixListener::bind(&socket).expect("bind the predecessor");
drop(corpse);
assert!(socket.exists(), "the corpse must outlive its listener");
let mut last_err = None;
let mut bound = None;
for _ in 0..10 {
match bind(&socket).await {
Ok(b) => {
bound = Some(b);
break;
}
Err(e) => {
last_err = Some(e);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
let bound = bound.unwrap_or_else(|| {
panic!(
"a socket nobody is serving must be reclaimed within ten starts; last error: {:#}",
last_err.expect("a failed loop records its error")
)
});
let (stop, handle) = serve_bound(bound, &socket).await;
assert!(
trusty_common::uds::socket_is_serving(&socket, GENEROUS).await,
"the successor must be serving the reclaimed path"
);
let _ = stop.send(());
handle.await.expect("the serve task must not panic");
}
#[test]
fn socket_path_is_the_product_named_socket_under_the_data_dir() {
let path = socket_path().expect("the data directory must resolve");
assert_eq!(
path.file_name().and_then(|n| n.to_str()),
Some("trusty-mpm.sock"),
"socket path {} does not follow the <product>.sock convention",
path.display()
);
assert!(
path.is_absolute(),
"a relative socket path would resolve against the daemon's cwd (/ under \
launchd): {}",
path.display()
);
}