Skip to main content

koan_core/remote/
devices.rs

1//! The devices this one can play on, and which of them it is controlling.
2//!
3//! Two sources: the account's other devices, which the server sends down the
4//! link, and whatever answers on the local network (`remote::nearby`), which
5//! may belong to anyone. A device found both ways is one device, reached over
6//! the local network while that connection is up: it is the shorter path.
7//!
8//! The device being controlled and the account's devices as last heard of are
9//! kept on disk, so an app iOS suspended or killed opens still controlling the
10//! same device, with the list drawn at once rather than after the link is back.
11//!
12//! A device is listed while it is present: linked, or on the network. One
13//! that stops answering is not dropped on the spot. It stays as it was for
14//! one full heartbeat past when it should have been heard from, so a single
15//! missed signal changes nothing. Then, if this device has used it (controlled
16//! it, or sent it music or a command), it is listed asleep, with when it was
17//! last seen, until it is heard from again or forgotten (`forget`); one never
18//! used is gone, since an asleep device nobody here plays on is only clutter.
19//! Asleep, it can be chosen only if a push can wake it. A device heard from
20//! again at any point is back at once, forgotten or not.
21
22use 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/// Another device, as the app shows it.
35#[derive(Debug, Clone, PartialEq)]
36pub struct Device {
37    pub id: String,
38    pub name: String,
39    pub platform: String,
40    /// Signed in to the same account on the same server.
41    pub account: bool,
42    /// Whose it is, for a device another account on the server shares with
43    /// this one: playback and the queue only, as on the local network.
44    pub owner: Option<String>,
45    /// Connected to over the local network now.
46    pub nearby: bool,
47    /// Reachable at once. An account device that is not is one iOS has
48    /// suspended: a command wakes it, and music reaches it as a notification.
49    pub awake: bool,
50    /// Not reachable for longer than a heartbeat. Not reachable for less, it
51    /// is neither awake nor asleep: reconnecting, as far as anyone can tell.
52    pub asleep: bool,
53    /// Can be woken from asleep: the server holds a push token for it.
54    /// An asleep device that cannot be is shown, and cannot be chosen.
55    pub wakeable: bool,
56    /// Unix seconds when it was last reachable; `None` if never, here.
57    pub last_seen: Option<i64>,
58    /// How far waking it has got, while it is being woken or once it failed.
59    pub waking: Option<Waking>,
60    /// Plays from the same library, so music can be handed between the two.
61    pub same_library: bool,
62    /// What it last reported.
63    pub state: Option<LinkState>,
64    /// When `state` was heard.
65    pub heard: Instant,
66    /// Why it cannot be reached, when found but not connected: shown rather
67    /// than leaving the device out, so the picker says what is wrong.
68    pub problem: Option<String>,
69}
70
71impl Device {
72    /// Where its playhead is now, from where it was reported to be.
73    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    /// Whether a command sent now can reach it.
87    pub fn reach(&self) -> Reach {
88        if let Some(problem) = &self.problem {
89            // The controlled device missing from every list has only the
90            // state itself to say, which is no reason.
91            return Reach::OutOfReach((problem != OUT_OF_REACH).then(|| problem.clone()));
92        }
93        if self.awake {
94            return Reach::Connected;
95        }
96        match &self.waking {
97            Some(Waking::Failed(_)) | None => {}
98            Some(_) => return Reach::Waking,
99        }
100        // Heard from within a heartbeat: a dropped connection is most likely
101        // being made again, and saying so is better than a flash of red.
102        if !self.asleep {
103            return Reach::Reconnecting;
104        }
105        if self.wakeable {
106            return Reach::Asleep;
107        }
108        Reach::OutOfReach(match &self.waking {
109            Some(Waking::Failed(why)) => Some(why.clone()),
110            _ => None,
111        })
112    }
113}
114
115/// The problem of the controlled device when no list carries it.
116const OUT_OF_REACH: &str = "Out of reach";
117
118/// How a device can be reached, as every front end shows it. Derived from
119/// the device's connections, not from a command failing, so a front end can
120/// say a device is out of reach before anything is sent to it.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum Reach {
123    /// Over the local network or the server's link, now.
124    Connected,
125    /// Not heard from for less than a heartbeat: a dropped connection being
126    /// made again, as far as anyone can tell.
127    Reconnecting,
128    /// Being woken, by the network, a push or a notification to tap.
129    Waking,
130    /// Suspended, and a command wakes it: it is queued for the device.
131    Asleep,
132    /// Nothing reaches it and nothing can wake it: a command goes nowhere.
133    /// Why, where that is known.
134    OutOfReach(Option<String>),
135}
136
137#[derive(Default)]
138struct Store {
139    linked: bool,
140    /// When the link that is up came up, in Unix seconds.
141    linked_at: Option<i64>,
142    account: Vec<(LinkDevice, Instant)>,
143    nearby: Vec<Nearby>,
144    seen: Vec<SeenNearby>,
145    target: Option<Remembered>,
146    version: u64,
147    /// When each device was last reachable, in Unix seconds.
148    live: HashMap<String, i64>,
149    /// Devices no list carries any more, kept until they are forgotten.
150    departed: Vec<Departed>,
151    /// The devices this one has used, by id: listed asleep when out of reach.
152    used: HashSet<String>,
153    /// `account` came down the link that is up now. One held from before the
154    /// link last dropped says nothing about who is reachable at this moment.
155    fresh: bool,
156    /// Devices being woken, or that did not wake, by id.
157    waking: HashMap<String, Waking>,
158    /// When each device was last heard from on the local network, in Unix
159    /// seconds: any message, and the moment it disconnected.
160    lan_heard: HashMap<String, i64>,
161    /// The wake under way, as its generation and the device: one at a time.
162    attempt: Option<(u64, String)>,
163    /// Bumped by every wake started or abandoned.
164    wake_gen: u64,
165    /// The accounts this device lets control it, as the server says.
166    shares: Vec<String>,
167    /// Why the server refused the last change to them.
168    share_error: Option<String>,
169    /// Every account on the server, to share with.
170    accounts: Vec<String>,
171    /// The last command that did not simply arrive, for the app to say so.
172    notice: Option<Notice>,
173    /// When each Apple TV was last looked for among AirPlay announcements, by
174    /// id and host: one with none, or none that matches, is not looked for
175    /// again at every hello.
176    tv_looked: HashMap<(String, String), Instant>,
177}
178
179/// A command that was queued for a device asleep, or did not reach one: what
180/// the app tells the person, since the device's state cannot show it.
181#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct Notice {
183    /// Counts up, so the same notice twice is still news.
184    pub seq: u64,
185    /// The device's name, as listed.
186    pub device: String,
187    pub outcome: acks::AckOutcome,
188    /// No way got it there: `outcome` is a failure with only the reason.
189    pub unreached: bool,
190}
191
192/// Why a command did not get to a device.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub enum Missed {
195    /// Nothing reaches it from here: not on this network, and no server that
196    /// relays to it. Says so by the device's name.
197    OutOfReach(String),
198    /// It was reached, or the server answered, and this is the reason.
199    Said(String),
200}
201
202impl Missed {
203    pub fn into_message(self) -> String {
204        match self {
205            Self::OutOfReach(m) | Self::Said(m) => m,
206        }
207    }
208}
209
210impl From<String> for Missed {
211    fn from(message: String) -> Self {
212        Self::Said(message)
213    }
214}
215
216/// The last command that did not simply arrive.
217pub fn notice() -> Option<Notice> {
218    with(|s| s.notice.clone())
219}
220
221/// What to do with a command's outcome once it is known: `None` when it was
222/// sent by a way that brings no answer.
223pub type Then = Box<dyn FnOnce(Option<acks::AckOutcome>) + Send>;
224
225/// A stage of waking a device, in the order they are tried.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub enum Waking {
228    /// Knocking on an Apple TV's AirPlay service until the box answers: a
229    /// sleeping one is woken by the sleep proxy answering for it, so that the
230    /// push after this can reach koan there.
231    Tv,
232    /// Dialling it on the local network: seen there a moment ago, it may not
233    /// be suspended yet.
234    Network,
235    /// A background push sent through the server. iOS delays or drops these
236    /// as it sees fit, and never delivers one to an app swiped away.
237    Push,
238    /// A notification on the device, asking to be tapped.
239    Notification,
240    /// None of it worked; why, for the device's row.
241    Failed(String),
242}
243
244/// A device that dropped out of every list: as it was when last reachable.
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
246struct Departed {
247    id: String,
248    name: String,
249    platform: String,
250    account: bool,
251    same_library: bool,
252    /// Another account's, shared with this one.
253    shared: bool,
254}
255
256/// One heartbeat: a link that hears nothing for this long pings, and one that
257/// still hears nothing is gone. A device is asleep only after a full one has
258/// passed without it.
259pub const STALE_SECS: i64 = super::wire::IDLE.as_secs() as i64;
260
261/// What is kept on disk. See the module note.
262#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
263struct Saved {
264    target: Option<Remembered>,
265    account: Vec<LinkDevice>,
266    #[serde(default)]
267    nearby: Vec<SeenNearby>,
268    /// When each device was last reachable, Unix seconds: kept so a device
269    /// the server no longer lists is not taken for one seen just now.
270    #[serde(default)]
271    live: HashMap<String, i64>,
272    /// Devices no list carries any more, kept until forgotten.
273    #[serde(default)]
274    departed: Vec<Departed>,
275    /// The devices this one has used. Absent from a file written before it
276    /// was kept, when only the device being controlled counts.
277    #[serde(default)]
278    used: Option<Vec<String>>,
279}
280
281/// A device reached on the local network, and where: dialled there at once
282/// on the next run, before Bonjour has announced anything.
283#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
284pub struct SeenNearby {
285    pub id: String,
286    pub name: String,
287    pub platform: String,
288    pub addr: String,
289    /// Unix seconds.
290    pub at: i64,
291    /// An Apple TV's AirPlay announcement, knocked on to wake it.
292    #[serde(default, skip_serializing_if = "Option::is_none")]
293    pub tv: Option<crate::remote::airplay::Tv>,
294}
295
296/// A device not reached on the network in this long is forgotten.
297const NEARBY_KEPT: i64 = 7 * 24 * 60 * 60;
298
299/// The device being controlled, named so that it can be shown while it is
300/// out of reach.
301#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
302struct Remembered {
303    id: String,
304    name: String,
305    platform: String,
306}
307
308struct Nearby {
309    hello: LinkHello,
310    state: Option<LinkState>,
311    at: Instant,
312}
313
314static STORE: Mutex<Option<Store>> = Mutex::new(None);
315static LOCAL: OnceLock<Local> = OnceLock::new();
316
317fn with<R>(f: impl FnOnce(&mut Store) -> R) -> R {
318    let mut store = STORE.lock();
319    f(store.get_or_insert_with(Store::default))
320}
321
322/// `f` changed something the app shows.
323fn changed<R>(f: impl FnOnce(&mut Store) -> R) -> R {
324    let out = with(|s| {
325        let out = f(s);
326        s.version += 1;
327        out
328    });
329    crate::signal::engine_changed().bump();
330    replan();
331    out
332}
333
334/// Rung when the store changes, so the clock looks again at when the list
335/// next changes on its own.
336static CLOCK: (Mutex<bool>, Condvar) = (Mutex::new(false), Condvar::new());
337
338/// A device going asleep, or past its grace period, changes the list with
339/// nothing having happened. One thread sleeps until the next such moment and
340/// announces it; it is woken early whenever the store changes.
341fn replan() {
342    static STARTED: OnceLock<()> = OnceLock::new();
343    *CLOCK.0.lock() = true;
344    CLOCK.1.notify_all();
345    STARTED.get_or_init(|| {
346        let _ = std::thread::Builder::new()
347            .name("koan-devices-clock".into())
348            .spawn(clock);
349    });
350}
351
352fn clock() {
353    loop {
354        let now = chrono::Utc::now().timestamp();
355        let wait = next_change(now).map(|at| Duration::from_secs((at - now).max(0) as u64 + 1));
356        let mut rung = CLOCK.0.lock();
357        if !*rung {
358            let timed_out = match wait {
359                Some(wait) => CLOCK.1.wait_for(&mut rung, wait).timed_out(),
360                None => {
361                    CLOCK.1.wait(&mut rung);
362                    false
363                }
364            };
365            if timed_out {
366                drop(rung);
367                // Not through `changed`, which would ring the clock again.
368                with(|s| s.version += 1);
369                crate::signal::engine_changed().bump();
370                continue;
371            }
372        }
373        *rung = false;
374    }
375}
376
377impl Store {
378    fn save(&self) {
379        let saved = Saved {
380            target: self.target.clone(),
381            account: self.account.iter().map(|(d, _)| d.clone()).collect(),
382            nearby: self.seen.clone(),
383            live: self.live.clone(),
384            departed: self.departed.clone(),
385            used: Some({
386                let mut used: Vec<String> = self.used.iter().cloned().collect();
387                used.sort();
388                used
389            }),
390        };
391        if let Ok(json) = serde_json::to_string(&saved) {
392            let _ = std::fs::write(saved_path(), json);
393        }
394    }
395}
396
397fn saved_path() -> std::path::PathBuf {
398    let files = LOCAL.get().map_or("", |l| l.identity.files);
399    crate::config::config_dir().join(format!("{files}devices.json"))
400}
401
402/// The target and account devices as the last run left them.
403fn restore() {
404    let Ok(text) = std::fs::read_to_string(saved_path()) else {
405        return;
406    };
407    let Ok(saved) = serde_json::from_str::<Saved>(&text) else {
408        return;
409    };
410    let now = Instant::now();
411    let cutoff = chrono::Utc::now().timestamp() - NEARBY_KEPT;
412    changed(|s| {
413        s.target = saved.target;
414        s.seen = saved
415            .nearby
416            .into_iter()
417            .filter(|n| n.at >= cutoff)
418            .collect();
419        s.live = saved.live;
420        s.used = match saved.used {
421            Some(used) => used.into_iter().collect(),
422            None => s.target.iter().map(|t| t.id.clone()).collect(),
423        };
424        let used = &s.used;
425        s.departed = saved
426            .departed
427            .into_iter()
428            .filter(|g| used.contains(&g.id))
429            .collect();
430        // Not linked until the server says so, and doing nothing anyone
431        // knows of: what it was playing then is not what it is playing now.
432        s.account = saved
433            .account
434            .into_iter()
435            .map(|d| {
436                (
437                    LinkDevice {
438                        linked: false,
439                        state: None,
440                        ..d
441                    },
442                    now,
443                )
444            })
445            .collect();
446    });
447}
448
449/// Link to the server and open this device to the local network, serving
450/// `local` to both. Once per process.
451pub fn start(local: Local) {
452    if LOCAL.set(local.clone()).is_err() {
453        return;
454    }
455    restore();
456    link::spawn(local.clone());
457    crate::remote::nearby::start(local);
458}
459
460/// This device, once started.
461pub fn local() -> Option<&'static Local> {
462    LOCAL.get()
463}
464
465/// The account's devices stay listed while the link is down, as they were
466/// last heard of: iOS drops the link each time it suspends the app, and a
467/// phone controlling a Mac should still be when it wakes. The server sends
468/// them afresh when the link is back.
469///
470/// Nothing here lets go of the target. A device that drops out of every list
471/// (a Mac asleep, a server restarting) is still the one the person picked,
472/// and the app shows it as out of reach until it is back or another is.
473pub fn set_linked(linked: bool) {
474    crate::remote::offline::link_changed(linked);
475    changed(|s| {
476        if linked != s.linked {
477            s.linked_at = linked.then(|| chrono::Utc::now().timestamp());
478        }
479        s.linked = linked;
480        if !linked {
481            s.fresh = false;
482        }
483    });
484}
485
486/// When the link that is up came up, in Unix seconds.
487pub fn linked_since() -> Option<i64> {
488    with(|s| s.linked.then_some(s.linked_at).flatten())
489}
490
491/// The account's other devices linked to the server now, each of which can
492/// control this one through it. Devices other accounts share with this one
493/// are this account's to control, not the other way round; the accounts this
494/// one is shared with are `shares`.
495pub fn linked_controllers() -> Vec<LinkDevice> {
496    let me = this_id();
497    with(|s| {
498        s.account
499            .iter()
500            .map(|(d, _)| d)
501            .filter(|d| d.linked && d.owner.is_none() && Some(&d.id) != me.as_ref())
502            .cloned()
503            .collect()
504    })
505}
506
507/// The name `id` goes by, as the server or the network last gave it.
508pub fn name_of(id: &str) -> Option<String> {
509    with(|s| {
510        s.account
511            .iter()
512            .map(|(d, _)| (&d.id, &d.name))
513            .chain(s.seen.iter().map(|n| (&n.id, &n.name)))
514            .find(|(i, _)| *i == id)
515            .map(|(_, name)| name.clone())
516    })
517}
518
519pub fn set_account(devices: Vec<LinkDevice>) {
520    let now = Instant::now();
521    let unix = chrono::Utc::now().timestamp();
522    changed(|s| {
523        // Reachable up to this message, if the list saying so came down this
524        // same link: the hysteresis runs from here, not from whenever it was
525        // last reported. A list held across a gap in the link proves nothing.
526        if s.fresh {
527            for (old, _) in &s.account {
528                if old.linked {
529                    s.live.insert(old.id.clone(), unix);
530                }
531            }
532        }
533        for d in &devices {
534            if d.linked {
535                s.live.insert(d.id.clone(), unix);
536                s.waking.remove(&d.id);
537            } else if let Some(seen) = d.last_seen {
538                // Whichever is later: this device may have reached it over
539                // the network since.
540                let live = s.live.entry(d.id.clone()).or_insert(seen);
541                *live = (*live).max(seen);
542            }
543            s.departed.retain(|g| g.id != d.id);
544        }
545        let leaving: Vec<Departed> = s
546            .account
547            .iter()
548            .filter(|(old, _)| !devices.iter().any(|d| d.id == old.id))
549            .map(|(old, _)| Departed {
550                id: old.id.clone(),
551                name: old.name.clone(),
552                platform: old.platform.clone(),
553                account: old.owner.is_none(),
554                shared: old.owner.is_some(),
555                same_library: true,
556            })
557            .collect();
558        for d in leaving {
559            // Never seen reachable as far as this device knows: gone, rather
560            // than taken for a device seen just now. And a device another
561            // account stops sharing is gone at once, not asleep.
562            if !s.live.contains_key(&d.id) || d.shared {
563                continue;
564            }
565            s.departed.retain(|g| g.id != d.id);
566            s.departed.push(d);
567        }
568        s.account = devices.into_iter().map(|d| (d, now)).collect();
569        s.fresh = s.linked;
570        s.save();
571    });
572}
573
574/// The accounts this device is shared with, as the server last said, and
575/// why it refused the last change, if it did.
576pub fn set_shares(grantees: Vec<String>, error: Option<String>, accounts: Vec<String>) {
577    changed(|s| {
578        s.shares = grantees;
579        s.share_error = error;
580        s.accounts = accounts;
581    });
582}
583
584/// The server's accounts, to share this device with.
585pub fn accounts() -> Vec<String> {
586    with(|s| s.accounts.clone())
587}
588
589pub fn shares() -> Vec<String> {
590    with(|s| s.shares.clone())
591}
592
593pub fn share_error() -> Option<String> {
594    with(|s| s.share_error.clone())
595}
596
597/// Signed out, or signed in elsewhere: the shares were that server's.
598pub fn forget_shares() {
599    set_shares(Vec::new(), None, Vec::new());
600}
601
602/// Send `cmd` to `id` because `source` asked this device to: a hand-off.
603///
604/// The account's own request goes anywhere. Anyone else's acts as the asker,
605/// never with this account's powers, so it does not reach this account's own
606/// devices up its link: a stranger's (Playback only) goes over the network or
607/// nowhere; a sharing account's, or a trusted network device's, goes over the
608/// network or to a device that is not this account's (the asker's own,
609/// through the server).
610pub fn send_for(source: CommandSource, id: &str, cmd: LinkCommand) -> Result<(), String> {
611    send_for_then(source, id, cmd, None)
612}
613
614/// `send_for`, handing the outcome to `then` rather than to the app's notice.
615pub fn send_for_then(
616    source: CommandSource,
617    id: &str,
618    cmd: LinkCommand,
619    then: Option<Then>,
620) -> Result<(), String> {
621    match source {
622        CommandSource::Account => send_then(id, cmd, then),
623        CommandSource::Stranger => {
624            send_nearby(id, cmd)?;
625            if let Some(then) = then {
626                then(None);
627            }
628            Ok(())
629        }
630        CommandSource::Shared | CommandSource::Nearby => {
631            let (nearby, own) = with(|s| {
632                (
633                    s.nearby.iter().any(|n| n.hello.id == id),
634                    s.account
635                        .iter()
636                        .any(|(d, _)| d.id == id && d.owner.is_none()),
637                )
638            });
639            if own && !nearby {
640                return Err(format!("{id} is this account's, not the asker's"));
641            }
642            send_then(id, cmd, then)
643        }
644    }
645}
646
647/// Send `cmd` to `id` over the local network only, never up this device's
648/// link. For what a stranger asked of this device: over the network it can
649/// only reach what the stranger could have reached itself; over the link it
650/// would act as this device's account.
651pub fn send_nearby(id: &str, cmd: LinkCommand) -> Result<(), String> {
652    let nearby = with(|s| s.nearby.iter().any(|n| n.hello.id == id));
653    if nearby && cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd) {
654        Ok(())
655    } else {
656        Err(format!("{id} is not on this network"))
657    }
658}
659
660/// Let the account `grantee` on this server control this device, or stop.
661/// The server answers with the list as it now stands.
662pub fn share(grantee: &str, allow: bool) -> Result<(), String> {
663    let grantee = grantee.trim();
664    if grantee.is_empty() {
665        return Err("Name an account on your server.".into());
666    }
667    if link::report(LinkReport::Share {
668        grantee: grantee.to_string(),
669        allow,
670    }) {
671        Ok(())
672    } else {
673        Err("Not connected to your server.".into())
674    }
675}
676
677/// The devices reached on this network before, to dial first.
678pub fn remembered_nearby() -> Vec<SeenNearby> {
679    with(|s| s.seen.clone())
680}
681
682pub fn nearby_hello(hello: LinkHello, addr: &str) {
683    let host = crate::remote::airplay::host_of(addr).to_string();
684    let look = changed(|s| {
685        let kept = s
686            .seen
687            .iter()
688            .find(|n| n.id == hello.id)
689            .and_then(|n| n.tv.clone());
690        let look = hello.platform == "tvos"
691            && !crate::remote::airplay::current(kept.as_ref(), &host)
692            && look_for_tv(&mut s.tv_looked, &hello.id, &host, Instant::now());
693        // A record the look replaces is dropped; with no look, the one kept
694        // stands, as one matched by name rather than host does.
695        let tv = if look { None } else { kept };
696        s.seen.retain(|n| n.id != hello.id);
697        s.seen.push(SeenNearby {
698            id: hello.id.clone(),
699            name: hello.name.clone(),
700            platform: hello.platform.clone(),
701            addr: addr.to_string(),
702            at: chrono::Utc::now().timestamp(),
703            tv,
704        });
705        s.save();
706        s.live
707            .insert(hello.id.clone(), chrono::Utc::now().timestamp());
708        s.departed.retain(|g| g.id != hello.id);
709        s.waking.remove(&hello.id);
710        s.lan_heard
711            .insert(hello.id.clone(), chrono::Utc::now().timestamp());
712        s.nearby.retain(|n| n.hello.id != hello.id);
713        let found = look.then(|| (hello.id.clone(), hello.name.clone()));
714        s.nearby.push(Nearby {
715            hello,
716            state: None,
717            at: Instant::now(),
718        });
719        found
720    });
721    if let Some((id, name)) = look {
722        record_tv(id, name, addr.to_string());
723    }
724}
725
726/// How long a look for an Apple TV's AirPlay announcement stands, found or
727/// not, before the same TV at the same host is looked for again.
728const TV_LOOK_AGAIN: Duration = Duration::from_secs(10 * 60);
729
730/// Whether to look for the AirPlay announcement of the Apple TV `id` at
731/// `host` now, noting the look if so. A look under way counts.
732fn look_for_tv(
733    looked: &mut HashMap<(String, String), Instant>,
734    id: &str,
735    host: &str,
736    now: Instant,
737) -> bool {
738    looked.retain(|_, at| now.saturating_duration_since(*at) < TV_LOOK_AGAIN);
739    let key = (id.to_string(), host.to_string());
740    if looked.contains_key(&key) {
741        return false;
742    }
743    looked.insert(key, now);
744    true
745}
746
747/// Find the AirPlay announcement of the Apple TV `id`, met at `addr`, and keep
748/// it to wake the TV by when koan there is asleep.
749fn record_tv(id: String, name: String, addr: String) {
750    let _ = std::thread::Builder::new()
751        .name("koan-airplay".into())
752        .spawn(move || {
753            let Some(tv) = crate::remote::airplay::record(&addr, &name) else {
754                log::info!("airplay: {name}: no AirPlay announcement from {addr}");
755                return;
756            };
757            log::info!(
758                "airplay: {name} is {} on {} ({})",
759                tv.name,
760                tv.host,
761                tv.mac.as_deref().unwrap_or("no MAC")
762            );
763            with(|s| {
764                if let Some(n) = s.seen.iter_mut().find(|n| n.id == id) {
765                    n.tv = Some(tv);
766                    s.save();
767                }
768            });
769        });
770}
771
772/// The Apple TV `id` is, as its AirPlay announcement had it while awake.
773fn tv_of(id: &str) -> Option<crate::remote::airplay::Tv> {
774    with(|s| {
775        s.seen
776            .iter()
777            .find(|n| n.id == id)
778            .and_then(|n| n.tv.clone())
779    })
780}
781
782pub fn nearby_state(id: &str, state: LinkState) {
783    changed(|s| {
784        if let Some(n) = s.nearby.iter_mut().find(|n| n.hello.id == id) {
785            n.state = Some(state);
786            n.at = Instant::now();
787            let now = chrono::Utc::now().timestamp();
788            s.live.insert(id.to_string(), now);
789            s.lan_heard.insert(id.to_string(), now);
790        }
791    });
792}
793
794pub fn nearby_gone(id: &str) {
795    let cfg = Config::load().unwrap_or_default();
796    let ours = link::library_fingerprint(&cfg);
797    changed(|s| {
798        let Some(at) = s.nearby.iter().position(|n| n.hello.id == id) else {
799            return;
800        };
801        let gone = s.nearby.remove(at);
802        let now = chrono::Utc::now().timestamp();
803        s.live.insert(id.to_string(), now);
804        s.lan_heard.insert(id.to_string(), now);
805        // An account device is still listed by the server; only a stranger
806        // has to be remembered here.
807        if s.account.iter().any(|(d, _)| d.id == id) {
808            return;
809        }
810        s.departed.retain(|g| g.id != id);
811        s.departed.push(Departed {
812            id: gone.hello.id.clone(),
813            name: gone.hello.name.clone(),
814            platform: gone.hello.platform.clone(),
815            account: false,
816            same_library: ours.is_some() && gone.hello.library == ours,
817            shared: false,
818        });
819        s.save();
820    });
821}
822
823/// Forget the device `id`: out of the list until it links or is heard on the
824/// network again, which brings it straight back. An account device is
825/// forgotten by the server too, push token and all, and every other device
826/// of the account drops it. A device another account shares with this one
827/// is forgotten by declining the share. Only a
828/// device out of reach is forgotten: one reachable would be back at once.
829pub fn forget(id: &str) -> Result<(), String> {
830    let device = list().into_iter().find(|d| d.id == id);
831    if device.as_ref().is_some_and(|d| d.awake) {
832        return Err("Only a device out of reach can be forgotten.".into());
833    }
834    let account = with(|s| s.account.iter().any(|(d, _)| d.id == id));
835    if account
836        && !link::report(LinkReport::Forget {
837            device: id.to_string(),
838        })
839    {
840        return Err("Not connected to your server, which remembers it too.".into());
841    }
842    forgotten(id);
843    Ok(())
844}
845
846/// This device sent `id` something: from now on it is kept, asleep, when it
847/// is out of reach.
848fn used(id: &str) {
849    with(|s| {
850        if s.used.insert(id.to_string()) {
851            s.save();
852        }
853    });
854}
855
856/// Drop `id` from everything this device remembers of it: the server forgot
857/// it, or this device was asked to.
858pub fn forgotten(id: &str) {
859    changed(|s| {
860        s.used.remove(id);
861        s.account.retain(|(d, _)| d.id != id);
862        s.departed.retain(|g| g.id != id);
863        s.seen.retain(|n| n.id != id);
864        s.live.remove(id);
865        s.lan_heard.remove(id);
866        s.waking.remove(id);
867        if s.target.as_ref().is_some_and(|t| t.id == id) {
868            s.target = None;
869        }
870        s.save();
871    });
872    log::info!("devices: forgot {id}");
873}
874
875/// Something beside the list that the app shows with it has changed: the
876/// server's profile, whether this device is listening.
877pub fn touch() {
878    changed(|_| ());
879}
880
881/// Bumped by every change to what `list` returns, and by `touch`.
882pub fn version() -> u64 {
883    with(|s| s.version)
884}
885
886/// Whether the link to the server is up.
887pub fn linked() -> bool {
888    with(|s| s.linked)
889}
890
891/// Every other device, those playing first.
892pub fn list() -> Vec<Device> {
893    list_at(chrono::Utc::now().timestamp())
894}
895
896/// `list` as of `now`, Unix seconds.
897fn list_at(now: i64) -> Vec<Device> {
898    let cfg = Config::load().unwrap_or_default();
899    let ours = link::library_fingerprint(&cfg);
900    with(|s| {
901        let live = |id: &str| s.live.get(id).copied();
902        // Missed for less than a heartbeat.
903        let fresh = |id: &str| live(id).is_some_and(|at| now - at <= STALE_SECS);
904        // Out of reach past the heartbeat, only a device used here is listed.
905        let kept = |id: &str| fresh(id) || s.used.contains(id);
906        let mut out: Vec<Device> = s
907            .account
908            .iter()
909            .filter_map(|(d, at)| {
910                let near = s.nearby.iter().find(|n| n.hello.id == d.id);
911                let awake = d.linked || near.is_some();
912                if !awake && !kept(&d.id) {
913                    return None;
914                }
915                Some(Device {
916                    id: d.id.clone(),
917                    name: d.name.clone(),
918                    platform: d.platform.clone(),
919                    account: d.owner.is_none(),
920                    owner: d.owner.clone(),
921                    nearby: near.is_some(),
922                    awake,
923                    asleep: !awake && !fresh(&d.id),
924                    // As the server says: a push token, and a push key to
925                    // send with. One that says nothing lists only those.
926                    wakeable: d.wakeable.unwrap_or(true),
927                    last_seen: live(&d.id),
928                    waking: None,
929                    same_library: true,
930                    // The network's state is fresher, but only the server's
931                    // carries the outputs: the network is told none.
932                    state: match (near.and_then(|n| n.state.clone()), &d.state) {
933                        (Some(mut fresh), Some(linked)) => {
934                            fresh.outputs = linked.outputs.clone();
935                            Some(fresh)
936                        }
937                        (Some(fresh), None) => Some(fresh),
938                        (None, linked) => linked.clone(),
939                    },
940                    heard: near.map_or(*at, |n| n.at),
941                    problem: None,
942                })
943            })
944            .collect();
945        for n in &s.nearby {
946            if out.iter().any(|d| d.id == n.hello.id) {
947                continue;
948            }
949            out.push(Device {
950                id: n.hello.id.clone(),
951                name: n.hello.name.clone(),
952                platform: n.hello.platform.clone(),
953                account: false,
954                owner: None,
955                nearby: true,
956                awake: true,
957                asleep: false,
958                wakeable: false,
959                last_seen: live(&n.hello.id),
960                waking: None,
961                same_library: ours.is_some() && n.hello.library == ours,
962                state: n.state.clone(),
963                heard: n.at,
964                problem: None,
965            });
966        }
967        for g in &s.departed {
968            if out.iter().any(|d| d.id == g.id) || !kept(&g.id) {
969                continue;
970            }
971            let last = live(&g.id);
972            // Nothing routes to it now, whatever the label: reconnecting for
973            // a heartbeat, then asleep.
974            out.push(Device {
975                id: g.id.clone(),
976                name: g.name.clone(),
977                platform: g.platform.clone(),
978                account: g.account,
979                owner: None,
980                nearby: false,
981                awake: false,
982                asleep: !fresh(&g.id),
983                // A stranger met on this network may be woken by the server,
984                // which does so for a device behind the same router as this
985                // one; one of the account's that left the list has no token.
986                wakeable: !g.account && s.linked,
987                last_seen: last,
988                waking: None,
989                same_library: g.same_library,
990                // What it was doing is not what it is doing now.
991                state: None,
992                heard: Instant::now(),
993                problem: None,
994            });
995        }
996        // A device announced on this network but not connected is left out:
997        // it cannot be played on, and a phone that left or a stranger's app
998        // that is not discoverable would sit in the list as a reason only.
999        // The device being controlled, out of every list for now.
1000        if let Some(t) = &s.target
1001            && !out.iter().any(|d| d.id == t.id)
1002        {
1003            out.push(Device {
1004                id: t.id.clone(),
1005                name: t.name.clone(),
1006                platform: t.platform.clone(),
1007                account: false,
1008                owner: None,
1009                nearby: false,
1010                awake: false,
1011                asleep: true,
1012                wakeable: false,
1013                last_seen: live(&t.id),
1014                waking: None,
1015                same_library: true,
1016                state: None,
1017                heard: Instant::now(),
1018                problem: Some(OUT_OF_REACH.into()),
1019            });
1020        }
1021        for d in &mut out {
1022            d.waking = s.waking.get(&d.id).cloned();
1023        }
1024        out.sort_by_key(|d| {
1025            (
1026                !d.state.as_ref().is_some_and(|st| st.playing),
1027                !d.awake,
1028                d.name.to_lowercase(),
1029            )
1030        });
1031        out
1032    })
1033}
1034
1035/// When `list` next changes on its own: a device crossing into asleep.
1036fn next_change(now: i64) -> Option<i64> {
1037    with(|s| {
1038        let reachable = |id: &str| {
1039            s.nearby.iter().any(|n| n.hello.id == id)
1040                || s.account.iter().any(|(d, _)| d.id == id && d.linked)
1041        };
1042        s.live
1043            .iter()
1044            .filter(|(id, _)| !reachable(id))
1045            .map(|(_, at)| at + STALE_SECS)
1046            .filter(|t| *t > now)
1047            .min()
1048    })
1049}
1050
1051/// Whether the app may choose `id`: not when nothing reaches it now and
1052/// nothing can wake it, where a command would go nowhere.
1053pub fn choosable(id: &str) -> Result<(), String> {
1054    match list().into_iter().find(|d| d.id == id) {
1055        Some(d) if !d.awake && !d.wakeable && d.problem.is_none() => Err(if d.asleep {
1056            format!("{} is asleep and cannot be woken from here.", d.name)
1057        } else {
1058            format!("{} is reconnecting.", d.name)
1059        }),
1060        _ => Ok(()),
1061    }
1062}
1063
1064/// A device seen on the local network this recently is dialled there first.
1065const NETWORK_RECENT: i64 = 60;
1066/// How long each stage is given to bring the device in before the next.
1067const NETWORK_WAIT: Duration = Duration::from_secs(3);
1068const PUSH_WAIT: Duration = Duration::from_secs(6);
1069const NOTIFICATION_WAIT: Duration = Duration::from_secs(45);
1070/// How long an Apple TV is knocked on before the push goes anyway.
1071const TV_KNOCK: Duration = Duration::from_secs(20);
1072/// After the box answers, how long koan there has to be heard from on the
1073/// network before the push: it may only have been asleep with the box.
1074const TV_WAIT: Duration = Duration::from_secs(2);
1075
1076/// The stages to try, each with how long to wait on it. A device that
1077/// nothing can wake through the server is only tried on the network. An
1078/// Apple TV with an AirPlay announcement on record is woken first, so the
1079/// box is up for what follows.
1080fn wake_plan(tv: bool, seen_nearby_recently: bool, wakeable: bool) -> Vec<(Waking, Duration)> {
1081    let mut plan = Vec::new();
1082    if tv {
1083        plan.push((Waking::Tv, TV_WAIT));
1084    }
1085    if seen_nearby_recently {
1086        plan.push((Waking::Network, NETWORK_WAIT));
1087    }
1088    if wakeable {
1089        plan.push((Waking::Push, PUSH_WAIT));
1090        plan.push((Waking::Notification, NOTIFICATION_WAIT));
1091    }
1092    plan
1093}
1094
1095/// How a wake ended.
1096#[derive(Debug, Clone, PartialEq, Eq)]
1097enum Ended {
1098    Reached(Waking),
1099    Failed(String),
1100    /// Another device was chosen meanwhile: nothing more is sent.
1101    Cancelled,
1102}
1103
1104/// Walk `plan`: `act` starts each stage, `wait` blocks until the device is
1105/// reachable, the stage's time is up or the wake is abandoned, saying whether
1106/// it was reached. `current` is false once another wake has replaced this one.
1107fn run_wake(
1108    plan: &[(Waking, Duration)],
1109    current: impl Fn() -> bool,
1110    mut act: impl FnMut(&Waking) -> Result<(), String>,
1111    mut wait: impl FnMut(Duration) -> bool,
1112) -> Ended {
1113    let mut last = None;
1114    for (stage, time) in plan {
1115        if !current() {
1116            return Ended::Cancelled;
1117        }
1118        if let Err(why) = act(stage) {
1119            return Ended::Failed(why);
1120        }
1121        last = Some(stage);
1122        if wait(*time) {
1123            return Ended::Reached(stage.clone());
1124        }
1125    }
1126    if !current() {
1127        return Ended::Cancelled;
1128    }
1129    Ended::Failed(match last {
1130        Some(Waking::Notification) => {
1131            "Did not wake. A notification is waiting on it to be tapped.".into()
1132        }
1133        _ => "Did not wake.".into(),
1134    })
1135}
1136
1137fn reachable(id: &str) -> bool {
1138    with(|s| {
1139        s.nearby.iter().any(|n| n.hello.id == id)
1140            || s.account.iter().any(|(d, _)| d.id == id && d.linked)
1141    })
1142}
1143
1144/// Heard from on the local network within `NETWORK_RECENT` of `now`.
1145fn recently_on_network(id: &str, now: i64) -> bool {
1146    with(|s| {
1147        s.lan_heard
1148            .get(id)
1149            .is_some_and(|at| now - at <= NETWORK_RECENT)
1150    })
1151}
1152
1153/// Start a wake of `id`: its generation, or `None` when one of `id` is
1154/// already under way, which this then joins. Abandons any other.
1155fn begin_wake(id: &str) -> Option<u64> {
1156    with(|s| {
1157        if s.attempt.as_ref().is_some_and(|(_, d)| d == id) {
1158            return None;
1159        }
1160        s.wake_gen += 1;
1161        s.attempt = Some((s.wake_gen, id.to_string()));
1162        Some(s.wake_gen)
1163    })
1164}
1165
1166fn wake_is_current(generation: u64) -> bool {
1167    with(|s| s.attempt.as_ref().is_some_and(|(g, _)| *g == generation))
1168}
1169
1170/// Abandon the wake under way, unless it is of `keep`.
1171fn cancel_wake(keep: Option<&str>) {
1172    let cancelled = with(|s| {
1173        let other = s
1174            .attempt
1175            .as_ref()
1176            .is_some_and(|(_, d)| Some(d.as_str()) != keep);
1177        if other {
1178            let (_, id) = s.attempt.take().expect("checked");
1179            s.waking.remove(&id);
1180            s.wake_gen += 1;
1181        }
1182        other
1183    });
1184    if cancelled {
1185        // The abandoned wake's thread is waiting on this.
1186        crate::signal::engine_changed().bump();
1187    }
1188}
1189
1190/// Wake `id`, which is not reachable, in the background: on the local network
1191/// if it was heard there in the last minute, then, when the server can wake
1192/// it, a background push and then a notification on it to tap. The device's
1193/// row says which stage it is at, and why it failed if it did. Each step is
1194/// logged with its time from the start, so a wake that did not happen shows
1195/// where it stopped.
1196///
1197/// One wake at a time: choosing another device abandons this one, so nothing
1198/// more reaches the device left behind, and choosing this one again joins it.
1199/// It does not wait for the device to be labelled asleep: a phone suspended
1200/// seconds ago is the one most worth waking.
1201pub fn wake(id: &str) {
1202    let id = id.to_string();
1203    if reachable(&id) {
1204        return;
1205    }
1206    let Some(device) = list().into_iter().find(|d| d.id == id) else {
1207        return;
1208    };
1209    let now = chrono::Utc::now().timestamp();
1210    // A stranger on the network has nothing to answer a push with.
1211    let tv = (device.platform == "tvos").then(|| tv_of(&id)).flatten();
1212    let plan = wake_plan(tv.is_some(), recently_on_network(&id, now), device.wakeable);
1213    if plan.is_empty() {
1214        return;
1215    }
1216    let Some(generation) = begin_wake(&id) else {
1217        log::info!("wake: {}: already being woken", device.name);
1218        return;
1219    };
1220    let name = device.name;
1221    let _ = std::thread::Builder::new()
1222        .name("koan-wake".into())
1223        .spawn(move || {
1224            let started = Instant::now();
1225            let ms = || started.elapsed().as_millis();
1226            let current = || wake_is_current(generation);
1227            let mut box_silent = false;
1228            let ended = run_wake(
1229                &plan,
1230                current,
1231                |stage| {
1232                    log::info!("wake: {name}: {stage:?} at +{}ms", ms());
1233                    changed(|s| s.waking.insert(id.clone(), stage.clone()));
1234                    match stage {
1235                        Waking::Tv => {
1236                            if let Some(tv) = &tv {
1237                                let awake = crate::remote::airplay::wake(tv, TV_KNOCK, || {
1238                                    !current() || reachable(&id)
1239                                });
1240                                box_silent = !awake && current() && !reachable(&id);
1241                                // Awake, koan there may answer the network
1242                                // before any push.
1243                                crate::remote::nearby::dial_now();
1244                            }
1245                            Ok(())
1246                        }
1247                        Waking::Network => {
1248                            crate::remote::nearby::dial_now();
1249                            Ok(())
1250                        }
1251                        Waking::Push | Waking::Notification => {
1252                            let notify = *stage == Waking::Notification;
1253                            if link::report(LinkReport::Wake {
1254                                to: id.clone(),
1255                                notify,
1256                            }) {
1257                                Ok(())
1258                            } else {
1259                                Err("Not connected to your server, which wakes it.".into())
1260                            }
1261                        }
1262                        Waking::Failed(_) => Ok(()),
1263                    }
1264                },
1265                |time| {
1266                    let until = Instant::now() + time;
1267                    let signal = crate::signal::engine_changed();
1268                    let mut seen = signal.generation();
1269                    while !reachable(&id) {
1270                        let left = until.saturating_duration_since(Instant::now());
1271                        if left.is_zero() || !current() {
1272                            return false;
1273                        }
1274                        seen = signal.wait_until(seen, left);
1275                    }
1276                    true
1277                },
1278            );
1279            let ended = match ended {
1280                Ended::Failed(_) if box_silent => Ended::Failed(format!(
1281                    "Couldn't wake {name}: the Apple TV did not answer on this network."
1282                )),
1283                ended => ended,
1284            };
1285            match &ended {
1286                Ended::Reached(stage) => {
1287                    log::info!("wake: {name}: reached at +{}ms, after {stage:?}", ms());
1288                }
1289                Ended::Failed(why) => {
1290                    log::warn!("wake: {name}: did not wake in {}ms: {why}", ms());
1291                }
1292                Ended::Cancelled => {
1293                    log::info!("wake: {name}: abandoned at +{}ms for another device", ms());
1294                }
1295            }
1296            changed(|s| {
1297                if !s.attempt.as_ref().is_some_and(|(g, _)| *g == generation) {
1298                    return;
1299                }
1300                s.attempt = None;
1301                match ended {
1302                    Ended::Failed(why) => {
1303                        s.waking.insert(id.clone(), Waking::Failed(why));
1304                    }
1305                    _ => {
1306                        s.waking.remove(&id);
1307                    }
1308                }
1309            });
1310        });
1311}
1312
1313/// The device the app is controlling; `None` for this one.
1314pub fn target() -> Option<String> {
1315    with(|s| s.target.as_ref().map(|t| t.id.clone()))
1316}
1317
1318pub fn set_target(id: Option<String>) {
1319    cancel_wake(id.as_deref());
1320    if let Some(id) = &id {
1321        crate::remote::nearby::release(id);
1322    }
1323    let listed = id
1324        .as_ref()
1325        .and_then(|id| list().into_iter().find(|d| d.id == *id));
1326    changed(|s| {
1327        if let Some(id) = &id {
1328            s.used.insert(id.clone());
1329        }
1330        s.target = id.map(|id| match listed {
1331            Some(d) => Remembered {
1332                id,
1333                name: d.name,
1334                platform: d.platform,
1335            },
1336            None => Remembered {
1337                name: id.clone(),
1338                id,
1339                platform: String::new(),
1340            },
1341        });
1342        s.save();
1343    });
1344    // Controlling another device keeps a backgrounded app awake.
1345    crate::quiet::reapply();
1346}
1347
1348/// The app is in front again after iOS may have suspended it: link now,
1349/// prove every connection is still alive, and look at the network afresh.
1350/// What makes the other devices appear the moment the app opens rather than
1351/// when a dead socket finally times out.
1352pub fn resume() {
1353    link::nudge();
1354    crate::remote::wire::probe_all();
1355    crate::remote::nearby::refresh();
1356}
1357
1358/// Where the target's playhead is now and whether it is playing, from what
1359/// it last reported: the network's report when there is one, as `list`
1360/// prefers it. Cheap enough for every display frame, which `list` is not.
1361pub fn target_playhead() -> Option<(u64, bool)> {
1362    with(|s| {
1363        let (state, at) = reported(s, &s.target.as_ref()?.id)?;
1364        let mut position = state.position_ms;
1365        if state.playing {
1366            position += at.elapsed().as_millis() as u64;
1367            if state.duration_ms > 0 {
1368                position = position.min(state.duration_ms);
1369            }
1370        }
1371        Some((position, state.playing))
1372    })
1373}
1374
1375/// What `id` last reported and when it was heard: the network's report when
1376/// there is one, as `list` prefers it.
1377fn reported<'s>(s: &'s Store, id: &str) -> Option<(&'s LinkState, Instant)> {
1378    s.nearby
1379        .iter()
1380        .find(|n| n.hello.id == id)
1381        .and_then(|n| Some((n.state.as_ref()?, n.at)))
1382        .or_else(|| {
1383            s.account
1384                .iter()
1385                .find(|(d, _)| d.id == id)
1386                .and_then(|(d, at)| Some((d.state.as_ref()?, *at)))
1387        })
1388}
1389
1390/// The server's id for the track `id` last reported as current.
1391pub fn current_track(id: &str) -> Option<String> {
1392    with(|s| {
1393        let (state, _) = reported(s, id)?;
1394        state.queue.iter().find(|e| e.current)?.track_id.clone()
1395    })
1396}
1397
1398/// What `id` last reported, to compare with what it reports next.
1399pub fn last_report(id: &str) -> Option<LinkState> {
1400    with(|s| reported(s, id).map(|(state, _)| state.clone()))
1401}
1402
1403/// Wait up to `within` for `id` to report `track_id` as its current track
1404/// in a queue it did not have before: `before` is `last_report` from just
1405/// before asking. Sending a command only queues it: on a socket that may
1406/// have died unnoticed, in the server's store for a device asleep, or in a
1407/// push nobody has tapped. A hand-off learns the music arrived from this.
1408pub fn await_current(
1409    id: &str,
1410    track_id: &str,
1411    before: Option<&LinkState>,
1412    within: Duration,
1413) -> bool {
1414    let took = || with(|s| reported(s, id).is_some_and(|(now, _)| took(before, now, track_id)));
1415    await_until(took, within)
1416}
1417
1418/// Whether `now` shows `track_id` arrived since `before`. A hand-off replaces
1419/// the queue, so its entries are new: the current one has an id `before`'s
1420/// did not. Being on the same track already, or any other report since (a
1421/// seek, the outputs, the sleep timer), is not an answer. A device that
1422/// names no entries has only its whole report to go on.
1423fn took(before: Option<&LinkState>, now: &LinkState, track_id: &str) -> bool {
1424    let current = |state: &LinkState| state.queue.iter().find(|e| e.current).cloned();
1425    let Some(entry) = current(now) else {
1426        return false;
1427    };
1428    if entry.track_id.as_deref() != Some(track_id) {
1429        return false;
1430    }
1431    let was = before.and_then(current);
1432    match (&entry.id, was.as_ref().and_then(|e| e.id.as_ref())) {
1433        (Some(id), Some(was)) => id != was,
1434        (Some(_), None) => true,
1435        (None, _) => before != Some(now),
1436    }
1437}
1438
1439/// Wait up to `within` for `done` to hold, looking again at each change to
1440/// the engine or to this store.
1441pub fn await_until(done: impl Fn() -> bool, within: Duration) -> bool {
1442    let until = Instant::now() + within;
1443    let signal = crate::signal::engine_changed();
1444    let mut seen = signal.generation();
1445    loop {
1446        if done() {
1447            return true;
1448        }
1449        let left = until.saturating_duration_since(Instant::now());
1450        if left.is_zero() {
1451            return false;
1452        }
1453        seen = signal.wait_until(seen, left);
1454    }
1455}
1456
1457/// The target as `list` would give it.
1458pub fn target_device() -> Option<Device> {
1459    let id = target()?;
1460    list().into_iter().find(|d| d.id == id)
1461}
1462
1463/// How the device `id` in `list` can be reached.
1464pub fn reach_of(list: &[Device], id: &str) -> Reach {
1465    list.iter()
1466        .find(|d| d.id == id)
1467        .map_or(Reach::OutOfReach(None), Device::reach)
1468}
1469
1470/// Get `cmd` to the device `id` over a connection that is up now: the local
1471/// network's, else the link. Never queued, never a push, never the HTTP
1472/// fallback `send` has: for what is only worth saying while someone is there
1473/// to hear it, like `WatchLevels`. A stop goes both ways, since the start may
1474/// have taken either. `false` when there was no way through.
1475pub fn send_live(id: &str, cmd: LinkCommand) -> bool {
1476    let everywhere = matches!(cmd, LinkCommand::WatchLevels { on: false });
1477    let near = cmd.allowed_nearby() && crate::remote::nearby::send(id, cmd.clone());
1478    if near && !everywhere {
1479        return true;
1480    }
1481    let linked = link::report(LinkReport::Command {
1482        to: id.to_string(),
1483        command: cmd,
1484        ack: None,
1485    });
1486    near || linked
1487}
1488
1489/// Get `cmd` to the device `id`: over the local network if connected there,
1490/// else up the link, else in one request to the server, which is what a Live
1491/// Activity's button has while iOS keeps the app's link down.
1492pub fn send(id: &str, cmd: LinkCommand) -> Result<(), String> {
1493    send_then(id, cmd, None)
1494}
1495
1496/// `send`, handing the outcome to `then` once it is known, rather than to the
1497/// app's notice. Returns as soon as the command is on its way.
1498pub fn send_then(id: &str, cmd: LinkCommand, then: Option<Then>) -> Result<(), String> {
1499    reach(id, cmd, then).map_err(Missed::into_message)
1500}
1501
1502/// Every way to `id` in turn. A device listed asleep or reconnecting is
1503/// tried all the same, since the list may be from before the link came up:
1504/// only the ways themselves say it is out of reach.
1505fn reach(id: &str, cmd: LinkCommand, then: Option<Then>) -> Result<(), Missed> {
1506    used(id);
1507    let (nearby, own, shared) = with(|s| {
1508        let listed = s.account.iter().find(|(d, _)| d.id == id);
1509        (
1510            s.nearby.iter().any(|n| n.hello.id == id),
1511            listed.is_some_and(|(d, _)| d.owner.is_none()),
1512            listed.is_some_and(|(d, _)| d.owner.is_some()),
1513        )
1514    });
1515    // Another account's device, or one on the network, takes the playback
1516    // set at most, and decides for itself; what only an account may send its
1517    // own devices goes to them alone.
1518    if !own && !cmd.allowed_playback() {
1519        return Err(Missed::Said(
1520            "Only your own devices can be asked that.".into(),
1521        ));
1522    }
1523    let routes = Routes {
1524        lan: nearby,
1525        lan_answers: nearby && answers_on_network(id),
1526        link_answers: answers_by_link(id),
1527    };
1528    // One id for the command whichever way it goes, so a device it reaches
1529    // twice acts on it once; and listened for before it is sent, so an answer
1530    // quicker than this thread is still heard.
1531    let ack = acks::next_id();
1532    let answer = acks::expect(ack, id);
1533    if routes.lan {
1534        let sent = if routes.lan_answers {
1535            crate::remote::nearby::send_acked(id, cmd.clone(), ack)
1536        } else {
1537            crate::remote::nearby::send(id, cmd.clone())
1538        };
1539        if sent {
1540            follow(id, cmd, ack, answer, Route::Network, routes, then);
1541            return Ok(());
1542        }
1543    }
1544    let account = own || shared;
1545    if link::report(LinkReport::Command {
1546        to: id.to_string(),
1547        command: cmd.clone(),
1548        ack: routes.link_answers.then_some(ack),
1549    }) {
1550        follow(id, cmd, ack, answer, Route::Link, routes, then);
1551        return Ok(());
1552    }
1553    if !account && !nearby {
1554        // Not heard of here, but perhaps by the server: the target of a Live
1555        // Activity outlives this process's list of devices.
1556        log::info!("devices: {id} is not listed; asking the server");
1557    }
1558    let outcome = match ask_server(id, &cmd, Some(ack)) {
1559        Ok(outcome) => outcome,
1560        // A connection the network dropped a moment ago comes back when
1561        // dialled: one more look there, under the same ack, before giving
1562        // the device up.
1563        Err(Missed::OutOfReach(why)) if crate::remote::nearby::dials(id) => {
1564            crate::remote::nearby::dial_now();
1565            let back = await_until(
1566                || with(|s| s.nearby.iter().any(|n| n.hello.id == id)),
1567                NETWORK_WAIT,
1568            );
1569            let sent = back
1570                && if answers_on_network(id) {
1571                    crate::remote::nearby::send_acked(id, cmd.clone(), ack)
1572                } else {
1573                    crate::remote::nearby::send(id, cmd.clone())
1574                };
1575            if !sent {
1576                acks::forget(ack);
1577                return Err(Missed::OutOfReach(why));
1578            }
1579            let routes = Routes {
1580                lan: true,
1581                lan_answers: answers_on_network(id),
1582                ..routes
1583            };
1584            follow(id, cmd, ack, answer, Route::Network, routes, then);
1585            return Ok(());
1586        }
1587        Err(missed) => {
1588            acks::forget(ack);
1589            return Err(missed);
1590        }
1591    };
1592    acks::forget(ack);
1593    told(id, &cmd, outcome, then);
1594    Ok(())
1595}
1596
1597/// What the person is told when neither the network nor the server reaches
1598/// `id`: a transport error, or a server that relays nothing, says nothing
1599/// about the device.
1600fn out_of_reach(id: &str) -> String {
1601    let name = list()
1602        .into_iter()
1603        .find(|d| d.id == id)
1604        .map_or_else(|| "That device".to_owned(), |d| d.name);
1605    format!("{name} is out of reach.")
1606}
1607
1608/// The ways a command can reach a device, and which of them bring an answer.
1609#[derive(Debug, Clone, Copy)]
1610struct Routes {
1611    lan: bool,
1612    lan_answers: bool,
1613    link_answers: bool,
1614}
1615
1616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1617enum Route {
1618    Network,
1619    Link,
1620}
1621
1622/// How long to wait for an answer by each way before trying the next. Up the
1623/// link it is the server's round trip and its own fallback to a push.
1624const NETWORK_ANSWER: Duration = Duration::from_secs(1);
1625const LINK_ANSWER: Duration = Duration::from_secs(3);
1626
1627/// Wait for `id`'s answer to the command sent under `ack` by `route`, off the
1628/// caller's thread, and try the next way when none comes. A way that brings
1629/// no answer is taken as sent, as it always was.
1630fn follow(
1631    id: &str,
1632    cmd: LinkCommand,
1633    ack: u64,
1634    answer: crossbeam_channel::Receiver<acks::AckOutcome>,
1635    route: Route,
1636    routes: Routes,
1637    then: Option<Then>,
1638) {
1639    let answers = match route {
1640        Route::Network => routes.lan_answers,
1641        Route::Link => routes.link_answers,
1642    };
1643    if !answers {
1644        acks::forget(ack);
1645        told(id, &cmd, None, then);
1646        return;
1647    }
1648    let id = id.to_owned();
1649    let _ = std::thread::Builder::new()
1650        .name("koan-ack".into())
1651        .spawn(move || {
1652            let walked = walk(&id, &cmd, ack, &answer, route, routes);
1653            acks::forget(ack);
1654            match walked {
1655                Ok(outcome) => told(&id, &cmd, outcome, then),
1656                Err(missed) => unanswered(&id, &cmd, missed.into_message(), then),
1657            }
1658        });
1659}
1660
1661/// What became of a command sent under `ack`: the first answer by any way.
1662/// `Err` when this device could not ask the server either: no answer, which
1663/// says nothing of whether an earlier way delivered it.
1664fn walk(
1665    id: &str,
1666    cmd: &LinkCommand,
1667    ack: u64,
1668    answer: &crossbeam_channel::Receiver<acks::AckOutcome>,
1669    first: Route,
1670    routes: Routes,
1671) -> Result<Option<acks::AckOutcome>, Missed> {
1672    if first == Route::Network {
1673        if let Ok(outcome) = answer.recv_timeout(NETWORK_ANSWER) {
1674            return Ok(Some(outcome));
1675        }
1676        log::info!("devices: no answer from {id} on the network; trying the link");
1677        if routes.link_answers
1678            && link::report(LinkReport::Command {
1679                to: id.to_string(),
1680                command: cmd.clone(),
1681                ack: Some(ack),
1682            })
1683            && let Ok(outcome) = answer.recv_timeout(LINK_ANSWER)
1684        {
1685            return Ok(Some(outcome));
1686        }
1687    } else if let Ok(outcome) = answer.recv_timeout(LINK_ANSWER) {
1688        return Ok(Some(outcome));
1689    }
1690    log::info!("devices: no answer from {id}; asking the server");
1691    ask_server(id, cmd, Some(ack))
1692}
1693
1694/// No answer came, and this device could not ask the server: the command may
1695/// have arrived by an earlier way or may not. A sender that takes the outcome
1696/// is told none, as for a way that brings no answer, so it does not act as
1697/// if refused; the app is told the device could not be reached.
1698fn unanswered(id: &str, cmd: &LinkCommand, error: String, then: Option<Then>) {
1699    match then {
1700        Some(then) => {
1701            log::warn!("devices: no answer from {id} for {cmd:?}, and the server: {error}");
1702            then(None);
1703        }
1704        None => {
1705            log::warn!("devices: {cmd:?} did not reach {id}: {error}");
1706            notify(id, acks::AckOutcome::Failed { error }, true);
1707        }
1708    }
1709}
1710
1711/// What a command came to, once known. `None`: sent by a way that brings no
1712/// answer.
1713/// `then` has it when given; otherwise one that was queued or did not arrive
1714/// becomes the app's notice.
1715fn told(id: &str, cmd: &LinkCommand, outcome: Option<acks::AckOutcome>, then: Option<Then>) {
1716    match &outcome {
1717        Some(acks::AckOutcome::Done) | None => {}
1718        Some(acks::AckOutcome::Queued) => {
1719            log::info!("devices: {id} is asleep; {cmd:?} waits for it")
1720        }
1721        Some(other) => log::warn!("devices: {cmd:?} did not reach {id}: {other:?}"),
1722    }
1723    if let Some(then) = then {
1724        then(outcome);
1725        return;
1726    }
1727    let Some(outcome) = outcome.filter(|o| *o != acks::AckOutcome::Done) else {
1728        return;
1729    };
1730    notify(id, outcome, false);
1731}
1732
1733/// Make `outcome` the app's notice about `id`.
1734fn notify(id: &str, outcome: acks::AckOutcome, unreached: bool) {
1735    let device = list()
1736        .into_iter()
1737        .find(|d| d.id == id)
1738        .map_or_else(|| id.to_owned(), |d| d.name);
1739    changed(|s| {
1740        let seq = s.notice.as_ref().map_or(1, |n| n.seq + 1);
1741        s.notice = Some(Notice {
1742            seq,
1743            device,
1744            outcome,
1745            unreached,
1746        });
1747    });
1748}
1749
1750/// Whether `id`, reached on the local network, answers commands.
1751fn answers_on_network(id: &str) -> bool {
1752    with(|s| s.nearby.iter().any(|n| n.hello.id == id && n.hello.acks))
1753}
1754
1755/// Whether `id`, reached up the link, answers: the server relays answers and
1756/// says the device gives them.
1757fn answers_by_link(id: &str) -> bool {
1758    crate::remote::profile::current().is_some_and(|p| p.offers(crate::remote::profile::ACK))
1759        && with(|s| s.account.iter().any(|(d, _)| d.id == id && d.acks))
1760}
1761
1762/// Get `cmd` to `id` in one request to the server, which relays it down the
1763/// device's link or wakes it. With `ack`, a server that answers waits a moment
1764/// for the device and says how it went; one that does not answers `None`.
1765fn ask_server(
1766    id: &str,
1767    cmd: &LinkCommand,
1768    ack: Option<u64>,
1769) -> Result<Option<acks::AckOutcome>, Missed> {
1770    let cfg = Config::load().unwrap_or_default();
1771    // No server and not on this network: nothing could carry it.
1772    let client = crate::helpers::subsonic_client(&cfg)
1773        .ok_or_else(|| Missed::OutOfReach(out_of_reach(id)))?;
1774    let json = serde_json::to_string(cmd).map_err(|e| e.to_string())?;
1775    client
1776        .koan_command(id, &json, ack)
1777        .map_err(|e| relay_error(id, e))
1778}
1779
1780/// Why the server did not get a command to `id`. Its word, an expired key or
1781/// a refusal, stands; not found, or no answer at all, is a server with nothing
1782/// to relay through.
1783fn relay_error(id: &str, e: crate::remote::client::SubsonicError) -> Missed {
1784    match e {
1785        crate::remote::client::SubsonicError::Api { code, message } if code != 70 => {
1786            Missed::Said(message)
1787        }
1788        e => {
1789            log::warn!("devices: the server cannot relay to {id}: {e}");
1790            Missed::OutOfReach(out_of_reach(id))
1791        }
1792    }
1793}
1794
1795/// `send`, by the server first: for the lock screen's buttons. iOS runs
1796/// their intent without waking the app's scene, so the link the app left
1797/// open is most likely dead, and `send` would put the command into it and
1798/// call it sent. The server knows whether the device is linked, and pushes
1799/// to it if not. A device the server cannot reach, one only on this
1800/// network, falls back to `send`.
1801pub fn send_by_server_first(id: &str, cmd: LinkCommand) -> Result<(), String> {
1802    server_first(
1803        || {
1804            ask_server(id, &cmd, Some(acks::next_id()))
1805                .map_err(Missed::into_message)
1806                .map(|outcome| match outcome {
1807                    Some(acks::AckOutcome::Refused { reason }) => Err(reason),
1808                    outcome => {
1809                        told(id, &cmd, outcome, None);
1810                        Ok(())
1811                    }
1812                })?
1813        },
1814        || send(id, cmd.clone()),
1815    )
1816}
1817
1818fn server_first(
1819    server: impl FnOnce() -> Result<(), String>,
1820    otherwise: impl FnOnce() -> Result<(), String>,
1821) -> Result<(), String> {
1822    server().or_else(|why| {
1823        log::info!("devices: the server did not take it ({why}); sending it here");
1824        otherwise()
1825    })
1826}
1827
1828/// Whether `id` is listed by the server as one of the account's devices
1829/// (`Some(None)`) or one shared with it (`Some(Some(owner))`).
1830pub fn listed_owner(id: &str) -> Option<Option<String>> {
1831    with(|s| {
1832        s.account
1833            .iter()
1834            .find(|(d, _)| d.id == id)
1835            .map(|(d, _)| d.owner.clone())
1836    })
1837}
1838
1839/// This device's id, as other devices know it.
1840pub fn this_id() -> Option<String> {
1841    local().map(|l| l.identity.device_id.clone())
1842}
1843
1844#[cfg(test)]
1845pub(crate) mod tests {
1846    use super::*;
1847
1848    /// Reach follows the device's connections: one not heard from is
1849    /// reconnecting until a heartbeat has passed, then asleep if something can
1850    /// wake it and out of reach if nothing can.
1851    #[test]
1852    fn reach_is_read_from_the_connections() {
1853        let device = |awake, asleep, wakeable| Device {
1854            id: "tv".into(),
1855            name: "TV".into(),
1856            platform: "tvos".into(),
1857            account: true,
1858            owner: None,
1859            nearby: false,
1860            awake,
1861            asleep,
1862            wakeable,
1863            last_seen: None,
1864            waking: None,
1865            same_library: true,
1866            state: None,
1867            heard: Instant::now(),
1868            problem: None,
1869        };
1870        assert_eq!(device(true, false, false).reach(), Reach::Connected);
1871        assert_eq!(device(false, false, true).reach(), Reach::Reconnecting);
1872        assert_eq!(device(false, false, false).reach(), Reach::Reconnecting);
1873        assert_eq!(device(false, true, true).reach(), Reach::Asleep);
1874        assert_eq!(device(false, true, false).reach(), Reach::OutOfReach(None));
1875        let waking = Device {
1876            waking: Some(Waking::Push),
1877            ..device(false, true, true)
1878        };
1879        assert_eq!(waking.reach(), Reach::Waking);
1880        let failed = Device {
1881            waking: Some(Waking::Failed("no answer".into())),
1882            ..device(false, true, false)
1883        };
1884        assert_eq!(failed.reach(), Reach::OutOfReach(Some("no answer".into())));
1885        let problem = Device {
1886            problem: Some("Not on this network".into()),
1887            ..device(true, false, false)
1888        };
1889        assert_eq!(
1890            problem.reach(),
1891            Reach::OutOfReach(Some("Not on this network".into()))
1892        );
1893        let unlisted = Device {
1894            problem: Some(OUT_OF_REACH.into()),
1895            ..device(false, true, false)
1896        };
1897        assert_eq!(
1898            unlisted.reach(),
1899            Reach::OutOfReach(None),
1900            "no reason beyond the state"
1901        );
1902        assert_eq!(reach_of(&[], "tv"), Reach::OutOfReach(None));
1903    }
1904
1905    /// The device controlled, restored on a network it is not on: a server
1906    /// that cannot relay to it says so by the device's name, and one that
1907    /// answers keeps its own words.
1908    #[test]
1909    fn a_command_to_a_device_out_of_reach_names_it() {
1910        use crate::remote::client::SubsonicError;
1911        let _held = STORE_LOCK.lock();
1912        crate::config::isolate_config_for_tests();
1913        with(|s| *s = Store::default());
1914        with(|s| {
1915            s.target = Some(Remembered {
1916                id: "mac".into(),
1917                name: "Studio Mac".into(),
1918                platform: "macos".into(),
1919            })
1920        });
1921
1922        let refused = reqwest::blocking::Client::new()
1923            .get("http://127.0.0.1:1/rest/koanCommand")
1924            .send()
1925            .unwrap_err();
1926        assert_eq!(
1927            relay_error("mac", SubsonicError::from(refused)),
1928            Missed::OutOfReach("Studio Mac is out of reach.".into())
1929        );
1930        let unknown = SubsonicError::Api {
1931            code: 70,
1932            message: "not found".into(),
1933        };
1934        assert_eq!(
1935            relay_error("mac", unknown),
1936            Missed::OutOfReach("Studio Mac is out of reach.".into())
1937        );
1938        let revoked = SubsonicError::Api {
1939            code: 44,
1940            message: "The key has been revoked.".into(),
1941        };
1942        assert_eq!(
1943            relay_error("mac", revoked),
1944            Missed::Said("The key has been revoked.".into())
1945        );
1946
1947        assert_eq!(
1948            send("mac", LinkCommand::Pause).unwrap_err(),
1949            "Studio Mac is out of reach.",
1950            "no server, and not on this network"
1951        );
1952        with(|s| *s = Store::default());
1953    }
1954
1955    /// Off the network the controlled device is on, with no server to relay:
1956    /// the command is out of reach and the device stays controlled, since
1957    /// acting here instead would play on a device nobody asked for. A device
1958    /// that answered, however it answered, is told apart from one unreached.
1959    #[test]
1960    fn an_unreachable_target_stays_controlled_and_a_refusal_is_said() {
1961        use crate::remote::client::SubsonicError;
1962        let _held = STORE_LOCK.lock();
1963        crate::config::isolate_config_for_tests();
1964        with(|s| *s = Store::default());
1965        with(|s| {
1966            s.target = Some(Remembered {
1967                id: "mac".into(),
1968                name: "Studio Mac".into(),
1969                platform: "macos".into(),
1970            })
1971        });
1972
1973        let missed = reach("mac", LinkCommand::Resume, None).unwrap_err();
1974        assert!(matches!(missed, Missed::OutOfReach(_)), "{missed:?}");
1975        assert_eq!(target().as_deref(), Some("mac"), "a miss keeps the target");
1976
1977        let refused = relay_error(
1978            "mac",
1979            SubsonicError::Api {
1980                code: 50,
1981                message: "Studio Mac is shared for playback only.".into(),
1982            },
1983        );
1984        assert_eq!(
1985            refused,
1986            Missed::Said("Studio Mac is shared for playback only.".into()),
1987            "the server answered"
1988        );
1989        with(|s| *s = Store::default());
1990    }
1991
1992    /// A TV dialled by address whose AirPlay record was matched by name, at
1993    /// its `.local.` host: a second hello inside the window, which does not
1994    /// look again, keeps the record to wake it by.
1995    #[test]
1996    fn a_hello_that_does_not_look_keeps_the_tv_record() {
1997        let _held = STORE_LOCK.lock();
1998        crate::config::isolate_config_for_tests();
1999        with(|s| *s = Store::default());
2000        let tv = crate::remote::airplay::Tv {
2001            name: "Living Room".into(),
2002            host: "Living-Room.local.".into(),
2003            mac: Some("AA:BB:CC:DD:EE:FF".into()),
2004            ip: Some("10.0.0.9".into()),
2005        };
2006        with(|s| {
2007            s.seen.push(SeenNearby {
2008                id: "tv".into(),
2009                name: "Living Room".into(),
2010                platform: "tvos".into(),
2011                addr: "10.0.0.9:5626".into(),
2012                at: 0,
2013                tv: Some(tv.clone()),
2014            });
2015            s.tv_looked
2016                .insert(("tv".into(), "10.0.0.9".into()), Instant::now());
2017        });
2018        let mut hello = hello("tv");
2019        hello.platform = "tvos".into();
2020        nearby_hello(hello, "10.0.0.9:5626");
2021        assert_eq!(tv_of("tv").map(|t| t.host), Some(tv.host));
2022    }
2023
2024    #[test]
2025    fn an_apple_tv_is_looked_for_once_per_host_for_a_while() {
2026        let mut looked = HashMap::new();
2027        let t0 = Instant::now();
2028        assert!(look_for_tv(&mut looked, "tv", "living-room.local.", t0));
2029        assert!(!look_for_tv(
2030            &mut looked,
2031            "tv",
2032            "living-room.local.",
2033            t0 + Duration::from_secs(5)
2034        ));
2035        // Another host, or another TV, is looked for at once.
2036        assert!(look_for_tv(
2037            &mut looked,
2038            "tv",
2039            "10.0.0.7",
2040            t0 + Duration::from_secs(5)
2041        ));
2042        assert!(look_for_tv(&mut looked, "other", "living-room.local.", t0));
2043        assert!(look_for_tv(
2044            &mut looked,
2045            "tv",
2046            "living-room.local.",
2047            t0 + TV_LOOK_AGAIN
2048        ));
2049    }
2050
2051    #[test]
2052    fn a_live_send_with_no_way_through_sends_nothing() {
2053        // No connection on the network and no link: false, and no fallback.
2054        assert!(!send_live(
2055            "nowhere-at-all",
2056            LinkCommand::WatchLevels { on: true }
2057        ));
2058    }
2059
2060    fn device(id: &str, playing: bool) -> LinkDevice {
2061        LinkDevice {
2062            id: id.into(),
2063            name: id.into(),
2064            platform: "macos".into(),
2065            linked: true,
2066            state: Some(LinkState {
2067                playing,
2068                position_ms: 1000,
2069                duration_ms: 2000,
2070                ..Default::default()
2071            }),
2072            last_seen: None,
2073            wakeable: None,
2074            owner: None,
2075            acks: false,
2076        }
2077    }
2078
2079    /// The store is process-wide: each test walks it through its states with
2080    /// this held, rather than racing the others over it.
2081    pub(crate) static STORE_LOCK: Mutex<()> = Mutex::new(());
2082
2083    fn hello(id: &str) -> LinkHello {
2084        LinkHello {
2085            id: id.into(),
2086            name: id.into(),
2087            platform: "ios".into(),
2088            library: None,
2089            acks: false,
2090            nonce: None,
2091        }
2092    }
2093
2094    #[test]
2095    fn the_lock_screen_goes_by_the_server_and_falls_back_only_when_refused() {
2096        let local = std::cell::Cell::new(0);
2097        let here = || {
2098            local.set(local.get() + 1);
2099            Ok(())
2100        };
2101        assert_eq!(server_first(|| Ok(()), here), Ok(()));
2102        assert_eq!(local.get(), 0, "the server took it: nothing sent here");
2103
2104        assert_eq!(server_first(|| Err("no such device".into()), here), Ok(()));
2105        assert_eq!(local.get(), 1, "refused there: sent here instead");
2106
2107        assert_eq!(
2108            server_first(|| Err("offline".into()), || Err("not reachable".into())),
2109            Err("not reachable".to_string())
2110        );
2111    }
2112
2113    /// A device on `track`, as the entry `entry` of its queue.
2114    fn playing(track: &str, entry: &str) -> LinkState {
2115        LinkState {
2116            queue: vec![crate::remote::link::LinkQueueEntry {
2117                id: Some(entry.into()),
2118                track_id: Some(track.into()),
2119                current: true,
2120                ..Default::default()
2121            }],
2122            ..Default::default()
2123        }
2124    }
2125
2126    /// What the apps are given for a device's playhead runs on from the
2127    /// report, so stamping it with the time the list was published does not
2128    /// set it back by however long ago the device reported.
2129    #[test]
2130    fn a_playing_devices_playhead_runs_on_from_its_report() {
2131        let _held = STORE_LOCK.lock();
2132        crate::config::isolate_config_for_tests();
2133        with(|s| *s = Store::default());
2134        nearby_hello(hello("speaker"), "10.0.0.6:5626");
2135        let at = |playing| LinkState {
2136            playing,
2137            position_ms: 1000,
2138            duration_ms: 60_000,
2139            ..Default::default()
2140        };
2141        let position = || {
2142            list()
2143                .into_iter()
2144                .find(|d| d.id == "speaker")
2145                .unwrap()
2146                .position_ms()
2147        };
2148
2149        nearby_state("speaker", at(true));
2150        std::thread::sleep(Duration::from_millis(50));
2151        assert!(position() >= 1050, "{}", position());
2152
2153        nearby_state("speaker", at(false));
2154        std::thread::sleep(Duration::from_millis(50));
2155        assert_eq!(position(), 1000, "paused, it stays put");
2156    }
2157
2158    #[test]
2159    fn a_command_queued_or_refused_becomes_the_notice_unless_its_sender_takes_it() {
2160        let _held = STORE_LOCK.lock();
2161        crate::config::isolate_config_for_tests();
2162        with(|s| *s = Store::default());
2163        nearby_hello(hello("phone"), "10.0.0.5:5626");
2164
2165        told(
2166            "phone",
2167            &LinkCommand::Pause,
2168            Some(acks::AckOutcome::Done),
2169            None,
2170        );
2171        told("phone", &LinkCommand::Pause, None, None);
2172        assert_eq!(notice(), None, "arrived, or sent as ever: nothing to say");
2173
2174        told(
2175            "phone",
2176            &LinkCommand::Pause,
2177            Some(acks::AckOutcome::Queued),
2178            None,
2179        );
2180        let queued = notice().unwrap();
2181        assert_eq!(
2182            (queued.device.as_str(), &queued.outcome),
2183            ("phone", &acks::AckOutcome::Queued)
2184        );
2185
2186        let failed = acks::AckOutcome::Failed {
2187            error: "gone".into(),
2188        };
2189        told("phone", &LinkCommand::Pause, Some(failed.clone()), None);
2190        assert_eq!(
2191            notice().unwrap().seq,
2192            queued.seq + 1,
2193            "the same twice is still news"
2194        );
2195
2196        // A sender that takes the outcome itself, as a hand-off does.
2197        let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
2198        let into = got.clone();
2199        let before = notice();
2200        told(
2201            "phone",
2202            &LinkCommand::Pause,
2203            Some(failed.clone()),
2204            Some(Box::new(move |o| *into.lock() = Some(o))),
2205        );
2206        assert_eq!(*got.lock(), Some(Some(failed)));
2207        assert_eq!(notice(), before, "not said twice");
2208    }
2209
2210    /// This device failing to reach the server, after no answer by the other
2211    /// ways, is not the target refusing: the command may have arrived by an
2212    /// earlier way. A hand-off waiting on it hears no answer, so it does not
2213    /// resume the music here; the app is told the device could not be reached.
2214    #[test]
2215    fn failing_to_ask_the_server_is_no_answer_not_a_refusal() {
2216        let _held = STORE_LOCK.lock();
2217        crate::config::isolate_config_for_tests();
2218        with(|s| *s = Store::default());
2219        nearby_hello(hello("phone"), "10.0.0.5:5626");
2220
2221        let got = std::sync::Arc::new(parking_lot::Mutex::new(None));
2222        let into = got.clone();
2223        unanswered(
2224            "phone",
2225            &LinkCommand::Pause,
2226            "server unreachable".into(),
2227            Some(Box::new(move |o| *into.lock() = Some(o))),
2228        );
2229        assert_eq!(*got.lock(), Some(None), "no answer, not a refusal");
2230        assert_eq!(notice(), None);
2231
2232        unanswered(
2233            "phone",
2234            &LinkCommand::Pause,
2235            "server unreachable".into(),
2236            None,
2237        );
2238        assert!(matches!(
2239            notice().map(|n| n.outcome),
2240            Some(acks::AckOutcome::Failed { .. })
2241        ));
2242    }
2243
2244    #[test]
2245    fn a_hand_off_is_taken_only_when_the_device_reports_the_track() {
2246        let _held = STORE_LOCK.lock();
2247        crate::config::isolate_config_for_tests();
2248        with(|s| *s = Store::default());
2249        nearby_hello(hello("phone"), "10.0.0.5:5626");
2250        nearby_state("phone", playing("before", "e1"));
2251        let short = Duration::from_millis(50);
2252
2253        // Sent, and nothing heard back: queued is not taken.
2254        let before = last_report("phone");
2255        assert!(!await_current("phone", "handed", before.as_ref(), short));
2256
2257        // Already on the handed track, and then any other report from that
2258        // same queue entry (a seek, say), is no answer.
2259        nearby_state("phone", playing("handed", "e2"));
2260        let before = last_report("phone");
2261        let mut seeked = playing("handed", "e2");
2262        seeked.position_ms = 5000;
2263        nearby_state("phone", seeked);
2264        assert!(!await_current("phone", "handed", before.as_ref(), short));
2265
2266        // A new queue on another track is not this one.
2267        let reporter = std::thread::spawn(|| {
2268            std::thread::sleep(Duration::from_millis(20));
2269            nearby_state("phone", playing("other", "e3"));
2270        });
2271        assert!(!await_current(
2272            "phone",
2273            "handed",
2274            before.as_ref(),
2275            Duration::from_millis(200)
2276        ));
2277        reporter.join().unwrap();
2278
2279        // The device saying it has the track in the queue it was sent.
2280        let before = last_report("phone");
2281        let reporter = std::thread::spawn(|| {
2282            std::thread::sleep(Duration::from_millis(20));
2283            nearby_state("phone", playing("handed", "e4"));
2284        });
2285        assert!(await_current(
2286            "phone",
2287            "handed",
2288            before.as_ref(),
2289            Duration::from_secs(5)
2290        ));
2291        reporter.join().unwrap();
2292    }
2293
2294    #[test]
2295    fn a_device_naming_no_entries_is_judged_on_its_whole_report() {
2296        let unnamed = |track: &str, position_ms| LinkState {
2297            position_ms,
2298            queue: vec![crate::remote::link::LinkQueueEntry {
2299                track_id: Some(track.into()),
2300                current: true,
2301                ..Default::default()
2302            }],
2303            ..Default::default()
2304        };
2305        let before = unnamed("handed", 0);
2306        assert!(!took(Some(&before), &before, "handed"));
2307        assert!(took(Some(&before), &unnamed("handed", 10), "handed"));
2308        assert!(took(None, &unnamed("handed", 0), "handed"));
2309    }
2310
2311    #[test]
2312    fn a_device_that_stops_answering_sleeps_after_a_heartbeat_and_stays() {
2313        let _held = STORE_LOCK.lock();
2314        crate::config::isolate_config_for_tests();
2315        with(|s| *s = Store::default());
2316        let week = 7 * 24 * 60 * 60;
2317
2318        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2319        used("stranger");
2320        nearby_gone("stranger");
2321        let now = with(|s| s.live["stranger"]);
2322        let at = |t: i64| list_at(t).into_iter().find(|d| d.id == "stranger");
2323
2324        let missed_one = at(now + STALE_SECS - 1).expect("still listed");
2325        assert!(
2326            !missed_one.asleep,
2327            "one missed heartbeat does not make it asleep"
2328        );
2329        assert!(
2330            !missed_one.awake && !missed_one.nearby,
2331            "nor does it pretend to be reachable"
2332        );
2333        let asleep = at(now + STALE_SECS + 1).expect("listed asleep");
2334        assert!(asleep.asleep && !asleep.awake && !asleep.wakeable);
2335        assert_eq!(asleep.last_seen, Some(now));
2336        assert!(asleep.state.is_none(), "what it was doing is not news");
2337        assert!(
2338            at(now + week).is_some_and(|d| d.asleep),
2339            "kept long past the old grace, until forgotten"
2340        );
2341        assert_eq!(
2342            next_change(now),
2343            Some(now + STALE_SECS),
2344            "the clock wakes for the next crossing"
2345        );
2346        assert_eq!(next_change(now + STALE_SECS), None, "and then for nothing");
2347
2348        // Heard from again: back at once, with no wait.
2349        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2350        let back = at(now + STALE_SECS + 5).unwrap();
2351        assert!(back.awake && back.nearby && !back.asleep);
2352
2353        // Asleep, a stranger cannot be chosen or sent to.
2354        nearby_gone("stranger");
2355        with(|s| *s.live.get_mut("stranger").unwrap() -= STALE_SECS + 10);
2356        assert!(choosable("stranger").is_err());
2357        assert!(send("stranger", LinkCommand::Pause).is_err());
2358
2359        // An account device a push can wake, asleep for days.
2360        let mut phone = device("phone", false);
2361        phone.linked = false;
2362        phone.last_seen = Some(now - week);
2363        phone.wakeable = Some(true);
2364        set_account(vec![phone]);
2365        used("phone");
2366        let phone = list_at(now).into_iter().find(|d| d.id == "phone").unwrap();
2367        assert!(phone.asleep && phone.wakeable);
2368        assert_eq!(phone.last_seen, Some(now - week));
2369        assert!(choosable("phone").is_ok());
2370
2371        // An account device that unlinks without a push token: reconnecting
2372        // for a heartbeat, then asleep, and kept.
2373        set_linked(true);
2374        set_account(vec![device("mac", false)]);
2375        used("mac");
2376        set_account(Vec::new());
2377        let left = with(|s| s.live["mac"]);
2378        let mac = |t: i64| list_at(t).into_iter().find(|d| d.id == "mac");
2379        let reconnecting = mac(left + 1).unwrap();
2380        assert!(!reconnecting.awake && !reconnecting.asleep);
2381        let asleep = mac(left + STALE_SECS + 1).unwrap();
2382        assert!(asleep.asleep && !asleep.wakeable && asleep.account);
2383        assert!(mac(left + week).is_some());
2384        with(|s| *s = Store::default());
2385    }
2386
2387    /// A device seen and never used is gone once out of reach past the
2388    /// heartbeat; one used is kept asleep, across a relaunch, until forgotten.
2389    #[test]
2390    fn only_a_device_used_here_is_kept_asleep() {
2391        let _held = STORE_LOCK.lock();
2392        crate::config::isolate_config_for_tests();
2393        with(|s| *s = Store::default());
2394
2395        nearby_hello(hello("passing"), "10.0.0.5:5626");
2396        nearby_hello(hello("played-on"), "10.0.0.6:5626");
2397        used("played-on");
2398        nearby_gone("passing");
2399        nearby_gone("played-on");
2400        let left = with(|s| s.live["passing"]);
2401        let listed = |t: i64, id: &str| list_at(t).into_iter().any(|d| d.id == id);
2402        assert!(listed(left + 1, "passing"), "reconnecting for a heartbeat");
2403        assert!(
2404            !listed(left + STALE_SECS + 1, "passing"),
2405            "never used: gone"
2406        );
2407        assert!(listed(left + STALE_SECS + 1, "played-on"), "used: asleep");
2408
2409        // An account device the server still lists, asleep, never used here.
2410        let mut phone = device("old-phone", false);
2411        phone.linked = false;
2412        phone.last_seen = Some(left - 3600);
2413        set_account(vec![phone]);
2414        assert!(!listed(left, "old-phone"));
2415
2416        with(|s| *s = Store::default());
2417        restore();
2418        let later = left + STALE_SECS + 1;
2419        assert!(
2420            list_at(later)
2421                .iter()
2422                .any(|d| d.id == "played-on" && d.asleep),
2423            "kept across a relaunch"
2424        );
2425        assert!(!listed(later, "passing"));
2426
2427        forget("played-on").unwrap();
2428        assert!(!list().iter().any(|d| d.id == "played-on"));
2429        with(|s| *s = Store::default());
2430        restore();
2431        assert!(
2432            !list().iter().any(|d| d.id == "played-on"),
2433            "forgotten on disk"
2434        );
2435        with(|s| *s = Store::default());
2436    }
2437
2438    /// The device being controlled is used, whatever else is not; a file from
2439    /// before this was kept counts only that one, so old clutter clears.
2440    #[test]
2441    fn the_controlled_device_is_kept_and_an_old_file_keeps_only_it() {
2442        let _held = STORE_LOCK.lock();
2443        crate::config::isolate_config_for_tests();
2444        with(|s| *s = Store::default());
2445        let gone = |id: &str| Departed {
2446            id: id.into(),
2447            name: id.into(),
2448            platform: "ios".into(),
2449            account: false,
2450            same_library: false,
2451            shared: false,
2452        };
2453        let old = serde_json::json!({
2454            "target": {"id": "tv", "name": "tv", "platform": "tvos"},
2455            "account": [],
2456            "live": {"tv": 1, "sim": 1},
2457            "departed": [gone("tv"), gone("sim")],
2458        });
2459        std::fs::write(saved_path(), old.to_string()).unwrap();
2460        restore();
2461        let ids: Vec<String> = list().into_iter().map(|d| d.id).collect();
2462        assert_eq!(
2463            ids,
2464            ["tv"],
2465            "the target stays; the simulator nobody used goes"
2466        );
2467
2468        set_target(None);
2469        nearby_hello(hello("mac"), "10.0.0.7:5626");
2470        set_target(Some("mac".into()));
2471        set_target(None);
2472        nearby_gone("mac");
2473        with(|s| *s.live.get_mut("mac").unwrap() -= STALE_SECS + 10);
2474        assert!(
2475            list().iter().any(|d| d.id == "mac" && d.asleep),
2476            "controlled once: kept"
2477        );
2478        with(|s| *s = Store::default());
2479    }
2480
2481    /// Forgotten here, a stranger is gone, and kept gone across a relaunch;
2482    /// heard on the network again, it is back. Forgetting is not a block.
2483    #[test]
2484    fn a_forgotten_device_is_gone_until_it_comes_back() {
2485        let _held = STORE_LOCK.lock();
2486        crate::config::isolate_config_for_tests();
2487        with(|s| *s = Store::default());
2488        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2489        assert!(forget("stranger").is_err(), "not while it is reachable");
2490        used("stranger");
2491        nearby_gone("stranger");
2492        // Kept across a relaunch, asleep, until forgotten.
2493        with(|s| *s = Store::default());
2494        restore();
2495        assert!(list().iter().any(|d| d.id == "stranger"));
2496        forget("stranger").unwrap();
2497        assert!(!list().iter().any(|d| d.id == "stranger"));
2498        assert!(remembered_nearby().iter().all(|n| n.id != "stranger"));
2499        with(|s| *s = Store::default());
2500        restore();
2501        assert!(
2502            !list().iter().any(|d| d.id == "stranger"),
2503            "forgotten on disk too"
2504        );
2505        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2506        assert!(list().iter().any(|d| d.id == "stranger" && d.awake));
2507        with(|s| *s = Store::default());
2508    }
2509
2510    /// One of the account's is forgotten through the server, which has to be
2511    /// reachable to hear it; what the server says it forgot goes from here.
2512    #[test]
2513    fn an_account_device_is_forgotten_through_the_server() {
2514        let _held = STORE_LOCK.lock();
2515        crate::config::isolate_config_for_tests();
2516        with(|s| *s = Store::default());
2517        let mut phone = device("phone", false);
2518        phone.linked = false;
2519        phone.wakeable = Some(true);
2520        phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2521        set_account(vec![phone]);
2522        used("phone");
2523        assert!(
2524            forget("phone").is_err(),
2525            "no link here: the server would still list it and wake it"
2526        );
2527        assert!(list().iter().any(|d| d.id == "phone"));
2528        // What every linked device of the account hears once it is.
2529        forgotten("phone");
2530        assert!(!list().iter().any(|d| d.id == "phone"));
2531        with(|s| *s = Store::default());
2532    }
2533
2534    /// Run a wake with a device that answers in stage `answers_in`, if any:
2535    /// the stages started, and how it ended.
2536    fn walk(recent: bool, answers_in: Option<Waking>) -> (Vec<Waking>, Ended) {
2537        let plan = wake_plan(false, recent, true);
2538        let mut started = Vec::new();
2539        let current = std::cell::RefCell::new(None);
2540        let ended = run_wake(
2541            &plan,
2542            || true,
2543            |stage| {
2544                started.push(stage.clone());
2545                *current.borrow_mut() = Some(stage.clone());
2546                Ok(())
2547            },
2548            |_| *current.borrow() == answers_in,
2549        );
2550        (started, ended)
2551    }
2552
2553    #[test]
2554    fn waking_tries_the_network_first_only_when_it_was_there_a_moment_ago() {
2555        let (tried, _) = walk(true, None);
2556        assert_eq!(tried, [Waking::Network, Waking::Push, Waking::Notification]);
2557        let (tried, _) = walk(false, None);
2558        assert_eq!(tried, [Waking::Push, Waking::Notification]);
2559        assert_eq!(
2560            wake_plan(false, true, false)
2561                .iter()
2562                .map(|(w, _)| w.clone())
2563                .collect::<Vec<_>>(),
2564            [Waking::Network],
2565            "a stranger is only ever tried on the network"
2566        );
2567        assert!(wake_plan(false, false, false).is_empty());
2568    }
2569
2570    #[test]
2571    fn an_apple_tv_on_record_is_woken_before_the_push() {
2572        let stages =
2573            |plan: Vec<(Waking, Duration)>| plan.into_iter().map(|(w, _)| w).collect::<Vec<_>>();
2574        assert_eq!(
2575            stages(wake_plan(true, false, true)),
2576            [Waking::Tv, Waking::Push, Waking::Notification]
2577        );
2578        assert_eq!(
2579            stages(wake_plan(true, true, true)),
2580            [
2581                Waking::Tv,
2582                Waking::Network,
2583                Waking::Push,
2584                Waking::Notification
2585            ]
2586        );
2587    }
2588
2589    #[test]
2590    fn an_apple_tv_met_on_the_network_keeps_its_airplay_record() {
2591        let _held = STORE_LOCK.lock();
2592        crate::config::isolate_config_for_tests();
2593        with(|s| *s = Store::default());
2594        // Named so that no TV on the network running the tests matches it.
2595        let tv = crate::remote::airplay::Tv {
2596            name: "koan test TV".into(),
2597            host: "koan-test-tv.local.".into(),
2598            mac: Some("AA:BB:CC:DD:EE:FF".into()),
2599            ip: Some("10.0.0.4".into()),
2600        };
2601        let hello = |id: &str| LinkHello {
2602            name: "koan test TV".into(),
2603            platform: "tvos".into(),
2604            ..hello(id)
2605        };
2606        nearby_hello(hello("tv"), "koan-test-tv.local:50000");
2607        with(|s| s.seen[0].tv = Some(tv.clone()));
2608        // Met again on the same host, on another port: the record stands.
2609        nearby_hello(hello("tv"), "KOAN-test-tv.local:50001");
2610        assert_eq!(tv_of("tv"), Some(tv));
2611        // On another host, it is not that TV's any more.
2612        nearby_hello(hello("tv"), "koan-test-other.local:50001");
2613        assert_eq!(tv_of("tv"), None);
2614        with(|s| *s = Store::default());
2615    }
2616
2617    #[test]
2618    fn a_wake_stops_at_the_stage_that_reached_it() {
2619        let (tried, ended) = walk(true, Some(Waking::Network));
2620        assert_eq!(tried, [Waking::Network], "no push for a phone still awake");
2621        assert_eq!(ended, Ended::Reached(Waking::Network));
2622        let (tried, ended) = walk(false, Some(Waking::Push));
2623        assert_eq!(
2624            tried,
2625            [Waking::Push],
2626            "no notification once the push worked"
2627        );
2628        assert_eq!(ended, Ended::Reached(Waking::Push));
2629    }
2630
2631    #[test]
2632    fn a_push_that_does_not_wake_it_falls_back_to_a_notification_then_says_so() {
2633        let (tried, ended) = walk(false, None);
2634        assert_eq!(tried.last(), Some(&Waking::Notification));
2635        assert!(matches!(ended, Ended::Failed(why) if why.contains("notification")));
2636        let plan = wake_plan(false, false, true);
2637        assert!(plan[0].1 <= Duration::from_secs(8) && plan[0].1 >= Duration::from_secs(5));
2638    }
2639
2640    #[test]
2641    fn a_stage_that_cannot_start_ends_the_wake_with_why() {
2642        let ended = run_wake(
2643            &wake_plan(false, false, true),
2644            || true,
2645            |_| Err("Not connected".into()),
2646            |_| true,
2647        );
2648        assert_eq!(ended, Ended::Failed("Not connected".into()));
2649    }
2650
2651    /// Choosing another device mid-wake sends nothing more to the one left
2652    /// behind: no notification "wants to play here" for a choice taken back.
2653    #[test]
2654    fn a_wake_abandoned_for_another_choice_sends_nothing_more() {
2655        let live = std::cell::Cell::new(true);
2656        let mut tried = Vec::new();
2657        let ended = run_wake(
2658            &wake_plan(false, true, true),
2659            || live.get(),
2660            |stage| {
2661                tried.push(stage.clone());
2662                Ok(())
2663            },
2664            |_| {
2665                live.set(false);
2666                false
2667            },
2668        );
2669        assert_eq!(tried, [Waking::Network]);
2670        assert_eq!(ended, Ended::Cancelled);
2671    }
2672
2673    #[test]
2674    fn one_wake_at_a_time_joined_by_the_same_choice_and_ended_by_another() {
2675        let _held = STORE_LOCK.lock();
2676        with(|s| *s = Store::default());
2677        let first = begin_wake("phone").expect("starts");
2678        assert!(begin_wake("phone").is_none(), "choosing it again joins it");
2679        assert!(wake_is_current(first));
2680        cancel_wake(Some("phone"));
2681        assert!(wake_is_current(first), "the same choice does not end it");
2682        cancel_wake(Some("mac"));
2683        assert!(!wake_is_current(first), "another choice does");
2684        let second = begin_wake("mac").unwrap();
2685        assert!(begin_wake("phone").is_some() && !wake_is_current(second));
2686        with(|s| *s = Store::default());
2687    }
2688
2689    /// The wake starts the moment a device stops being reachable, not once
2690    /// the heartbeat has passed and it is labelled asleep.
2691    #[test]
2692    fn a_device_chosen_seconds_after_it_dropped_is_woken() {
2693        let _held = STORE_LOCK.lock();
2694        crate::config::isolate_config_for_tests();
2695        with(|s| *s = Store::default());
2696        set_linked(true);
2697        set_account(vec![device("phone", false)]);
2698        let mut phone = device("phone", false);
2699        phone.linked = false;
2700        phone.wakeable = Some(true);
2701        phone.last_seen = Some(chrono::Utc::now().timestamp() - 10);
2702        set_account(vec![phone]);
2703        let listed = list().into_iter().find(|d| d.id == "phone").unwrap();
2704        assert!(!listed.asleep && !listed.awake, "reconnecting, not asleep");
2705        wake("phone");
2706        // Its thread may already have finished: with no link here, the push
2707        // stage fails at once and says so on the row.
2708        let ran = (0..100).any(|_| {
2709            let seen = with(|s| {
2710                s.attempt.as_ref().is_some_and(|(_, d)| d == "phone")
2711                    || s.waking.contains_key("phone")
2712            });
2713            if !seen {
2714                std::thread::sleep(Duration::from_millis(10));
2715            }
2716            seen
2717        });
2718        assert!(ran, "the pipeline runs");
2719        cancel_wake(None);
2720        with(|s| *s = Store::default());
2721    }
2722
2723    /// A device linked for ten minutes that left half a minute ago is tried
2724    /// on the network first: the time is when it was last heard there, not
2725    /// when it first connected.
2726    #[test]
2727    fn last_heard_on_the_network_is_the_last_message_not_the_first() {
2728        let _held = STORE_LOCK.lock();
2729        crate::config::isolate_config_for_tests();
2730        with(|s| *s = Store::default());
2731        nearby_hello(hello("phone"), "10.0.0.7:5626");
2732        with(|s| {
2733            for seen in &mut s.seen {
2734                seen.at -= 10 * 60;
2735            }
2736        });
2737        nearby_state("phone", LinkState::default());
2738        nearby_gone("phone");
2739        let left = with(|s| s.lan_heard["phone"]);
2740        assert!(recently_on_network("phone", left + 30));
2741        assert!(!recently_on_network("phone", left + NETWORK_RECENT + 1));
2742        with(|s| *s = Store::default());
2743    }
2744
2745    /// A device met only on the network, asleep, is woken through the
2746    /// server, which decides whether it may by where the two devices are; with
2747    /// no server to ask, it is not woken at all.
2748    #[test]
2749    fn a_stranger_asleep_on_the_network_is_woken_through_the_server_if_linked() {
2750        let _held = STORE_LOCK.lock();
2751        crate::config::isolate_config_for_tests();
2752        with(|s| *s = Store::default());
2753        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2754        used("stranger");
2755        nearby_gone("stranger");
2756        with(|s| {
2757            *s.live.get_mut("stranger").unwrap() -= 10 * 60;
2758            *s.lan_heard.get_mut("stranger").unwrap() -= 10 * 60;
2759        });
2760        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2761        assert!(listed.asleep && !listed.wakeable, "no server to ask");
2762        wake("stranger");
2763        assert!(
2764            with(|s| s.attempt.is_none() && s.waking.is_empty()),
2765            "too long gone for the network, and no server"
2766        );
2767
2768        set_linked(true);
2769        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2770        assert!(listed.asleep && listed.wakeable);
2771        assert!(choosable("stranger").is_ok());
2772        let plan = wake_plan(false, false, listed.wakeable);
2773        assert_eq!(plan[0].0, Waking::Push, "asks the server");
2774        with(|s| *s = Store::default());
2775    }
2776
2777    /// What a stranger or a sharing account asks this device to pass on goes
2778    /// over the network or nowhere: never up this device's link, as its
2779    /// account, to devices the asker has no claim on.
2780    #[test]
2781    fn a_hand_off_asked_by_a_stranger_never_goes_up_the_link() {
2782        let _held = STORE_LOCK.lock();
2783        crate::config::isolate_config_for_tests();
2784        with(|s| *s = Store::default());
2785        set_linked(true);
2786        set_account(vec![device("mac", false)]);
2787        let play = LinkCommand::Play {
2788            track_ids: vec!["t".into()],
2789            start_at: 0,
2790            position_ms: 0,
2791            paused: false,
2792            handoff: true,
2793            mode: crate::player::state::QueueMode::Keep,
2794        };
2795        assert!(
2796            send_nearby("mac", play).is_err(),
2797            "the account's Mac is reached only through the link"
2798        );
2799        with(|s| *s = Store::default());
2800    }
2801
2802    /// A hand-off asked by anyone but the account acts as the asker: never up
2803    /// this device's link to the account's own devices.
2804    #[test]
2805    fn a_hand_off_for_someone_else_never_reaches_the_accounts_own_devices() {
2806        let _held = STORE_LOCK.lock();
2807        crate::config::isolate_config_for_tests();
2808        with(|s| *s = Store::default());
2809        set_linked(true);
2810        set_account(vec![device("mac", false)]);
2811        let play = || LinkCommand::Play {
2812            track_ids: vec!["t".into()],
2813            start_at: 0,
2814            position_ms: 0,
2815            paused: false,
2816            handoff: true,
2817            mode: crate::player::state::QueueMode::Keep,
2818        };
2819        for source in [
2820            CommandSource::Shared,
2821            CommandSource::Nearby,
2822            CommandSource::Stranger,
2823        ] {
2824            let refused = send_for(source, "mac", play()).unwrap_err();
2825            assert!(
2826                refused.contains("this account's") || refused.contains("not on this network"),
2827                "{source:?}: {refused}"
2828            );
2829        }
2830        with(|s| *s = Store::default());
2831    }
2832
2833    /// Another account's device that stops being shared is gone at once.
2834    #[test]
2835    fn a_device_no_longer_shared_is_dropped_at_once() {
2836        let _held = STORE_LOCK.lock();
2837        crate::config::isolate_config_for_tests();
2838        with(|s| *s = Store::default());
2839        set_linked(true);
2840        let mut theirs = device("their-phone", false);
2841        theirs.owner = Some("k".into());
2842        set_account(vec![theirs]);
2843        let listed = list().into_iter().find(|d| d.id == "their-phone").unwrap();
2844        assert_eq!(listed.owner.as_deref(), Some("k"));
2845        assert!(!listed.account, "not this account's own");
2846        assert!(send("their-phone", LinkCommand::Sync { full: false }).is_err());
2847        set_account(Vec::new());
2848        assert!(!list().iter().any(|d| d.id == "their-phone"));
2849        with(|s| *s = Store::default());
2850    }
2851
2852    /// A stranger whose connection just closed reads as reconnecting, and is
2853    /// not offered: nothing would carry a command to it.
2854    #[test]
2855    fn a_stranger_that_just_left_cannot_be_chosen() {
2856        let _held = STORE_LOCK.lock();
2857        crate::config::isolate_config_for_tests();
2858        with(|s| *s = Store::default());
2859        nearby_hello(hello("stranger"), "10.0.0.5:5626");
2860        nearby_gone("stranger");
2861        let listed = list().into_iter().find(|d| d.id == "stranger").unwrap();
2862        assert!(!listed.awake && !listed.asleep && !listed.nearby);
2863        assert!(choosable("stranger").is_err());
2864        assert!(send("stranger", LinkCommand::Pause).is_err());
2865        with(|s| *s = Store::default());
2866    }
2867
2868    /// A list held while this app's own link was down says nothing about
2869    /// when anyone was seen: the server's `last_seen` stands.
2870    #[test]
2871    fn a_list_held_across_a_gap_in_the_link_does_not_stamp_now() {
2872        let _held = STORE_LOCK.lock();
2873        crate::config::isolate_config_for_tests();
2874        with(|s| *s = Store::default());
2875        set_linked(true);
2876        set_account(vec![device("phone", true)]);
2877        used("phone");
2878        // Suspended for hours; the phone left meanwhile.
2879        with(|s| *s.live.get_mut("phone").unwrap() -= 4 * 60 * 60);
2880        set_linked(false);
2881        let hours_ago = chrono::Utc::now().timestamp() - 3 * 60 * 60;
2882        set_linked(true);
2883        let mut phone = device("phone", false);
2884        phone.linked = false;
2885        phone.last_seen = Some(hours_ago);
2886        set_account(vec![phone]);
2887        let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2888        assert!(phone.asleep, "not reconnecting: it went hours ago");
2889        // The stamp from when it was linked is earlier than the server's, so
2890        // the server's stands.
2891        assert!(phone.last_seen.unwrap() >= hours_ago);
2892        assert!(
2893            phone.last_seen.unwrap() < chrono::Utc::now().timestamp() - STALE_SECS,
2894            "not seen just now"
2895        );
2896        with(|s| *s = Store::default());
2897    }
2898
2899    /// Relaunched, a device the server no longer lists is gone, not a device
2900    /// seen just now; and one it does list shows nothing it was doing.
2901    #[test]
2902    fn a_restored_device_the_server_drops_is_gone() {
2903        let _held = STORE_LOCK.lock();
2904        crate::config::isolate_config_for_tests();
2905        with(|s| *s = Store::default());
2906        // What an older run left: no times for these.
2907        let saved = Saved {
2908            target: None,
2909            account: vec![device("mac", true), device("phone", true)],
2910            nearby: Vec::new(),
2911            live: HashMap::new(),
2912            departed: Vec::new(),
2913            used: None,
2914        };
2915        std::fs::write(saved_path(), serde_json::to_string(&saved).unwrap()).unwrap();
2916        restore();
2917        let listed = list();
2918        assert!(listed.iter().all(|d| d.state.is_none() && !d.awake));
2919        set_linked(true);
2920        let mut phone = device("phone", false);
2921        phone.linked = false;
2922        phone.wakeable = Some(true);
2923        set_account(vec![phone]);
2924        assert!(
2925            !list().iter().any(|d| d.id == "mac"),
2926            "never seen here, and not listed: dropped"
2927        );
2928        with(|s| *s = Store::default());
2929    }
2930
2931    /// A server with no push key cannot wake anything, whatever tokens it
2932    /// holds: such a device is shown asleep and cannot be chosen.
2933    #[test]
2934    fn a_device_the_server_cannot_push_is_not_wakeable() {
2935        let _held = STORE_LOCK.lock();
2936        crate::config::isolate_config_for_tests();
2937        with(|s| *s = Store::default());
2938        let mut phone = device("phone", false);
2939        phone.linked = false;
2940        phone.wakeable = Some(false);
2941        phone.last_seen = Some(chrono::Utc::now().timestamp() - 3600);
2942        set_account(vec![phone]);
2943        used("phone");
2944        let phone = list().into_iter().find(|d| d.id == "phone").unwrap();
2945        assert!(phone.asleep && !phone.wakeable);
2946        assert!(choosable("phone").is_err());
2947        with(|s| *s = Store::default());
2948    }
2949
2950    #[test]
2951    fn a_device_found_both_ways_is_one_and_a_lost_target_is_kept() {
2952        let _held = STORE_LOCK.lock();
2953        crate::config::isolate_config_for_tests();
2954        with(|s| *s = Store::default());
2955        set_account(vec![device("mac", false), device("phone", true)]);
2956        let listed = list();
2957        assert_eq!(listed[0].id, "phone", "playing first");
2958        assert_eq!(listed.len(), 2);
2959
2960        nearby_hello(
2961            LinkHello {
2962                id: "mac".into(),
2963                name: "Mac".into(),
2964                platform: "macos".into(),
2965                library: None,
2966                acks: false,
2967                nonce: None,
2968            },
2969            "mac.local:5626",
2970        );
2971        nearby_hello(
2972            LinkHello {
2973                id: "tv".into(),
2974                name: "Living room".into(),
2975                platform: "macos".into(),
2976                library: Some("elsewhere".into()),
2977                acks: false,
2978                nonce: None,
2979            },
2980            "10.0.0.9:5626",
2981        );
2982        let listed = list();
2983        assert_eq!(listed.len(), 3);
2984        let mac = listed.iter().find(|d| d.id == "mac").unwrap();
2985        assert!(mac.account && mac.nearby && mac.same_library);
2986        let tv = listed.iter().find(|d| d.id == "tv").unwrap();
2987        assert!(!tv.account && !tv.same_library);
2988
2989        set_target(Some("tv".into()));
2990        nearby_gone("tv");
2991        // Long past the heartbeat: asleep, kept, and still the target.
2992        with(|s| *s.live.get_mut("tv").unwrap() -= STALE_SECS + 31 * 60);
2993        assert_eq!(target(), Some("tv".into()), "kept while out of reach");
2994        let tv = list().into_iter().find(|d| d.id == "tv").unwrap();
2995        assert_eq!(tv.name, "Living room");
2996        assert!(!tv.awake && tv.asleep);
2997
2998        // What a relaunch finds.
2999        with(|s| *s = Store::default());
3000        restore();
3001        assert_eq!(target(), Some("tv".into()));
3002        let listed = list();
3003        assert!(
3004            listed.iter().any(|d| d.id == "mac" && !d.awake),
3005            "last heard of, not linked"
3006        );
3007        let remembered = remembered_nearby();
3008        assert!(
3009            remembered
3010                .iter()
3011                .any(|n| n.id == "tv" && n.addr == "10.0.0.9:5626")
3012        );
3013
3014        // What a device on the network asks this one to pass on stays on the
3015        // network: the account's Mac, out of reach there, is not sent to up
3016        // the link.
3017        nearby_gone("mac");
3018        let play = LinkCommand::Play {
3019            track_ids: vec!["t".into()],
3020            start_at: 0,
3021            position_ms: 0,
3022            paused: false,
3023            handoff: true,
3024            mode: crate::player::state::QueueMode::Keep,
3025        };
3026        assert!(listed.iter().any(|d| d.id == "mac" && d.account));
3027        assert!(send_nearby("mac", play).is_err());
3028
3029        set_target(None);
3030        set_account(Vec::new());
3031        nearby_gone("mac");
3032        assert!(
3033            list().iter().all(|d| d.last_seen.is_some()),
3034            "only what has just left, kept for a heartbeat"
3035        );
3036        with(|s| *s = Store::default());
3037    }
3038}