1use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::time::Duration;
13
14use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
15use tokio::net::unix::{OwnedReadHalf, OwnedWriteHalf};
16use tokio::net::UnixStream;
17use tokio::sync::mpsc;
18
19use super::handle::CMD_CHAN_CAP;
20use super::transport::{ClientTransport, TransportError, ATTACH_TIMEOUT, ATTACH_TIMEOUT_PARKED};
21use super::types::*;
22use super::view::RuntimeView;
23use super::wire::*;
24
25pub const CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
26pub const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2);
27pub const CLOSE_TIMEOUT: Duration = Duration::from_secs(
31 crate::daemon::close::CLOSE_SEND_RETRY.as_secs()
32 + crate::daemon::close::CLOSE_REPLY_BUDGET.as_secs()
33 + crate::daemon::close::CLOSE_END_BUDGET.as_secs()
34 + 2,
35);
36pub const PIN_TIMEOUT: Duration =
39 Duration::from_secs(crate::daemon::pin::PIN_UNPARK_BUDGET.as_secs() + 2);
40pub const RELOAD_EXTENSION_TIMEOUT: Duration = Duration::from_secs(60);
46
47pub struct Connected {
49 reader: BufReader<OwnedReadHalf>,
50 writer: mpsc::Sender<ClientFrame>,
51 writer_task: tokio::task::JoinHandle<()>,
52 pub welcome: Welcome,
53 path: PathBuf,
55 hello: Hello,
56 buf: FrameBuf,
57}
58
59pub struct Pong {
60 pub pid: u32,
61 pub uptime_s: u64,
62 pub sessions: usize,
63}
64
65#[derive(Debug)]
67pub enum AuthStateReply {
68 Report(agent_core::auth::AuthStateReport),
69 DaemonPredates { daemon_version: String, daemon_build: String },
73}
74
75pub fn predates_auth_state(message: &str) -> bool {
78 message.starts_with("malformed frame: unknown variant `auth_state`")
79}
80
81#[derive(Default)]
85pub struct FrameBuf {
86 line: Vec<u8>,
87 last_frame_bytes: usize,
89}
90
91const FRAME_BUF_KEEP: usize = 64 * 1024;
92const FRAME_BUF_SHRINK_ABOVE: usize = 1024 * 1024;
93
94impl FrameBuf {
95 fn after_frame(&mut self, n: usize) {
96 self.last_frame_bytes = n;
97 if self.line.capacity() > FRAME_BUF_SHRINK_ABOVE {
98 self.line = Vec::new();
99 self.line.reserve(FRAME_BUF_KEEP);
100 }
101 }
102}
103
104async fn read_frame(reader: &mut BufReader<OwnedReadHalf>, buf: &mut FrameBuf) -> Result<Option<DaemonFrame>, TransportError> {
105 loop {
109 let limit = (MAX_FRAME_BYTES as u64 + 1).saturating_sub(buf.line.len() as u64);
111 let n = tokio::io::AsyncReadExt::take(&mut *reader, limit)
112 .read_until(b'\n', &mut buf.line)
113 .await?;
114 if n == 0 {
115 return Ok(None);
116 }
117 if buf.line.len() > MAX_FRAME_BYTES {
118 buf.line.clear();
119 return Err(TransportError::Protocol(frame_limit_msg()));
120 }
121 let decoded = {
122 let t = std::str::from_utf8(&buf.line)
123 .map_err(|e| TransportError::Protocol(format!("frame is not utf-8: {e}")))
124 .map(str::trim_end);
125 match t {
126 Ok("") => None,
127 Ok(t) => Some((decode_line::<DaemonFrame>(t).map(Some).map_err(TransportError::Protocol), t.len())),
128 Err(e) => Some((Err(e), 0)),
129 }
130 };
131 buf.line.clear();
132 let Some((decoded, len)) = decoded else { continue };
133 buf.after_frame(len);
134 return decoded;
135 }
136}
137
138fn spawn_writer(mut w: OwnedWriteHalf) -> (mpsc::Sender<ClientFrame>, tokio::task::JoinHandle<()>) {
139 let (tx, mut rx) = mpsc::channel::<ClientFrame>(CMD_CHAN_CAP);
140 let task = tokio::spawn(async move {
141 while let Some(frame) = rx.recv().await {
142 let line = match encode_line(&frame) {
143 Ok(l) => l,
144 Err(e) => {
145 tracing::warn!(frame = ?frame, "socket_transport: encode failed: {e}");
146 continue;
147 }
148 };
149 if let Err(e) = w.write_all(line.as_bytes()).await {
150 tracing::debug!("socket_transport: write failed: {e}");
151 break;
152 }
153 if matches!(frame, ClientFrame::Bye) {
154 let _ = w.shutdown().await;
155 break;
156 }
157 }
158 });
159 (tx, task)
160}
161
162impl Connected {
163 pub async fn connect(path: &Path, hello: Hello) -> Result<Self, TransportError> {
166 let stream = tokio::time::timeout(CONNECT_TIMEOUT, UnixStream::connect(path))
167 .await
168 .map_err(|_| TransportError::Protocol("connect timed out".into()))??;
169 let (r, w) = stream.into_split();
170 let mut reader = BufReader::new(r);
171 let (writer, writer_task) = spawn_writer(w);
172 let client_version = hello.protocol_version;
173 let hello_copy = hello.clone();
174 let mut buf = FrameBuf::default();
175 writer
176 .send(ClientFrame::Hello(hello))
177 .await
178 .map_err(|_| TransportError::Closed)?;
179 let frame = tokio::time::timeout(HANDSHAKE_TIMEOUT, read_frame(&mut reader, &mut buf))
180 .await
181 .map_err(|_| TransportError::Protocol("handshake timed out".into()))??;
182 match frame {
183 Some(DaemonFrame::Welcome(welcome)) => {
184 if welcome.protocol_version != client_version {
185 return Err(TransportError::Version { client: client_version, daemon: welcome.protocol_version });
186 }
187 Ok(Self { reader, writer, writer_task, welcome, path: path.to_path_buf(), hello: hello_copy, buf })
188 }
189 Some(DaemonFrame::Refused { reason: RefuseReason::Version { daemon_version, .. }, .. }) => {
190 Err(TransportError::Version { client: client_version, daemon: daemon_version })
191 }
192 Some(DaemonFrame::Refused { reason, message }) => {
193 Err(TransportError::Protocol(format!("refused ({reason:?}): {message}")))
194 }
195 Some(other) => Err(TransportError::Protocol(format!("expected welcome, got {other:?}"))),
196 None => Err(TransportError::Closed),
197 }
198 }
199
200 async fn request(&mut self, frame: ClientFrame) -> Result<DaemonFrame, TransportError> {
201 self.writer.send(frame).await.map_err(|_| TransportError::Closed)?;
202 tokio::time::timeout(HANDSHAKE_TIMEOUT, read_frame(&mut self.reader, &mut self.buf))
203 .await
204 .map_err(|_| TransportError::Protocol("reply timed out".into()))??
205 .ok_or(TransportError::Closed)
206 }
207
208 pub async fn ping(&mut self) -> Result<Pong, TransportError> {
209 match self.request(ClientFrame::Ping).await? {
210 DaemonFrame::Pong { pid, uptime_s, sessions } => Ok(Pong { pid, uptime_s, sessions }),
211 other => Err(TransportError::Protocol(format!("expected pong, got {other:?}"))),
212 }
213 }
214
215 pub async fn sessions(&mut self) -> Result<Vec<SessionMeta>, TransportError> {
216 match self.request(ClientFrame::Sessions).await? {
217 DaemonFrame::SessionList { sessions } => Ok(sessions),
218 other => Err(TransportError::Protocol(format!("expected session list, got {other:?}"))),
219 }
220 }
221
222 pub async fn purge(&mut self) -> Result<Pong, TransportError> {
224 match self.request(ClientFrame::Purge).await? {
225 DaemonFrame::Pong { pid, uptime_s, sessions } => Ok(Pong { pid, uptime_s, sessions }),
226 DaemonFrame::Error { message, .. } => Err(TransportError::Protocol(message)),
227 other => Err(TransportError::Protocol(format!("expected pong, got {other:?}"))),
228 }
229 }
230
231 pub async fn auth_state(&mut self, query: agent_core::auth::AuthStateQuery) -> Result<AuthStateReply, TransportError> {
235 match self.request(ClientFrame::AuthState(query)).await? {
236 DaemonFrame::AuthState { report } => Ok(AuthStateReply::Report(report)),
237 DaemonFrame::Error { session_id: None, message } if predates_auth_state(&message) => {
238 Ok(AuthStateReply::DaemonPredates {
239 daemon_version: self.welcome.daemon_version.clone(),
240 daemon_build: self.welcome.daemon_build.clone(),
241 })
242 }
243 DaemonFrame::Error { message, .. } => Err(TransportError::Protocol(message)),
244 other => Err(TransportError::Protocol(format!("expected auth state, got {other:?}"))),
245 }
246 }
247
248 pub async fn shutdown(&mut self, force: bool) -> Result<(), TransportError> {
249 match self.request(ClientFrame::Shutdown { force }).await? {
250 DaemonFrame::Bye { .. } => Ok(()),
251 DaemonFrame::Error { message, .. } => Err(TransportError::Protocol(message)),
252 other => Err(TransportError::Protocol(format!("expected bye, got {other:?}"))),
253 }
254 }
255
256 pub async fn close(&mut self, id: &SessionId, force: bool, attached: bool) -> Result<String, TransportError> {
264 self.writer
265 .send(ClientFrame::Close { session_id: id.clone(), force, attached })
266 .await
267 .map_err(|_| TransportError::Closed)?;
268 let frame = tokio::time::timeout(CLOSE_TIMEOUT, read_frame(&mut self.reader, &mut self.buf))
269 .await
270 .map_err(|_| TransportError::Protocol("close reply timed out".into()))??;
271 close_reply(frame, &self.welcome.daemon_version)
272 }
273
274 pub async fn pin(&mut self, id: &SessionId, on: bool) -> Result<bool, TransportError> {
280 self.writer
281 .send(ClientFrame::Pin { session_id: id.clone(), on })
282 .await
283 .map_err(|_| TransportError::Closed)?;
284 let frame = tokio::time::timeout(PIN_TIMEOUT, read_frame(&mut self.reader, &mut self.buf))
285 .await
286 .map_err(|_| TransportError::Protocol("pin reply timed out".into()))??;
287 pin_reply(frame, &self.welcome.daemon_version)
288 }
289
290 pub async fn reload_extension(
296 &mut self,
297 extension: &str,
298 ) -> Result<Result<ExtensionReloadReport, ExtensionReloadFailure>, TransportError> {
299 self.writer
300 .send(ClientFrame::ReloadExtension { extension: extension.to_string() })
301 .await
302 .map_err(|_| TransportError::Closed)?;
303 let frame = tokio::time::timeout(RELOAD_EXTENSION_TIMEOUT, read_frame(&mut self.reader, &mut self.buf))
304 .await
305 .map_err(|_| TransportError::Protocol("extension reload reply timed out".into()))??;
306 reload_extension_reply(frame, &self.welcome.daemon_version)
307 }
308
309 pub async fn bye(self) {
311 let _ = self.writer.send(ClientFrame::Bye).await;
312 let _ = tokio::time::timeout(Duration::from_millis(200), self.writer_task).await;
313 }
314}
315
316fn close_reply(frame: Option<DaemonFrame>, daemon_version: &str) -> Result<String, TransportError> {
318 match frame {
319 Some(DaemonFrame::Closed { text, .. }) => Ok(text),
320 Some(DaemonFrame::Refused { reason: RefuseReason::UnknownSession, message }) => {
321 Err(TransportError::UnknownSession(message))
322 }
323 Some(DaemonFrame::Refused { message, .. }) => Err(TransportError::Refused(message)),
324 Some(DaemonFrame::Error { message, .. }) if message.contains("unknown variant `close`") => {
327 Err(TransportError::DaemonTooOld { daemon_version: daemon_version.to_string(), request: "daemon close" })
328 }
329 Some(DaemonFrame::Error { message, .. }) => Err(TransportError::Protocol(message)),
330 Some(other) => Err(TransportError::Protocol(format!("expected closed, got {other:?}"))),
331 None => Err(TransportError::Closed),
332 }
333}
334
335fn pin_reply(frame: Option<DaemonFrame>, daemon_version: &str) -> Result<bool, TransportError> {
337 match frame {
338 Some(DaemonFrame::Pinned { on, .. }) => Ok(on),
339 Some(DaemonFrame::Refused { reason: RefuseReason::UnknownSession, message }) => {
340 Err(TransportError::UnknownSession(message))
341 }
342 Some(DaemonFrame::Refused { message, .. }) => Err(TransportError::Refused(message)),
343 Some(DaemonFrame::Error { message, .. }) if message.contains("unknown variant `pin`") => {
346 Err(TransportError::DaemonTooOld { daemon_version: daemon_version.to_string(), request: "daemon pin" })
347 }
348 Some(DaemonFrame::Error { message, .. }) => Err(TransportError::Protocol(message)),
349 Some(other) => Err(TransportError::Protocol(format!("expected pinned, got {other:?}"))),
350 None => Err(TransportError::Closed),
351 }
352}
353
354pub use crate::extensions::manager::ExtensionReloadReport;
355
356#[derive(Debug, Clone, PartialEq, Eq)]
359pub struct ExtensionReloadFailure {
360 pub message: String,
361 pub unloaded: bool,
365}
366
367fn reload_extension_reply(
370 frame: Option<DaemonFrame>,
371 daemon_version: &str,
372) -> Result<Result<ExtensionReloadReport, ExtensionReloadFailure>, TransportError> {
373 match frame {
374 Some(DaemonFrame::ExtensionReloaded { report }) => Ok(Ok(report)),
375 Some(DaemonFrame::ExtensionReloadFailed { message, unloaded, .. }) => {
376 Ok(Err(ExtensionReloadFailure { message, unloaded }))
377 }
378 Some(DaemonFrame::Refused { message, .. }) => Err(TransportError::Refused(message)),
379 Some(DaemonFrame::Error { message, .. }) if message.contains("unknown variant `reload_extension`") => {
382 Err(TransportError::DaemonTooOld {
383 daemon_version: daemon_version.to_string(),
384 request: "daemon reload-extension",
385 })
386 }
387 Some(DaemonFrame::Error { message, .. }) => Err(TransportError::Protocol(message)),
388 Some(other) => Err(TransportError::Protocol(format!("expected extension_reloaded, got {other:?}"))),
389 None => Err(TransportError::Closed),
390 }
391}
392
393pub struct SocketTransport {
394 reader: BufReader<OwnedReadHalf>,
395 writer: mpsc::Sender<ClientFrame>,
396 _writer_task: tokio::task::JoinHandle<()>,
397 session: SessionId,
398 meta: SessionMeta,
399 client: ClientId,
400 view: Arc<arc_swap::ArcSwap<RuntimeView>>,
401 pub welcome: Welcome,
402 mode: AttachMode,
403 input_owner: Option<ClientId>,
404 roster: Roster,
405 client_input: ClientInput,
406 ended: bool,
407 path: PathBuf,
408 hello: Hello,
409 reload_pending: Option<u64>,
411 last_error: Option<TransportError>,
414 messages: Vec<crate::SharedMessage>,
419 pending_digest: Option<ConversationDigest>,
421 history: HistoryMode,
423 buf: FrameBuf,
424}
425
426impl SocketTransport {
427 pub async fn connect(path: &Path, hello: Hello) -> Result<Connected, TransportError> {
429 Connected::connect(path, hello).await
430 }
431
432 pub async fn attach(mut conn: Connected, attach: Attach) -> Result<(Self, AttachSnapshot), TransportError> {
436 let (mode, parked) = match &attach {
437 Attach::Existing { session_id, mode } => (
438 *mode,
439 conn.welcome
440 .sessions
441 .iter()
442 .any(|m| &m.id == session_id && m.lifecycle == SessionLifecycle::Parked),
443 ),
444 Attach::Create { mode, .. } => (*mode, false),
445 };
446 conn.writer
447 .send(ClientFrame::Attach(attach))
448 .await
449 .map_err(|_| TransportError::Closed)?;
450 let budget = if parked { ATTACH_TIMEOUT_PARKED } else { ATTACH_TIMEOUT };
451 let deadline = tokio::time::Instant::now() + budget;
452 let attached = loop {
453 let frame = tokio::time::timeout_at(deadline, read_frame(&mut conn.reader, &mut conn.buf))
454 .await
455 .map_err(|_| TransportError::Protocol("attach timed out".into()))??;
456 match frame {
457 Some(DaemonFrame::Attached(a)) => break a,
458 Some(DaemonFrame::Error { message, .. }) => return Err(TransportError::Refused(message)),
459 Some(DaemonFrame::Event(w)) => match w.event {
460 WireSessionEvent::AttachRefused { message } => return Err(TransportError::Refused(message)),
461 _ => continue,
463 },
464 Some(DaemonFrame::Bye { .. }) | None => return Err(TransportError::Closed),
465 Some(other) => return Err(TransportError::Protocol(format!("expected attached, got {other:?}"))),
466 }
467 };
468 let (client, snapshot) = attached.into_snapshot();
469 let meta = snapshot.meta.clone();
470 let view = Arc::new(arc_swap::ArcSwap::from_pointee(snapshot.view.clone()));
471 let history = conn.hello.client.history;
472 let messages = match history {
473 HistoryMode::Full => snapshot.conversation.api_messages.clone(),
474 HistoryMode::Digest => Vec::new(),
475 };
476 Ok((
477 Self {
478 reader: conn.reader,
479 writer: conn.writer,
480 _writer_task: conn.writer_task,
481 session: meta.id.clone(),
482 meta,
483 client,
484 view,
485 welcome: conn.welcome,
486 mode,
487 input_owner: snapshot.input_owner,
488 roster: Roster::from_snapshot(&snapshot),
489 client_input: snapshot.client_input,
490 ended: false,
491 path: conn.path,
492 hello: conn.hello,
493 reload_pending: None,
494 last_error: None,
495 messages,
496 pending_digest: None,
497 history,
498 buf: conn.buf,
499 },
500 snapshot,
501 ))
502 }
503
504 pub fn last_error(&self) -> Option<&TransportError> {
506 self.last_error.as_ref()
507 }
508
509 pub fn is_reload_pending(&self) -> bool {
512 self.reload_pending.is_some()
513 }
514
515 pub fn daemon_pid(&self) -> u32 {
517 self.welcome.pid
518 }
519
520 pub fn reconnect_budget() -> Duration {
522 std::env::var("SYNAPS_TUI_ATTACH_RECONNECT_SECS")
523 .ok()
524 .and_then(|s| s.parse().ok())
525 .map(Duration::from_secs)
526 .unwrap_or(Duration::from_secs(60))
527 }
528
529 pub async fn reconnect_once(&mut self, mode: AttachMode) -> Result<AttachSnapshot, TransportError> {
533 let was_owner = self.input_owner == Some(self.client);
534 let generation = self.reload_pending.unwrap_or(self.welcome.generation);
535 let mut hello = self.hello.clone();
536 hello.reconnect_of = Some(ClientReconnect {
537 previous_client: self.client,
538 session_id: self.session.clone(),
539 was_owner,
540 generation,
541 });
542 let mode = if was_owner { AttachMode::Takeover } else { mode };
543 match Connected::connect(&self.path, hello).await {
544 Ok(conn) => {
545 match Self::attach(conn, Attach::Existing { session_id: self.session.clone(), mode }).await {
546 Ok((t, snap)) => {
547 *self = t;
548 Ok(snap)
549 }
550 Err(e) => Err(e),
551 }
552 }
553 Err(e) => Err(e),
554 }
555 }
556
557 pub async fn ping(path: &Path) -> Result<Pong, TransportError> {
560 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
561 let p = c.ping().await;
562 c.bye().await;
563 p
564 }
565
566 pub async fn sessions(path: &Path) -> Result<Vec<SessionMeta>, TransportError> {
567 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
568 let s = c.sessions().await;
569 c.bye().await;
570 s
571 }
572
573 pub async fn shutdown(path: &Path, force: bool) -> Result<(), TransportError> {
574 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
575 c.shutdown(force).await
576 }
577
578 pub async fn purge(path: &Path) -> Result<Pong, TransportError> {
579 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
580 let p = c.purge().await;
581 c.bye().await;
582 p
583 }
584
585 pub async fn close(path: &Path, id: &SessionId, force: bool, attached: bool) -> Result<String, TransportError> {
588 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
589 let r = c.close(id, force, attached).await;
590 c.bye().await;
591 r
592 }
593
594 pub async fn pin(path: &Path, id: &SessionId, on: bool) -> Result<bool, TransportError> {
596 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
597 let r = c.pin(id, on).await;
598 c.bye().await;
599 r
600 }
601
602 pub async fn auth_state(path: &Path, query: agent_core::auth::AuthStateQuery) -> Result<AuthStateReply, TransportError> {
604 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
605 let reply = c.auth_state(query).await;
606 c.bye().await;
607 reply
608 }
609
610 pub async fn reload(path: &Path, now: bool, drain_secs: Option<u64>, exe: Option<PathBuf>) -> Result<u64, TransportError> {
614 let mut c = Connected::connect(path, Hello::new(ClientKind::Attach)).await?;
615 c.writer
616 .send(ClientFrame::Reload { now, drain_secs, exe })
617 .await
618 .map_err(|_| TransportError::Closed)?;
619 let budget = Duration::from_secs(drain_secs.unwrap_or(30) + 60);
621 let frame = tokio::time::timeout(budget, read_frame(&mut c.reader, &mut c.buf))
622 .await
623 .map_err(|_| TransportError::Protocol("reload reply timed out".into()))??;
624 match frame {
625 Some(DaemonFrame::Bye { reason: Some(ByeReason::Reloading { generation, .. }) }) => Ok(generation),
626 Some(DaemonFrame::Bye { .. }) => Err(TransportError::Protocol("daemon said bye without a reload reason".into())),
627 Some(DaemonFrame::Refused { reason: RefuseReason::ReloadRefused { why }, .. }) => Err(TransportError::Refused(why)),
628 Some(DaemonFrame::Refused { message, .. }) | Some(DaemonFrame::Error { message, .. }) => Err(TransportError::Refused(message)),
629 Some(other) => Err(TransportError::Protocol(format!("expected bye, got {other:?}"))),
630 None => Err(TransportError::Closed),
631 }
632 }
633
634 pub async fn detach(&self) {
636 let _ = self
637 .writer
638 .send(ClientFrame::Cmd { session_id: self.session.clone(), cmd: SessionCommand::Detach { client: self.client } })
639 .await;
640 let _ = self.writer.send(ClientFrame::Bye).await;
641 }
642
643 fn notice(&self, text: String) -> Envelope {
644 Envelope { session_id: self.session.clone(), seq: u64::MAX, ts: chrono::Utc::now(), event: SessionEventWire::SystemNotice(text) }
645 }
646
647 pub fn messages(&self) -> &[crate::SharedMessage] {
650 &self.messages
651 }
652
653 pub fn last_frame_bytes(&self) -> usize {
656 self.buf.last_frame_bytes
657 }
658
659 pub fn history_mode(&self) -> HistoryMode {
660 self.history
661 }
662
663 fn absorb(&mut self, w: WireEnvelope) -> Option<Envelope> {
668 if self.history == HistoryMode::Digest {
669 return self.absorb_digest(w);
670 }
671 match w.event {
672 WireSessionEvent::Conversation { digest } => {
673 if digest.matches(&self.messages) {
674 let event = SessionEventWire::Conversation(digest.into_snapshot(self.messages.clone()));
675 return Some(Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event });
676 }
677 if digest.messages_len == 0 {
678 self.messages.clear();
679 let event = SessionEventWire::Conversation(digest.into_snapshot(Vec::new()));
680 return Some(Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event });
681 }
682 let first = self.pending_digest.is_none();
684 self.pending_digest = Some(digest);
685 if first {
686 let _ = self.writer.try_send(ClientFrame::Cmd {
687 session_id: self.session.clone(),
688 cmd: SessionCommand::Query { id: DIGEST_RESYNC_QUERY_ID, query: SessionQuery::Messages },
689 });
690 }
691 None
692 }
693 WireSessionEvent::QueryResult { id, value } if id == DIGEST_RESYNC_QUERY_ID => {
694 if let Ok(msgs) = serde_json::from_value::<Vec<serde_json::Value>>(value) {
695 self.messages = msgs.into_iter().map(Arc::new).collect();
696 }
697 let digest = self.pending_digest.take()?;
698 if !digest.matches(&self.messages) {
699 tracing::debug!("socket_transport: messages re-query still disagrees with digest; using fetched history");
700 }
701 let event = SessionEventWire::Conversation(digest.into_snapshot(self.messages.clone()));
702 Some(Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event })
703 }
704 WireSessionEvent::QueryResult { id, .. } if id >= RESERVED_QUERY_ID_BASE => None,
705 event => {
706 let env = Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event: event.into() };
707 match &env.event {
708 SessionEventWire::Stream(crate::StreamEvent::Session(crate::SessionEvent::MessageHistory(m))) => {
709 self.messages = m.clone();
710 }
711 SessionEventWire::Attached { snapshot, .. } => {
712 self.messages = snapshot.conversation.api_messages.clone();
713 }
714 _ => {}
715 }
716 Some(env)
717 }
718 }
719 }
720}
721
722impl SocketTransport {
723 fn absorb_digest(&mut self, w: WireEnvelope) -> Option<Envelope> {
728 match w.event {
729 WireSessionEvent::Conversation { digest } => {
730 let event = SessionEventWire::Conversation(digest.into_snapshot(Vec::new()));
731 Some(Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event })
732 }
733 WireSessionEvent::QueryResult { id, .. } if id >= RESERVED_QUERY_ID_BASE => None,
734 event => {
735 let env = Envelope { session_id: w.session_id, seq: w.seq, ts: w.ts, event: event.into() };
736 if matches!(
737 env.event,
738 SessionEventWire::Stream(crate::StreamEvent::Session(crate::SessionEvent::MessageHistory(_)))
739 ) {
740 return None;
741 }
742 Some(env)
743 }
744 }
745 }
746}
747
748#[async_trait::async_trait]
749impl ClientTransport for SocketTransport {
750 fn session_id(&self) -> &SessionId {
751 &self.session
752 }
753
754 fn meta(&self) -> &SessionMeta {
755 &self.meta
756 }
757
758 async fn send(&self, cmd: SessionCommand) -> Result<(), TransportError> {
759 match self.writer.try_send(ClientFrame::Cmd { session_id: self.session.clone(), cmd }) {
760 Ok(()) => Ok(()),
761 Err(mpsc::error::TrySendError::Full(_)) => Err(TransportError::Backpressure),
762 Err(mpsc::error::TrySendError::Closed(_)) => Err(TransportError::Closed),
763 }
764 }
765
766 async fn send_from_self(&self, cmd: SessionCommand) -> Result<(), TransportError> {
769 self.send(cmd).await
770 }
771
772 async fn next_event(&mut self) -> Option<Envelope> {
773 if self.ended {
774 return None;
775 }
776 loop {
777 match read_frame(&mut self.reader, &mut self.buf).await {
778 Ok(Some(DaemonFrame::Event(w))) => {
779 let Some(env) = self.absorb(w) else { continue };
780 match &env.event {
781 SessionEventWire::SettingChanged(applied) => {
782 self.view.store(Arc::new(applied.view.clone()));
783 }
784 SessionEventWire::Ended { .. } => self.ended = true,
785 SessionEventWire::InputOwnerChanged { to, .. } => self.input_owner = *to,
786 SessionEventWire::Reloading { generation, .. } => {
787 self.reload_pending = Some(*generation);
788 }
789 other => self.roster.apply(other),
790 }
791 return Some(env);
792 }
793 Ok(Some(DaemonFrame::Error { message, .. })) => return Some(self.notice(message)),
794 Ok(Some(DaemonFrame::Bye { reason })) => {
795 if let Some(ByeReason::Reloading { generation, .. }) = reason {
796 self.reload_pending = Some(generation);
797 }
798 self.ended = true;
799 if let Some(generation) = self.reload_pending {
800 self.last_error = Some(TransportError::Reloading { generation });
801 }
802 return None;
803 }
804 Ok(None) => {
805 self.ended = true;
806 if let Some(generation) = self.reload_pending {
807 self.last_error = Some(TransportError::Reloading { generation });
808 }
809 return None;
810 }
811 Ok(Some(other)) => {
812 tracing::debug!("socket_transport: unexpected frame after attach: {other:?}");
813 continue;
814 }
815 Err(e) => {
816 tracing::debug!("socket_transport: read error: {e}");
817 self.ended = true;
818 return None;
819 }
820 }
821 }
822 }
823
824 fn view(&self) -> Arc<RuntimeView> {
825 Arc::clone(&self.view.load())
826 }
827
828 fn client_id(&self) -> ClientId {
829 self.client
830 }
831
832 fn mode(&self) -> AttachMode {
833 self.mode
834 }
835
836 fn input_owner(&self) -> Option<ClientId> {
837 self.input_owner
838 }
839
840 fn roster(&self) -> Roster {
841 self.roster.clone()
842 }
843
844 fn client_input(&self) -> ClientInput {
845 self.client_input
846 }
847
848 async fn reconnect(&mut self, mode: AttachMode) -> Result<AttachSnapshot, TransportError> {
855 let was_owner = self.input_owner == Some(self.client);
856 let generation = self.reload_pending.unwrap_or(self.welcome.generation);
857 let mut hello = self.hello.clone();
858 hello.reconnect_of = Some(ClientReconnect {
859 previous_client: self.client,
860 session_id: self.session.clone(),
861 was_owner,
862 generation,
863 });
864 let mode = if was_owner { AttachMode::Takeover } else { mode };
865 let total = std::env::var("SYNAPS_TUI_ATTACH_RECONNECT_SECS")
866 .ok()
867 .and_then(|s| s.parse().ok())
868 .map(Duration::from_secs)
869 .unwrap_or(Duration::from_secs(60));
870 let deadline = tokio::time::Instant::now() + total;
871 let mut backoff = Duration::from_millis(100);
872 let mut last;
873 loop {
874 match Connected::connect(&self.path, hello.clone()).await {
875 Ok(conn) => {
876 match Self::attach(conn, Attach::Existing { session_id: self.session.clone(), mode }).await {
877 Ok((t, snap)) => {
878 *self = t;
879 return Ok(snap);
880 }
881 Err(TransportError::Refused(m)) => return Err(TransportError::Refused(m)),
882 Err(e) => last = e,
883 }
884 }
885 Err(TransportError::Version { client, daemon }) => {
886 return Err(TransportError::Version { client, daemon })
887 }
888 Err(e) => last = e,
889 }
890 if tokio::time::Instant::now() + backoff > deadline {
891 return Err(last);
892 }
893 tokio::time::sleep(backoff).await;
894 backoff = (backoff * 2).min(Duration::from_secs(5));
895 }
896 }
897
898 async fn reconnect_once(&mut self, mode: AttachMode) -> Result<AttachSnapshot, TransportError> {
899 SocketTransport::reconnect_once(self, mode).await
901 }
902
903 fn is_reload_pending(&self) -> bool {
904 SocketTransport::is_reload_pending(self)
905 }
906
907 fn daemon_pid(&self) -> u32 {
908 SocketTransport::daemon_pid(self)
909 }
910
911 fn daemon_binary(&self) -> Option<crate::session::transport::BinaryId> {
912 Some(crate::session::transport::BinaryId::of(&self.welcome))
913 }
914
915 fn socket_path(&self) -> Option<&Path> {
916 Some(&self.path)
917 }
918}
919
920#[cfg(test)]
921mod tests {
922 use super::*;
923 use crate::{LlmEvent, StreamEvent};
924 use tokio::net::UnixListener;
925
926 async fn fake_daemon(listener: UnixListener, protocol_version: u32) {
929 let (stream, _) = listener.accept().await.unwrap();
930 let (r, mut w) = stream.into_split();
931 let mut reader = BufReader::new(r);
932 let mut line = String::new();
933 let mut seq = 0u64;
934 let sid = SessionId::from("fake-1");
935 loop {
936 line.clear();
937 if reader.read_line(&mut line).await.unwrap() == 0 {
938 break;
939 }
940 let frame: ClientFrame = decode_line(line.trim_end()).unwrap();
941 let reply = match frame {
942 ClientFrame::Hello(h) => {
943 if h.protocol_version != protocol_version {
944 let f = DaemonFrame::Refused {
945 reason: RefuseReason::Version { daemon_version: protocol_version, min: protocol_version, max: protocol_version },
946 message: "version".into(),
947 };
948 w.write_all(encode_line(&f).unwrap().as_bytes()).await.unwrap();
949 break;
950 }
951 DaemonFrame::Welcome(Welcome {
952 protocol_version,
953 daemon_version: "t".into(),
954 pid: 1,
955 profile: None,
956 sessions: vec![],
957 progressive_tool_disclosure: true,
958 generation: 1,
959 daemon_build: String::new(),
960 })
961 }
962 ClientFrame::Ping => DaemonFrame::Pong { pid: 1, uptime_s: 0, sessions: 0 },
963 ClientFrame::Attach(_) => DaemonFrame::Attached(AttachedWire::new(
964 ClientId(7),
965 AttachSnapshot {
966 meta: crate::session::handle::echo::meta_for(&sid),
967 view: crate::session::handle::echo::view_for(),
968 conversation: ConversationSnapshot::default(),
969 streaming: false,
970 replay: vec![],
971 pending_prompts: vec![],
972 clients: vec![],
973 roster: Vec::new(),
974 client_input: ClientInput::wire_default(),
975 input_owner: Some(ClientId(7)),
976 display_tail: None,
977 },
978 )),
979 ClientFrame::Cmd { cmd: SessionCommand::Submit { text, .. }, .. } => {
980 seq += 1;
981 DaemonFrame::Event(WireEnvelope {
982 session_id: sid.clone(),
983 seq,
984 ts: chrono::Utc::now(),
985 event: SessionEventWire::Stream(StreamEvent::Llm(LlmEvent::Text(text))).into(),
986 })
987 }
988 ClientFrame::Cmd { cmd: SessionCommand::Set { id, .. }, .. } => {
989 seq += 1;
990 let mut view = crate::session::handle::echo::view_for();
991 view.model = "changed".into();
992 DaemonFrame::Event(WireEnvelope {
993 session_id: sid.clone(),
994 seq,
995 ts: chrono::Utc::now(),
996 event: SessionEventWire::SettingChanged(SettingApplied {
997 id,
998 setting: "model".into(),
999 ok: true,
1000 message: None,
1001 view,
1002 clamp: None,
1003 })
1004 .into(),
1005 })
1006 }
1007 ClientFrame::Cmd { cmd: SessionCommand::Checkpoint { .. }, .. } => {
1009 seq += 1;
1010 let ev = DaemonFrame::Event(WireEnvelope {
1011 session_id: sid.clone(),
1012 seq,
1013 ts: chrono::Utc::now(),
1014 event: WireSessionEvent::Reloading { generation: 2, retry_after_ms: 500 },
1015 });
1016 w.write_all(encode_line(&ev).unwrap().as_bytes()).await.unwrap();
1017 let bye = DaemonFrame::Bye { reason: Some(ByeReason::Reloading { generation: 2, retry_after_ms: 500 }) };
1018 w.write_all(encode_line(&bye).unwrap().as_bytes()).await.unwrap();
1019 break;
1020 }
1021 ClientFrame::Cmd { cmd: SessionCommand::KeepWarm { .. }, .. } => {
1023 seq += 1;
1024 DaemonFrame::Event(WireEnvelope {
1025 session_id: sid.clone(),
1026 seq,
1027 ts: chrono::Utc::now(),
1028 event: WireSessionEvent::InputOwnerChanged {
1029 from: Some(ClientId(7)),
1030 to: Some(ClientId(9)),
1031 reason: OwnerChangeReason::Takeover,
1032 },
1033 })
1034 }
1035 ClientFrame::Cmd { cmd: SessionCommand::Steer { text }, .. } => {
1037 seq += 1;
1038 let snap = ConversationSnapshot {
1039 api_messages: vec![Arc::new(serde_json::json!({"role":"user","content":text}))],
1040 cost: 1.5,
1041 ..Default::default()
1042 };
1043 DaemonFrame::Event(WireEnvelope {
1044 session_id: sid.clone(),
1045 seq,
1046 ts: chrono::Utc::now(),
1047 event: SessionEventWire::Conversation(snap).into(),
1048 })
1049 }
1050 ClientFrame::Cmd { cmd: SessionCommand::Query { id, query: SessionQuery::Messages }, .. } => {
1051 seq += 1;
1052 DaemonFrame::Event(WireEnvelope {
1053 session_id: sid.clone(),
1054 seq,
1055 ts: chrono::Utc::now(),
1056 event: WireSessionEvent::QueryResult {
1057 id,
1058 value: serde_json::json!([{"role":"user","content":"drifted"}]),
1059 },
1060 })
1061 }
1062 ClientFrame::Bye => {
1063 let _ = w.write_all(encode_line(&DaemonFrame::Bye { reason: None }).unwrap().as_bytes()).await;
1064 break;
1065 }
1066 ClientFrame::Cmd { cmd: SessionCommand::Detach { .. }, .. } => continue,
1067 _ => DaemonFrame::Error { session_id: None, message: "nope".into() },
1068 };
1069 let bytes = encode_line(&reply).unwrap();
1070 if bytes.contains("split:") {
1073 let (a, b) = bytes.as_bytes().split_at(bytes.len() / 2);
1074 w.write_all(a).await.unwrap();
1075 w.flush().await.unwrap();
1076 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1077 w.write_all(b).await.unwrap();
1078 continue;
1079 }
1080 w.write_all(bytes.as_bytes()).await.unwrap();
1081 }
1082 }
1083
1084 fn sock() -> (tempfile::TempDir, std::path::PathBuf) {
1085 let d = tempfile::tempdir().unwrap();
1086 let p = d.path().join("d.sock");
1087 (d, p)
1088 }
1089
1090 #[test]
1095 fn predates_auth_state_matches_an_old_daemons_decode_error_and_nothing_else() {
1096 #[derive(serde::Deserialize)]
1097 #[serde(tag = "type", rename_all = "snake_case")]
1098 #[allow(dead_code, clippy::large_enum_variant)]
1099 enum OldClientFrame {
1100 Hello(Hello),
1101 Ping,
1102 Sessions,
1103 Shutdown {
1104 #[serde(default)]
1105 force: bool,
1106 },
1107 Purge,
1108 Bye,
1109 }
1110 for query in [
1111 agent_core::auth::AuthStateQuery::default(),
1112 agent_core::auth::AuthStateQuery { provider: Some("anthropic".into()), account: Some("b".into()), clear: true },
1113 ] {
1114 let line = encode_line(&ClientFrame::AuthState(query)).unwrap();
1115 let err = decode_line::<OldClientFrame>(line.trim_end()).err().expect("an old daemon cannot decode it");
1116 let reply = format!("malformed frame: {err}");
1117 assert!(predates_auth_state(&reply), "{reply}");
1118 }
1119 for other in [
1120 "auth state: unknown account `zzz`",
1121 "malformed frame: expected value at line 1 column 1",
1122 "malformed frame: unknown variant `auth_status`, expected one of `hello`, `ping`",
1123 "malformed frame: missing field `clear`",
1124 "control frames are not accepted on an attached connection",
1125 "duplicate hello",
1126 "",
1127 ] {
1128 assert!(!predates_auth_state(other), "{other:?}");
1129 }
1130 }
1131
1132 #[tokio::test]
1136 async fn next_event_survives_cancellation_mid_frame() {
1137 let (_d, path) = sock();
1138 let l = UnixListener::bind(&path).unwrap();
1139 let srv = tokio::spawn(fake_daemon(l, PROTOCOL_VERSION));
1140 let conn = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.unwrap();
1141 let (mut t, _) = SocketTransport::attach(conn, Attach::Existing { session_id: "fake-1".into(), mode: AttachMode::Mirror })
1142 .await
1143 .unwrap();
1144 t.send(SessionCommand::Submit { text: "split:hello".into(), attachments: vec![] }).await.unwrap();
1145 tokio::select! {
1147 e = t.next_event() => panic!("frame cannot be complete yet: {e:?}"),
1148 _ = tokio::time::sleep(std::time::Duration::from_millis(20)) => {}
1149 }
1150 let e = tokio::time::timeout(std::time::Duration::from_secs(2), t.next_event())
1151 .await
1152 .expect("no event");
1153 match e.map(|e| e.event) {
1154 Some(SessionEventWire::Stream(StreamEvent::Llm(LlmEvent::Text(s)))) => assert_eq!(s, "split:hello"),
1155 other => panic!("frame lost to cancellation: {other:?} (last_error={:?})", t.last_error()),
1156 }
1157 t.detach().await;
1158 let _ = srv.await;
1159 }
1160
1161 #[tokio::test]
1162 async fn socket_transport_handshake_attach_pump() {
1163 let (_d, path) = sock();
1164 let l = UnixListener::bind(&path).unwrap();
1165 let srv = tokio::spawn(fake_daemon(l, PROTOCOL_VERSION));
1166
1167 let mut conn = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.unwrap();
1168 assert_eq!(conn.welcome.protocol_version, PROTOCOL_VERSION);
1169 assert_eq!(conn.ping().await.unwrap().pid, 1);
1170 let (mut t, snap) = SocketTransport::attach(conn, Attach::Existing { session_id: "fake-1".into(), mode: AttachMode::Mirror })
1171 .await
1172 .unwrap();
1173 assert_eq!(t.client_id(), ClientId(7));
1174 assert_eq!(t.mode(), AttachMode::Mirror);
1175 assert_eq!(t.input_owner(), Some(ClientId(7)));
1176 assert_eq!(snap.meta.id.as_str(), "fake-1");
1177 assert_eq!(t.session_id().as_str(), "fake-1");
1178 assert_eq!(t.view().model, "echo");
1179
1180 t.send(SessionCommand::Submit { text: "hello".into(), attachments: vec![] }).await.unwrap();
1181 let e = t.next_event().await.unwrap();
1182 match e.event {
1183 SessionEventWire::Stream(StreamEvent::Llm(LlmEvent::Text(s))) => assert_eq!(s, "hello"),
1184 other => panic!("{other:?}"),
1185 }
1186 t.send_from_self(SessionCommand::Set { id: 11, setting: SessionSetting::Model { model: "changed".into() } })
1187 .await
1188 .unwrap();
1189 let e = t.next_event().await.unwrap();
1190 assert!(matches!(e.event, SessionEventWire::SettingChanged(SettingApplied { id: 11, .. })));
1191 assert_eq!(t.view().model, "changed");
1192 t.send(SessionCommand::KeepWarm { on: true }).await.unwrap();
1193 let e = t.next_event().await.unwrap();
1194 assert!(matches!(e.event, SessionEventWire::InputOwnerChanged { .. }));
1195 assert_eq!(t.input_owner(), Some(ClientId(9)));
1196
1197 t.send(SessionCommand::Save).await.unwrap();
1199 assert!(matches!(t.next_event().await.unwrap().event, SessionEventWire::SystemNotice(_)));
1200 t.detach().await;
1201 assert!(t.next_event().await.is_none());
1202 assert!(t.next_event().await.is_none());
1203 srv.await.unwrap();
1204 }
1205
1206 #[tokio::test]
1207 async fn conversation_digest_miss_requeries_messages() {
1208 let (_d, path) = sock();
1209 let l = UnixListener::bind(&path).unwrap();
1210 let srv = tokio::spawn(fake_daemon(l, PROTOCOL_VERSION));
1211 let conn = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.unwrap();
1212 let (mut t, _) = SocketTransport::attach(conn, Attach::Existing { session_id: "fake-1".into(), mode: AttachMode::Mirror })
1213 .await
1214 .unwrap();
1215 assert!(t.messages().is_empty());
1216 t.send(SessionCommand::Steer { text: "drifted".into() }).await.unwrap();
1217 let e = t.next_event().await.unwrap();
1220 match e.event {
1221 SessionEventWire::Conversation(c) => {
1222 assert_eq!(c.api_messages.len(), 1);
1223 assert_eq!(c.api_messages[0]["content"], "drifted");
1224 assert_eq!(c.cost, 1.5);
1225 }
1226 other => panic!("{other:?}"),
1227 }
1228 assert_eq!(t.messages().len(), 1);
1229 t.send(SessionCommand::Steer { text: "drifted".into() }).await.unwrap();
1231 let e = t.next_event().await.unwrap();
1232 assert!(matches!(e.event, SessionEventWire::Conversation(ref c) if c.api_messages.len() == 1));
1233 t.detach().await;
1234 assert!(t.next_event().await.is_none());
1235 srv.await.unwrap();
1236 }
1237
1238 #[tokio::test]
1239 async fn reloading_then_bye_sets_last_error_and_reconnect_gives_up_on_a_dead_socket() {
1240 let (_d, path) = sock();
1241 let l = UnixListener::bind(&path).unwrap();
1242 let srv = tokio::spawn(fake_daemon(l, PROTOCOL_VERSION));
1243 let conn = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.unwrap();
1244 let (mut t, _) = SocketTransport::attach(conn, Attach::Existing { session_id: "fake-1".into(), mode: AttachMode::Observe })
1245 .await
1246 .unwrap();
1247 assert_eq!(t.mode(), AttachMode::Observe);
1248 assert!(t.last_error().is_none());
1249 t.send(SessionCommand::Checkpoint { reason: CheckpointReason::Reload }).await.unwrap();
1250 let e = t.next_event().await.unwrap();
1251 assert!(matches!(e.event, SessionEventWire::Reloading { generation: 2, .. }));
1252 assert!(t.next_event().await.is_none());
1253 assert!(matches!(t.last_error(), Some(TransportError::Reloading { generation: 2 })));
1254 srv.await.unwrap();
1255 std::env::set_var("SYNAPS_TUI_ATTACH_RECONNECT_SECS", "1");
1257 let r = t.reconnect(AttachMode::Observe).await;
1258 std::env::remove_var("SYNAPS_TUI_ATTACH_RECONNECT_SECS");
1259 assert!(r.is_err());
1260 }
1261
1262 #[tokio::test]
1263 async fn attach_error_frame_is_refused() {
1264 let (_d, path) = sock();
1265 let l = UnixListener::bind(&path).unwrap();
1266 let srv = tokio::spawn(async move {
1268 let (stream, _) = l.accept().await.unwrap();
1269 let (r, mut w) = stream.into_split();
1270 let mut reader = BufReader::new(r);
1271 let mut line = String::new();
1272 reader.read_line(&mut line).await.unwrap();
1273 let welcome = DaemonFrame::Welcome(Welcome {
1274 protocol_version: PROTOCOL_VERSION,
1275 daemon_version: "t".into(),
1276 pid: 1,
1277 profile: None,
1278 sessions: vec![],
1279 progressive_tool_disclosure: true,
1280 generation: 1,
1281 daemon_build: String::new(),
1282 });
1283 w.write_all(encode_line(&welcome).unwrap().as_bytes()).await.unwrap();
1284 line.clear();
1285 reader.read_line(&mut line).await.unwrap();
1286 let err = DaemonFrame::Error { session_id: None, message: "unknown session".into() };
1287 w.write_all(encode_line(&err).unwrap().as_bytes()).await.unwrap();
1288 });
1289 let conn = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.unwrap();
1290 let err = SocketTransport::attach(conn, Attach::Existing { session_id: "nope".into(), mode: AttachMode::Mirror })
1291 .await
1292 .err()
1293 .unwrap();
1294 assert!(matches!(err, TransportError::Refused(ref m) if m == "unknown session"), "{err}");
1295 srv.await.unwrap();
1296 }
1297
1298 #[tokio::test]
1299 async fn socket_transport_refuses_version_mismatch() {
1300 let (_d, path) = sock();
1301 let l = UnixListener::bind(&path).unwrap();
1302 let srv = tokio::spawn(fake_daemon(l, 99));
1303 let err = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.err().unwrap();
1304 assert!(matches!(err, TransportError::Version { client: PROTOCOL_VERSION, daemon: 99 }), "{err}");
1305 srv.await.unwrap();
1306 }
1307
1308 #[test]
1311 fn close_maps_each_daemon_answer_and_an_old_daemons_unknown_variant_error() {
1312 let old = DaemonFrame::Error {
1313 session_id: None,
1314 message: "malformed frame: unknown variant `close`, expected one of `hello`, `ping`".into(),
1315 };
1316 let err = close_reply(Some(old), "0.9.0").unwrap_err();
1317 assert!(matches!(err, TransportError::DaemonTooOld { ref daemon_version, .. } if daemon_version == "0.9.0"), "{err}");
1318 assert!(err.to_string().contains("synaps daemon reload"), "{err}");
1319
1320 let closed = DaemonFrame::Closed { session_id: "s".into(), text: "closing session s".into() };
1321 assert_eq!(close_reply(Some(closed), "1").unwrap(), "closing session s");
1322 let busy = DaemonFrame::Refused { reason: RefuseReason::Busy, message: "not closed: a turn is running".into() };
1323 assert!(matches!(close_reply(Some(busy), "1"), Err(TransportError::Refused(ref m)) if m.contains("turn")));
1324 let unknown = DaemonFrame::Refused { reason: RefuseReason::UnknownSession, message: "no live session s".into() };
1325 assert!(matches!(close_reply(Some(unknown), "1"), Err(TransportError::UnknownSession(_))));
1326 let other = DaemonFrame::Error { session_id: None, message: "close: no reply".into() };
1327 assert!(matches!(close_reply(Some(other), "1"), Err(TransportError::Protocol(_))));
1328 assert!(matches!(close_reply(None, "1"), Err(TransportError::Closed)));
1329 }
1330
1331 #[test]
1332 fn pin_maps_each_daemon_answer_and_an_old_daemons_unknown_variant_error() {
1333 let old = DaemonFrame::Error {
1334 session_id: None,
1335 message: "malformed frame: unknown variant `pin`, expected one of `hello`, `ping`".into(),
1336 };
1337 let err = pin_reply(Some(old), "0.9.0").unwrap_err();
1338 assert!(
1339 matches!(err, TransportError::DaemonTooOld { ref daemon_version, request: "daemon pin" } if daemon_version == "0.9.0"),
1340 "{err}"
1341 );
1342 assert!(err.to_string().contains("synaps daemon reload"), "{err}");
1343
1344 assert!(pin_reply(Some(DaemonFrame::Pinned { session_id: "s".into(), on: true }), "1").unwrap());
1345 assert!(!pin_reply(Some(DaemonFrame::Pinned { session_id: "s".into(), on: false }), "1").unwrap());
1346 let busy = DaemonFrame::Refused { reason: RefuseReason::Busy, message: "daemon reloading".into() };
1347 assert!(matches!(pin_reply(Some(busy), "1"), Err(TransportError::Refused(ref m)) if m.contains("reloading")));
1348 let unknown = DaemonFrame::Refused { reason: RefuseReason::UnknownSession, message: "no live session s".into() };
1349 assert!(matches!(pin_reply(Some(unknown), "1"), Err(TransportError::UnknownSession(_))));
1350 let other = DaemonFrame::Error { session_id: None, message: "pin: timed out".into() };
1351 assert!(matches!(pin_reply(Some(other), "1"), Err(TransportError::Protocol(_))));
1352 let closed = DaemonFrame::Closed { session_id: "s".into(), text: "x".into() };
1353 assert!(matches!(pin_reply(Some(closed), "1"), Err(TransportError::Protocol(_))));
1354 assert!(matches!(pin_reply(None, "1"), Err(TransportError::Closed)));
1355 }
1356
1357 #[test]
1358 fn reload_extension_maps_each_daemon_answer_and_an_old_daemons_unknown_variant_error() {
1359 let old = DaemonFrame::Error {
1360 session_id: None,
1361 message: "malformed frame: unknown variant `reload_extension`, expected one of `hello`, `ping`".into(),
1362 };
1363 let err = reload_extension_reply(Some(old), "0.9.0").unwrap_err();
1364 assert!(
1365 matches!(err, TransportError::DaemonTooOld { ref daemon_version, request: "daemon reload-extension" } if daemon_version == "0.9.0"),
1366 "{err}"
1367 );
1368
1369 let report = ExtensionReloadReport {
1370 extension: "axel".into(),
1371 was_loaded: true,
1372 deferred: false,
1373 tools: vec!["axel:axel_search".into()],
1374 removed_tools: vec![],
1375 plugin_dir: std::path::PathBuf::from("/p/axel"),
1376 };
1377 let ok = DaemonFrame::ExtensionReloaded { report: report.clone() };
1378 assert_eq!(reload_extension_reply(Some(ok), "1").unwrap(), Ok(report));
1379 let failed = DaemonFrame::ExtensionReloadFailed { extension: "axel".into(), message: "spawn failed".into(), unloaded: true };
1380 assert_eq!(
1381 reload_extension_reply(Some(failed), "1").unwrap(),
1382 Err(ExtensionReloadFailure { message: "spawn failed".into(), unloaded: true })
1383 );
1384 let busy = DaemonFrame::Refused { reason: RefuseReason::Busy, message: "daemon reloading".into() };
1385 assert!(matches!(reload_extension_reply(Some(busy), "1"), Err(TransportError::Refused(ref m)) if m.contains("reloading")));
1386 let other = DaemonFrame::Error { session_id: None, message: "frame too large".into() };
1388 assert!(matches!(reload_extension_reply(Some(other), "1"), Err(TransportError::Protocol(_))));
1389 let pinned = DaemonFrame::Pinned { session_id: "s".into(), on: true };
1390 assert!(matches!(reload_extension_reply(Some(pinned), "1"), Err(TransportError::Protocol(_))));
1391 assert!(matches!(reload_extension_reply(None, "1"), Err(TransportError::Closed)));
1392 }
1393
1394 #[tokio::test]
1395 async fn socket_transport_connect_refused_when_nobody_listens() {
1396 let (_d, path) = sock();
1397 let err = SocketTransport::connect(&path, Hello::new(ClientKind::Test)).await.err().unwrap();
1398 assert!(matches!(err, TransportError::Io(_)));
1399 }
1400}