use futures_util::{SinkExt, StreamExt};
use super::{serve_backend_connection, serve_backend_endpoint};
use crate::broker::backend_handle::{BackendHandle, DaemonProcess};
use crate::broker::protocol::Endpoint;
use crate::broker::protocol_v2::{session_frame, SessionFrame, SessionStart};
use crate::daemon::compile_session::session_framed;
use crate::platform::ipc::{AsyncListener, AsyncStream, Endpoint as IpcEndpoint};
fn fixture_program() -> String {
let exe = std::env::current_exe().expect("test executable path");
let dir = exe
.parent()
.and_then(std::path::Path::parent)
.expect("test binary should live in <profile>/deps/");
dir.join(format!(
"testbin-stdio-scripted{}",
std::env::consts::EXE_SUFFIX
))
.to_string_lossy()
.into_owned()
}
fn frame(kind: session_frame::Kind) -> SessionFrame {
SessionFrame { kind: Some(kind) }
}
fn identity_for(path: &std::path::Path) -> (Endpoint, DaemonProcess) {
let endpoint = Endpoint {
namespace_id: "shared".into(),
path: path.to_string_lossy().into_owned(),
};
let identity = DaemonProcess::current_process(endpoint.clone(), Some(30))
.expect("current-process daemon identity");
(endpoint, identity)
}
fn ipc_endpoint(path: &std::path::Path) -> IpcEndpoint {
IpcEndpoint::new(path.to_string_lossy().into_owned()).expect("IPC endpoint")
}
#[cfg(unix)]
#[tokio::test]
async fn mux_backend_endpoint_serves_a_session_compile() {
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("rp-mux-sess-{pid}.sock"));
let _ = std::fs::remove_file(&path);
let (_endpoint, identity) = identity_for(&path);
let ipc_endpoint = ipc_endpoint(&path);
let listener = AsyncListener::bind(&ipc_endpoint).expect("bind backend endpoint");
let daemon = tokio::spawn(serve_backend_endpoint(listener, identity));
let stream = AsyncStream::connect(&ipc_endpoint)
.await
.expect("client dials backend endpoint");
let mut client = session_framed(stream);
client
.send(frame(session_frame::Kind::Start(SessionStart {
program: fixture_program(),
args: vec![
"out:HELLO".to_owned(),
"err:WORLD".to_owned(),
"exit:9".to_owned(),
],
cwd: String::new(),
env: Vec::new(),
clear_inherited_env: false,
environment_policy: 0,
})))
.await
.expect("send start");
client
.send(frame(session_frame::Kind::StdinEof(true)))
.await
.expect("send stdin eof");
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut code = None;
while let Some(Ok(sf)) = client.next().await {
match sf.kind {
Some(session_frame::Kind::Stdout(b)) => stdout.extend_from_slice(&b),
Some(session_frame::Kind::Stderr(b)) => stderr.extend_from_slice(&b),
Some(session_frame::Kind::Exit(e)) => {
code = Some(e.code);
break;
}
_ => panic!("unexpected inbound-only frame on the outbound lane"),
}
}
daemon.abort();
let _ = std::fs::remove_file(&path);
assert_eq!(
stdout, b"HELLO",
"stdout proxied over the mux SESSION endpoint"
);
assert_eq!(
stderr, b"WORLD",
"stderr proxied over the mux SESSION endpoint"
);
assert_eq!(
code,
Some(9),
"exit code proxied over the mux SESSION endpoint"
);
}
#[cfg(unix)]
#[tokio::test]
async fn mux_backend_endpoint_answers_identity_probe() {
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("rp-mux-probe-{pid}.sock"));
let _ = std::fs::remove_file(&path);
let (endpoint, identity) = identity_for(&path);
let ipc_endpoint = ipc_endpoint(&path);
let listener = AsyncListener::bind(&ipc_endpoint).expect("bind backend endpoint");
let daemon = tokio::spawn(serve_backend_endpoint(listener, identity.clone()));
let expected = identity.clone();
let probe = tokio::task::spawn_blocking(move || {
BackendHandle::probe_with_service("zccache", "1.11.20", &endpoint, &expected)
})
.await
.expect("probe task joined");
daemon.abort();
let _ = std::fs::remove_file(&path);
assert!(
probe.is_ok(),
"the mux endpoint must answer the identity probe: {:?}",
probe.err()
);
}
#[cfg(unix)]
#[tokio::test]
async fn mux_backend_connection_rejects_non_session_first_party_frame() {
use crate::broker::protocol::{encode_framed, Frame, CONTROL_PAYLOAD_PROTOCOL};
let (client, server) = tokio::io::duplex(4096);
let (_endpoint, identity) = identity_for(std::path::Path::new(
"/tmp/rp-mux-reject-does-not-bind.sock",
));
let server = tokio::spawn(async move { serve_backend_connection(server, &identity).await });
let wire = encode_framed(&Frame::request(CONTROL_PAYLOAD_PROTOCOL, Vec::new()))
.expect("encode control frame");
use tokio::io::AsyncWriteExt;
let mut client = client;
client.write_all(&wire).await.expect("send control frame");
client.flush().await.expect("flush");
let result = server.await.expect("server task joined");
assert!(
result.is_err(),
"a first-party control frame on the backend endpoint must be rejected"
);
}
#[cfg(unix)]
#[tokio::test]
async fn full_vertical_client_broker_relay_daemon_mux_compile() {
use crate::broker::session_relay::relay_local_socket_session;
let pid = std::process::id();
let daemon_path = std::env::temp_dir().join(format!("rp-vert-d-{pid}.sock"));
let broker_path = std::env::temp_dir().join(format!("rp-vert-b-{pid}.sock"));
let _ = std::fs::remove_file(&daemon_path);
let _ = std::fs::remove_file(&broker_path);
let (_endpoint, identity) = identity_for(&daemon_path);
let daemon_ipc_endpoint = ipc_endpoint(&daemon_path);
let daemon_listener = AsyncListener::bind(&daemon_ipc_endpoint).expect("bind daemon endpoint");
let daemon = tokio::spawn(serve_backend_endpoint(daemon_listener, identity));
let broker_listener = AsyncListener::bind(
&IpcEndpoint::new(broker_path.as_path().to_string_lossy().into_owned())
.expect("broker fs name"),
)
.expect("bind broker endpoint");
let daemon_path_str = daemon_path.to_string_lossy().into_owned();
let broker = tokio::spawn(async move {
let client_conn = broker_listener.accept().await.expect("broker accept");
let _ = relay_local_socket_session(client_conn, &daemon_path_str).await;
});
let stream = AsyncStream::connect(
&IpcEndpoint::new(broker_path.as_path().to_string_lossy().into_owned())
.expect("client fs name"),
)
.await
.expect("client dials broker");
let mut client = session_framed(stream);
client
.send(frame(session_frame::Kind::Start(SessionStart {
program: fixture_program(),
args: vec!["out:PROXIED".to_owned(), "exit:3".to_owned()],
cwd: String::new(),
env: Vec::new(),
clear_inherited_env: false,
environment_policy: 0,
})))
.await
.expect("send start");
client
.send(frame(session_frame::Kind::StdinEof(true)))
.await
.expect("send stdin eof");
let mut stdout = Vec::new();
let mut code = None;
while let Some(Ok(sf)) = client.next().await {
match sf.kind {
Some(session_frame::Kind::Stdout(b)) => stdout.extend_from_slice(&b),
Some(session_frame::Kind::Stderr(_)) => {}
Some(session_frame::Kind::Exit(e)) => {
code = Some(e.code);
break;
}
_ => panic!("unexpected inbound-only frame on the outbound lane"),
}
}
daemon.abort();
broker.abort();
let _ = std::fs::remove_file(&daemon_path);
let _ = std::fs::remove_file(&broker_path);
assert_eq!(
stdout, b"PROXIED",
"stdout proxied client<-broker<-daemon mux"
);
assert_eq!(code, Some(3), "exit code proxied across both real sockets");
}