use std::io::{BufReader, ErrorKind, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
use std::path::{Path, PathBuf};
use std::time::Duration;
use ashlar_command::{CommandError, CommandInfo, Dispatch};
use serde_json::{Value, json};
use crate::ControlError;
use crate::key::{key_path, read_key_file};
use crate::lines::{Plain, ReadError, read_bounded_line};
use crate::protocol::Reply;
pub const MAX_REPLY: usize = 256 * 1024 * 1024;
enum KeySource {
File(PathBuf),
Fixed(String),
}
struct Conn {
reader: BufReader<Plain<TcpStream>>,
writer: TcpStream,
}
pub struct Client {
port: u16,
key: KeySource,
conn: Option<Conn>,
next_id: u64,
timeout: Duration,
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client").field("port", &self.port).field("connected", &self.conn.is_some()).finish_non_exhaustive()
}
}
impl Client {
pub fn connect(port: u16, key_dir: &Path) -> Result<Self, ControlError> {
Self::open(port, KeySource::File(key_path(key_dir, port)))
}
pub fn connect_with_key(port: u16, key: impl Into<String>) -> Result<Self, ControlError> {
Self::open(port, KeySource::Fixed(key.into()))
}
fn open(port: u16, key: KeySource) -> Result<Self, ControlError> {
let mut c = Self { port, key, conn: None, next_id: 1, timeout: Duration::from_secs(90) };
c.reconnect()?;
Ok(c)
}
pub fn port(&self) -> u16 {
self.port
}
pub fn set_timeout(&mut self, t: Duration) {
self.timeout = t;
if let Some(c) = &self.conn {
let _ = c.writer.set_read_timeout(Some(t));
}
}
fn key(&self) -> Result<String, ControlError> {
match &self.key {
KeySource::File(p) => read_key_file(p).map_err(ControlError::Key),
KeySource::Fixed(k) => Ok(k.clone()),
}
}
fn reconnect(&mut self) -> Result<(), ControlError> {
self.conn = None;
let key = self.key()?;
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, self.port));
let s = TcpStream::connect_timeout(&addr, Duration::from_millis(800))
.map_err(|e| ControlError::Io(format!("no app is listening on 127.0.0.1:{} ({e})", self.port)))?;
let _ = s.set_nodelay(true);
let _ = s.set_read_timeout(Some(self.timeout));
let _ = s.set_write_timeout(Some(Duration::from_secs(30)));
let read = s.try_clone().map_err(|e| ControlError::Io(e.to_string()))?;
self.conn = Some(Conn { reader: BufReader::new(Plain(read)), writer: s });
match self.roundtrip(&json!({"id": 0, "method": "auth", "key": key}))? {
Reply::Ok(_) => Ok(()),
Reply::Err { error, .. } => {
self.conn = None;
Err(if error == "unauthorized" { ControlError::Unauthorized } else { ControlError::Protocol(error) })
}
}
}
fn roundtrip(&mut self, req: &Value) -> Result<Reply, ControlError> {
match self.send(req) {
Ok(()) => self.receive(),
Err((_, e)) => Err(e),
}
}
fn send(&mut self, req: &Value) -> Result<(), (bool, ControlError)> {
let port = self.port;
let Some(c) = self.conn.as_mut() else { return Err((true, ControlError::Io("not connected".into()))) };
let mut line = req.to_string().into_bytes();
line.push(b'\n');
let mut sent = 0usize;
while let Some(rest) = line.get(sent..).filter(|r| !r.is_empty()) {
match c.writer.write(rest) {
Ok(0) => return Err((sent == 0, ControlError::Io(format!("control connection to 127.0.0.1:{port} closed while sending")))),
Ok(n) => sent = sent.saturating_add(n),
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => return Err((sent == 0, ControlError::Io(format!("control connection to 127.0.0.1:{port} failed: {e}")))),
}
}
Ok(())
}
fn receive(&mut self) -> Result<Reply, ControlError> {
let Some(c) = self.conn.as_mut() else { return Err(ControlError::Io("not connected".into())) };
let mut buf = Vec::new();
match read_bounded_line(&mut c.reader, &mut buf, MAX_REPLY) {
Ok(true) => {}
Ok(false) => return Err(ControlError::Io("the app closed the control connection".into())),
Err(ReadError::TooLong) => return Err(ControlError::Protocol(format!("reply longer than {MAX_REPLY} bytes"))),
Err(ReadError::Io) => return Err(ControlError::Io("no reply from the app (timed out or connection lost)".into())),
}
let v: Value = serde_json::from_slice(&buf).map_err(|e| ControlError::Protocol(e.to_string()))?;
Reply::from_json(&v).ok_or_else(|| ControlError::Protocol("reply without a boolean `ok`".into()))
}
fn stale(&self) -> bool {
let Some(c) = self.conn.as_ref() else { return true };
if !c.reader.buffer().is_empty() || c.writer.set_nonblocking(true).is_err() {
return true;
}
let mut probe = [0u8; 1];
let alive = matches!(c.writer.peek(&mut probe), Err(e) if e.kind() == ErrorKind::WouldBlock);
c.writer.set_nonblocking(false).is_err() || !alive
}
pub fn request(&mut self, method: &str, params: Value) -> Result<Reply, ControlError> {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
let req = json!({"id": id, "method": method, "params": params});
if self.stale() {
self.reconnect()?;
}
let r = match self.send(&req) {
Ok(()) => self.receive(),
Err((true, _)) => self.reconnect().and_then(|()| self.roundtrip(&req)),
Err((false, e)) => Err(e),
};
r.inspect_err(|_| self.conn = None)
}
pub fn call(&mut self, method: &str, params: Value) -> Result<Value, ControlError> {
match self.request(method, params)? {
Reply::Ok(v) => Ok(v),
Reply::Err { error, detail } => Err(ControlError::Remote { error, detail }),
}
}
}
impl Dispatch for Client {
fn commands(&self) -> Vec<CommandInfo> {
let key = match self.key() {
Ok(k) => k,
Err(_) => return Vec::new(),
};
let Ok(mut c) = Client::connect_with_key(self.port, key) else { return Vec::new() };
c.timeout = self.timeout;
c.call("engine.commands", json!({})).ok().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default()
}
fn execute(&mut self, id: &str, params: Value) -> Result<Value, CommandError> {
self.call("engine.execute", json!({"command": id, "params": params})).map_err(|e| e.into_command_error(id))
}
}
impl ControlError {
pub fn into_command_error(self, id: &str) -> CommandError {
match self {
ControlError::Remote { detail: Some(d), error } => {
serde_json::from_value(d).unwrap_or(CommandError::Failed { id: id.to_string(), msg: error })
}
e => CommandError::Failed { id: id.to_string(), msg: e.to_string() },
}
}
}