1use std::collections::HashMap;
25use std::io::{BufRead, BufReader, Write};
26use std::net::{Shutdown, TcpListener, TcpStream, ToSocketAddrs};
27use std::sync::mpsc::{self, Receiver, Sender};
28use std::sync::{Arc, Mutex};
29use std::thread;
30use std::time::Duration;
31
32use socket2::{SockRef, TcpKeepalive};
33
34use crate::AppError;
35use crate::events::SemanticEvent;
36use crate::wire::{InjectionCommand, WIRE_VERSION};
37
38pub use omni_sdk::transport::relay::{ControlFrame, DEVICE_ALL, TOKEN_ENV};
39
40const KEEPALIVE_IDLE: Duration = Duration::from_secs(20);
43
44const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(5);
46
47fn unauthorized_line() -> String {
51 ControlFrame::Error {
52 v: WIRE_VERSION,
53 reason: "unauthorized".to_owned(),
54 }
55 .json_line()
56 .unwrap_or_else(|_| r#"{"op":"error","v":1,"reason":"unauthorized"}"#.to_owned())
57}
58
59fn result_line(device: &str, delivered: usize) -> String {
63 ControlFrame::Result {
64 v: WIRE_VERSION,
65 device: device.to_owned(),
66 delivered,
67 }
68 .json_line()
69 .unwrap_or_else(|_| format!(r#"{{"op":"result","v":1,"device":"","delivered":{delivered}}}"#))
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
77enum DeviceOut {
78 Cmd(InjectionCommand),
80 Event(SemanticEvent),
81 Pong,
83}
84
85#[derive(Clone, Default)]
88pub struct Registry {
89 inner: Arc<Mutex<HashMap<String, RegisteredDevice>>>,
90}
91
92struct RegisteredDevice {
93 sender: Sender<DeviceOut>,
94 events: bool,
95}
96
97impl Registry {
98 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 fn register(&self, device: &str, tx: Sender<DeviceOut>) {
104 self.register_capable(device, tx, false);
105 }
106
107 fn register_capable(&self, device: &str, tx: Sender<DeviceOut>, events: bool) {
108 self.inner
109 .lock()
110 .unwrap()
111 .insert(device.to_owned(), RegisteredDevice { sender: tx, events });
112 }
113
114 pub fn route_event(&self, device: &str, event: &SemanticEvent) -> usize {
116 if event.validate().is_err() {
117 return 0;
118 }
119 self.inner
120 .lock()
121 .unwrap()
122 .iter()
123 .filter(|(id, peer)| peer.events && (device == DEVICE_ALL || id.as_str() == device))
124 .filter(|(_, peer)| peer.sender.send(DeviceOut::Event(event.clone())).is_ok())
125 .count()
126 }
127
128 fn unregister(&self, device: &str) {
129 self.inner.lock().unwrap().remove(device);
130 }
131
132 #[must_use]
134 pub fn is_registered(&self, device: &str) -> bool {
135 self.inner.lock().unwrap().contains_key(device)
136 }
137
138 #[must_use]
142 pub fn device_ids(&self) -> Vec<String> {
143 let mut ids: Vec<String> = self.inner.lock().unwrap().keys().cloned().collect();
144 ids.sort();
145 ids
146 }
147
148 #[must_use]
150 pub fn device_count(&self) -> usize {
151 self.inner.lock().unwrap().len()
152 }
153
154 pub fn route(&self, device: &str, cmd: &InjectionCommand) -> usize {
157 let map = self.inner.lock().unwrap();
158 if device == DEVICE_ALL {
159 let mut n = 0;
160 for tx in map.values() {
161 if tx.sender.send(DeviceOut::Cmd(cmd.clone())).is_ok() {
162 n += 1;
163 }
164 }
165 n
166 } else if let Some(tx) = map.get(device) {
167 usize::from(tx.sender.send(DeviceOut::Cmd(cmd.clone())).is_ok())
168 } else {
169 0
170 }
171 }
172}
173
174#[derive(Debug, Clone)]
176pub struct ServeOptions {
177 pub token: String,
179 pub max_conns: Option<usize>,
181}
182
183pub fn bind(addr: &str) -> Result<TcpListener, AppError> {
188 TcpListener::bind(addr).map_err(|e| AppError::internal("relay_bind", format!("{addr}: {e}")))
189}
190
191pub fn serve(listener: &TcpListener, opts: &ServeOptions) -> Result<(), AppError> {
197 serve_with_registry(listener, opts, &Registry::new())
198}
199
200pub fn serve_with_registry(
207 listener: &TcpListener,
208 opts: &ServeOptions,
209 registry: &Registry,
210) -> Result<(), AppError> {
211 let mut accepted = 0usize;
212 for incoming in listener.incoming() {
213 let stream = incoming.map_err(|e| AppError::internal("relay_accept", e.to_string()))?;
214 let registry = registry.clone();
215 let token = opts.token.clone();
216 thread::spawn(move || {
217 if let Err(error) = handle_conn(stream, ®istry, &token) {
218 eprintln!("omni serve: connection error: {error}");
219 }
220 });
221 accepted += 1;
222 if let Some(max) = opts.max_conns {
223 if accepted >= max {
224 break;
225 }
226 }
227 }
228 Ok(())
229}
230
231fn write_line<W: Write>(stream: &mut W, line: &str) -> std::io::Result<()> {
232 stream.write_all(line.as_bytes())?;
233 stream.write_all(b"\n")?;
234 stream.flush()
235}
236
237fn enable_device_keepalive(stream: &TcpStream) {
244 let keepalive = TcpKeepalive::new()
245 .with_time(KEEPALIVE_IDLE)
246 .with_interval(KEEPALIVE_INTERVAL);
247 let _ = SockRef::from(stream).set_tcp_keepalive(&keepalive);
248}
249
250fn run_device_writer<W: Write>(
259 rx: &Receiver<DeviceOut>,
260 writer: &mut W,
261 registry: &Registry,
262 device: &str,
263 on_dead: impl FnOnce(),
264) {
265 while let Ok(out) = rx.recv() {
266 let line = match out {
267 DeviceOut::Cmd(cmd) => match cmd.json_line() {
268 Ok(line) => line,
269 Err(_) => continue,
270 },
271 DeviceOut::Event(event) => match event.json_line() {
272 Ok(line) => line,
273 Err(_) => continue,
274 },
275 DeviceOut::Pong => r#"{"v":1,"op":"pong"}"#.to_owned(),
276 };
277 if write_line(writer, &line).is_err() {
278 registry.unregister(device);
279 on_dead();
280 return;
281 }
282 }
283}
284
285fn handle_conn(stream: TcpStream, registry: &Registry, token: &str) -> Result<(), AppError> {
286 let mut reader = BufReader::new(
287 stream
288 .try_clone()
289 .map_err(|e| AppError::internal("relay_clone", e.to_string()))?,
290 );
291 let mut first = String::new();
292 let n = reader
293 .read_line(&mut first)
294 .map_err(|e| AppError::internal("relay_read", e.to_string()))?;
295 if n == 0 {
296 return Ok(()); }
298 let frame: ControlFrame = serde_json::from_str(first.trim())
299 .map_err(|e| AppError::validation("relay_frame", e.to_string()))?;
300
301 match frame {
302 ControlFrame::Register {
303 device, token: t, ..
304 } => {
305 if t.trim() != token.trim() {
306 let mut s = stream;
307 let _ = write_line(&mut s, &unauthorized_line());
308 return Ok(());
309 }
310 let events = serde_json::from_str::<serde_json::Value>(first.trim())
312 .ok()
313 .is_some_and(|v| v["events"] == true);
314 serve_device(stream, reader, registry, &device, events)
315 }
316 ControlFrame::Cast {
317 device,
318 cmd,
319 token: t,
320 ..
321 } => {
322 if t.trim() != token.trim() {
323 let mut s = stream;
324 let _ = write_line(&mut s, &unauthorized_line());
325 return Ok(());
326 }
327 let delivered = registry.route(&device, &cmd);
328 if let Ok(mut ack) = stream.try_clone() {
334 let _ = write_line(&mut ack, &result_line(&device, delivered));
335 }
336 serve_sender(stream, reader, registry, token)
338 }
339 frame @ ControlFrame::EventCast { .. } => {
340 serve_event_sender(stream, reader, registry, token, frame)
341 }
342 ControlFrame::Subscribe { .. } | ControlFrame::SubscribeEvents { .. } => Ok(()),
344 ControlFrame::Ping { .. } => {
345 let mut s = stream;
346 let _ = write_line(&mut s, r#"{"v":1,"op":"pong"}"#);
347 Ok(())
348 }
349 ControlFrame::Pong { .. } => Ok(()),
350 ControlFrame::List { token: t, .. } => {
351 let mut s = stream;
352 if t.trim() != token.trim() {
353 let _ = write_line(&mut s, &unauthorized_line());
354 return Ok(());
355 }
356 let frame = ControlFrame::Devices {
357 v: WIRE_VERSION,
358 devices: registry.device_ids(),
359 };
360 let _ = write_line(&mut s, &frame.json_line().unwrap_or_default());
361 Ok(())
362 }
363 ControlFrame::Error { .. }
366 | ControlFrame::Result { .. }
367 | ControlFrame::EventResult { .. }
368 | ControlFrame::EventsSubscribed { .. }
369 | ControlFrame::Devices { .. } => Ok(()),
370 }
371}
372
373fn serve_device(
382 stream: TcpStream,
383 mut reader: BufReader<TcpStream>,
384 registry: &Registry,
385 device: &str,
386 events: bool,
387) -> Result<(), AppError> {
388 enable_device_keepalive(&stream);
389
390 let (tx, rx) = mpsc::channel::<DeviceOut>();
391 if events {
392 registry.register_capable(device, tx.clone(), true);
393 } else {
394 registry.register(device, tx.clone());
395 }
396 let pong_tx = tx;
400
401 let mut writer_stream = stream
402 .try_clone()
403 .map_err(|e| AppError::internal("relay_clone", e.to_string()))?;
404 let shutdown_stream = stream
405 .try_clone()
406 .map_err(|e| AppError::internal("relay_clone", e.to_string()))?;
407 let writer_registry = registry.clone();
408 let writer_device = device.to_owned();
409 let writer = thread::spawn(move || {
410 run_device_writer(
411 &rx,
412 &mut writer_stream,
413 &writer_registry,
414 &writer_device,
415 || {
416 let _ = shutdown_stream.shutdown(Shutdown::Both);
419 },
420 );
421 });
422
423 let mut line = String::new();
424 loop {
425 line.clear();
426 match reader.read_line(&mut line) {
427 Ok(0) | Err(_) => break, Ok(_) => {
429 if let Ok(ControlFrame::Ping { .. }) =
430 serde_json::from_str::<ControlFrame>(line.trim())
431 {
432 let _ = pong_tx.send(DeviceOut::Pong);
436 }
437 }
438 }
439 }
440
441 drop(pong_tx); registry.unregister(device); let _ = writer.join();
444 Ok(())
445}
446
447fn serve_event_sender(
450 mut writer: TcpStream,
451 mut reader: BufReader<TcpStream>,
452 registry: &Registry,
453 token: &str,
454 first: ControlFrame,
455) -> Result<(), AppError> {
456 let mut frame = first;
457 loop {
458 let (reply, close) = match frame {
459 ControlFrame::EventCast {
460 v,
461 device,
462 event,
463 token: got,
464 } => {
465 if got.trim() != token.trim() {
466 (unauthorized_line(), true)
467 } else if v != WIRE_VERSION || event.validate().is_err() {
468 (
469 ControlFrame::Error {
470 v: WIRE_VERSION,
471 reason: "invalid_event".into(),
472 }
473 .json_line()?,
474 true,
475 )
476 } else {
477 let delivered = registry.route_event(&device, &event);
478 (
479 ControlFrame::EventResult {
480 v: WIRE_VERSION,
481 device,
482 delivered,
483 }
484 .json_line()?,
485 false,
486 )
487 }
488 }
489 ControlFrame::Ping { .. } => {
490 (ControlFrame::Pong { v: WIRE_VERSION }.json_line()?, false)
491 }
492 _ => (
493 ControlFrame::Error {
494 v: WIRE_VERSION,
495 reason: "expected_event_frame".into(),
496 }
497 .json_line()?,
498 true,
499 ),
500 };
501 write_line(&mut writer, &reply)
502 .map_err(|e| AppError::internal("event_relay_write", e.to_string()))?;
503 if close {
504 return Ok(());
505 }
506 let mut line = String::new();
507 let n = std::io::Read::take(&mut reader, (crate::events::MAX_EVENT_BYTES + 4097) as u64)
508 .read_line(&mut line)
509 .map_err(|e| AppError::internal("event_relay_read", e.to_string()))?;
510 if n == 0 {
511 return Ok(());
512 }
513 if n > crate::events::MAX_EVENT_BYTES + 4096 {
514 return Err(AppError::validation("event_size", "event frame too large"));
515 }
516 frame = serde_json::from_str(line.trim())
517 .map_err(|e| AppError::validation("event_frame", e.to_string()))?;
518 }
519}
520
521fn serve_sender(
524 stream: TcpStream,
525 mut reader: BufReader<TcpStream>,
526 registry: &Registry,
527 token: &str,
528) -> Result<(), AppError> {
529 let mut pong_stream = stream;
530 let mut line = String::new();
531 loop {
532 line.clear();
533 match reader.read_line(&mut line) {
534 Ok(0) | Err(_) => break,
535 Ok(_) => match serde_json::from_str::<ControlFrame>(line.trim()) {
536 Ok(ControlFrame::Cast {
537 device,
538 cmd,
539 token: t,
540 ..
541 }) if t.trim() == token.trim() => {
542 registry.route(&device, &cmd);
543 }
544 Ok(ControlFrame::Ping { .. }) => {
545 let _ = write_line(&mut pong_stream, r#"{"v":1,"op":"pong"}"#);
546 }
547 _ => {}
548 },
549 }
550 }
551 Ok(())
552}
553
554const RESULT_READ_TIMEOUT: Duration = Duration::from_millis(800);
557
558#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub struct RelayDelivery {
562 pub acked: bool,
566 pub delivered: usize,
569}
570
571fn read_delivery(stream: &mut TcpStream) -> RelayDelivery {
575 let _ = stream.set_read_timeout(Some(RESULT_READ_TIMEOUT));
576 let mut reader = BufReader::new(stream);
577 let mut line = String::new();
578 match reader.read_line(&mut line) {
579 Ok(n) if n > 0 => match serde_json::from_str::<ControlFrame>(line.trim()) {
580 Ok(ControlFrame::Result { delivered, .. }) => RelayDelivery {
581 acked: true,
582 delivered,
583 },
584 _ => RelayDelivery {
585 acked: false,
586 delivered: 0,
587 },
588 },
589 _ => RelayDelivery {
590 acked: false,
591 delivered: 0,
592 },
593 }
594}
595
596pub fn cast_via_relay<A: ToSocketAddrs>(
604 addr: A,
605 token: &str,
606 device: &str,
607 cmds: &[InjectionCommand],
608) -> Result<RelayDelivery, AppError> {
609 let mut stream = super::connect_bounded(addr, super::CONNECT_TIMEOUT)
610 .map_err(|e| AppError::internal("relay_connect", e.to_string()))?;
611 for cmd in cmds {
612 let frame = ControlFrame::Cast {
613 v: WIRE_VERSION,
614 device: device.to_owned(),
615 cmd: cmd.clone(),
616 token: token.to_owned(),
617 };
618 let line = frame.json_line()?;
619 write_line(&mut stream, &line)
620 .map_err(|e| AppError::internal("relay_send", e.to_string()))?;
621 }
622 Ok(read_delivery(&mut stream))
623}
624
625pub fn list_relay_devices<A: ToSocketAddrs>(addr: A, token: &str) -> Result<Vec<String>, AppError> {
635 let mut stream = super::connect_bounded(addr, super::CONNECT_TIMEOUT)
636 .map_err(|e| AppError::internal("relay_connect", e.to_string()))?;
637 let frame = ControlFrame::List {
638 v: WIRE_VERSION,
639 token: token.to_owned(),
640 };
641 write_line(&mut stream, &frame.json_line()?)
642 .map_err(|e| AppError::internal("relay_send", e.to_string()))?;
643 let _ = stream.set_read_timeout(Some(RESULT_READ_TIMEOUT));
644 let mut reader = BufReader::new(stream);
645 let mut line = String::new();
646 match reader.read_line(&mut line) {
647 Ok(n) if n > 0 => match serde_json::from_str::<ControlFrame>(line.trim()) {
648 Ok(ControlFrame::Devices { devices, .. }) => Ok(devices),
649 Ok(ControlFrame::Error { reason, .. }) => {
650 Err(AppError::validation("relay_list", reason))
651 }
652 _ => Ok(Vec::new()),
653 },
654 _ => Ok(Vec::new()),
655 }
656}
657
658pub struct RelaySender {
664 addr: String,
665 device: String,
666 token: String,
667 stream: Option<TcpStream>,
668}
669
670impl RelaySender {
671 #[must_use]
674 pub fn new(
675 addr: impl Into<String>,
676 device: impl Into<String>,
677 token: impl Into<String>,
678 ) -> Self {
679 Self {
680 addr: addr.into(),
681 device: device.into(),
682 token: token.into(),
683 stream: None,
684 }
685 }
686
687 pub fn cast_one(&mut self, cmd: &InjectionCommand) -> Result<(), AppError> {
695 if self.stream.is_none() {
696 let s = super::connect_bounded(&self.addr, super::CONNECT_TIMEOUT)
697 .map_err(|e| AppError::internal("relay_connect", format!("{}: {e}", self.addr)))?;
698 self.stream = Some(s);
699 }
700 let frame = ControlFrame::Cast {
701 v: WIRE_VERSION,
702 device: self.device.clone(),
703 cmd: cmd.clone(),
704 token: self.token.clone(),
705 };
706 let line = frame.json_line()?;
707 let stream = self.stream.as_mut().expect("stream just set");
708 if let Err(e) = write_line(stream, &line) {
709 self.stream = None; return Err(AppError::internal(
711 "relay_send",
712 format!("{}: {e}", self.addr),
713 ));
714 }
715 Ok(())
716 }
717}
718
719pub fn resolve_token(flag: Option<&str>) -> Result<String, AppError> {
724 resolve_token_with(flag, std::env::var(TOKEN_ENV).ok())
725}
726
727pub fn resolve_token_with(
733 flag: Option<&str>,
734 env_value: Option<String>,
735) -> Result<String, AppError> {
736 if let Some(t) = flag {
741 let t = t.trim();
742 if !t.is_empty() {
743 return Ok(t.to_owned());
744 }
745 }
746 match env_value {
747 Some(t) if !t.trim().is_empty() => Ok(t.trim().to_owned()),
748 _ => Err(AppError::validation(
749 "relay_token",
750 format!("a shared token is required; pass --token or set {TOKEN_ENV}"),
751 )),
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use std::time::Duration;
759
760 #[test]
761 fn frames_round_trip() {
762 let reg = ControlFrame::Register {
763 v: 1,
764 device: "harry-watch".into(),
765 platform: "watchos".into(),
766 token: "secret".into(),
767 };
768 let line = reg.json_line().unwrap();
769 assert_eq!(
770 line,
771 r#"{"op":"register","v":1,"device":"harry-watch","platform":"watchos","token":"secret"}"#
772 );
773 assert_eq!(serde_json::from_str::<ControlFrame>(&line).unwrap(), reg);
774
775 let cast = ControlFrame::Cast {
776 v: 1,
777 device: "all".into(),
778 cmd: InjectionCommand::text("hi"),
779 token: "secret".into(),
780 };
781 let line = cast.json_line().unwrap();
782 assert_eq!(serde_json::from_str::<ControlFrame>(&line).unwrap(), cast);
783
784 let err = ControlFrame::Error {
785 v: 1,
786 reason: "unauthorized".into(),
787 };
788 let line = err.json_line().unwrap();
789 assert_eq!(line, r#"{"op":"error","v":1,"reason":"unauthorized"}"#);
790 assert_eq!(serde_json::from_str::<ControlFrame>(&line).unwrap(), err);
791 }
792
793 #[test]
794 fn registry_routes_to_device_and_all() {
795 let reg = Registry::new();
796 let (tx, rx) = mpsc::channel();
797 reg.register("dev1", tx);
798 assert_eq!(reg.route("dev1", &InjectionCommand::key("enter")), 1);
799 assert_eq!(
800 rx.recv_timeout(Duration::from_secs(1)).unwrap(),
801 DeviceOut::Cmd(InjectionCommand::key("enter"))
802 );
803 assert_eq!(reg.route("nope", &InjectionCommand::text("x")), 0);
805 assert_eq!(reg.route(DEVICE_ALL, &InjectionCommand::scroll(2)), 1);
807 assert_eq!(
808 rx.recv_timeout(Duration::from_secs(1)).unwrap(),
809 DeviceOut::Cmd(InjectionCommand::scroll(2))
810 );
811 }
812
813 #[test]
814 fn registry_is_registered_and_count_track_membership() {
815 let reg = Registry::new();
816 assert_eq!(reg.device_count(), 0);
817 assert!(!reg.is_registered("dev1"));
818 let (tx, _rx) = mpsc::channel();
819 reg.register("dev1", tx);
820 assert!(reg.is_registered("dev1"));
821 assert_eq!(reg.device_count(), 1);
822 reg.unregister("dev1");
823 assert!(!reg.is_registered("dev1"));
824 assert_eq!(reg.device_count(), 0);
825 }
826
827 struct DeadWriter;
830 impl Write for DeadWriter {
831 fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
832 Err(std::io::Error::new(
833 std::io::ErrorKind::BrokenPipe,
834 "dead socket",
835 ))
836 }
837 fn flush(&mut self) -> std::io::Result<()> {
838 Ok(())
839 }
840 }
841
842 #[test]
843 fn dead_device_writer_evicts_registration_on_failed_cast() {
844 let registry = Registry::new();
848 let (tx, rx) = mpsc::channel::<DeviceOut>();
849 registry.register("harry-watch", tx.clone());
850 assert!(registry.is_registered("harry-watch"));
851
852 tx.send(DeviceOut::Cmd(InjectionCommand::text("lost")))
854 .unwrap();
855 drop(tx);
856
857 let dead_signalled = std::cell::Cell::new(false);
858 run_device_writer(&rx, &mut DeadWriter, ®istry, "harry-watch", || {
859 dead_signalled.set(true);
860 });
861
862 assert!(
863 !registry.is_registered("harry-watch"),
864 "a dead device must be evicted on the failed write"
865 );
866 assert!(
867 dead_signalled.get(),
868 "on_dead must fire so the reader socket is shut down"
869 );
870 assert_eq!(
873 registry.route("harry-watch", &InjectionCommand::text("after")),
874 0
875 );
876 }
877
878 #[test]
879 fn healthy_device_writer_delivers_without_eviction() {
880 let registry = Registry::new();
884 let (tx, rx) = mpsc::channel::<DeviceOut>();
885 tx.send(DeviceOut::Cmd(InjectionCommand::text("a")))
886 .unwrap();
887 tx.send(DeviceOut::Cmd(InjectionCommand::key("enter")))
888 .unwrap();
889 drop(tx); let (keep_tx, _keep_rx) = mpsc::channel();
894 registry.register("dev", keep_tx);
895
896 let mut buf: Vec<u8> = Vec::new();
897 let mut dead_signalled = false;
898 run_device_writer(&rx, &mut buf, ®istry, "dev", || dead_signalled = true);
899
900 assert!(
901 !dead_signalled,
902 "a healthy channel close must not signal death"
903 );
904 assert!(
905 registry.is_registered("dev"),
906 "channel-close (normal disconnect) does not evict via the writer path"
907 );
908 let text = String::from_utf8(buf).unwrap();
909 assert_eq!(text.lines().count(), 2, "both commands delivered as NDJSON");
910 }
911
912 #[test]
913 fn device_writer_serializes_pongs_and_commands_on_one_writer() {
914 let registry = Registry::new();
918 let (tx, rx) = mpsc::channel::<DeviceOut>();
919 tx.send(DeviceOut::Cmd(InjectionCommand::text("hi")))
920 .unwrap();
921 tx.send(DeviceOut::Pong).unwrap();
922 tx.send(DeviceOut::Cmd(InjectionCommand::key("enter")))
923 .unwrap();
924 drop(tx); let mut buf: Vec<u8> = Vec::new();
927 let mut dead_signalled = false;
928 run_device_writer(&rx, &mut buf, ®istry, "dev", || dead_signalled = true);
929 assert!(!dead_signalled, "no write failed, so no eviction");
930
931 let lines: Vec<&str> = std::str::from_utf8(&buf).unwrap().lines().collect();
932 assert_eq!(lines.len(), 3, "three whole NDJSON lines, none spliced");
933 assert_eq!(
934 InjectionCommand::decode_line(lines[0]).unwrap(),
935 InjectionCommand::text("hi")
936 );
937 assert_eq!(lines[1], r#"{"v":1,"op":"pong"}"#);
938 assert_eq!(
939 InjectionCommand::decode_line(lines[2]).unwrap(),
940 InjectionCommand::key("enter")
941 );
942 }
943
944 #[test]
945 fn end_to_end_dead_device_is_reaped_from_registry() {
946 let listener = bind("127.0.0.1:0").unwrap();
949 let addr = listener.local_addr().unwrap();
950 let opts = ServeOptions {
951 token: "tok".into(),
952 max_conns: Some(1),
953 };
954 let registry = Registry::new();
955 let serve_registry = registry.clone();
956 thread::spawn(move || {
957 let _ = serve_with_registry(&listener, &opts, &serve_registry);
958 });
959
960 let dev = TcpStream::connect(addr).unwrap();
961 let mut dev_w = dev.try_clone().unwrap();
962 let reg = ControlFrame::Register {
963 v: 1,
964 device: "harry-watch".into(),
965 platform: "watchos".into(),
966 token: "tok".into(),
967 };
968 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
969
970 assert!(
972 wait_until(|| registry.is_registered("harry-watch"), 2000),
973 "device should register"
974 );
975
976 drop(dev_w);
978 drop(dev);
979 assert!(
980 wait_until(|| !registry.is_registered("harry-watch"), 2000),
981 "a disconnected device should be reaped from the registry"
982 );
983 }
984
985 fn wait_until(mut cond: impl FnMut() -> bool, timeout_ms: u64) -> bool {
987 let start = std::time::Instant::now();
988 while start.elapsed() < Duration::from_millis(timeout_ms) {
989 if cond() {
990 return true;
991 }
992 thread::sleep(Duration::from_millis(10));
993 }
994 cond()
995 }
996
997 #[test]
998 fn resolve_token_prefers_flag_then_env() {
999 assert_eq!(
1000 resolve_token_with(Some("flagtok"), None).unwrap(),
1001 "flagtok"
1002 );
1003 assert_eq!(
1004 resolve_token_with(None, Some("envtok".to_owned())).unwrap(),
1005 "envtok"
1006 );
1007 assert_eq!(
1009 resolve_token_with(Some(""), None).unwrap_err().code(),
1010 "relay_token"
1011 );
1012 assert_eq!(
1013 resolve_token_with(None, Some(String::new()))
1014 .unwrap_err()
1015 .code(),
1016 "relay_token"
1017 );
1018 }
1019
1020 #[test]
1021 fn resolve_token_trims_surrounding_whitespace() {
1022 assert_eq!(
1025 resolve_token_with(Some("flagtok\n"), None).unwrap(),
1026 "flagtok"
1027 );
1028 assert_eq!(
1029 resolve_token_with(None, Some(" envtok \n".to_owned())).unwrap(),
1030 "envtok"
1031 );
1032 assert_eq!(
1034 resolve_token_with(Some(" \n"), None).unwrap_err().code(),
1035 "relay_token"
1036 );
1037 }
1038
1039 #[test]
1040 fn end_to_end_trailing_newline_token_authenticates() {
1041 let listener = bind("127.0.0.1:0").unwrap();
1045 let addr = listener.local_addr().unwrap();
1046 let opts = ServeOptions {
1047 token: "tok".into(),
1048 max_conns: Some(2),
1049 };
1050 thread::spawn(move || {
1051 let _ = serve(&listener, &opts);
1052 });
1053
1054 let dev = TcpStream::connect(addr).unwrap();
1055 let mut dev_w = dev.try_clone().unwrap();
1056 let mut dev_r = BufReader::new(dev);
1057 let reg = ControlFrame::Register {
1058 v: 1,
1059 device: "harry-watch".into(),
1060 platform: "watchos".into(),
1061 token: "tok\n".into(),
1062 };
1063 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
1064 thread::sleep(Duration::from_millis(100));
1065
1066 cast_via_relay(
1067 addr,
1068 "tok\n",
1069 "harry-watch",
1070 &[InjectionCommand::text("hi")],
1071 )
1072 .unwrap();
1073
1074 let mut got = String::new();
1075 dev_r.read_line(&mut got).unwrap();
1076 let cmd = InjectionCommand::decode_line(got.trim()).unwrap();
1077 assert_eq!(cmd, InjectionCommand::text("hi"));
1078 }
1079
1080 #[test]
1081 fn end_to_end_device_receives_cast() {
1082 let listener = bind("127.0.0.1:0").unwrap();
1083 let addr = listener.local_addr().unwrap();
1084 let opts = ServeOptions {
1085 token: "tok".into(),
1086 max_conns: Some(2),
1087 };
1088 thread::spawn(move || {
1089 let _ = serve(&listener, &opts);
1090 });
1091
1092 let dev = TcpStream::connect(addr).unwrap();
1094 let mut dev_w = dev.try_clone().unwrap();
1095 let mut dev_r = BufReader::new(dev);
1096 let reg = ControlFrame::Register {
1097 v: 1,
1098 device: "harry-watch".into(),
1099 platform: "watchos".into(),
1100 token: "tok".into(),
1101 };
1102 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
1103
1104 thread::sleep(Duration::from_millis(100));
1106
1107 cast_via_relay(
1109 addr,
1110 "tok",
1111 "harry-watch",
1112 &[InjectionCommand::text("hello watch")],
1113 )
1114 .unwrap();
1115
1116 let mut got = String::new();
1118 dev_r.read_line(&mut got).unwrap();
1119 let cmd = InjectionCommand::decode_line(got.trim()).unwrap();
1120 assert_eq!(cmd, InjectionCommand::text("hello watch"));
1121 }
1122
1123 #[test]
1124 fn end_to_end_unauthorized_register_is_rejected() {
1125 let listener = bind("127.0.0.1:0").unwrap();
1126 let addr = listener.local_addr().unwrap();
1127 let opts = ServeOptions {
1128 token: "right".into(),
1129 max_conns: Some(1),
1130 };
1131 thread::spawn(move || {
1132 let _ = serve(&listener, &opts);
1133 });
1134
1135 let dev = TcpStream::connect(addr).unwrap();
1136 let mut dev_w = dev.try_clone().unwrap();
1137 let mut dev_r = BufReader::new(dev);
1138 let reg = ControlFrame::Register {
1139 v: 1,
1140 device: "d".into(),
1141 platform: "ios".into(),
1142 token: "wrong".into(),
1143 };
1144 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
1145 let mut got = String::new();
1146 dev_r.read_line(&mut got).unwrap();
1147 assert!(
1148 got.contains("unauthorized"),
1149 "expected unauthorized, got: {got}"
1150 );
1151 }
1152
1153 #[test]
1154 fn cast_via_relay_reports_delivered_to_registered_device() {
1155 let listener = bind("127.0.0.1:0").unwrap();
1157 let addr = listener.local_addr().unwrap();
1158 let opts = ServeOptions {
1159 token: "tok".into(),
1160 max_conns: Some(2),
1161 };
1162 let registry = Registry::new();
1163 let serve_registry = registry.clone();
1164 thread::spawn(move || {
1165 let _ = serve_with_registry(&listener, &opts, &serve_registry);
1166 });
1167
1168 let dev = TcpStream::connect(addr).unwrap();
1169 let mut dev_w = dev.try_clone().unwrap();
1170 let reg = ControlFrame::Register {
1171 v: 1,
1172 device: "harry-watch".into(),
1173 platform: "watchos".into(),
1174 token: "tok".into(),
1175 };
1176 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
1177 assert!(
1178 wait_until(|| registry.is_registered("harry-watch"), 2000),
1179 "device should register"
1180 );
1181
1182 let delivery =
1183 cast_via_relay(addr, "tok", "harry-watch", &[InjectionCommand::text("hi")]).unwrap();
1184 assert!(delivery.acked, "relay should ack the cast");
1185 assert_eq!(
1186 delivery.delivered, 1,
1187 "delivered to the one connected device"
1188 );
1189 }
1190
1191 #[test]
1192 fn cast_via_relay_reports_zero_for_unconnected_device() {
1193 let listener = bind("127.0.0.1:0").unwrap();
1196 let addr = listener.local_addr().unwrap();
1197 let opts = ServeOptions {
1198 token: "tok".into(),
1199 max_conns: Some(1),
1200 };
1201 thread::spawn(move || {
1202 let _ = serve(&listener, &opts);
1203 });
1204
1205 let delivery =
1206 cast_via_relay(addr, "tok", "ghost-device", &[InjectionCommand::text("hi")]).unwrap();
1207 assert!(delivery.acked, "relay should still ack (with delivered=0)");
1208 assert_eq!(delivery.delivered, 0, "no device connected -> delivered 0");
1209 }
1210
1211 #[test]
1212 fn cast_via_relay_does_not_hang_without_a_result() {
1213 let listener = bind("127.0.0.1:0").unwrap();
1217 let addr = listener.local_addr().unwrap();
1218 thread::spawn(move || {
1219 if let Ok((sock, _)) = listener.accept() {
1220 let mut r = BufReader::new(sock.try_clone().unwrap());
1221 let mut line = String::new();
1222 let _ = r.read_line(&mut line);
1223 thread::sleep(Duration::from_secs(2));
1226 drop(sock);
1227 }
1228 });
1229
1230 let start = std::time::Instant::now();
1231 let delivery =
1232 cast_via_relay(addr, "tok", "local", &[InjectionCommand::text("hi")]).unwrap();
1233 assert!(!delivery.acked, "no result -> acked must be false");
1234 assert!(
1235 start.elapsed() < Duration::from_secs(2),
1236 "must return on the read timeout, not block for the full hold"
1237 );
1238 }
1239
1240 #[test]
1241 fn device_ids_returns_sorted_registered() {
1242 let reg = Registry::new();
1244 let (tx_b, _rx_b) = mpsc::channel();
1245 let (tx_a, _rx_a) = mpsc::channel();
1246 reg.register("phone", tx_b);
1247 reg.register("harry-watch", tx_a);
1248 assert_eq!(
1249 reg.device_ids(),
1250 vec!["harry-watch".to_owned(), "phone".to_owned()]
1251 );
1252 reg.unregister("phone");
1253 assert_eq!(reg.device_ids(), vec!["harry-watch".to_owned()]);
1254 }
1255
1256 #[test]
1257 fn end_to_end_list_devices_returns_registered() {
1258 let listener = bind("127.0.0.1:0").unwrap();
1260 let addr = listener.local_addr().unwrap();
1261 let opts = ServeOptions {
1262 token: "tok".into(),
1263 max_conns: Some(3),
1264 };
1265 let registry = Registry::new();
1266 let serve_registry = registry.clone();
1267 thread::spawn(move || {
1268 let _ = serve_with_registry(&listener, &opts, &serve_registry);
1269 });
1270
1271 let mut held = Vec::new();
1273 for id in ["harry-watch", "phone"] {
1274 let dev = TcpStream::connect(addr).unwrap();
1275 let mut dev_w = dev.try_clone().unwrap();
1276 let reg = ControlFrame::Register {
1277 v: 1,
1278 device: id.into(),
1279 platform: "watchos".into(),
1280 token: "tok".into(),
1281 };
1282 write_line(&mut dev_w, ®.json_line().unwrap()).unwrap();
1283 held.push(dev);
1284 held.push(dev_w);
1285 }
1286 assert!(
1287 wait_until(|| registry.device_ids().len() == 2, 2000),
1288 "both devices register"
1289 );
1290
1291 let mut got = list_relay_devices(addr, "tok").unwrap();
1292 got.sort();
1293 assert_eq!(got, vec!["harry-watch".to_owned(), "phone".to_owned()]);
1294 drop(held);
1295 }
1296
1297 #[test]
1298 fn list_devices_unauthorized_rejected() {
1299 let listener = bind("127.0.0.1:0").unwrap();
1301 let addr = listener.local_addr().unwrap();
1302 let opts = ServeOptions {
1303 token: "right".into(),
1304 max_conns: Some(1),
1305 };
1306 thread::spawn(move || {
1307 let _ = serve(&listener, &opts);
1308 });
1309 let err = list_relay_devices(addr, "wrong").unwrap_err();
1310 assert_eq!(err.code(), "relay_list");
1311 }
1312
1313 #[test]
1314 fn list_relay_devices_no_hang_without_response() {
1315 let listener = bind("127.0.0.1:0").unwrap();
1318 let addr = listener.local_addr().unwrap();
1319 thread::spawn(move || {
1320 if let Ok((sock, _)) = listener.accept() {
1321 let mut r = BufReader::new(sock.try_clone().unwrap());
1322 let mut line = String::new();
1323 let _ = r.read_line(&mut line);
1324 thread::sleep(Duration::from_secs(2));
1325 drop(sock);
1326 }
1327 });
1328 let start = std::time::Instant::now();
1329 let devices = list_relay_devices(addr, "tok").unwrap();
1330 assert!(devices.is_empty(), "no reply -> empty list");
1331 assert!(
1332 start.elapsed() < Duration::from_secs(2),
1333 "must return on the read timeout, not hang"
1334 );
1335 }
1336}