1use std::io::{BufRead, BufReader, Write};
10use std::thread;
11use std::time::Duration;
12
13use crate::AppError;
14use crate::transport::relay::ControlFrame;
15use crate::transport::{CONNECT_TIMEOUT, connect_bounded};
16use crate::wire::{InjectionCommand, WIRE_VERSION};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SessionEnd {
21 Closed,
23 Rejected,
25 Complete,
27}
28
29pub fn receive_session<R: BufRead, W: Write, O: Write>(
39 reader: R,
40 daemon_writer: W,
41 output: O,
42 token: &str,
43 max_commands: Option<usize>,
44) -> Result<SessionEnd, AppError> {
45 receive_session_kind(reader, daemon_writer, output, token, max_commands, false)
46}
47
48pub fn receive_events_session<R: BufRead, W: Write, O: Write>(
50 reader: R,
51 daemon_writer: W,
52 output: O,
53 token: &str,
54 max_events: Option<usize>,
55) -> Result<SessionEnd, AppError> {
56 receive_session_kind(reader, daemon_writer, output, token, max_events, true)
57}
58
59fn receive_session_kind<R: BufRead, W: Write, O: Write>(
60 reader: R,
61 mut daemon_writer: W,
62 mut output: O,
63 token: &str,
64 max_commands: Option<usize>,
65 events: bool,
66) -> Result<SessionEnd, AppError> {
67 if max_commands == Some(0) {
68 return Ok(SessionEnd::Complete);
69 }
70
71 let subscribe = if events {
72 ControlFrame::SubscribeEvents {
73 v: WIRE_VERSION,
74 token: token.to_owned(),
75 }
76 } else {
77 ControlFrame::Subscribe {
78 v: WIRE_VERSION,
79 token: token.to_owned(),
80 }
81 };
82 let line = subscribe.json_line()?;
83 daemon_writer
84 .write_all(line.as_bytes())
85 .and_then(|()| daemon_writer.write_all(b"\n"))
86 .and_then(|()| daemon_writer.flush())
87 .map_err(|e| AppError::internal("receive_subscribe", e.to_string()))?;
88
89 let mut received = 0usize;
90 for line in reader.lines() {
91 let line = line.map_err(|e| AppError::internal("receive_read", e.to_string()))?;
92 let trimmed = line.trim();
93 if trimmed.is_empty() {
94 continue;
95 }
96
97 let encoded = if events {
98 crate::events::SemanticEvent::decode_line(trimmed).and_then(|e| e.json_line())
99 } else {
100 InjectionCommand::decode_line(trimmed).and_then(|c| c.json_line())
101 };
102 if let Ok(encoded) = encoded {
103 output
104 .write_all(encoded.as_bytes())
105 .and_then(|()| output.write_all(b"\n"))
106 .and_then(|()| output.flush())
107 .map_err(|e| AppError::internal("receive_output", e.to_string()))?;
108 received += 1;
109 if max_commands.is_some_and(|max| received >= max) {
110 return Ok(SessionEnd::Complete);
111 }
112 continue;
113 }
114
115 if let Ok(frame) = serde_json::from_str::<ControlFrame>(trimmed) {
116 match frame {
117 ControlFrame::Error { .. } => return Ok(SessionEnd::Rejected),
118 ControlFrame::Ping { .. } => {
119 daemon_writer
121 .write_all(br#"{"v":1,"op":"pong"}"#)
122 .and_then(|()| daemon_writer.write_all(b"\n"))
123 .and_then(|()| daemon_writer.flush())
124 .map_err(|e| AppError::internal("receive_pong", e.to_string()))?;
125 }
126 _ => {}
127 }
128 }
129 }
130 Ok(SessionEnd::Closed)
131}
132
133#[derive(Debug, Clone)]
135pub struct ReceiveOptions {
136 pub token: String,
138 pub max_commands: Option<usize>,
140 pub reconnect: bool,
142 pub backoff: Duration,
144 pub max_attempts: Option<usize>,
146}
147
148pub fn receive_from_daemon<O: Write>(
157 daemon_addr: &str,
158 output: &mut O,
159 opts: &ReceiveOptions,
160) -> Result<(), AppError> {
161 receive_from_daemon_kind(daemon_addr, output, opts, false)
162}
163
164pub fn receive_events_from_daemon<O: Write>(
167 daemon_addr: &str,
168 output: &mut O,
169 opts: &ReceiveOptions,
170) -> Result<(), AppError> {
171 receive_from_daemon_kind(daemon_addr, output, opts, true)
172}
173
174fn receive_from_daemon_kind<O: Write>(
175 daemon_addr: &str,
176 output: &mut O,
177 opts: &ReceiveOptions,
178 events: bool,
179) -> Result<(), AppError> {
180 let mut attempts = 0usize;
181 loop {
182 attempts += 1;
183 let result = match connect_bounded(daemon_addr, CONNECT_TIMEOUT) {
184 Ok(stream) => {
185 let reader = stream
186 .try_clone()
187 .map(BufReader::new)
188 .map_err(|e| AppError::internal("receive_clone", e.to_string()))?;
189 receive_session_kind(
190 reader,
191 stream,
192 &mut *output,
193 &opts.token,
194 opts.max_commands,
195 events,
196 )
197 }
198 Err(e) => Err(AppError::internal(
199 "receive_connect",
200 format!("{daemon_addr}: {e}"),
201 )),
202 };
203
204 match result {
205 Ok(SessionEnd::Complete) => return Ok(()),
206 Ok(SessionEnd::Rejected) => {
207 return Err(AppError::validation(
208 "receive_auth",
209 "daemon rejected subscription; check the token",
210 ));
211 }
212 Ok(SessionEnd::Closed) => {
213 if !opts.reconnect {
214 return Err(AppError::internal(
215 "receive_closed",
216 "daemon closed the connection",
217 ));
218 }
219 }
220 Err(error) => {
221 if error.code() == "receive_output" || !opts.reconnect {
223 return Err(error);
224 }
225 eprintln!("omni receive: {error}; reconnecting");
226 }
227 }
228
229 if events {
230 let mut reset = crate::events::SemanticEvent::new(
231 "omni_stream_reset",
232 crate::events::Phase::Cancel,
233 )?;
234 reset.data = Some(serde_json::json!({"reason":"stream_discontinuity"}));
235 let line = reset.json_line()?;
236 output
237 .write_all(line.as_bytes())
238 .and_then(|()| output.write_all(b"\n"))
239 .and_then(|()| output.flush())
240 .map_err(|e| AppError::internal("receive_output", e.to_string()))?;
241 }
242 if opts.max_attempts.is_some_and(|max| attempts >= max) {
243 return Err(AppError::internal(
244 "receive_attempts",
245 format!("daemon receive stopped after {attempts} attempt(s)"),
246 ));
247 }
248 thread::sleep(opts.backoff);
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use crate::daemon::{CaptureInjector, CommandBus, DaemonOptions, run_daemon_with_bus};
256 use crate::transport::relay::{bind, cast_via_relay};
257 use std::io::Cursor;
258 use std::sync::{Arc, Mutex};
259
260 #[test]
261 fn session_subscribes_and_emits_only_commands() {
262 let input = concat!(
263 "{\"v\":1,\"op\":\"ping\"}\n",
264 "{\"v\":1,\"type\":\"key\",\"key\":\"enter\"}\n",
265 "{\"v\":1,\"type\":\"scroll\",\"amount\":3}\n",
266 );
267 let mut daemon_out = Vec::new();
268 let mut stdout = Vec::new();
269 let end = receive_session(
270 Cursor::new(input),
271 &mut daemon_out,
272 &mut stdout,
273 "tok",
274 Some(2),
275 )
276 .unwrap();
277
278 assert_eq!(end, SessionEnd::Complete);
279 let daemon_text = String::from_utf8(daemon_out).unwrap();
280 assert!(daemon_text.contains("\"op\":\"subscribe\""));
281 assert!(daemon_text.contains("\"token\":\"tok\""));
282 assert!(daemon_text.contains("\"op\":\"pong\""));
283 assert_eq!(
284 String::from_utf8(stdout).unwrap(),
285 concat!(
286 "{\"v\":1,\"type\":\"key\",\"key\":\"enter\"}\n",
287 "{\"v\":1,\"type\":\"scroll\",\"amount\":3}\n",
288 )
289 );
290 }
291
292 #[test]
293 fn session_reports_rejection_without_polluting_stdout() {
294 let input = "{\"op\":\"error\",\"v\":1,\"reason\":\"unauthorized\"}\n";
295 let mut daemon_out = Vec::new();
296 let mut stdout = Vec::new();
297 let end = receive_session(
298 Cursor::new(input),
299 &mut daemon_out,
300 &mut stdout,
301 "bad",
302 None,
303 )
304 .unwrap();
305 assert_eq!(end, SessionEnd::Rejected);
306 assert!(stdout.is_empty());
307 }
308
309 #[test]
310 fn zero_limit_completes_without_registering() {
311 let mut daemon_out = Vec::new();
312 let mut stdout = Vec::new();
313 let end = receive_session(
314 Cursor::new(Vec::<u8>::new()),
315 &mut daemon_out,
316 &mut stdout,
317 "tok",
318 Some(0),
319 )
320 .unwrap();
321 assert_eq!(end, SessionEnd::Complete);
322 assert!(daemon_out.is_empty());
323 }
324
325 #[derive(Clone, Default)]
326 struct SharedWriter(Arc<Mutex<Vec<u8>>>);
327
328 impl Write for SharedWriter {
329 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
330 self.0.lock().unwrap().extend_from_slice(bytes);
331 Ok(bytes.len())
332 }
333
334 fn flush(&mut self) -> std::io::Result<()> {
335 Ok(())
336 }
337 }
338
339 #[test]
340 fn daemon_cast_reaches_local_subscriber_end_to_end() {
341 let listener = bind("127.0.0.1:0").unwrap();
342 let addr = listener.local_addr().unwrap().to_string();
343 let bus = CommandBus::new();
344 let server_bus = bus.clone();
345 let injector = Arc::new(CaptureInjector::new());
346 let server = thread::spawn(move || {
347 run_daemon_with_bus(
348 &listener,
349 injector,
350 &DaemonOptions {
351 token: Some("tok".to_owned()),
352 max_conns: Some(2),
353 },
354 &server_bus,
355 )
356 .unwrap();
357 });
358
359 let output = SharedWriter::default();
360 let captured = output.0.clone();
361 let receive_addr = addr.clone();
362 let receiver = thread::spawn(move || {
363 let mut output = output;
364 receive_from_daemon(
365 &receive_addr,
366 &mut output,
367 &ReceiveOptions {
368 token: String::new(),
371 max_commands: Some(1),
372 reconnect: true,
373 backoff: Duration::from_millis(5),
374 max_attempts: Some(5),
375 },
376 )
377 .unwrap();
378 });
379
380 for _ in 0..100 {
381 if bus.subscriber_count() == 1 {
382 break;
383 }
384 thread::sleep(Duration::from_millis(5));
385 }
386 assert_eq!(bus.subscriber_count(), 1);
387 let delivery =
388 cast_via_relay(&addr, "tok", "local", &[InjectionCommand::key("enter")]).unwrap();
389 assert!(
390 !delivery.acked,
391 "a daemon does not send relay delivery acks"
392 );
393
394 receiver.join().unwrap();
395 server.join().unwrap();
396 assert_eq!(
397 String::from_utf8(captured.lock().unwrap().clone()).unwrap(),
398 "{\"v\":1,\"type\":\"key\",\"key\":\"enter\"}\n"
399 );
400 }
401}