1use std::io::{BufReader, ErrorKind, Write};
5use std::net::{Ipv4Addr, SocketAddr, TcpStream};
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use ashlar_command::{CommandError, CommandInfo, Dispatch};
10use serde_json::{Value, json};
11
12use crate::ControlError;
13use crate::key::{key_path, read_key_file};
14use crate::lines::{Plain, ReadError, read_bounded_line};
15use crate::protocol::Reply;
16
17pub const MAX_REPLY: usize = 256 * 1024 * 1024;
19
20enum KeySource {
21 File(PathBuf),
22 Fixed(String),
23}
24
25struct Conn {
26 reader: BufReader<Plain<TcpStream>>,
27 writer: TcpStream,
28}
29
30pub struct Client {
36 port: u16,
37 key: KeySource,
38 conn: Option<Conn>,
39 next_id: u64,
40 timeout: Duration,
41}
42
43impl std::fmt::Debug for Client {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("Client").field("port", &self.port).field("connected", &self.conn.is_some()).finish_non_exhaustive()
46 }
47}
48
49impl Client {
50 pub fn connect(port: u16, key_dir: &Path) -> Result<Self, ControlError> {
53 Self::open(port, KeySource::File(key_path(key_dir, port)))
54 }
55
56 pub fn connect_with_key(port: u16, key: impl Into<String>) -> Result<Self, ControlError> {
58 Self::open(port, KeySource::Fixed(key.into()))
59 }
60
61 fn open(port: u16, key: KeySource) -> Result<Self, ControlError> {
62 let mut c = Self { port, key, conn: None, next_id: 1, timeout: Duration::from_secs(90) };
63 c.reconnect()?;
64 Ok(c)
65 }
66
67 pub fn port(&self) -> u16 {
68 self.port
69 }
70
71 pub fn set_timeout(&mut self, t: Duration) {
74 self.timeout = t;
75 if let Some(c) = &self.conn {
76 let _ = c.writer.set_read_timeout(Some(t));
77 }
78 }
79
80 fn key(&self) -> Result<String, ControlError> {
81 match &self.key {
82 KeySource::File(p) => read_key_file(p).map_err(ControlError::Key),
83 KeySource::Fixed(k) => Ok(k.clone()),
84 }
85 }
86
87 fn reconnect(&mut self) -> Result<(), ControlError> {
88 self.conn = None;
89 let key = self.key()?;
90 let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, self.port));
91 let s = TcpStream::connect_timeout(&addr, Duration::from_millis(800))
92 .map_err(|e| ControlError::Io(format!("no app is listening on 127.0.0.1:{} ({e})", self.port)))?;
93 let _ = s.set_nodelay(true);
94 let _ = s.set_read_timeout(Some(self.timeout));
95 let _ = s.set_write_timeout(Some(Duration::from_secs(30)));
96 let read = s.try_clone().map_err(|e| ControlError::Io(e.to_string()))?;
97 self.conn = Some(Conn { reader: BufReader::new(Plain(read)), writer: s });
98 match self.roundtrip(&json!({"id": 0, "method": "auth", "key": key}))? {
100 Reply::Ok(_) => Ok(()),
101 Reply::Err { error, .. } => {
102 self.conn = None;
103 Err(if error == "unauthorized" { ControlError::Unauthorized } else { ControlError::Protocol(error) })
104 }
105 }
106 }
107
108 fn roundtrip(&mut self, req: &Value) -> Result<Reply, ControlError> {
109 match self.send(req) {
110 Ok(()) => self.receive(),
111 Err((_, e)) => Err(e),
112 }
113 }
114
115 fn send(&mut self, req: &Value) -> Result<(), (bool, ControlError)> {
118 let port = self.port;
119 let Some(c) = self.conn.as_mut() else { return Err((true, ControlError::Io("not connected".into()))) };
120 let mut line = req.to_string().into_bytes();
121 line.push(b'\n');
122 let mut sent = 0usize;
123 while let Some(rest) = line.get(sent..).filter(|r| !r.is_empty()) {
124 match c.writer.write(rest) {
125 Ok(0) => return Err((sent == 0, ControlError::Io(format!("control connection to 127.0.0.1:{port} closed while sending")))),
126 Ok(n) => sent = sent.saturating_add(n),
127 Err(e) if e.kind() == ErrorKind::Interrupted => {}
128 Err(e) => return Err((sent == 0, ControlError::Io(format!("control connection to 127.0.0.1:{port} failed: {e}")))),
129 }
130 }
131 Ok(())
132 }
133
134 fn receive(&mut self) -> Result<Reply, ControlError> {
135 let Some(c) = self.conn.as_mut() else { return Err(ControlError::Io("not connected".into())) };
136 let mut buf = Vec::new();
137 match read_bounded_line(&mut c.reader, &mut buf, MAX_REPLY) {
138 Ok(true) => {}
139 Ok(false) => return Err(ControlError::Io("the app closed the control connection".into())),
140 Err(ReadError::TooLong) => return Err(ControlError::Protocol(format!("reply longer than {MAX_REPLY} bytes"))),
141 Err(ReadError::Io) => return Err(ControlError::Io("no reply from the app (timed out or connection lost)".into())),
142 }
143 let v: Value = serde_json::from_slice(&buf).map_err(|e| ControlError::Protocol(e.to_string()))?;
144 Reply::from_json(&v).ok_or_else(|| ControlError::Protocol("reply without a boolean `ok`".into()))
145 }
146
147 fn stale(&self) -> bool {
150 let Some(c) = self.conn.as_ref() else { return true };
151 if !c.reader.buffer().is_empty() || c.writer.set_nonblocking(true).is_err() {
152 return true;
153 }
154 let mut probe = [0u8; 1];
155 let alive = matches!(c.writer.peek(&mut probe), Err(e) if e.kind() == ErrorKind::WouldBlock);
156 c.writer.set_nonblocking(false).is_err() || !alive
157 }
158
159 pub fn request(&mut self, method: &str, params: Value) -> Result<Reply, ControlError> {
161 let id = self.next_id;
162 self.next_id = self.next_id.wrapping_add(1);
163 let req = json!({"id": id, "method": method, "params": params});
164 if self.stale() {
166 self.reconnect()?;
167 }
168 let r = match self.send(&req) {
169 Ok(()) => self.receive(),
170 Err((true, _)) => self.reconnect().and_then(|()| self.roundtrip(&req)),
172 Err((false, e)) => Err(e),
174 };
175 r.inspect_err(|_| self.conn = None)
176 }
177
178 pub fn call(&mut self, method: &str, params: Value) -> Result<Value, ControlError> {
180 match self.request(method, params)? {
181 Reply::Ok(v) => Ok(v),
182 Reply::Err { error, detail } => Err(ControlError::Remote { error, detail }),
183 }
184 }
185}
186
187impl Dispatch for Client {
188 fn commands(&self) -> Vec<CommandInfo> {
191 let key = match self.key() {
193 Ok(k) => k,
194 Err(_) => return Vec::new(),
195 };
196 let Ok(mut c) = Client::connect_with_key(self.port, key) else { return Vec::new() };
197 c.timeout = self.timeout;
198 c.call("engine.commands", json!({})).ok().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default()
199 }
200
201 fn execute(&mut self, id: &str, params: Value) -> Result<Value, CommandError> {
202 self.call("engine.execute", json!({"command": id, "params": params})).map_err(|e| e.into_command_error(id))
203 }
204}
205
206impl ControlError {
207 pub fn into_command_error(self, id: &str) -> CommandError {
209 match self {
210 ControlError::Remote { detail: Some(d), error } => {
211 serde_json::from_value(d).unwrap_or(CommandError::Failed { id: id.to_string(), msg: error })
212 }
213 e => CommandError::Failed { id: id.to_string(), msg: e.to_string() },
214 }
215 }
216}