use std::collections::HashMap;
use std::io::{BufReader, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::{Duration, Instant};
use crate::ControlError;
use crate::key::{KeyFile, key_path, random_key, write_key_file};
use crate::lines::{Auth, DEFAULT_MAX_LINE, Source, serve_lines};
use crate::protocol::{Reply, Request};
#[derive(Debug, Clone)]
pub struct Config {
pub port: u16,
pub key_dir: PathBuf,
pub max_line: usize,
pub max_connections: usize,
pub first_request: Duration,
pub reply_timeout: Duration,
}
impl Config {
pub fn new(port: u16, key_dir: impl Into<PathBuf>) -> Self {
Self {
port,
key_dir: key_dir.into(),
max_line: DEFAULT_MAX_LINE,
max_connections: 16,
first_request: Duration::from_secs(30),
reply_timeout: Duration::from_secs(60),
}
}
}
pub type Wake = Arc<dyn Fn() + Send + Sync>;
pub struct Server {
port: u16,
key: Arc<str>,
key_file: KeyFile,
stop: Arc<AtomicBool>,
conns: Arc<Mutex<Conns>>,
}
#[derive(Default)]
struct Conns {
next: u64,
open: HashMap<u64, TcpStream>,
}
impl std::fmt::Debug for Server {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Server").field("port", &self.port).field("key_file", &self.key_file).finish_non_exhaustive()
}
}
impl Server {
pub fn start(config: Config, wake: impl Fn() + Send + Sync + 'static) -> Result<(Server, Receiver<Request>), ControlError> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, config.port))
.map_err(|e| ControlError::Io(format!("cannot bind 127.0.0.1:{}: {e}", config.port)))?;
let port = listener.local_addr().map_err(|e| ControlError::Io(e.to_string()))?.port();
let key: Arc<str> = random_key().map_err(ControlError::Key)?.into();
let path = key_path(&config.key_dir, port);
write_key_file(&path, &key).map_err(ControlError::Key)?;
let key_file = KeyFile::new(path);
log::info!("control server listening on 127.0.0.1:{port}, key in {}", key_file.path().display());
let (tx, rx) = channel();
let stop = Arc::new(AtomicBool::new(false));
let ctx = Ctx { tx, wake: Arc::new(wake), key: key.clone(), config: Arc::new(config) };
let conns = Arc::new(Mutex::new(Conns::default()));
let (stop2, conns2) = (stop.clone(), conns.clone());
std::thread::Builder::new()
.name("control-accept".into())
.spawn(move || accept_loop(listener, ctx, &stop2, &conns2))
.map_err(|e| ControlError::Io(format!("cannot start the control thread: {e}")))?;
Ok((Server { port, key, key_file, stop, conns }, rx))
}
pub fn port(&self) -> u16 {
self.port
}
pub fn key(&self) -> &str {
&self.key
}
pub fn key_file(&self) -> &std::path::Path {
self.key_file.path()
}
}
impl Drop for Server {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
for s in self.conns.lock().unwrap_or_else(PoisonError::into_inner).open.values() {
let _ = s.shutdown(std::net::Shutdown::Both);
}
let _ = TcpStream::connect_timeout(&SocketAddr::from((Ipv4Addr::LOCALHOST, self.port)), Duration::from_millis(200));
}
}
#[derive(Clone)]
struct Ctx {
tx: Sender<Request>,
wake: Wake,
key: Arc<str>,
config: Arc<Config>,
}
fn accept_loop(listener: TcpListener, ctx: Ctx, stop: &AtomicBool, conns: &Arc<Mutex<Conns>>) {
for stream in listener.incoming() {
if stop.load(Ordering::SeqCst) {
return;
}
let Ok(mut stream) = stream else { continue };
let slot = {
let mut c = conns.lock().unwrap_or_else(PoisonError::into_inner);
match stream.try_clone() {
Ok(handle) if c.open.len() < ctx.config.max_connections => {
let id = c.next;
c.next = c.next.wrapping_add(1);
c.open.insert(id, handle);
Some(Slot(id, Arc::clone(conns)))
}
_ => None,
}
};
let Some(slot) = slot else {
let _ = stream.set_write_timeout(Some(Duration::from_millis(200)));
let _ = stream.write_all(format!("{}\n", Reply::err("too many control connections").to_json(None)).as_bytes());
continue;
};
let ctx = ctx.clone();
let spawned = std::thread::Builder::new().name("control-conn".into()).spawn(move || {
let _slot = slot;
serve(stream, &ctx);
});
if let Err(e) = spawned {
log::error!("control server could not start a connection thread: {e}");
}
}
}
struct Slot(u64, Arc<Mutex<Conns>>);
impl Drop for Slot {
fn drop(&mut self) {
self.1.lock().unwrap_or_else(PoisonError::into_inner).open.remove(&self.0);
}
}
struct Timed {
s: TcpStream,
deadline: Option<Instant>,
}
impl Read for Timed {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if let Some(d) = self.deadline {
let left = d.checked_duration_since(Instant::now()).filter(|l| !l.is_zero()).ok_or(std::io::ErrorKind::TimedOut)?;
self.s.set_read_timeout(Some(left))?;
}
self.s.read(buf)
}
}
impl Source for Timed {
fn set_deadline(&mut self, d: Option<Instant>) {
self.deadline = d;
if d.is_none() {
let _ = self.s.set_read_timeout(None);
}
}
}
fn serve(stream: TcpStream, ctx: &Ctx) {
let Ok(read) = stream.try_clone() else { return };
let _ = stream.set_nodelay(true);
let _ = stream.set_write_timeout(Some(Duration::from_secs(30)));
let deadline = Instant::now().checked_add(ctx.config.first_request);
let mut reader = BufReader::new(Timed { s: read, deadline });
serve_lines(&mut reader, stream, ctx.config.max_line, Auth::Key(&ctx.key), |method, params| {
let (req, rrx) = Request::new(method, params);
ctx.tx.send(req).ok()?;
(ctx.wake)();
Some(match rrx.recv_timeout(ctx.config.reply_timeout) {
Ok(r) => r,
Err(RecvTimeoutError::Timeout) => Reply::err(format!("timeout: the app did not answer within {} s", ctx.config.reply_timeout.as_secs())),
Err(RecvTimeoutError::Disconnected) => Reply::err("the app dropped the request"),
})
});
}