#![cfg(unix)]
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{UnixListener, UnixStream};
use tokio::task::JoinHandle;
use crate::libpod::Client;
type Responder = dyn Fn(&str, &str) -> (u16, String) + Send + Sync;
pub(super) struct FakePodman {
sock_path: std::path::PathBuf,
pub(super) requests: Arc<Mutex<Vec<String>>>,
_dir: tempfile::TempDir,
task: JoinHandle<()>,
}
impl FakePodman {
pub(super) fn client(&self) -> Client {
Client::new(self.sock_path.to_string_lossy().into_owned())
}
}
impl Drop for FakePodman {
fn drop(&mut self) {
self.task.abort();
}
}
pub(super) fn start<F>(respond: F) -> FakePodman
where
F: Fn(&str, &str) -> (u16, String) + Send + Sync + 'static,
{
let dir = tempfile::tempdir().expect("create temp dir for fake podman socket");
let sock_path = dir.path().join("podman.sock");
let listener = UnixListener::bind(&sock_path).expect("bind fake podman socket");
let respond: Arc<Responder> = Arc::new(respond);
let requests: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let task_requests = requests.clone();
let task = tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let respond = respond.clone();
let requests = task_requests.clone();
tokio::spawn(async move {
let _ = serve_one(stream, respond.as_ref(), &requests).await;
});
}
});
FakePodman {
sock_path,
requests,
_dir: dir,
task,
}
}
async fn serve_one(
mut stream: UnixStream,
respond: &Responder,
requests: &Mutex<Vec<String>>,
) -> std::io::Result<()> {
let mut buf = Vec::new();
let mut chunk = [0u8; 1024];
loop {
let n = stream.read(&mut chunk).await?;
if n == 0 {
break;
}
buf.extend_from_slice(&chunk[..n]);
if buf.windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
let head = String::from_utf8_lossy(&buf);
let request_line = head.lines().next().unwrap_or_default();
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or_default().to_string();
let target = parts.next().unwrap_or_default().to_string();
requests.lock().unwrap().push(format!("{method} {target}"));
let (status, body) = respond(&method, &target);
let response = format!(
"HTTP/1.1 {status} {reason}\r\ncontent-type: application/json\r\ncontent-length: {len}\r\nconnection: close\r\n\r\n{body}",
reason = reason_phrase(status),
len = body.len(),
);
stream.write_all(response.as_bytes()).await?;
stream.shutdown().await?;
Ok(())
}
fn reason_phrase(status: u16) -> &'static str {
match status {
200 => "OK",
204 => "No Content",
304 => "Not Modified",
404 => "Not Found",
409 => "Conflict",
500 => "Internal Server Error",
_ => "Unknown",
}
}