use std::io::{self, Read, Write};
use std::net::{Ipv4Addr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
use crate::types::OSD_SYNC_SLOTS;
use super::{lock, SHUTDOWN_POLL};
const REQUEST_HEAD_MAX: usize = 8192;
const READ_TIMEOUT: Duration = Duration::from_secs(2);
const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
pub(super) type Images = [Arc<[u8]>; OSD_SYNC_SLOTS];
pub(super) struct LogoServer {
port: u16,
images: Arc<Mutex<Images>>,
stop: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
impl LogoServer {
pub(super) fn start(port: u16) -> io::Result<Self> {
let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, port))?;
listener.set_nonblocking(true)?;
let port = listener.local_addr()?.port();
let empty: Images = core::array::from_fn(|_| Arc::from(&[][..]));
let images = Arc::new(Mutex::new(empty));
let stop = Arc::new(AtomicBool::new(false));
let thread = {
let images = Arc::clone(&images);
let stop = Arc::clone(&stop);
std::thread::Builder::new()
.name("mxr-logos".to_owned())
.spawn(move || accept_loop(&listener, &images, &stop))?
};
Ok(Self {
port,
images,
stop,
thread: Some(thread),
})
}
pub(super) fn port(&self) -> u16 {
self.port
}
pub(super) fn set_images(&self, images: Images) {
*lock(&self.images) = images;
}
}
impl Drop for LogoServer {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
fn accept_loop(listener: &TcpListener, images: &Mutex<Images>, stop: &AtomicBool) {
while !stop.load(Ordering::SeqCst) {
match listener.accept() {
Ok((stream, _)) => {
let _ = serve(stream, images);
}
Err(_) => std::thread::sleep(SHUTDOWN_POLL),
}
}
}
fn serve(mut stream: TcpStream, images: &Mutex<Images>) -> io::Result<()> {
stream.set_nonblocking(false)?;
stream.set_read_timeout(Some(READ_TIMEOUT))?;
stream.set_write_timeout(Some(WRITE_TIMEOUT))?;
let mut head = Vec::new();
let mut buf = [0u8; 1024];
while !head.windows(4).any(|w| w == b"\r\n\r\n") {
let n = stream.read(&mut buf)?;
if n == 0 || head.len() + n > REQUEST_HEAD_MAX {
return Ok(());
}
head.extend_from_slice(&buf[..n]);
}
let image = slot_of(&head).map(|slot| Arc::clone(&lock(images)[slot]));
match image.filter(|png| !png.is_empty()) {
Some(png) => {
let crc = crate::wire::crc32(&png);
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: {}\r\n\
ETag: \"{crc:08x}\"\r\nConnection: close\r\n\r\n",
png.len()
)?;
stream.write_all(&png)?;
}
None => stream.write_all(
b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
)?,
}
stream.flush()
}
fn slot_of(head: &[u8]) -> Option<usize> {
let line = head.split(|&b| b == b'\r').next()?;
let mut parts = line.split(|&b| b == b' ');
if parts.next()? != b"GET" {
return None;
}
let path = parts.next()?;
let n = path
.strip_prefix(b"/osd-sync/slot")?
.strip_suffix(b".png")?;
match n {
[d @ b'1'..=b'4'] => Some(usize::from(d - b'1')),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn get(port: u16, path: &str) -> Vec<u8> {
request(port, "GET", path)
}
fn request(port: u16, method: &str, path: &str) -> Vec<u8> {
let mut s = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).expect("connect");
write!(s, "{method} {path} HTTP/1.1\r\nHost: x\r\n\r\n").expect("send");
let mut out = Vec::new();
s.read_to_end(&mut out).expect("read");
out
}
fn split(response: &[u8]) -> (String, Vec<u8>) {
let at = response
.windows(4)
.position(|w| w == b"\r\n\r\n")
.expect("no head");
(
String::from_utf8_lossy(&response[..at]).into_owned(),
response[at + 4..].to_vec(),
)
}
#[test]
fn a_slot_is_served_whole_with_its_length() {
let server = LogoServer::start(0).expect("start");
let png: Vec<u8> = (0..70_000u32).map(|i| (i * 7) as u8).collect();
server.set_images([
Arc::from(&b""[..]),
Arc::from(&b"two"[..]),
Arc::from(&b""[..]),
Arc::from(png.as_slice()),
]);
let (head, body) = split(&get(server.port(), "/osd-sync/slot4.png"));
assert!(head.starts_with("HTTP/1.1 200 "), "{head}");
assert!(head.contains("\r\nContent-Length: 70000\r\n"), "{head}");
assert!(!head.contains("Transfer-Encoding"), "{head}");
assert_eq!(body, png);
let (_, body) = split(&get(server.port(), "/osd-sync/slot2.png"));
assert_eq!(body, b"two");
}
#[test]
fn an_empty_or_unknown_slot_is_not_found() {
let server = LogoServer::start(0).expect("start");
server.set_images([
Arc::from(&b""[..]),
Arc::from(&b"two"[..]),
Arc::from(&b"three"[..]),
Arc::from(&b"four"[..]),
]);
for path in [
"/osd-sync/slot1.png",
"/osd-sync/slot0.png",
"/osd-sync/slot5.png",
"/osd-sync/slot2.jpg",
"/other/slot2.png",
"/osd-sinc/slot2.png",
] {
let (head, body) = split(&get(server.port(), path));
assert!(head.starts_with("HTTP/1.1 404 "), "{path}: {head}");
assert!(body.is_empty(), "{path}");
}
let (head, _) = split(&request(server.port(), "POST", "/osd-sync/slot2.png"));
assert!(head.starts_with("HTTP/1.1 404 "), "POST: {head}");
}
}