1use std::io::BufReader;
4use std::io::ErrorKind::{BrokenPipe, ConnectionReset, TimedOut, UnexpectedEof, WouldBlock};
5use std::os::unix::net::UnixStream;
6use std::path::{Path, PathBuf};
7use std::time::Duration;
8
9use serde::de::DeserializeOwned;
10use serde_json::Value;
11
12use crate::protocol::{ErrorBody, MAX_LINE, Request, Response, read_line_limited, write_line};
13use crate::trust::Trust;
14
15pub struct Client {
16 socket: PathBuf,
17 trust: Trust,
18 reader: BufReader<UnixStream>,
19 writer: UnixStream,
20 next: u64,
21 timeout: Option<Duration>,
22 broken: bool,
25 pub token: Option<String>,
26 pub repo: Option<String>,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33#[non_exhaustive]
34pub enum ErrorKind {
35 Invalid,
36 Unauthenticated,
37 Forbidden,
38 NotInSandbox,
39 SessionInactive,
40 UnknownMethod,
41 NotFound,
42 TooLarge,
43 RateLimited,
44 Busy,
45 Verifying,
46 Closed,
47 Timeout,
49 Conflict,
50 Denied,
51 ForeignKey,
52 UpToDate,
53 NotFastForward,
54 Git,
55 Integrity,
56 Unsupported,
57 Internal,
58 Io,
60 TimedOut,
63 Other,
65}
66
67impl ErrorKind {
68 pub fn of(code: &str) -> Self {
69 use ErrorKind::*;
70 match code {
71 "invalid" => Invalid,
72 "unauthenticated" => Unauthenticated,
73 "forbidden" => Forbidden,
74 "not_in_sandbox" => NotInSandbox,
75 "session_inactive" => SessionInactive,
76 "unknown_method" => UnknownMethod,
77 "not_found" => NotFound,
78 "too_large" => TooLarge,
79 "rate_limited" => RateLimited,
80 "busy" => Busy,
81 "verifying" => Verifying,
82 "closed" => Closed,
83 "timeout" => Timeout,
84 "conflict" => Conflict,
85 "denied" => Denied,
86 "foreign_key" => ForeignKey,
87 "up_to_date" => UpToDate,
88 "not_fast_forward" => NotFastForward,
89 "git" => Git,
90 "integrity" => Integrity,
91 "unsupported" => Unsupported,
92 "internal" => Internal,
93 CODE_IO => Io,
94 CODE_TIMED_OUT => TimedOut,
95 _ => Other,
96 }
97 }
98
99 pub fn is_transient(self) -> bool {
103 matches!(
104 self,
105 ErrorKind::RateLimited | ErrorKind::Busy | ErrorKind::Closed
106 )
107 }
108}
109
110const CODE_IO: &str = "io";
111const CODE_TIMED_OUT: &str = "timed_out";
112
113#[derive(Debug)]
114pub struct CallError(pub ErrorBody);
115
116impl CallError {
117 pub fn code(&self) -> &str {
118 &self.0.code
119 }
120
121 pub fn kind(&self) -> ErrorKind {
122 ErrorKind::of(&self.0.code)
123 }
124
125 pub fn is_transient(&self) -> bool {
127 self.kind().is_transient()
128 }
129}
130
131impl std::fmt::Display for CallError {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "{}: {}", self.0.code, self.0.message)
134 }
135}
136
137impl std::error::Error for CallError {}
138
139enum Exchange {
141 Write(std::io::Error),
142 Read(std::io::Error),
143 Stray(u64),
145}
146
147impl From<Exchange> for CallError {
148 fn from(e: Exchange) -> Self {
149 match e {
150 Exchange::Write(e) | Exchange::Read(e) if matches!(e.kind(), WouldBlock | TimedOut) => {
151 error(CODE_TIMED_OUT, "no answer within the client's timeout")
152 }
153 Exchange::Write(e) | Exchange::Read(e) if e.kind() == UnexpectedEof => {
154 gone("daemon closed the connection")
155 }
156 Exchange::Write(e) | Exchange::Read(e) => io(e),
157 Exchange::Stray(id) => gone(&format!("an answer to request {id}, not this one")),
158 }
159 }
160}
161
162fn error(code: &str, message: &str) -> CallError {
163 CallError(ErrorBody {
164 code: code.into(),
165 message: message.into(),
166 })
167}
168
169fn gone(message: &str) -> CallError {
170 error(CODE_IO, message)
171}
172
173fn io(e: std::io::Error) -> CallError {
174 gone(&e.to_string())
175}
176
177fn not_sent(e: &Exchange) -> bool {
179 matches!(e, Exchange::Write(e) if matches!(e.kind(), BrokenPipe | ConnectionReset))
180}
181
182fn not_read(r: &Response) -> bool {
185 r.id == 0 && r.error.as_ref().is_some_and(|e| e.code == "closed")
186}
187
188impl Client {
189 pub fn connect(path: &Path) -> std::io::Result<Self> {
192 Self::connect_to(path, &Trust::load()?)
193 }
194
195 pub fn connect_to(path: &Path, trust: &Trust) -> std::io::Result<Self> {
196 trust.check_socket(path)?;
197 let s = UnixStream::connect(path)?;
198 trust.check_peer(&s)?;
199 Ok(Self {
200 socket: path.to_path_buf(),
201 trust: *trust,
202 reader: BufReader::new(s.try_clone()?),
203 writer: s,
204 next: 1,
205 timeout: None,
206 broken: false,
207 token: None,
208 repo: None,
209 })
210 }
211
212 pub fn set_timeout(&mut self, timeout: Option<Duration>) -> std::io::Result<()> {
217 if timeout == Some(Duration::ZERO) {
218 return Err(std::io::Error::new(
219 std::io::ErrorKind::InvalidInput,
220 "a timeout of zero",
221 ));
222 }
223 self.timeout = timeout;
224 self.writer.set_read_timeout(timeout)?;
225 self.writer.set_write_timeout(timeout)
226 }
227
228 pub fn timeout(&self) -> Option<Duration> {
229 self.timeout
230 }
231
232 pub fn call(&mut self, method: &str, params: Value) -> Result<Value, CallError> {
233 if self.broken {
234 self.reconnect()?;
235 }
236 let id = self.next;
237 self.next += 1;
238 let req = Request {
239 id,
240 method: method.into(),
241 repo: self.repo.clone(),
242 token: self.token.clone(),
243 params,
244 };
245 let resp = match self.exchange(&req) {
246 Err(e) if not_sent(&e) => self.again(&req),
247 Ok(r) if not_read(&r) => self.again(&req),
248 done => done.map_err(CallError::from),
249 };
250 let resp = resp.inspect_err(|_| self.broken = true)?;
251 if resp.ok {
252 Ok(resp.result)
253 } else {
254 Err(CallError(resp.error.unwrap_or(ErrorBody {
255 code: "unknown".into(),
256 message: String::new(),
257 })))
258 }
259 }
260
261 fn reconnect(&mut self) -> Result<(), CallError> {
262 let mut fresh = Self::connect_to(&self.socket, &self.trust).map_err(io)?;
263 fresh.set_timeout(self.timeout).map_err(io)?;
264 self.reader = fresh.reader;
265 self.writer = fresh.writer;
266 self.broken = false;
267 Ok(())
268 }
269
270 fn again(&mut self, req: &Request) -> Result<Response, CallError> {
273 self.reconnect()?;
274 Ok(self.exchange(req)?)
275 }
276
277 fn exchange(&mut self, req: &Request) -> Result<Response, Exchange> {
278 write_line(&mut self.writer, req).map_err(Exchange::Write)?;
279 let line = read_line_limited(&mut self.reader, MAX_LINE)
280 .map_err(Exchange::Read)?
281 .ok_or_else(|| Exchange::Read(UnexpectedEof.into()))?;
282 let resp: Response = serde_json::from_str(&line).map_err(|e| Exchange::Read(e.into()))?;
283 if resp.id != req.id && resp.id != 0 {
285 return Err(Exchange::Stray(resp.id));
286 }
287 Ok(resp)
288 }
289
290 pub fn call_as<T: DeserializeOwned>(
291 &mut self,
292 method: &str,
293 params: Value,
294 ) -> Result<T, CallError> {
295 let v = self.call(method, params)?;
296 serde_json::from_value(v).map_err(|e| gone(&format!("bad result: {e}")))
297 }
298
299 pub fn watch(
304 &mut self,
305 watch: &crate::protocol::Watch,
306 ) -> Result<crate::protocol::Watched, CallError> {
307 let params = serde_json::to_value(watch).map_err(|e| gone(&format!("bad watch: {e}")))?;
308 self.call_as("events.watch", params)
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use crate::protocol::write_line;
316 use std::io::BufRead;
317 use std::os::unix::net::UnixListener;
318
319 fn fake(
322 answer: impl Fn(usize, &Request) -> Option<Value> + Send + Sync + 'static,
323 ) -> (tempfile::TempDir, PathBuf) {
324 let dir = tempfile::Builder::new().prefix("lc").tempdir().unwrap();
325 let sock = dir.path().join("s");
326 let listener = UnixListener::bind(&sock).unwrap();
327 let answer = std::sync::Arc::new(answer);
328 let count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
329 std::thread::spawn(move || {
330 for conn in listener.incoming() {
331 let Ok(conn) = conn else { return };
332 let (answer, count) = (answer.clone(), count.clone());
333 std::thread::spawn(move || {
334 let mut reader = std::io::BufReader::new(conn.try_clone().unwrap());
335 let mut w = conn;
336 let mut line = String::new();
337 while reader.read_line(&mut line).unwrap_or(0) > 0 {
338 let req: Request = serde_json::from_str(&line).unwrap();
339 line.clear();
340 let n = count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
341 if let Some(v) = answer(n, &req) {
342 let _ = write_line(&mut w, &v);
343 }
344 }
345 });
346 }
347 });
348 (dir, sock)
349 }
350
351 fn client(sock: &Path) -> Client {
352 Client::connect_to(sock, &Trust::cooperative()).unwrap()
353 }
354
355 #[test]
356 fn every_code_of_the_table_has_its_kind() {
357 let codes = [
358 ("invalid", ErrorKind::Invalid),
359 ("unauthenticated", ErrorKind::Unauthenticated),
360 ("forbidden", ErrorKind::Forbidden),
361 ("not_in_sandbox", ErrorKind::NotInSandbox),
362 ("session_inactive", ErrorKind::SessionInactive),
363 ("unknown_method", ErrorKind::UnknownMethod),
364 ("not_found", ErrorKind::NotFound),
365 ("too_large", ErrorKind::TooLarge),
366 ("rate_limited", ErrorKind::RateLimited),
367 ("busy", ErrorKind::Busy),
368 ("verifying", ErrorKind::Verifying),
369 ("closed", ErrorKind::Closed),
370 ("timeout", ErrorKind::Timeout),
371 ("conflict", ErrorKind::Conflict),
372 ("denied", ErrorKind::Denied),
373 ("foreign_key", ErrorKind::ForeignKey),
374 ("up_to_date", ErrorKind::UpToDate),
375 ("not_fast_forward", ErrorKind::NotFastForward),
376 ("git", ErrorKind::Git),
377 ("integrity", ErrorKind::Integrity),
378 ("unsupported", ErrorKind::Unsupported),
379 ("internal", ErrorKind::Internal),
380 ("io", ErrorKind::Io),
381 ("timed_out", ErrorKind::TimedOut),
382 ("something_new", ErrorKind::Other),
383 ];
384 for (code, kind) in codes {
385 assert_eq!(ErrorKind::of(code), kind, "{code}");
386 let transient = matches!(code, "rate_limited" | "busy" | "closed");
387 assert_eq!(kind.is_transient(), transient, "{code}");
388 let e = error(code, "m");
389 assert_eq!(
390 (e.code(), e.kind(), e.is_transient()),
391 (code, kind, transient)
392 );
393 assert_eq!(e.to_string(), format!("{code}: m"));
394 }
395 }
396
397 #[test]
398 fn a_call_gets_its_result_or_the_daemons_error() {
399 let (_d, sock) = fake(|_, req| {
400 Some(match req.method.as_str() {
401 "ping" => serde_json::json!({"id": req.id, "ok": true, "result": {"protocol": 1}}),
402 _ => serde_json::json!({"id": req.id, "ok": false,
403 "error": {"code": "rate_limited", "message": "try again in 3 s"}}),
404 })
405 });
406 let mut c = client(&sock);
407 c.token = Some("lonca_t".into());
408 c.repo = Some("demo".into());
409 let v = c.call("ping", serde_json::json!({})).unwrap();
410 assert_eq!(v["protocol"], crate::PROTOCOL);
411 #[derive(serde::Deserialize)]
412 struct Ping {
413 protocol: u32,
414 }
415 let p: Ping = c.call_as("ping", serde_json::json!({})).unwrap();
416 assert_eq!(p.protocol, 1);
417 let bad: Result<Vec<u8>, _> = c.call_as("ping", serde_json::json!({}));
418 assert_eq!(bad.unwrap_err().kind(), ErrorKind::Io);
419 let e = c.call("status", serde_json::json!({})).unwrap_err();
420 assert_eq!(e.kind(), ErrorKind::RateLimited);
421 let (_d, sock) = fake(|_, req| {
423 Some(serde_json::json!({"id": req.id, "ok": true,
424 "result": [req.id, req.token, req.repo]}))
425 });
426 let mut c = client(&sock);
427 c.token = Some("lonca_t".into());
428 let first = c.call("ping", serde_json::json!({})).unwrap();
429 assert_eq!(first, serde_json::json!([1, "lonca_t", null]));
430 c.repo = Some("demo".into());
431 let second = c.call("ping", serde_json::json!({})).unwrap();
432 assert_eq!(second, serde_json::json!([2, "lonca_t", "demo"]));
433 assert!(e.is_transient());
434 assert!(e.to_string().contains("try again"), "{e}");
435 }
436
437 #[test]
439 fn an_error_without_a_body_is_unknown() {
440 let (_d, sock) = fake(|_, req| Some(serde_json::json!({"id": req.id, "ok": false})));
441 let e = client(&sock)
442 .call("ping", serde_json::json!({}))
443 .unwrap_err();
444 assert_eq!((e.code(), e.kind()), ("unknown", ErrorKind::Other));
445 }
446
447 #[test]
450 fn an_answer_to_another_request_is_refused() {
451 let (_d, sock) = fake(|n, req| {
452 let id = if n == 1 { req.id + 7 } else { req.id };
453 Some(serde_json::json!({"id": id, "ok": true, "result": n}))
454 });
455 let mut c = client(&sock);
456 let e = c.call("ping", serde_json::json!({})).unwrap_err();
457 assert_eq!(e.kind(), ErrorKind::Io);
458 assert!(e.to_string().contains("request 8"), "{e}");
459 assert_eq!(c.call("ping", serde_json::json!({})).unwrap(), 2);
461 }
462
463 #[test]
466 fn a_timeout_drops_the_connection() {
467 let (_d, sock) = fake(|n, req| match n {
468 1 => {
469 std::thread::sleep(Duration::from_millis(400));
470 Some(serde_json::json!({"id": req.id, "ok": true, "result": "late"}))
471 }
472 _ => Some(serde_json::json!({"id": req.id, "ok": true, "result": "fresh"})),
473 });
474 let mut c = client(&sock);
475 assert!(c.set_timeout(Some(Duration::ZERO)).is_err());
476 assert_eq!(c.timeout(), None);
477 c.set_timeout(Some(Duration::from_millis(100))).unwrap();
478 assert_eq!(c.timeout(), Some(Duration::from_millis(100)));
479 let e = c.call("ping", serde_json::json!({})).unwrap_err();
480 assert_eq!(e.kind(), ErrorKind::TimedOut, "{e}");
481 assert!(!e.is_transient(), "it may have run");
482 assert_eq!(c.call("ping", serde_json::json!({})).unwrap(), "fresh");
483 assert_eq!(c.timeout(), Some(Duration::from_millis(100)), "kept");
484 }
485
486 #[test]
489 fn a_closed_connection_is_sent_again_once() {
490 let (_d, sock) = fake(|n, req| {
491 Some(match n {
492 1 | 3 | 4 => serde_json::json!({"id": 0, "ok": false,
493 "error": {"code": "closed", "message": "quiet"}}),
494 _ => serde_json::json!({"id": req.id, "ok": true, "result": n}),
495 })
496 });
497 let mut c = client(&sock);
498 assert_eq!(c.call("ping", serde_json::json!({})).unwrap(), 2);
499 let e = c.call("ping", serde_json::json!({})).unwrap_err();
500 assert_eq!(e.kind(), ErrorKind::Closed, "not twice");
501 assert!(e.is_transient());
502 }
503
504 #[test]
505 fn a_closed_socket_is_an_io_error() {
506 let (_d, sock) = fake(|_, _| None);
507 let mut c = client(&sock);
508 c.writer.shutdown(std::net::Shutdown::Read).unwrap();
509 let e = c.call("ping", serde_json::json!({})).unwrap_err();
510 assert_eq!(e.kind(), ErrorKind::Io, "{e}");
511 }
512}