#![cfg(target_os = "linux")]
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::OwnedFd;
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
use oc2_hlapi_v2::client::HlapiClient;
use oc2_hlapi_v2::codecs::{BaseCodec, BaseStreams};
use oc2_hlapi_v2::messages::*;
use oc2_hlapi_v2::transports::{BusdUnixSocket, LinuxVirtio, TimeoutFile};
use oc2_hlapi_v2::{Codec, Stream, Transport};
use serde_json::{Value, json};
static ENV: Mutex<()> = Mutex::new(());
fn framed(v: &Value) -> Vec<u8> {
let mut out = vec![0];
out.extend(serde_json::to_vec(v).unwrap());
out.push(0);
out
}
fn read_frame(s: &mut impl Read) -> io::Result<Value> {
let mut frame = Vec::new();
loop {
let mut b = [0];
s.read_exact(&mut b)?;
if b[0] == 0 {
if frame.is_empty() {
continue;
}
return Ok(serde_json::from_slice(&frame).unwrap());
}
frame.push(b[0]);
}
}
struct PairTransport(Option<BaseStreams<UnixStream>>);
impl Transport for PairTransport {
type Streams = BaseStreams<UnixStream>;
type Error = io::Error;
fn open_streams(&mut self) -> io::Result<Self::Streams> {
self.0
.take()
.ok_or_else(|| io::Error::other("no more connections"))
}
}
#[test]
fn the_codec_works_over_unix_stream_pairs() {
let (rpc, mut rpc_h) = UnixStream::pair().unwrap();
let (blob, _blob_h) = UnixStream::pair().unwrap();
let (event, mut event_h) = UnixStream::pair().unwrap();
let mut codec = BaseCodec::new(PairTransport(Some(BaseStreams { rpc, blob, event }))).unwrap();
codec
.send_request(
Request {
id: Some(1),
data: RequestData::List,
},
&[],
)
.unwrap();
assert_eq!(
read_frame(&mut rpc_h).unwrap(),
json!({"type": "list", "id": 1})
);
rpc_h
.write_all(&framed(
&json!({"type": "list", "id": 1, "gen": 4, "data": []}),
))
.unwrap();
assert_eq!(
codec
.read_response(Some(Duration::from_secs(10)))
.unwrap()
.unwrap()
.generation,
Some(4)
);
assert!(
codec
.read_event(Some(Duration::from_millis(50)))
.unwrap()
.is_none()
);
assert!(codec.read_event(Some(Duration::ZERO)).unwrap().is_none());
event_h
.write_all(&framed(&json!({"type": "devicesChanged", "gen": 2})))
.unwrap();
assert_eq!(
codec
.read_event(Some(Duration::from_secs(10)))
.unwrap()
.unwrap()
.generation,
2
);
}
fn file_pair() -> (TimeoutFile, UnixStream) {
let (a, b) = UnixStream::pair().unwrap();
(TimeoutFile::from(File::from(OwnedFd::from(a))), b)
}
#[test]
fn timeout_file_reads_and_writes() {
let (mut f, mut peer) = file_pair();
f.write_all(b"out").unwrap();
f.flush().unwrap();
let mut buf = [0; 3];
peer.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"out");
peer.write_all(b"in!").unwrap();
f.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"in!");
}
#[test]
fn timeout_file_read_timeout() {
let (mut f, mut peer) = file_pair();
f.set_read_timeout(Some(Duration::from_millis(80))).unwrap();
let start = Instant::now();
let err = f.read(&mut [0; 4]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
assert!(start.elapsed() >= Duration::from_millis(70));
peer.write_all(b"x").unwrap();
assert_eq!(f.read(&mut [0; 4]).unwrap(), 1);
}
#[test]
fn timeout_file_without_a_timeout_blocks() {
let (mut f, mut peer) = file_pair();
f.set_read_timeout(Some(Duration::from_millis(10))).unwrap();
f.set_read_timeout(None).unwrap();
let writer = thread::spawn(move || {
thread::sleep(Duration::from_millis(100));
peer.write_all(b"late").unwrap();
peer
});
let start = Instant::now();
assert_eq!(f.read(&mut [0; 8]).unwrap(), 4);
assert!(start.elapsed() >= Duration::from_millis(80));
drop(writer.join().unwrap());
}
#[test]
fn timeout_file_rejects_a_zero_timeout() {
let (f, _peer) = file_pair();
let err = f.set_read_timeout(Some(Duration::ZERO)).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
#[test]
fn timeout_file_nonblocking_mode() {
let (mut f, mut peer) = file_pair();
f.set_nonblocking(true).unwrap();
assert_eq!(
f.read(&mut [0; 4]).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
peer.write_all(b"ab").unwrap();
assert_eq!(f.read(&mut [0; 4]).unwrap(), 2);
f.set_nonblocking(false).unwrap();
f.set_read_timeout(Some(Duration::from_millis(50))).unwrap();
assert_eq!(
f.read(&mut [0; 4]).unwrap_err().kind(),
io::ErrorKind::TimedOut
);
}
#[test]
fn timeout_file_reports_eof() {
let (mut f, peer) = file_pair();
f.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
drop(peer);
assert_eq!(f.read(&mut [0; 4]).unwrap(), 0);
}
struct FileTransport(Option<BaseStreams<TimeoutFile>>);
impl Transport for FileTransport {
type Streams = BaseStreams<TimeoutFile>;
type Error = io::Error;
fn open_streams(&mut self) -> io::Result<Self::Streams> {
self.0
.take()
.ok_or_else(|| io::Error::other("no more connections"))
}
}
#[test]
fn the_codec_works_over_timeout_files() {
let (rpc, mut rpc_h) = file_pair();
let (blob, mut blob_h) = file_pair();
let (event, mut event_h) = file_pair();
let mut codec = BaseCodec::new(FileTransport(Some(BaseStreams { rpc, blob, event }))).unwrap();
assert!(
codec
.read_event(Some(Duration::from_millis(60)))
.unwrap()
.is_none()
);
assert!(codec.read_event(Some(Duration::ZERO)).unwrap().is_none());
event_h
.write_all(&framed(&json!({"type": "devicesChanged", "gen": 6})))
.unwrap();
assert_eq!(
codec
.read_event(Some(Duration::from_secs(10)))
.unwrap()
.unwrap()
.generation,
6
);
codec
.send_request(
Request {
id: Some(2),
data: RequestData::List,
},
&[b"b"],
)
.unwrap();
assert_eq!(read_frame(&mut rpc_h).unwrap()["id"], 2);
let mut one = [0];
blob_h.read_exact(&mut one).unwrap();
rpc_h
.write_all(&framed(&json!({"type": "list", "id": 2, "data": []})))
.unwrap();
assert_eq!(
codec
.read_response(Some(Duration::from_secs(10)))
.unwrap()
.unwrap()
.id,
2
);
blob_h.write_all(b"stale").unwrap();
codec.clear_blob_stream().unwrap();
blob_h.write_all(b"fresh").unwrap();
assert_eq!(
codec
.read_blob(BlobDescriptor::describe(b"fresh").unwrap())
.unwrap(),
b"fresh"
);
}
fn socket_path(tag: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"oc2_hlapi_v2_test_{}_{tag}.sock",
std::process::id()
));
let _ = std::fs::remove_file(&p);
p
}
fn fake_daemon(listener: UnixListener) -> thread::JoinHandle<()> {
thread::spawn(move || {
let (mut rpc, _) = listener.accept().unwrap();
let mut role = [0];
rpc.read_exact(&mut role).unwrap();
assert_eq!(role[0], b'R');
let hello = read_frame(&mut rpc).unwrap();
assert_eq!(hello["type"], "hello");
rpc.write_all(&framed(
&json!({"type": "hello", "version": 1, "token": "tok", "gen": 1}),
))
.unwrap();
let (mut blob, _) = listener.accept().unwrap();
let mut buf = [0; 4];
blob.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"Btok");
let (mut event, _) = listener.accept().unwrap();
event.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"Etok");
while let Ok(req) = read_frame(&mut rpc) {
let reply = json!({"type": "list", "id": req["id"], "gen": 1, "data": [
{"deviceId": "00000000-0000-0000-0000-000000000009", "typeNames": ["redstone"]},
]});
rpc.write_all(&framed(&reply)).unwrap();
}
})
}
#[test]
fn busd_unix_socket_connects_to_the_given_path() {
let path = socket_path("explicit");
let listener = UnixListener::bind(&path).unwrap();
let accept = thread::spawn(move || listener.accept().map(|_| ()));
let mut transport = BusdUnixSocket::new_with_path(&path);
transport.open_streams().unwrap();
accept.join().unwrap().unwrap();
std::fs::remove_file(&path).unwrap();
assert!(transport.open_streams().is_err());
}
#[test]
fn busd_unix_socket_path_selection() {
let _guard = ENV.lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(BusdUnixSocket::DEFAULT_PATH, "/run/oc2/bus");
unsafe { std::env::remove_var("OC2_BUS_SOCKET") };
assert!(format!("{:?}", BusdUnixSocket::new()).contains("/run/oc2/bus"));
assert!(format!("{:?}", BusdUnixSocket::default()).contains("/run/oc2/bus"));
unsafe { std::env::set_var("OC2_BUS_SOCKET", "/somewhere/else") };
assert!(format!("{:?}", BusdUnixSocket::new()).contains("/somewhere/else"));
unsafe { std::env::remove_var("OC2_BUS_SOCKET") };
}
#[test]
fn connect_busd_client_talks_to_a_daemon_end_to_end() {
let _guard = ENV.lock().unwrap_or_else(|e| e.into_inner());
let path = socket_path("e2e");
let listener = UnixListener::bind(&path).unwrap();
let daemon = fake_daemon(listener);
unsafe { std::env::set_var("OC2_BUS_SOCKET", &path) };
let client = HlapiClient::connect_busd_client();
unsafe { std::env::remove_var("OC2_BUS_SOCKET") };
let client = client.unwrap();
let devices = client.list_devices().unwrap();
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].type_names, ["redstone"]);
drop(client);
daemon.join().unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn connect_busd_client_without_a_daemon_is_a_connection_error() {
let _guard = ENV.lock().unwrap_or_else(|e| e.into_inner());
unsafe { std::env::set_var("OC2_BUS_SOCKET", socket_path("missing")) };
let err = HlapiClient::connect_busd_client().unwrap_err();
unsafe { std::env::remove_var("OC2_BUS_SOCKET") };
assert!(
matches!(err, oc2_hlapi_v2::client::HlapiClientError::Connection(_)),
"{err:?}"
);
}
#[test]
fn virtio_discovery_fails_cleanly_when_there_are_no_ports() {
let _ = LinuxVirtio::new();
let _ = HlapiClient::connect_virtio();
}