1use std::collections::{HashMap, HashSet};
23use std::sync::OnceLock;
24use std::time::{Duration, Instant};
25
26use parking_lot::{Condvar, Mutex};
27
28use crate::config::Config;
29use crate::remote::acks;
30use crate::remote::link::{
31 self, CommandSource, LinkCommand, LinkDevice, LinkHello, LinkReport, LinkState, Local,
32};
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct Device {
37 pub id: String,
38 pub name: String,
39 pub platform: String,
40 pub account: bool,
42 pub owner: Option<String>,
45 pub nearby: bool,
47 pub awake: bool,
50 pub asleep: bool,
53 pub wakeable: bool,
56 pub last_seen: Option<i64>,
58 pub waking: Option<Waking>,
60 pub same_library: bool,
62 pub state: Option<LinkState>,
64 pub heard: Instant,
66 pub problem: Option<String>,
69}
70
71impl Device {
72 pub fn position_ms(&self) -> u64 {
74 let Some(state) = &self.state else { return 0 };
75 if !state.playing {
76 return state.position_ms;
77 }
78 let pos = state.position_ms + self.heard.elapsed().as_millis() as u64;
79 if state.duration_ms > 0 {
80 pos.min(state.duration_ms)
81 } else {
82 pos
83 }
84 }
85}
86
87#[derive(Default)]
88struct Store {
89 linked: bool,
90 linked_at: Option<i64>,
92 account: Vec<(LinkDevice, Instant)>,
93 nearby: Vec<Nearby>,
94 seen: Vec<SeenNearby>,
95 target: Option<Remembered>,
96 version: u64,
97 live: HashMap<String, i64>,
99 departed: Vec<Departed>,
101 used: HashSet<String>,
103 fresh: bool,
106 waking: HashMap<String, Waking>,
108 lan_heard: HashMap<String, i64>,
111 attempt: Option<(u64, String)>,
113 wake_gen: u64,
115 shares: Vec<String>,
117 share_error: Option<String>,
119 accounts: Vec<String>,
121 notice: Option<Notice>,
123 tv_looked: HashMap<(String, String), Instant>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct Notice {
133 pub seq: u64,
135 pub device: String,
137 pub outcome: acks::AckOutcome,
138}
139
140pub fn notice() -> Option<Notice> {
142 with(|s| s.notice.clone())
143}
144
145pub type Then = Box<dyn FnOnce(Option<acks::AckOutcome>) + Send>;
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum Waking {
152 Tv,
156 Network,
159 Push,
162 Notification,
164 Failed(String),
166}
167
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
170struct Departed {
171 id: String,
172 name: String,
173 platform: String,
174 account: bool,
175 same_library: bool,
176 shared: bool,
178}
179
180pub const STALE_SECS: i64 = super::wire::IDLE.as_secs() as i64;
184
185#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
187struct Saved {
188 target: Option<Remembered>,
189 account: Vec<LinkDevice>,
190 #[serde(default)]
191 nearby: Vec<SeenNearby>,
192 #[serde(default)]
195 live: HashMap<String, i64>,
196 #[serde(default)]
198 departed: Vec<Departed>,
199 #[serde(default)]
202 used: Option<Vec<String>>,
203}
204
205#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
208pub struct SeenNearby {
209 pub id: String,
210 pub name: String,
211 pub platform: String,
212 pub addr: String,
213 pub at: i64,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub tv: Option<crate::remote::airplay::Tv>,
218}
219
220const NEARBY_KEPT: i64 = 7 * 24 * 60 * 60;
222
223#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
226struct Remembered {
227 id: String,
228 name: String,
229 platform: String,
230}
231
232struct Nearby {
233 hello: LinkHello,
234 state: Option<LinkState>,
235 at: Instant,
236}
237
238static STORE: Mutex<Option<Store>> = Mutex::new(None);
239static LOCAL: OnceLock<Local> = OnceLock::new();
240
241fn with<R>(f: impl FnOnce(&mut Store) -> R) -> R {
242 let mut store = STORE.lock();
243 f(store.get_or_insert_with(Store::default))
244}
245
246fn changed<R>(f: impl FnOnce(&mut Store) -> R) -> R {
248 let out = with(|s| {
249 let out = f(s);
250 s.version += 1;
251 out
252 });
253 crate::signal::engine_changed().bump();
254 replan();
255 out
256}
257
258static CLOCK: (Mutex<bool>, Condvar) = (Mutex::new(false), Condvar::new());
261
262fn replan() {
266 static STARTED: OnceLock<()> = OnceLock::new();
267 *CLOCK.0.lock() = true;
268 CLOCK.1.notify_all();
269 STARTED.get_or_init(|| {
270 let _ = std::thread::Builder::new()
271 .name("koan-devices-clock".into())
272 .spawn(clock);
273 });
274}
275
276fn clock() {
277 loop {
278 let now = chrono::Utc::now().timestamp();
279 let wait = next_change(now).map(|at| Duration::from_secs((at - now).max(0) as u64 + 1));
280 let mut rung = CLOCK.0.lock();
281 if !*rung {
282 let timed_out = match wait {
283 Some(wait) => CLOCK.1.wait_for(&mut rung, wait).timed_out(),
284 None => {
285 CLOCK.1.wait(&mut rung);
286 false
287 }
288 };
289 if timed_out {
290 drop(rung);
291 with(|s| s.version += 1);
293 crate::signal::engine_changed().bump();
294 continue;
295 }
296 }
297 *rung = false;
298 }
299}
300
301impl Store {
302 fn save(&self) {
303 let saved = Saved {
304 target: self.target.clone(),
305 account: self.account.iter().map(|(d, _)| d.clone()).collect(),
306 nearby: self.seen.clone(),
307 live: self.live.clone(),
308 departed: self.departed.clone(),
309 used: Some({
310 let mut used: Vec<String> = self.used.iter().cloned().collect();
311 used.sort();
312 used
313 }),
314 };
315 if let Ok(json) = serde_json::to_string(&saved) {
316 let _ = std::fs::write(saved_path(), json);
317 }
318 }
319}
320
321fn saved_path() -> std::path::PathBuf {
322 crate::config::config_dir().join("devices.json")
323}
324
325fn restore() {
327 let Ok(text) = std::fs::read_to_string(saved_path()) else {
328 return;
329 };
330 let Ok(saved) = serde_json::from_str::<Saved>(&text) else {
331 return;
332 };
333 let now = Instant::now();
334 let cutoff = chrono::Utc::now().timestamp() - NEARBY_KEPT;
335 changed(|s| {
336 s.target = saved.target;
337 s.seen = saved
338 .nearby
339 .into_iter()
340 .filter(|n| n.at >= cutoff)
341 .collect();
342 s.live = saved.live;
343 s.used = match saved.used {
344 Some(used) => used.into_iter().collect(),
345 None => s.target.iter().map(|t| t.id.clone()).collect(),
346 };
347 let used = &s.used;
348 s.departed = saved
349 .departed
350 .into_iter()
351 .filter(|g| used.contains(&g.id))
352 .collect();
353 s.account = saved
356 .account
357 .into_iter()
358 .map(|d| {
359 (
360 LinkDevice {
361 linked: false,
362 state: None,
363 ..d
364 },
365 now,
366 )
367 })
368 .collect();
369 });
370}
371
372pub fn start(local: Local) {
375 if LOCAL.set(local.clone()).is_err() {
376 return;
377 }
378 restore();
379 link::spawn(local.clone());
380 crate::remote::nearby::start(local);
381}
382
383pub fn local() -> Option<&'static Local> {
385 LOCAL.get()
386}
387
388pub fn set_linked(linked: bool) {
397 crate::remote::offline::link_changed(linked);
398 changed(|s| {
399 if linked != s.linked {
400 s.linked_at = linked.then(|| chrono::Utc::now().timestamp());
401 }
402 s.linked = linked;
403 if !linked {
404 s.fresh = false;
405 }
406 });
407}
408
409pub fn linked_since() -> Option<i64> {
411 with(|s| s.linked.then_some(s.linked_at).flatten())
412}
413
414pub fn linked_controllers() -> Vec<LinkDevice> {
419 let me = this_id();
420 with(|s| {
421 s.account
422 .iter()
423 .map(|(d, _)| d)
424 .filter(|d| d.linked && d.owner.is_none() && Some(&d.id) != me.as_ref())
425 .cloned()
426 .collect()
427 })
428}
429
430pub fn name_of(id: &str) -> Option<String> {
432 with(|s| {
433 s.account
434 .iter()
435 .map(|(d, _)| (&d.id, &d.name))
436 .chain(s.seen.iter().map(|n| (&n.id, &n.name)))
437 .find(|(i, _)| *i == id)
438 .map(|(_, name)| name.clone())
439 })
440}
441
442pub fn set_account(devices: Vec<LinkDevice>) {
443 let now = Instant::now();
444 let unix = chrono::Utc::now().timestamp();
445 changed(|s| {
446 if s.fresh {
450 for (old, _) in &s.account {
451 if old.linked {
452 s.live.insert(old.id.clone(), unix);
453 }
454 }
455 }
456 for d in &devices {
457 if d.linked {
458 s.live.insert(d.id.clone(), unix);
459 s.waking.remove(&d.id);
460 } else if let Some(seen) = d.last_seen {
461 let live = s.live.entry(d.id.clone()).or_insert(seen);
464 *live = (*live).max(seen);
465 }
466 s.departed.retain(|g| g.id != d.id);
467 }
468 let leaving: Vec<Departed> = s
469 .account
470 .iter()
471 .filter(|(old, _)| !devices.iter().any(|d| d.id == old.id))
472 .map(|(old, _)| Departed {
473 id: old.id.clone(),
474 name: old.name.clone(),
475 platform: old.platform.clone(),
476 account: old.owner.is_none(),
477 shared: old.owner.is_some(),
478 same_library: true,
479 })
480 .collect();
481 for d in leaving {
482 if !s.live.contains_key(&d.id) || d.shared {
486 continue;
487 }
488 s.departed.retain(|g| g.id != d.id);
489 s.departed.push(d);
490 }
491 s.account = devices.into_iter().map(|d| (d, now)).collect();
492 s.fresh = s.linked;
493 s.save();
494 });
495}
496
497pub fn set_shares(grantees: Vec<String>, error: Option<String>, accounts: Vec<String>) {
500 changed(|s| {
501 s.shares = grantees;
502 s.share_error = error;
503 s.accounts = accounts;
504 });
505}
506
507pub fn accounts() -> Vec<String> {
509 with(|s| s.accounts.clone())
510}
511
512pub fn shares() -> Vec<String> {
513 with(|s| s.shares.clone())
514}
515
516pub fn share_error() -> Option<String> {
517 with(|s| s.share_error.clone())
518}
519
520pub fn forget_shares() {
522 set_shares(Vec::new(), None, Vec::new());
523}
524
525pub fn send_for(source: CommandSource, id: &str, cmd: LinkCommand) -> Result<(), String> {
534 send_for_then(source, id, cmd, None)
535}
536
537pub fn send_for_then(
539 source: CommandSource,
540 id: &str,
541 cmd: LinkCommand,
542 then: Option<Then>,
543) -> Result<(), String> {
544 match source {
545 CommandSource::Account => send_then(id, cmd, then),
546 CommandSource::Stranger => {
547 send_nearby(id, cmd)?;
548 if let Some(then) = then {
549 then(None);
550 }
551 Ok(())
552 }
553 CommandSource::Shared | CommandSource::Nearby => {
554 let (nearby, own) = with(|s| {
555 (
556 s.nearby.iter().any(|n| n.hello.id == id),
557 s.account
558 .iter()
559 .any(|(d, _)| d.id == id && d.owner.is_none()),
560 )
561 });
562 if own && !nearby {
563 return Err(format!("{id} is this account's, not the asker's"));
564 }
565 send_then(id, cmd, then)
566 }
567 }
568}
569
570pub fn send_nearby(id: &str, cmd: LinkCommand) -> Result<(), String> {
575 let nearby = with(|s| s.nearby.iter().any(|n| n.hello.id == id));
576 if nearby && cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd) {
577 Ok(())
578 } else {
579 Err(format!("{id} is not on this network"))
580 }
581}
582
583pub fn share(grantee: &str, allow: bool) -> Result<(), String> {
586 let grantee = grantee.trim();
587 if grantee.is_empty() {
588 return Err("Name an account on your server.".into());
589 }
590 if link::report(LinkReport::Share {
591 grantee: grantee.to_string(),
592 allow,
593 }) {
594 Ok(())
595 } else {
596 Err("Not connected to your server.".into())
597 }
598}
599
600pub fn remembered_nearby() -> Vec<SeenNearby> {
602 with(|s| s.seen.clone())
603}
604
605pub fn nearby_hello(hello: LinkHello, addr: &str) {
606 let host = crate::remote::airplay::host_of(addr).to_string();
607 let look = changed(|s| {
608 let kept = s
609 .seen
610 .iter()
611 .find(|n| n.id == hello.id)
612 .and_then(|n| n.tv.clone());
613 let look = hello.platform == "tvos"
614 && !crate::remote::airplay::current(kept.as_ref(), &host)
615 && look_for_tv(&mut s.tv_looked, &hello.id, &host, Instant::now());
616 let tv = if look { None } else { kept };
619 s.seen.retain(|n| n.id != hello.id);
620 s.seen.push(SeenNearby {
621 id: hello.id.clone(),
622 name: hello.name.clone(),
623 platform: hello.platform.clone(),
624 addr: addr.to_string(),
625 at: chrono::Utc::now().timestamp(),
626 tv,
627 });
628 s.save();
629 s.live
630 .insert(hello.id.clone(), chrono::Utc::now().timestamp());
631 s.departed.retain(|g| g.id != hello.id);
632 s.waking.remove(&hello.id);
633 s.lan_heard
634 .insert(hello.id.clone(), chrono::Utc::now().timestamp());
635 s.nearby.retain(|n| n.hello.id != hello.id);
636 let found = look.then(|| (hello.id.clone(), hello.name.clone()));
637 s.nearby.push(Nearby {
638 hello,
639 state: None,
640 at: Instant::now(),
641 });
642 found
643 });
644 if let Some((id, name)) = look {
645 record_tv(id, name, addr.to_string());
646 }
647}
648
649const TV_LOOK_AGAIN: Duration = Duration::from_secs(10 * 60);
652
653fn look_for_tv(
656 looked: &mut HashMap<(String, String), Instant>,
657 id: &str,
658 host: &str,
659 now: Instant,
660) -> bool {
661 looked.retain(|_, at| now.saturating_duration_since(*at) < TV_LOOK_AGAIN);
662 let key = (id.to_string(), host.to_string());
663 if looked.contains_key(&key) {
664 return false;
665 }
666 looked.insert(key, now);
667 true
668}
669
670fn record_tv(id: String, name: String, addr: String) {
673 let _ = std::thread::Builder::new()
674 .name("koan-airplay".into())
675 .spawn(move || {
676 let Some(tv) = crate::remote::airplay::record(&addr, &name) else {
677 log::info!("airplay: {name}: no AirPlay announcement from {addr}");
678 return;
679 };
680 log::info!(
681 "airplay: {name} is {} on {} ({})",
682 tv.name,
683 tv.host,
684 tv.mac.as_deref().unwrap_or("no MAC")
685 );
686 with(|s| {
687 if let Some(n) = s.seen.iter_mut().find(|n| n.id == id) {
688 n.tv = Some(tv);
689 s.save();
690 }
691 });
692 });
693}
694
695fn tv_of(id: &str) -> Option<crate::remote::airplay::Tv> {
697 with(|s| {
698 s.seen
699 .iter()
700 .find(|n| n.id == id)
701 .and_then(|n| n.tv.clone())
702 })
703}
704
705pub fn nearby_state(id: &str, state: LinkState) {
706 changed(|s| {
707 if let Some(n) = s.nearby.iter_mut().find(|n| n.hello.id == id) {
708 n.state = Some(state);
709 n.at = Instant::now();
710 let now = chrono::Utc::now().timestamp();
711 s.live.insert(id.to_string(), now);
712 s.lan_heard.insert(id.to_string(), now);
713 }
714 });
715}
716
717pub fn nearby_gone(id: &str) {
718 let cfg = Config::load().unwrap_or_default();
719 let ours = link::library_fingerprint(&cfg);
720 changed(|s| {
721 let Some(at) = s.nearby.iter().position(|n| n.hello.id == id) else {
722 return;
723 };
724 let gone = s.nearby.remove(at);
725 let now = chrono::Utc::now().timestamp();
726 s.live.insert(id.to_string(), now);
727 s.lan_heard.insert(id.to_string(), now);
728 if s.account.iter().any(|(d, _)| d.id == id) {
731 return;
732 }
733 s.departed.retain(|g| g.id != id);
734 s.departed.push(Departed {
735 id: gone.hello.id.clone(),
736 name: gone.hello.name.clone(),
737 platform: gone.hello.platform.clone(),
738 account: false,
739 same_library: ours.is_some() && gone.hello.library == ours,
740 shared: false,
741 });
742 s.save();
743 });
744}
745
746pub fn forget(id: &str) -> Result<(), String> {
753 let device = list().into_iter().find(|d| d.id == id);
754 if device.as_ref().is_some_and(|d| d.awake) {
755 return Err("Only a device out of reach can be forgotten.".into());
756 }
757 let account = with(|s| s.account.iter().any(|(d, _)| d.id == id));
758 if account
759 && !link::report(LinkReport::Forget {
760 device: id.to_string(),
761 })
762 {
763 return Err("Not connected to your server, which remembers it too.".into());
764 }
765 forgotten(id);
766 Ok(())
767}
768
769fn used(id: &str) {
772 with(|s| {
773 if s.used.insert(id.to_string()) {
774 s.save();
775 }
776 });
777}
778
779pub fn forgotten(id: &str) {
782 changed(|s| {
783 s.used.remove(id);
784 s.account.retain(|(d, _)| d.id != id);
785 s.departed.retain(|g| g.id != id);
786 s.seen.retain(|n| n.id != id);
787 s.live.remove(id);
788 s.lan_heard.remove(id);
789 s.waking.remove(id);
790 if s.target.as_ref().is_some_and(|t| t.id == id) {
791 s.target = None;
792 }
793 s.save();
794 });
795 log::info!("devices: forgot {id}");
796}
797
798pub fn touch() {
801 changed(|_| ());
802}
803
804pub fn version() -> u64 {
806 with(|s| s.version)
807}
808
809pub fn linked() -> bool {
811 with(|s| s.linked)
812}
813
814pub fn list() -> Vec<Device> {
816 list_at(chrono::Utc::now().timestamp())
817}
818
819fn list_at(now: i64) -> Vec<Device> {
821 let cfg = Config::load().unwrap_or_default();
822 let ours = link::library_fingerprint(&cfg);
823 with(|s| {
824 let live = |id: &str| s.live.get(id).copied();
825 let fresh = |id: &str| live(id).is_some_and(|at| now - at <= STALE_SECS);
827 let kept = |id: &str| fresh(id) || s.used.contains(id);
829 let mut out: Vec<Device> = s
830 .account
831 .iter()
832 .filter_map(|(d, at)| {
833 let near = s.nearby.iter().find(|n| n.hello.id == d.id);
834 let awake = d.linked || near.is_some();
835 if !awake && !kept(&d.id) {
836 return None;
837 }
838 Some(Device {
839 id: d.id.clone(),
840 name: d.name.clone(),
841 platform: d.platform.clone(),
842 account: d.owner.is_none(),
843 owner: d.owner.clone(),
844 nearby: near.is_some(),
845 awake,
846 asleep: !awake && !fresh(&d.id),
847 wakeable: d.wakeable.unwrap_or(true),
850 last_seen: live(&d.id),
851 waking: None,
852 same_library: true,
853 state: match (near.and_then(|n| n.state.clone()), &d.state) {
856 (Some(mut fresh), Some(linked)) => {
857 fresh.outputs = linked.outputs.clone();
858 Some(fresh)
859 }
860 (Some(fresh), None) => Some(fresh),
861 (None, linked) => linked.clone(),
862 },
863 heard: near.map_or(*at, |n| n.at),
864 problem: None,
865 })
866 })
867 .collect();
868 for n in &s.nearby {
869 if out.iter().any(|d| d.id == n.hello.id) {
870 continue;
871 }
872 out.push(Device {
873 id: n.hello.id.clone(),
874 name: n.hello.name.clone(),
875 platform: n.hello.platform.clone(),
876 account: false,
877 owner: None,
878 nearby: true,
879 awake: true,
880 asleep: false,
881 wakeable: false,
882 last_seen: live(&n.hello.id),
883 waking: None,
884 same_library: ours.is_some() && n.hello.library == ours,
885 state: n.state.clone(),
886 heard: n.at,
887 problem: None,
888 });
889 }
890 for g in &s.departed {
891 if out.iter().any(|d| d.id == g.id) || !kept(&g.id) {
892 continue;
893 }
894 let last = live(&g.id);
895 out.push(Device {
898 id: g.id.clone(),
899 name: g.name.clone(),
900 platform: g.platform.clone(),
901 account: g.account,
902 owner: None,
903 nearby: false,
904 awake: false,
905 asleep: !fresh(&g.id),
906 wakeable: !g.account && s.linked,
910 last_seen: last,
911 waking: None,
912 same_library: g.same_library,
913 state: None,
915 heard: Instant::now(),
916 problem: None,
917 });
918 }
919 if let Some(t) = &s.target
924 && !out.iter().any(|d| d.id == t.id)
925 {
926 out.push(Device {
927 id: t.id.clone(),
928 name: t.name.clone(),
929 platform: t.platform.clone(),
930 account: false,
931 owner: None,
932 nearby: false,
933 awake: false,
934 asleep: true,
935 wakeable: false,
936 last_seen: live(&t.id),
937 waking: None,
938 same_library: true,
939 state: None,
940 heard: Instant::now(),
941 problem: Some("Out of reach".into()),
942 });
943 }
944 for d in &mut out {
945 d.waking = s.waking.get(&d.id).cloned();
946 }
947 out.sort_by_key(|d| {
948 (
949 !d.state.as_ref().is_some_and(|st| st.playing),
950 !d.awake,
951 d.name.to_lowercase(),
952 )
953 });
954 out
955 })
956}
957
958fn next_change(now: i64) -> Option<i64> {
960 with(|s| {
961 let reachable = |id: &str| {
962 s.nearby.iter().any(|n| n.hello.id == id)
963 || s.account.iter().any(|(d, _)| d.id == id && d.linked)
964 };
965 s.live
966 .iter()
967 .filter(|(id, _)| !reachable(id))
968 .map(|(_, at)| at + STALE_SECS)
969 .filter(|t| *t > now)
970 .min()
971 })
972}
973
974pub fn choosable(id: &str) -> Result<(), String> {
977 match list().into_iter().find(|d| d.id == id) {
978 Some(d) if !d.awake && !d.wakeable && d.problem.is_none() => Err(if d.asleep {
979 format!("{} is asleep and cannot be woken from here.", d.name)
980 } else {
981 format!("{} is reconnecting.", d.name)
982 }),
983 _ => Ok(()),
984 }
985}
986
987const NETWORK_RECENT: i64 = 60;
989const NETWORK_WAIT: Duration = Duration::from_secs(3);
991const PUSH_WAIT: Duration = Duration::from_secs(6);
992const NOTIFICATION_WAIT: Duration = Duration::from_secs(45);
993const TV_KNOCK: Duration = Duration::from_secs(20);
995const TV_WAIT: Duration = Duration::from_secs(2);
998
999fn wake_plan(tv: bool, seen_nearby_recently: bool, wakeable: bool) -> Vec<(Waking, Duration)> {
1004 let mut plan = Vec::new();
1005 if tv {
1006 plan.push((Waking::Tv, TV_WAIT));
1007 }
1008 if seen_nearby_recently {
1009 plan.push((Waking::Network, NETWORK_WAIT));
1010 }
1011 if wakeable {
1012 plan.push((Waking::Push, PUSH_WAIT));
1013 plan.push((Waking::Notification, NOTIFICATION_WAIT));
1014 }
1015 plan
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq)]
1020enum Ended {
1021 Reached(Waking),
1022 Failed(String),
1023 Cancelled,
1025}
1026
1027fn run_wake(
1031 plan: &[(Waking, Duration)],
1032 current: impl Fn() -> bool,
1033 mut act: impl FnMut(&Waking) -> Result<(), String>,
1034 mut wait: impl FnMut(Duration) -> bool,
1035) -> Ended {
1036 let mut last = None;
1037 for (stage, time) in plan {
1038 if !current() {
1039 return Ended::Cancelled;
1040 }
1041 if let Err(why) = act(stage) {
1042 return Ended::Failed(why);
1043 }
1044 last = Some(stage);
1045 if wait(*time) {
1046 return Ended::Reached(stage.clone());
1047 }
1048 }
1049 if !current() {
1050 return Ended::Cancelled;
1051 }
1052 Ended::Failed(match last {
1053 Some(Waking::Notification) => {
1054 "Did not wake. A notification is waiting on it to be tapped.".into()
1055 }
1056 _ => "Did not wake.".into(),
1057 })
1058}
1059
1060fn reachable(id: &str) -> bool {
1061 with(|s| {
1062 s.nearby.iter().any(|n| n.hello.id == id)
1063 || s.account.iter().any(|(d, _)| d.id == id && d.linked)
1064 })
1065}
1066
1067fn recently_on_network(id: &str, now: i64) -> bool {
1069 with(|s| {
1070 s.lan_heard
1071 .get(id)
1072 .is_some_and(|at| now - at <= NETWORK_RECENT)
1073 })
1074}
1075
1076fn begin_wake(id: &str) -> Option<u64> {
1079 with(|s| {
1080 if s.attempt.as_ref().is_some_and(|(_, d)| d == id) {
1081 return None;
1082 }
1083 s.wake_gen += 1;
1084 s.attempt = Some((s.wake_gen, id.to_string()));
1085 Some(s.wake_gen)
1086 })
1087}
1088
1089fn wake_is_current(generation: u64) -> bool {
1090 with(|s| s.attempt.as_ref().is_some_and(|(g, _)| *g == generation))
1091}
1092
1093fn cancel_wake(keep: Option<&str>) {
1095 let cancelled = with(|s| {
1096 let other = s
1097 .attempt
1098 .as_ref()
1099 .is_some_and(|(_, d)| Some(d.as_str()) != keep);
1100 if other {
1101 let (_, id) = s.attempt.take().expect("checked");
1102 s.waking.remove(&id);
1103 s.wake_gen += 1;
1104 }
1105 other
1106 });
1107 if cancelled {
1108 crate::signal::engine_changed().bump();
1110 }
1111}
1112
1113pub fn wake(id: &str) {
1125 let id = id.to_string();
1126 if reachable(&id) {
1127 return;
1128 }
1129 let Some(device) = list().into_iter().find(|d| d.id == id) else {
1130 return;
1131 };
1132 let now = chrono::Utc::now().timestamp();
1133 let tv = (device.platform == "tvos").then(|| tv_of(&id)).flatten();
1135 let plan = wake_plan(tv.is_some(), recently_on_network(&id, now), device.wakeable);
1136 if plan.is_empty() {
1137 return;
1138 }
1139 let Some(generation) = begin_wake(&id) else {
1140 log::info!("wake: {}: already being woken", device.name);
1141 return;
1142 };
1143 let name = device.name;
1144 let _ = std::thread::Builder::new()
1145 .name("koan-wake".into())
1146 .spawn(move || {
1147 let started = Instant::now();
1148 let ms = || started.elapsed().as_millis();
1149 let current = || wake_is_current(generation);
1150 let mut box_silent = false;
1151 let ended = run_wake(
1152 &plan,
1153 current,
1154 |stage| {
1155 log::info!("wake: {name}: {stage:?} at +{}ms", ms());
1156 changed(|s| s.waking.insert(id.clone(), stage.clone()));
1157 match stage {
1158 Waking::Tv => {
1159 if let Some(tv) = &tv {
1160 let awake = crate::remote::airplay::wake(tv, TV_KNOCK, || {
1161 !current() || reachable(&id)
1162 });
1163 box_silent = !awake && current() && !reachable(&id);
1164 crate::remote::nearby::dial_now();
1167 }
1168 Ok(())
1169 }
1170 Waking::Network => {
1171 crate::remote::nearby::dial_now();
1172 Ok(())
1173 }
1174 Waking::Push | Waking::Notification => {
1175 let notify = *stage == Waking::Notification;
1176 if link::report(LinkReport::Wake {
1177 to: id.clone(),
1178 notify,
1179 }) {
1180 Ok(())
1181 } else {
1182 Err("Not connected to your server, which wakes it.".into())
1183 }
1184 }
1185 Waking::Failed(_) => Ok(()),
1186 }
1187 },
1188 |time| {
1189 let until = Instant::now() + time;
1190 let signal = crate::signal::engine_changed();
1191 let mut seen = signal.generation();
1192 while !reachable(&id) {
1193 let left = until.saturating_duration_since(Instant::now());
1194 if left.is_zero() || !current() {
1195 return false;
1196 }
1197 seen = signal.wait_until(seen, left);
1198 }
1199 true
1200 },
1201 );
1202 let ended = match ended {
1203 Ended::Failed(_) if box_silent => Ended::Failed(format!(
1204 "Couldn't wake {name}: the Apple TV did not answer on this network."
1205 )),
1206 ended => ended,
1207 };
1208 match &ended {
1209 Ended::Reached(stage) => {
1210 log::info!("wake: {name}: reached at +{}ms, after {stage:?}", ms());
1211 }
1212 Ended::Failed(why) => {
1213 log::warn!("wake: {name}: did not wake in {}ms: {why}", ms());
1214 }
1215 Ended::Cancelled => {
1216 log::info!("wake: {name}: abandoned at +{}ms for another device", ms());
1217 }
1218 }
1219 changed(|s| {
1220 if !s.attempt.as_ref().is_some_and(|(g, _)| *g == generation) {
1221 return;
1222 }
1223 s.attempt = None;
1224 match ended {
1225 Ended::Failed(why) => {
1226 s.waking.insert(id.clone(), Waking::Failed(why));
1227 }
1228 _ => {
1229 s.waking.remove(&id);
1230 }
1231 }
1232 });
1233 });
1234}
1235
1236pub fn target() -> Option<String> {
1238 with(|s| s.target.as_ref().map(|t| t.id.clone()))
1239}
1240
1241pub fn set_target(id: Option<String>) {
1242 cancel_wake(id.as_deref());
1243 if let Some(id) = &id {
1244 crate::remote::nearby::release(id);
1245 }
1246 let listed = id
1247 .as_ref()
1248 .and_then(|id| list().into_iter().find(|d| d.id == *id));
1249 changed(|s| {
1250 if let Some(id) = &id {
1251 s.used.insert(id.clone());
1252 }
1253 s.target = id.map(|id| match listed {
1254 Some(d) => Remembered {
1255 id,
1256 name: d.name,
1257 platform: d.platform,
1258 },
1259 None => Remembered {
1260 name: id.clone(),
1261 id,
1262 platform: String::new(),
1263 },
1264 });
1265 s.save();
1266 });
1267 crate::quiet::reapply();
1269}
1270
1271pub fn resume() {
1276 link::nudge();
1277 crate::remote::wire::probe_all();
1278 crate::remote::nearby::refresh();
1279}
1280
1281pub fn target_playhead() -> Option<(u64, bool)> {
1285 with(|s| {
1286 let (state, at) = reported(s, &s.target.as_ref()?.id)?;
1287 let mut position = state.position_ms;
1288 if state.playing {
1289 position += at.elapsed().as_millis() as u64;
1290 if state.duration_ms > 0 {
1291 position = position.min(state.duration_ms);
1292 }
1293 }
1294 Some((position, state.playing))
1295 })
1296}
1297
1298fn reported<'s>(s: &'s Store, id: &str) -> Option<(&'s LinkState, Instant)> {
1301 s.nearby
1302 .iter()
1303 .find(|n| n.hello.id == id)
1304 .and_then(|n| Some((n.state.as_ref()?, n.at)))
1305 .or_else(|| {
1306 s.account
1307 .iter()
1308 .find(|(d, _)| d.id == id)
1309 .and_then(|(d, at)| Some((d.state.as_ref()?, *at)))
1310 })
1311}
1312
1313pub fn current_track(id: &str) -> Option<String> {
1315 with(|s| {
1316 let (state, _) = reported(s, id)?;
1317 state.queue.iter().find(|e| e.current)?.track_id.clone()
1318 })
1319}
1320
1321pub fn last_report(id: &str) -> Option<LinkState> {
1323 with(|s| reported(s, id).map(|(state, _)| state.clone()))
1324}
1325
1326pub fn await_current(
1332 id: &str,
1333 track_id: &str,
1334 before: Option<&LinkState>,
1335 within: Duration,
1336) -> bool {
1337 let took = || with(|s| reported(s, id).is_some_and(|(now, _)| took(before, now, track_id)));
1338 await_until(took, within)
1339}
1340
1341fn took(before: Option<&LinkState>, now: &LinkState, track_id: &str) -> bool {
1347 let current = |state: &LinkState| state.queue.iter().find(|e| e.current).cloned();
1348 let Some(entry) = current(now) else {
1349 return false;
1350 };
1351 if entry.track_id.as_deref() != Some(track_id) {
1352 return false;
1353 }
1354 let was = before.and_then(current);
1355 match (&entry.id, was.as_ref().and_then(|e| e.id.as_ref())) {
1356 (Some(id), Some(was)) => id != was,
1357 (Some(_), None) => true,
1358 (None, _) => before != Some(now),
1359 }
1360}
1361
1362pub fn await_until(done: impl Fn() -> bool, within: Duration) -> bool {
1365 let until = Instant::now() + within;
1366 let signal = crate::signal::engine_changed();
1367 let mut seen = signal.generation();
1368 loop {
1369 if done() {
1370 return true;
1371 }
1372 let left = until.saturating_duration_since(Instant::now());
1373 if left.is_zero() {
1374 return false;
1375 }
1376 seen = signal.wait_until(seen, left);
1377 }
1378}
1379
1380pub fn target_device() -> Option<Device> {
1382 let id = target()?;
1383 list().into_iter().find(|d| d.id == id)
1384}
1385
1386pub fn send_live(id: &str, cmd: LinkCommand) -> bool {
1392 let everywhere = matches!(cmd, LinkCommand::WatchLevels { on: false });
1393 let near = cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd.clone());
1394 if near && !everywhere {
1395 return true;
1396 }
1397 let linked = link::report(LinkReport::Command {
1398 to: id.to_string(),
1399 command: cmd,
1400 ack: None,
1401 });
1402 near || linked
1403}
1404
1405pub fn send(id: &str, cmd: LinkCommand) -> Result<(), String> {
1409 send_then(id, cmd, None)
1410}
1411
1412pub fn send_then(id: &str, cmd: LinkCommand, then: Option<Then>) -> Result<(), String> {
1415 choosable(id)?;
1416 used(id);
1417 let (nearby, own, shared) = with(|s| {
1418 let listed = s.account.iter().find(|(d, _)| d.id == id);
1419 (
1420 s.nearby.iter().any(|n| n.hello.id == id),
1421 listed.is_some_and(|(d, _)| d.owner.is_none()),
1422 listed.is_some_and(|(d, _)| d.owner.is_some()),
1423 )
1424 });
1425 if !own && !cmd.allowed_playback() {
1429 return Err("Only your own devices can be asked that.".into());
1430 }
1431 let routes = Routes {
1432 lan: nearby,
1433 lan_answers: nearby && answers_on_network(id),
1434 link_answers: answers_by_link(id),
1435 };
1436 let ack = acks::next_id();
1440 let answer = acks::expect(ack, id);
1441 if routes.lan {
1442 let sent = if routes.lan_answers {
1443 crate::remote::nearby::send_acked(id, cmd.clone(), ack)
1444 } else {
1445 crate::remote::nearby::send(id, cmd.clone())
1446 };
1447 if sent {
1448 follow(id, cmd, ack, answer, Route::Network, routes, then);
1449 return Ok(());
1450 }
1451 }
1452 let account = own || shared;
1453 if link::report(LinkReport::Command {
1454 to: id.to_string(),
1455 command: cmd.clone(),
1456 ack: routes.link_answers.then_some(ack),
1457 }) {
1458 follow(id, cmd, ack, answer, Route::Link, routes, then);
1459 return Ok(());
1460 }
1461 if !account && !nearby {
1462 log::info!("devices: {id} is not listed; asking the server");
1465 }
1466 acks::forget(ack);
1467 let outcome = ask_server(id, &cmd, Some(ack))?;
1468 told(id, &cmd, outcome, then);
1469 Ok(())
1470}
1471
1472#[derive(Debug, Clone, Copy)]
1474struct Routes {
1475 lan: bool,
1476 lan_answers: bool,
1477 link_answers: bool,
1478}
1479
1480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1481enum Route {
1482 Network,
1483 Link,
1484}
1485
1486const NETWORK_ANSWER: Duration = Duration::from_secs(1);
1489const LINK_ANSWER: Duration = Duration::from_secs(3);
1490
1491fn follow(
1495 id: &str,
1496 cmd: LinkCommand,
1497 ack: u64,
1498 answer: crossbeam_channel::Receiver<acks::AckOutcome>,
1499 route: Route,
1500 routes: Routes,
1501 then: Option<Then>,
1502) {
1503 let answers = match route {
1504 Route::Network => routes.lan_answers,
1505 Route::Link => routes.link_answers,
1506 };
1507 if !answers {
1508 acks::forget(ack);
1509 told(id, &cmd, None, then);
1510 return;
1511 }
1512 let id = id.to_owned();
1513 let _ = std::thread::Builder::new()
1514 .name("koan-ack".into())
1515 .spawn(move || {
1516 let walked = walk(&id, &cmd, ack, &answer, route, routes);
1517 acks::forget(ack);
1518 match walked {
1519 Ok(outcome) => told(&id, &cmd, outcome, then),
1520 Err(error) => unanswered(&id, &cmd, error, then),
1521 }
1522 });
1523}
1524
1525fn walk(
1529 id: &str,
1530 cmd: &LinkCommand,
1531 ack: u64,
1532 answer: &crossbeam_channel::Receiver<acks::AckOutcome>,
1533 first: Route,
1534 routes: Routes,
1535) -> Result<Option<acks::AckOutcome>, String> {
1536 if first == Route::Network {
1537 if let Ok(outcome) = answer.recv_timeout(NETWORK_ANSWER) {
1538 return Ok(Some(outcome));
1539 }
1540 log::info!("devices: no answer from {id} on the network; trying the link");
1541 if routes.link_answers
1542 && link::report(LinkReport::Command {
1543 to: id.to_string(),
1544 command: cmd.clone(),
1545 ack: Some(ack),
1546 })
1547 && let Ok(outcome) = answer.recv_timeout(LINK_ANSWER)
1548 {
1549 return Ok(Some(outcome));
1550 }
1551 } else if let Ok(outcome) = answer.recv_timeout(LINK_ANSWER) {
1552 return Ok(Some(outcome));
1553 }
1554 log::info!("devices: no answer from {id}; asking the server");
1555 ask_server(id, cmd, Some(ack))
1556}
1557
1558fn unanswered(id: &str, cmd: &LinkCommand, error: String, then: Option<Then>) {
1563 match then {
1564 Some(then) => {
1565 log::warn!("devices: no answer from {id} for {cmd:?}, and the server: {error}");
1566 then(None);
1567 }
1568 None => told(id, cmd, Some(acks::AckOutcome::Failed { error }), None),
1569 }
1570}
1571
1572fn told(id: &str, cmd: &LinkCommand, outcome: Option<acks::AckOutcome>, then: Option<Then>) {
1577 match &outcome {
1578 Some(acks::AckOutcome::Done) | None => {}
1579 Some(acks::AckOutcome::Queued) => {
1580 log::info!("devices: {id} is asleep; {cmd:?} waits for it")
1581 }
1582 Some(other) => log::warn!("devices: {cmd:?} did not reach {id}: {other:?}"),
1583 }
1584 if let Some(then) = then {
1585 then(outcome);
1586 return;
1587 }
1588 let Some(outcome) = outcome.filter(|o| *o != acks::AckOutcome::Done) else {
1589 return;
1590 };
1591 let device = list()
1592 .into_iter()
1593 .find(|d| d.id == id)
1594 .map_or_else(|| id.to_owned(), |d| d.name);
1595 changed(|s| {
1596 let seq = s.notice.as_ref().map_or(1, |n| n.seq + 1);
1597 s.notice = Some(Notice {
1598 seq,
1599 device,
1600 outcome,
1601 });
1602 });
1603}
1604
1605fn answers_on_network(id: &str) -> bool {
1607 with(|s| s.nearby.iter().any(|n| n.hello.id == id && n.hello.acks))
1608}
1609
1610fn answers_by_link(id: &str) -> bool {
1613 crate::remote::profile::current().is_some_and(|p| p.offers(crate::remote::profile::ACK))
1614 && with(|s| s.account.iter().any(|(d, _)| d.id == id && d.acks))
1615}
1616
1617fn ask_server(
1621 id: &str,
1622 cmd: &LinkCommand,
1623 ack: Option<u64>,
1624) -> Result<Option<acks::AckOutcome>, String> {
1625 let cfg = Config::load().unwrap_or_default();
1626 let client = crate::helpers::subsonic_client(&cfg).ok_or("not signed in to a server")?;
1627 let json = serde_json::to_string(cmd).map_err(|e| e.to_string())?;
1628 client
1629 .koan_command(id, &json, ack)
1630 .map_err(|e| e.to_string())
1631}
1632
1633pub fn send_by_server_first(id: &str, cmd: LinkCommand) -> Result<(), String> {
1640 server_first(
1641 || {
1642 ask_server(id, &cmd, Some(acks::next_id())).map(|outcome| match outcome {
1643 Some(acks::AckOutcome::Refused { reason }) => Err(reason),
1644 outcome => {
1645 told(id, &cmd, outcome, None);
1646 Ok(())
1647 }
1648 })?
1649 },
1650 || send(id, cmd.clone()),
1651 )
1652}
1653
1654fn server_first(
1655 server: impl FnOnce() -> Result<(), String>,
1656 otherwise: impl FnOnce() -> Result<(), String>,
1657) -> Result<(), String> {
1658 server().or_else(|why| {
1659 log::info!("devices: the server did not take it ({why}); sending it here");
1660 otherwise()
1661 })
1662}
1663
1664pub fn listed_owner(id: &str) -> Option<Option<String>> {
1667 with(|s| {
1668 s.account
1669 .iter()
1670 .find(|(d, _)| d.id == id)
1671 .map(|(d, _)| d.owner.clone())
1672 })
1673}
1674
1675pub fn this_id() -> Option<String> {
1677 local().map(|l| l.identity.device_id.clone())
1678}
1679
1680#[cfg(test)]
1681pub(crate) mod tests {
1682 use super::*;
1683
1684 #[test]
1688 fn a_hello_that_does_not_look_keeps_the_tv_record() {
1689 let _held = STORE_LOCK.lock();
1690 crate::config::isolate_config_for_tests();
1691 with(|s| *s = Store::default());
1692 let tv = crate::remote::airplay::Tv {
1693 name: "Living Room".into(),
1694 host: "Living-Room.local.".into(),
1695 mac: Some("AA:BB:CC:DD:EE:FF".into()),
1696 ip: Some("10.0.0.9".into()),
1697 };
1698 with(|s| {
1699 s.seen.push(SeenNearby {
1700 id: "tv".into(),
1701 name: "Living Room".into(),
1702 platform: "tvos".into(),
1703 addr: "10.0.0.9:5626".into(),
1704 at: 0,
1705 tv: Some(tv.clone()),
1706 });
1707 s.tv_looked
1708 .insert(("tv".into(), "10.0.0.9".into()), Instant::now());
1709 });
1710 let mut hello = hello("tv");
1711 hello.platform = "tvos".into();
1712 nearby_hello(hello, "10.0.0.9:5626");
1713 assert_eq!(tv_of("tv").map(|t| t.host), Some(tv.host));
1714 }
1715
1716 #[test]
1717 fn an_apple_tv_is_looked_for_once_per_host_for_a_while() {
1718 let mut looked = HashMap::new();
1719 let t0 = Instant::now();
1720 assert!(look_for_tv(&mut looked, "tv", "living-room.local.", t0));
1721 assert!(!look_for_tv(
1722 &mut looked,
1723 "tv",
1724 "living-room.local.",
1725 t0 + Duration::from_secs(5)
1726 ));
1727 assert!(look_for_tv(
1729 &mut looked,
1730 "tv",
1731 "10.0.0.7",
1732 t0 + Duration::from_secs(5)
1733 ));
1734 assert!(look_for_tv(&mut looked, "other", "living-room.local.", t0));
1735 assert!(look_for_tv(
1736 &mut looked,
1737 "tv",
1738 "living-room.local.",
1739 t0 + TV_LOOK_AGAIN
1740 ));
1741 }
1742
1743 #[test]
1744 fn a_live_send_with_no_way_through_sends_nothing() {
1745 assert!(!send_live(
1747 "nowhere-at-all",
1748 LinkCommand::WatchLevels { on: true }
1749 ));
1750 }
1751
1752 fn device(id: &str, playing: bool) -> LinkDevice {
1753 LinkDevice {
1754 id: id.into(),
1755 name: id.into(),
1756 platform: "macos".into(),
1757 linked: true,
1758 state: Some(LinkState {
1759 playing,
1760 position_ms: 1000,
1761 duration_ms: 2000,
1762 ..Default::default()
1763 }),
1764 last_seen: None,
1765 wakeable: None,
1766 owner: None,
1767 acks: false,
1768 }
1769 }
1770
1771 pub(crate) static STORE_LOCK: Mutex<()> = Mutex::new(());
1774
1775 fn hello(id: &str) -> LinkHello {
1776 LinkHello {
1777 id: id.into(),
1778 name: id.into(),
1779 platform: "ios".into(),
1780 library: None,
1781 acks: false,
1782 nonce: None,
1783 }
1784 }
1785
1786 #[test]
1787 fn the_lock_screen_goes_by_the_server_and_falls_back_only_when_refused() {
1788 let local = std::cell::Cell::new(0);
1789 let here = || {
1790 local.set(local.get() + 1);
1791 Ok(())
1792 };
1793 assert_eq!(server_first(|| Ok(()), here), Ok(()));
1794 assert_eq!(local.get(), 0, "the server took it: nothing sent here");
1795
1796 assert_eq!(server_first(|| Err("no such device".into()), here), Ok(()));
1797 assert_eq!(local.get(), 1, "refused there: sent here instead");
1798
1799 assert_eq!(
1800 server_first(|| Err("offline".into()), || Err("not reachable".into())),
1801 Err("not reachable".to_string())
1802 );
1803 }
1804
1805 fn playing(track: &str, entry: &str) -> LinkState {
1807 LinkState {
1808 queue: vec![crate::remote::link::LinkQueueEntry {
1809 id: Some(entry.into()),
1810 track_id: Some(track.into()),
1811 current: true,
1812 ..Default::default()
1813 }],
1814 ..Default::default()
1815 }
1816 }
1817
1818 #[test]
1822 fn a_playing_devices_playhead_runs_on_from_its_report() {
1823 let _held = STORE_LOCK.lock();
1824 crate::config::isolate_config_for_tests();
1825 with(|s| *s = Store::default());
1826 nearby_hello(hello("speaker"), "10.0.0.6:5626");
1827 let at = |playing| LinkState {
1828 playing,
1829 position_ms: 1000,
1830 duration_ms: 60_000,
1831 ..Default::default()
1832 };
1833 let position = || {
1834 list()
1835 .into_iter()
1836 .find(|d| d.id == "speaker")
1837 .unwrap()
1838 .position_ms()
1839 };
1840
1841 nearby_state("speaker", at(true));
1842 std::thread::sleep(Duration::from_millis(50));
1843 assert!(position() >= 1050, "{}", position());
1844
1845 nearby_state("speaker", at(false));
1846 std::thread::sleep(Duration::from_millis(50));
1847 assert_eq!(position(), 1000, "paused, it stays put");
1848 }
1849
1850 #[test]
1851 fn a_command_queued_or_refused_becomes_the_notice_unless_its_sender_takes_it() {
1852 let _held = STORE_LOCK.lock();
1853 crate::config::isolate_config_for_tests();
1854 with(|s| *s = Store::default());
1855 nearby_hello(hello("phone"), "10.0.0.5:5626");
1856
1857 told(
1858 "phone",
1859 &LinkCommand::Pause,
1860 Some(acks::AckOutcome::Done),
1861 None,
1862 );
1863 told("phone", &LinkCommand::Pause, None, None);
1864 assert_eq!(notice(), None, "arrived, or sent as ever: nothing to say");
1865
1866 told(
1867 "phone",
1868 &LinkCommand::Pause,
1869 Some(acks::AckOutcome::Queued),
1870 None,
1871 );
1872 let queued = notice().unwrap();
1873 assert_eq!(
1874 (queued.device.as_str(), &queued.outcome),
1875 ("phone", &acks::AckOutcome::Queued)
1876 );
1877
1878 let failed = acks::AckOutcome::Failed {
1879 error: "gone".into(),
1880 };
1881 told("phone", &LinkCommand::Pause, Some(failed.clone()), None);
1882 assert_eq!(
1883 notice().unwrap().seq,
1884 queued.seq + 1,
1885 "the same twice is still news"
1886 );
1887
1888 let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
1890 let into = got.clone();
1891 let before = notice();
1892 told(
1893 "phone",
1894 &LinkCommand::Pause,
1895 Some(failed.clone()),
1896 Some(Box::new(move |o| *into.lock() = Some(o))),
1897 );
1898 assert_eq!(*got.lock(), Some(Some(failed)));
1899 assert_eq!(notice(), before, "not said twice");
1900 }
1901
1902 #[test]
1907 fn failing_to_ask_the_server_is_no_answer_not_a_refusal() {
1908 let _held = STORE_LOCK.lock();
1909 crate::config::isolate_config_for_tests();
1910 with(|s| *s = Store::default());
1911 nearby_hello(hello("phone"), "10.0.0.5:5626");
1912
1913 let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
1914 let into = got.clone();
1915 unanswered(
1916 "phone",
1917 &LinkCommand::Pause,
1918 "server unreachable".into(),
1919 Some(Box::new(move |o| *into.lock() = Some(o))),
1920 );
1921 assert_eq!(*got.lock(), Some(None), "no answer, not a refusal");
1922 assert_eq!(notice(), None);
1923
1924 unanswered(
1925 "phone",
1926 &LinkCommand::Pause,
1927 "server unreachable".into(),
1928 None,
1929 );
1930 assert!(matches!(
1931 notice().map(|n| n.outcome),
1932 Some(acks::AckOutcome::Failed { .. })
1933 ));
1934 }
1935
1936 #[test]
1937 fn a_hand_off_is_taken_only_when_the_device_reports_the_track() {
1938 let _held = STORE_LOCK.lock();
1939 crate::config::isolate_config_for_tests();
1940 with(|s| *s = Store::default());
1941 nearby_hello(hello("phone"), "10.0.0.5:5626");
1942 nearby_state("phone", playing("before", "e1"));
1943 let short = Duration::from_millis(50);
1944
1945 let before = last_report("phone");
1947 assert!(!await_current("phone", "handed", before.as_ref(), short));
1948
1949 nearby_state("phone", playing("handed", "e2"));
1952 let before = last_report("phone");
1953 let mut seeked = playing("handed", "e2");
1954 seeked.position_ms = 5000;
1955 nearby_state("phone", seeked);
1956 assert!(!await_current("phone", "handed", before.as_ref(), short));
1957
1958 let reporter = std::thread::spawn(|| {
1960 std::thread::sleep(Duration::from_millis(20));
1961 nearby_state("phone", playing("other", "e3"));
1962 });
1963 assert!(!await_current(
1964 "phone",
1965 "handed",
1966 before.as_ref(),
1967 Duration::from_millis(200)
1968 ));
1969 reporter.join().unwrap();
1970
1971 let before = last_report("phone");
1973 let reporter = std::thread::spawn(|| {
1974 std::thread::sleep(Duration::from_millis(20));
1975 nearby_state("phone", playing("handed", "e4"));
1976 });
1977 assert!(await_current(
1978 "phone",
1979 "handed",
1980 before.as_ref(),
1981 Duration::from_secs(5)
1982 ));
1983 reporter.join().unwrap();
1984 }
1985
1986 #[test]
1987 fn a_device_naming_no_entries_is_judged_on_its_whole_report() {
1988 let unnamed = |track: &str, position_ms| LinkState {
1989 position_ms,
1990 queue: vec![crate::remote::link::LinkQueueEntry {
1991 track_id: Some(track.into()),
1992 current: true,
1993 ..Default::default()
1994 }],
1995 ..Default::default()
1996 };
1997 let before = unnamed("handed", 0);
1998 assert!(!took(Some(&before), &before, "handed"));
1999 assert!(took(Some(&before), &unnamed("handed", 10), "handed"));
2000 assert!(took(None, &unnamed("handed", 0), "handed"));
2001 }
2002
2003 #[test]
2004 fn a_device_that_stops_answering_sleeps_after_a_heartbeat_and_stays() {
2005 let _held = STORE_LOCK.lock();
2006 crate::config::isolate_config_for_tests();
2007 with(|s| *s = Store::default());
2008 let week = 7 * 24 * 60 * 60;
2009
2010 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2011 used("stranger");
2012 nearby_gone("stranger");
2013 let now = with(|s| s.live["stranger"]);
2014 let at = |t: i64| list_at(t).into_iter().find(|d| d.id == "stranger");
2015
2016 let missed_one = at(now + STALE_SECS - 1).expect("still listed");
2017 assert!(
2018 !missed_one.asleep,
2019 "one missed heartbeat does not make it asleep"
2020 );
2021 assert!(
2022 !missed_one.awake && !missed_one.nearby,
2023 "nor does it pretend to be reachable"
2024 );
2025 let asleep = at(now + STALE_SECS + 1).expect("listed asleep");
2026 assert!(asleep.asleep && !asleep.awake && !asleep.wakeable);
2027 assert_eq!(asleep.last_seen, Some(now));
2028 assert!(asleep.state.is_none(), "what it was doing is not news");
2029 assert!(
2030 at(now + week).is_some_and(|d| d.asleep),
2031 "kept long past the old grace, until forgotten"
2032 );
2033 assert_eq!(
2034 next_change(now),
2035 Some(now + STALE_SECS),
2036 "the clock wakes for the next crossing"
2037 );
2038 assert_eq!(next_change(now + STALE_SECS), None, "and then for nothing");
2039
2040 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2042 let back = at(now + STALE_SECS + 5).unwrap();
2043 assert!(back.awake && back.nearby && !back.asleep);
2044
2045 nearby_gone("stranger");
2047 with(|s| *s.live.get_mut("stranger").unwrap() -= STALE_SECS + 10);
2048 assert!(choosable("stranger").is_err());
2049 assert!(send("stranger", LinkCommand::Pause).is_err());
2050
2051 let mut phone = device("phone", false);
2053 phone.linked = false;
2054 phone.last_seen = Some(now - week);
2055 phone.wakeable = Some(true);
2056 set_account(vec![phone]);
2057 used("phone");
2058 let phone = list_at(now).into_iter().find(|d| d.id == "phone").unwrap();
2059 assert!(phone.asleep && phone.wakeable);
2060 assert_eq!(phone.last_seen, Some(now - week));
2061 assert!(choosable("phone").is_ok());
2062
2063 set_linked(true);
2066 set_account(vec![device("mac", false)]);
2067 used("mac");
2068 set_account(Vec::new());
2069 let left = with(|s| s.live["mac"]);
2070 let mac = |t: i64| list_at(t).into_iter().find(|d| d.id == "mac");
2071 let reconnecting = mac(left + 1).unwrap();
2072 assert!(!reconnecting.awake && !reconnecting.asleep);
2073 let asleep = mac(left + STALE_SECS + 1).unwrap();
2074 assert!(asleep.asleep && !asleep.wakeable && asleep.account);
2075 assert!(mac(left + week).is_some());
2076 with(|s| *s = Store::default());
2077 }
2078
2079 #[test]
2082 fn only_a_device_used_here_is_kept_asleep() {
2083 let _held = STORE_LOCK.lock();
2084 crate::config::isolate_config_for_tests();
2085 with(|s| *s = Store::default());
2086
2087 nearby_hello(hello("passing"), "10.0.0.5:5626");
2088 nearby_hello(hello("played-on"), "10.0.0.6:5626");
2089 used("played-on");
2090 nearby_gone("passing");
2091 nearby_gone("played-on");
2092 let left = with(|s| s.live["passing"]);
2093 let listed = |t: i64, id: &str| list_at(t).into_iter().any(|d| d.id == id);
2094 assert!(listed(left + 1, "passing"), "reconnecting for a heartbeat");
2095 assert!(
2096 !listed(left + STALE_SECS + 1, "passing"),
2097 "never used: gone"
2098 );
2099 assert!(listed(left + STALE_SECS + 1, "played-on"), "used: asleep");
2100
2101 let mut phone = device("old-phone", false);
2103 phone.linked = false;
2104 phone.last_seen = Some(left - 3600);
2105 set_account(vec![phone]);
2106 assert!(!listed(left, "old-phone"));
2107
2108 with(|s| *s = Store::default());
2109 restore();
2110 let later = left + STALE_SECS + 1;
2111 assert!(
2112 list_at(later)
2113 .iter()
2114 .any(|d| d.id == "played-on" && d.asleep),
2115 "kept across a relaunch"
2116 );
2117 assert!(!listed(later, "passing"));
2118
2119 forget("played-on").unwrap();
2120 assert!(!list().iter().any(|d| d.id == "played-on"));
2121 with(|s| *s = Store::default());
2122 restore();
2123 assert!(
2124 !list().iter().any(|d| d.id == "played-on"),
2125 "forgotten on disk"
2126 );
2127 with(|s| *s = Store::default());
2128 }
2129
2130 #[test]
2133 fn the_controlled_device_is_kept_and_an_old_file_keeps_only_it() {
2134 let _held = STORE_LOCK.lock();
2135 crate::config::isolate_config_for_tests();
2136 with(|s| *s = Store::default());
2137 let gone = |id: &str| Departed {
2138 id: id.into(),
2139 name: id.into(),
2140 platform: "ios".into(),
2141 account: false,
2142 same_library: false,
2143 shared: false,
2144 };
2145 let old = serde_json::json!({
2146 "target": {"id": "tv", "name": "tv", "platform": "tvos"},
2147 "account": [],
2148 "live": {"tv": 1, "sim": 1},
2149 "departed": [gone("tv"), gone("sim")],
2150 });
2151 std::fs::write(saved_path(), old.to_string()).unwrap();
2152 restore();
2153 let ids: Vec<String> = list().into_iter().map(|d| d.id).collect();
2154 assert_eq!(
2155 ids,
2156 ["tv"],
2157 "the target stays; the simulator nobody used goes"
2158 );
2159
2160 set_target(None);
2161 nearby_hello(hello("mac"), "10.0.0.7:5626");
2162 set_target(Some("mac".into()));
2163 set_target(None);
2164 nearby_gone("mac");
2165 with(|s| *s.live.get_mut("mac").unwrap() -= STALE_SECS + 10);
2166 assert!(
2167 list().iter().any(|d| d.id == "mac" && d.asleep),
2168 "controlled once: kept"
2169 );
2170 with(|s| *s = Store::default());
2171 }
2172
2173 #[test]
2176 fn a_forgotten_device_is_gone_until_it_comes_back() {
2177 let _held = STORE_LOCK.lock();
2178 crate::config::isolate_config_for_tests();
2179 with(|s| *s = Store::default());
2180 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2181 assert!(forget("stranger").is_err(), "not while it is reachable");
2182 used("stranger");
2183 nearby_gone("stranger");
2184 with(|s| *s = Store::default());
2186 restore();
2187 assert!(list().iter().any(|d| d.id == "stranger"));
2188 forget("stranger").unwrap();
2189 assert!(!list().iter().any(|d| d.id == "stranger"));
2190 assert!(remembered_nearby().iter().all(|n| n.id != "stranger"));
2191 with(|s| *s = Store::default());
2192 restore();
2193 assert!(
2194 !list().iter().any(|d| d.id == "stranger"),
2195 "forgotten on disk too"
2196 );
2197 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2198 assert!(list().iter().any(|d| d.id == "stranger" && d.awake));
2199 with(|s| *s = Store::default());
2200 }
2201
2202 #[test]
2205 fn an_account_device_is_forgotten_through_the_server() {
2206 let _held = STORE_LOCK.lock();
2207 crate::config::isolate_config_for_tests();
2208 with(|s| *s = Store::default());
2209 let mut phone = device("phone", false);
2210 phone.linked = false;
2211 phone.wakeable = Some(true);
2212 phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2213 set_account(vec![phone]);
2214 used("phone");
2215 assert!(
2216 forget("phone").is_err(),
2217 "no link here: the server would still list it and wake it"
2218 );
2219 assert!(list().iter().any(|d| d.id == "phone"));
2220 forgotten("phone");
2222 assert!(!list().iter().any(|d| d.id == "phone"));
2223 with(|s| *s = Store::default());
2224 }
2225
2226 fn walk(recent: bool, answers_in: Option<Waking>) -> (Vec<Waking>, Ended) {
2229 let plan = wake_plan(false, recent, true);
2230 let mut started = Vec::new();
2231 let current = std::cell::RefCell::new(None);
2232 let ended = run_wake(
2233 &plan,
2234 || true,
2235 |stage| {
2236 started.push(stage.clone());
2237 *current.borrow_mut() = Some(stage.clone());
2238 Ok(())
2239 },
2240 |_| *current.borrow() == answers_in,
2241 );
2242 (started, ended)
2243 }
2244
2245 #[test]
2246 fn waking_tries_the_network_first_only_when_it_was_there_a_moment_ago() {
2247 let (tried, _) = walk(true, None);
2248 assert_eq!(tried, [Waking::Network, Waking::Push, Waking::Notification]);
2249 let (tried, _) = walk(false, None);
2250 assert_eq!(tried, [Waking::Push, Waking::Notification]);
2251 assert_eq!(
2252 wake_plan(false, true, false)
2253 .iter()
2254 .map(|(w, _)| w.clone())
2255 .collect::<Vec<_>>(),
2256 [Waking::Network],
2257 "a stranger is only ever tried on the network"
2258 );
2259 assert!(wake_plan(false, false, false).is_empty());
2260 }
2261
2262 #[test]
2263 fn an_apple_tv_on_record_is_woken_before_the_push() {
2264 let stages =
2265 |plan: Vec<(Waking, Duration)>| plan.into_iter().map(|(w, _)| w).collect::<Vec<_>>();
2266 assert_eq!(
2267 stages(wake_plan(true, false, true)),
2268 [Waking::Tv, Waking::Push, Waking::Notification]
2269 );
2270 assert_eq!(
2271 stages(wake_plan(true, true, true)),
2272 [
2273 Waking::Tv,
2274 Waking::Network,
2275 Waking::Push,
2276 Waking::Notification
2277 ]
2278 );
2279 }
2280
2281 #[test]
2282 fn an_apple_tv_met_on_the_network_keeps_its_airplay_record() {
2283 let _held = STORE_LOCK.lock();
2284 crate::config::isolate_config_for_tests();
2285 with(|s| *s = Store::default());
2286 let tv = crate::remote::airplay::Tv {
2288 name: "koan test TV".into(),
2289 host: "koan-test-tv.local.".into(),
2290 mac: Some("AA:BB:CC:DD:EE:FF".into()),
2291 ip: Some("10.0.0.4".into()),
2292 };
2293 let hello = |id: &str| LinkHello {
2294 name: "koan test TV".into(),
2295 platform: "tvos".into(),
2296 ..hello(id)
2297 };
2298 nearby_hello(hello("tv"), "koan-test-tv.local:50000");
2299 with(|s| s.seen[0].tv = Some(tv.clone()));
2300 nearby_hello(hello("tv"), "KOAN-test-tv.local:50001");
2302 assert_eq!(tv_of("tv"), Some(tv));
2303 nearby_hello(hello("tv"), "koan-test-other.local:50001");
2305 assert_eq!(tv_of("tv"), None);
2306 with(|s| *s = Store::default());
2307 }
2308
2309 #[test]
2310 fn a_wake_stops_at_the_stage_that_reached_it() {
2311 let (tried, ended) = walk(true, Some(Waking::Network));
2312 assert_eq!(tried, [Waking::Network], "no push for a phone still awake");
2313 assert_eq!(ended, Ended::Reached(Waking::Network));
2314 let (tried, ended) = walk(false, Some(Waking::Push));
2315 assert_eq!(
2316 tried,
2317 [Waking::Push],
2318 "no notification once the push worked"
2319 );
2320 assert_eq!(ended, Ended::Reached(Waking::Push));
2321 }
2322
2323 #[test]
2324 fn a_push_that_does_not_wake_it_falls_back_to_a_notification_then_says_so() {
2325 let (tried, ended) = walk(false, None);
2326 assert_eq!(tried.last(), Some(&Waking::Notification));
2327 assert!(matches!(ended, Ended::Failed(why) if why.contains("notification")));
2328 let plan = wake_plan(false, false, true);
2329 assert!(plan[0].1 <= Duration::from_secs(8) && plan[0].1 >= Duration::from_secs(5));
2330 }
2331
2332 #[test]
2333 fn a_stage_that_cannot_start_ends_the_wake_with_why() {
2334 let ended = run_wake(
2335 &wake_plan(false, false, true),
2336 || true,
2337 |_| Err("Not connected".into()),
2338 |_| true,
2339 );
2340 assert_eq!(ended, Ended::Failed("Not connected".into()));
2341 }
2342
2343 #[test]
2346 fn a_wake_abandoned_for_another_choice_sends_nothing_more() {
2347 let live = std::cell::Cell::new(true);
2348 let mut tried = Vec::new();
2349 let ended = run_wake(
2350 &wake_plan(false, true, true),
2351 || live.get(),
2352 |stage| {
2353 tried.push(stage.clone());
2354 Ok(())
2355 },
2356 |_| {
2357 live.set(false);
2358 false
2359 },
2360 );
2361 assert_eq!(tried, [Waking::Network]);
2362 assert_eq!(ended, Ended::Cancelled);
2363 }
2364
2365 #[test]
2366 fn one_wake_at_a_time_joined_by_the_same_choice_and_ended_by_another() {
2367 let _held = STORE_LOCK.lock();
2368 with(|s| *s = Store::default());
2369 let first = begin_wake("phone").expect("starts");
2370 assert!(begin_wake("phone").is_none(), "choosing it again joins it");
2371 assert!(wake_is_current(first));
2372 cancel_wake(Some("phone"));
2373 assert!(wake_is_current(first), "the same choice does not end it");
2374 cancel_wake(Some("mac"));
2375 assert!(!wake_is_current(first), "another choice does");
2376 let second = begin_wake("mac").unwrap();
2377 assert!(begin_wake("phone").is_some() && !wake_is_current(second));
2378 with(|s| *s = Store::default());
2379 }
2380
2381 #[test]
2384 fn a_device_chosen_seconds_after_it_dropped_is_woken() {
2385 let _held = STORE_LOCK.lock();
2386 crate::config::isolate_config_for_tests();
2387 with(|s| *s = Store::default());
2388 set_linked(true);
2389 set_account(vec![device("phone", false)]);
2390 let mut phone = device("phone", false);
2391 phone.linked = false;
2392 phone.wakeable = Some(true);
2393 phone.last_seen = Some(chrono::Utc::now().timestamp() - 10);
2394 set_account(vec![phone]);
2395 let listed = list().into_iter().find(|d| d.id == "phone").unwrap();
2396 assert!(!listed.asleep && !listed.awake, "reconnecting, not asleep");
2397 wake("phone");
2398 let ran = (0..100).any(|_| {
2401 let seen = with(|s| {
2402 s.attempt.as_ref().is_some_and(|(_, d)| d == "phone")
2403 || s.waking.contains_key("phone")
2404 });
2405 if !seen {
2406 std::thread::sleep(Duration::from_millis(10));
2407 }
2408 seen
2409 });
2410 assert!(ran, "the pipeline runs");
2411 cancel_wake(None);
2412 with(|s| *s = Store::default());
2413 }
2414
2415 #[test]
2419 fn last_heard_on_the_network_is_the_last_message_not_the_first() {
2420 let _held = STORE_LOCK.lock();
2421 crate::config::isolate_config_for_tests();
2422 with(|s| *s = Store::default());
2423 nearby_hello(hello("phone"), "10.0.0.7:5626");
2424 with(|s| {
2425 for seen in &mut s.seen {
2426 seen.at -= 10 * 60;
2427 }
2428 });
2429 nearby_state("phone", LinkState::default());
2430 nearby_gone("phone");
2431 let left = with(|s| s.lan_heard["phone"]);
2432 assert!(recently_on_network("phone", left + 30));
2433 assert!(!recently_on_network("phone", left + NETWORK_RECENT + 1));
2434 with(|s| *s = Store::default());
2435 }
2436
2437 #[test]
2441 fn a_stranger_asleep_on_the_network_is_woken_through_the_server_if_linked() {
2442 let _held = STORE_LOCK.lock();
2443 crate::config::isolate_config_for_tests();
2444 with(|s| *s = Store::default());
2445 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2446 used("stranger");
2447 nearby_gone("stranger");
2448 with(|s| {
2449 *s.live.get_mut("stranger").unwrap() -= 10 * 60;
2450 *s.lan_heard.get_mut("stranger").unwrap() -= 10 * 60;
2451 });
2452 let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2453 assert!(listed.asleep && !listed.wakeable, "no server to ask");
2454 wake("stranger");
2455 assert!(
2456 with(|s| s.attempt.is_none() && s.waking.is_empty()),
2457 "too long gone for the network, and no server"
2458 );
2459
2460 set_linked(true);
2461 let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2462 assert!(listed.asleep && listed.wakeable);
2463 assert!(choosable("stranger").is_ok());
2464 let plan = wake_plan(false, false, listed.wakeable);
2465 assert_eq!(plan[0].0, Waking::Push, "asks the server");
2466 with(|s| *s = Store::default());
2467 }
2468
2469 #[test]
2473 fn a_hand_off_asked_by_a_stranger_never_goes_up_the_link() {
2474 let _held = STORE_LOCK.lock();
2475 crate::config::isolate_config_for_tests();
2476 with(|s| *s = Store::default());
2477 set_linked(true);
2478 set_account(vec![device("mac", false)]);
2479 let play = LinkCommand::Play {
2480 track_ids: vec!["t".into()],
2481 start_at: 0,
2482 position_ms: 0,
2483 paused: false,
2484 handoff: true,
2485 mode: crate::player::state::QueueMode::Keep,
2486 };
2487 assert!(
2488 send_nearby("mac", play).is_err(),
2489 "the account's Mac is reached only through the link"
2490 );
2491 with(|s| *s = Store::default());
2492 }
2493
2494 #[test]
2497 fn a_hand_off_for_someone_else_never_reaches_the_accounts_own_devices() {
2498 let _held = STORE_LOCK.lock();
2499 crate::config::isolate_config_for_tests();
2500 with(|s| *s = Store::default());
2501 set_linked(true);
2502 set_account(vec![device("mac", false)]);
2503 let play = || LinkCommand::Play {
2504 track_ids: vec!["t".into()],
2505 start_at: 0,
2506 position_ms: 0,
2507 paused: false,
2508 handoff: true,
2509 mode: crate::player::state::QueueMode::Keep,
2510 };
2511 for source in [
2512 CommandSource::Shared,
2513 CommandSource::Nearby,
2514 CommandSource::Stranger,
2515 ] {
2516 let refused = send_for(source, "mac", play()).unwrap_err();
2517 assert!(
2518 refused.contains("this account's") || refused.contains("not on this network"),
2519 "{source:?}: {refused}"
2520 );
2521 }
2522 with(|s| *s = Store::default());
2523 }
2524
2525 #[test]
2527 fn a_device_no_longer_shared_is_dropped_at_once() {
2528 let _held = STORE_LOCK.lock();
2529 crate::config::isolate_config_for_tests();
2530 with(|s| *s = Store::default());
2531 set_linked(true);
2532 let mut theirs = device("their-phone", false);
2533 theirs.owner = Some("k".into());
2534 set_account(vec![theirs]);
2535 let listed = list().into_iter().find(|d| d.id == "their-phone").unwrap();
2536 assert_eq!(listed.owner.as_deref(), Some("k"));
2537 assert!(!listed.account, "not this account's own");
2538 assert!(send("their-phone", LinkCommand::Sync { full: false }).is_err());
2539 set_account(Vec::new());
2540 assert!(!list().iter().any(|d| d.id == "their-phone"));
2541 with(|s| *s = Store::default());
2542 }
2543
2544 #[test]
2547 fn a_stranger_that_just_left_cannot_be_chosen() {
2548 let _held = STORE_LOCK.lock();
2549 crate::config::isolate_config_for_tests();
2550 with(|s| *s = Store::default());
2551 nearby_hello(hello("stranger"), "10.0.0.5:5626");
2552 nearby_gone("stranger");
2553 let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2554 assert!(!listed.awake && !listed.asleep && !listed.nearby);
2555 assert!(choosable("stranger").is_err());
2556 assert!(send("stranger", LinkCommand::Pause).is_err());
2557 with(|s| *s = Store::default());
2558 }
2559
2560 #[test]
2563 fn a_list_held_across_a_gap_in_the_link_does_not_stamp_now() {
2564 let _held = STORE_LOCK.lock();
2565 crate::config::isolate_config_for_tests();
2566 with(|s| *s = Store::default());
2567 set_linked(true);
2568 set_account(vec![device("phone", true)]);
2569 used("phone");
2570 with(|s| *s.live.get_mut("phone").unwrap() -= 4 * 60 * 60);
2572 set_linked(false);
2573 let hours_ago = chrono::Utc::now().timestamp() - 3 * 60 * 60;
2574 set_linked(true);
2575 let mut phone = device("phone", false);
2576 phone.linked = false;
2577 phone.last_seen = Some(hours_ago);
2578 set_account(vec![phone]);
2579 let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2580 assert!(phone.asleep, "not reconnecting: it went hours ago");
2581 assert!(phone.last_seen.unwrap() >= hours_ago);
2584 assert!(
2585 phone.last_seen.unwrap() < chrono::Utc::now().timestamp() - STALE_SECS,
2586 "not seen just now"
2587 );
2588 with(|s| *s = Store::default());
2589 }
2590
2591 #[test]
2594 fn a_restored_device_the_server_drops_is_gone() {
2595 let _held = STORE_LOCK.lock();
2596 crate::config::isolate_config_for_tests();
2597 with(|s| *s = Store::default());
2598 let saved = Saved {
2600 target: None,
2601 account: vec![device("mac", true), device("phone", true)],
2602 nearby: Vec::new(),
2603 live: HashMap::new(),
2604 departed: Vec::new(),
2605 used: None,
2606 };
2607 std::fs::write(saved_path(), serde_json::to_string(&saved).unwrap()).unwrap();
2608 restore();
2609 let listed = list();
2610 assert!(listed.iter().all(|d| d.state.is_none() && !d.awake));
2611 set_linked(true);
2612 let mut phone = device("phone", false);
2613 phone.linked = false;
2614 phone.wakeable = Some(true);
2615 set_account(vec![phone]);
2616 assert!(
2617 !list().iter().any(|d| d.id == "mac"),
2618 "never seen here, and not listed: dropped"
2619 );
2620 with(|s| *s = Store::default());
2621 }
2622
2623 #[test]
2626 fn a_device_the_server_cannot_push_is_not_wakeable() {
2627 let _held = STORE_LOCK.lock();
2628 crate::config::isolate_config_for_tests();
2629 with(|s| *s = Store::default());
2630 let mut phone = device("phone", false);
2631 phone.linked = false;
2632 phone.wakeable = Some(false);
2633 phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2634 set_account(vec![phone]);
2635 used("phone");
2636 let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2637 assert!(phone.asleep && !phone.wakeable);
2638 assert!(choosable("phone").is_err());
2639 with(|s| *s = Store::default());
2640 }
2641
2642 #[test]
2643 fn a_device_found_both_ways_is_one_and_a_lost_target_is_kept() {
2644 let _held = STORE_LOCK.lock();
2645 crate::config::isolate_config_for_tests();
2646 with(|s| *s = Store::default());
2647 set_account(vec![device("mac", false), device("phone", true)]);
2648 let listed = list();
2649 assert_eq!(listed[0].id, "phone", "playing first");
2650 assert_eq!(listed.len(), 2);
2651
2652 nearby_hello(
2653 LinkHello {
2654 id: "mac".into(),
2655 name: "Mac".into(),
2656 platform: "macos".into(),
2657 library: None,
2658 acks: false,
2659 nonce: None,
2660 },
2661 "mac.local:5626",
2662 );
2663 nearby_hello(
2664 LinkHello {
2665 id: "tv".into(),
2666 name: "Living room".into(),
2667 platform: "macos".into(),
2668 library: Some("elsewhere".into()),
2669 acks: false,
2670 nonce: None,
2671 },
2672 "10.0.0.9:5626",
2673 );
2674 let listed = list();
2675 assert_eq!(listed.len(), 3);
2676 let mac = listed.iter().find(|d| d.id == "mac").unwrap();
2677 assert!(mac.account && mac.nearby && mac.same_library);
2678 let tv = listed.iter().find(|d| d.id == "tv").unwrap();
2679 assert!(!tv.account && !tv.same_library);
2680
2681 set_target(Some("tv".into()));
2682 nearby_gone("tv");
2683 with(|s| *s.live.get_mut("tv").unwrap() -= STALE_SECS + 31 * 60);
2685 assert_eq!(target(), Some("tv".into()), "kept while out of reach");
2686 let tv = list().into_iter().find(|d| d.id == "tv").unwrap();
2687 assert_eq!(tv.name, "Living room");
2688 assert!(!tv.awake && tv.asleep);
2689
2690 with(|s| *s = Store::default());
2692 restore();
2693 assert_eq!(target(), Some("tv".into()));
2694 let listed = list();
2695 assert!(
2696 listed.iter().any(|d| d.id == "mac" && !d.awake),
2697 "last heard of, not linked"
2698 );
2699 let remembered = remembered_nearby();
2700 assert!(
2701 remembered
2702 .iter()
2703 .any(|n| n.id == "tv" && n.addr == "10.0.0.9:5626")
2704 );
2705
2706 nearby_gone("mac");
2710 let play = LinkCommand::Play {
2711 track_ids: vec!["t".into()],
2712 start_at: 0,
2713 position_ms: 0,
2714 paused: false,
2715 handoff: true,
2716 mode: crate::player::state::QueueMode::Keep,
2717 };
2718 assert!(listed.iter().any(|d| d.id == "mac" && d.account));
2719 assert!(send_nearby("mac", play).is_err());
2720
2721 set_target(None);
2722 set_account(Vec::new());
2723 nearby_gone("mac");
2724 assert!(
2725 list().iter().all(|d| d.last_seen.is_some()),
2726 "only what has just left, kept for a heartbeat"
2727 );
2728 with(|s| *s = Store::default());
2729 }
2730}