Skip to main content

rightkit_send/
lib.rs

1//! Nearby sharing over the open LocalSend protocol v2 (github.com/localsend/protocol),
2//! with the Pulse extensions (`pulse` key in prepare-upload, `px` in Bonjour TXT).
3//!
4//! `Service` is a long-running object a host owns: it announces this device,
5//! keeps a list of nearby devices, receives files over HTTPS after the owner
6//! accepts, and sends files to a chosen device. The host supplies a stored
7//! `Identity`, the trust store path, the folders and the wording of anything it
8//! shows; it hears about changes through the `Event` callback and reads
9//! `Service::snapshot`. Errors are typed (`Error`); no user-facing English is
10//! produced here.
11
12#[cfg(all(feature = "bonjour", target_os = "macos"))]
13mod bonjour;
14#[cfg(all(feature = "multicast", not(target_os = "ios")))]
15pub mod discovery;
16mod error;
17pub mod net;
18mod policy;
19pub mod proto;
20mod receive;
21pub mod send;
22pub mod trust;
23mod workers;
24
25pub use error::Error;
26pub use net::Identity;
27pub use policy::Policy;
28pub use send::{Entry, Peer, SendItem};
29use trust::normalize;
30pub use trust::{TrustEntry, TrustState};
31use workers::{Stopper, Worker};
32
33use proto::DeviceInfo;
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::net::{IpAddr, Ipv4Addr, TcpListener, TcpStream};
37use std::path::PathBuf;
38use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
39use std::sync::{Arc, Condvar, Mutex, MutexGuard};
40use std::thread;
41use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
42
43/// What the host configures. Every string here is the host's own choice: the
44/// crate never names a product.
45#[derive(Clone, Debug)]
46pub struct Config {
47    /// The host's product name, reported in `Snapshot::product`.
48    pub product_name: String,
49    /// How other devices list this device.
50    pub alias: String,
51    /// Listen port (TLS). 0 picks a free port; `Service::bound_port` reports it.
52    pub port: u16,
53    /// Device class announced to others: "desktop", "mobile", "web", "headless" or "server".
54    pub device_type: String,
55    /// Model announced to others (empty for none).
56    pub device_model: String,
57    /// Where received files go.
58    pub save_dir: PathBuf,
59    /// The JSON trust store file. Created on the first change.
60    pub trust_path: PathBuf,
61    /// Sent as the HTTP User-Agent on every outgoing request.
62    pub user_agent: String,
63    /// Written into the macOS quarantine attribute as the agent name.
64    pub quarantine_agent: String,
65    /// Suffix of the temporary file a receive is written to until it is complete (".pulse-part" for Pulse).
66    pub temp_suffix: String,
67    /// Prefix of the diagnostic lines written to stderr (e.g. "pulse sharing:").
68    pub log_prefix: String,
69    /// Limits and folder names. `Policy::default()` is Pulse's current behaviour.
70    pub policy: Policy,
71}
72
73/// A device on the network.
74#[derive(Clone, Debug, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct Device {
77    pub fingerprint: String,
78    pub alias: String,
79    pub device_model: Option<String>,
80    pub device_type: Option<String>,
81    pub ip: String,
82    pub port: u16,
83    pub protocol: String,
84    /// The device announced the Pulse extensions (Bonjour TXT `px=1`). A host
85    /// decides what to offer; LocalSend-only devices report false.
86    pub px: bool,
87}
88
89#[derive(Clone, Debug, Serialize)]
90pub struct IncomingFile {
91    pub name: String,
92    pub size: u64,
93}
94
95/// A request waiting for the user's answer.
96#[derive(Clone, Debug, Serialize)]
97#[serde(rename_all = "camelCase")]
98pub struct Incoming {
99    pub id: String,
100    pub from: String,
101    pub device_model: Option<String>,
102    pub fingerprint: String,
103    pub ip: String,
104    pub file_count: usize,
105    pub total_bytes: u64,
106    pub is_message: bool,
107    /// Always None: a text message is not shown until it is accepted.
108    pub preview: Option<String>,
109    /// The first few files, for display (none for a message).
110    pub files: Vec<IncomingFile>,
111    /// The sender proved it holds the key behind its fingerprint by presenting its
112    /// certificate. Anything less is unproven: saved apart, no clipboard, capped.
113    pub verified: bool,
114}
115
116/// One send or receive, from the first request to the last byte.
117#[derive(Clone, Debug, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Transfer {
120    pub id: String,
121    /// "send" or "receive".
122    pub direction: String,
123    pub peer: String,
124    pub peer_fingerprint: String,
125    /// waiting, active, done, failed, cancelled or declined.
126    pub state: String,
127    pub total_bytes: u64,
128    pub done_bytes: u64,
129    pub files_total: usize,
130    pub files_done: usize,
131    pub current: Option<String>,
132    pub saved_to: Option<String>,
133    pub saved_files: Vec<String>,
134    pub error: Option<Error>,
135    /// A received text message, shown with Copy rather than saved as a file.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub message: Option<String>,
138    /// Sent from the clipboard: the receiver puts it on its clipboard.
139    #[serde(default)]
140    pub clipboard: bool,
141    pub started: u64,
142    pub finished: Option<u64>,
143}
144
145impl Transfer {
146    pub fn is_open(&self) -> bool {
147        self.state == "waiting" || self.state == "active"
148    }
149}
150
151#[derive(Clone, Debug, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct Snapshot {
154    /// The host product name from `Config::product_name`.
155    pub product: String,
156    pub alias: String,
157    pub fingerprint: String,
158    pub port: u16,
159    pub save_dir: String,
160    pub devices: Vec<Device>,
161    pub incoming: Vec<Incoming>,
162    pub transfers: Vec<Transfer>,
163    /// Things that don't work, typed (discovery socket, …).
164    pub warnings: Vec<Error>,
165    /// "unknown", "granted" or "blocked" (macOS Local Network access).
166    pub local_network: String,
167    /// A sweep of the local networks is running.
168    pub scanning: bool,
169}
170
171#[derive(Clone, Debug)]
172pub enum Event {
173    /// The device list changed.
174    Devices,
175    /// A device wants to send; the user must answer (`Service::respond`).
176    Incoming(Incoming),
177    /// A request was answered, withdrawn or timed out.
178    IncomingResolved(String),
179    /// Bytes moved (rate-limited).
180    Progress(Transfer),
181    /// A transfer reached an end state.
182    Finished(Transfer),
183    /// Anything else (a transfer dismissed).
184    Changed,
185}
186
187/// One entry of `known-devices.json`: a bare fingerprint (accepted before).
188/// Older files held objects with extra fields; only the fingerprint is kept.
189#[derive(Deserialize)]
190#[serde(untagged)]
191enum KnownEntry {
192    Plain(String),
193    Object {
194        fingerprint: String,
195        #[serde(default)]
196        label: Option<String>,
197    },
198}
199
200/// The old list as (fingerprint, label) pairs; the label is empty when none.
201fn load_known(path: &std::path::Path) -> Vec<(String, String)> {
202    let entries = std::fs::read(path)
203        .ok()
204        .and_then(|bytes| serde_json::from_slice::<Vec<KnownEntry>>(&bytes).ok())
205        .unwrap_or_default();
206    entries
207        .into_iter()
208        .map(|entry| match entry {
209            KnownEntry::Plain(fingerprint) => (fingerprint, String::new()),
210            KnownEntry::Object { fingerprint, label } => (fingerprint, label.unwrap_or_default()),
211        })
212        .collect()
213}
214
215/// Write `contents` next to `path` and rename it into place; the file is private
216/// (0600) on Unix. The temp file is `.<name>.tmp-<hex>` in the same directory,
217/// created exclusively (a name collision retries), fsynced, renamed over the
218/// target, and the directory is fsynced so the rename survives power loss. A
219/// failure removes the temp file and leaves the target as it was.
220///
221/// Deliberately not `rightkit-fs`: that crate requires rustc 1.98 and this one
222/// is pinned to 1.90 (see docs/plans/2026-10-10-rightkit-send-design.md).
223fn write_atomic(path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
224    use std::io::Write;
225    let parent = match path.parent() {
226        Some(p) if !p.as_os_str().is_empty() => p,
227        _ => std::path::Path::new("."),
228    };
229    let name = path
230        .file_name()
231        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "no file name"))?
232        .to_string_lossy()
233        .into_owned();
234    std::fs::create_dir_all(parent)?;
235    let mut options = std::fs::OpenOptions::new();
236    options.write(true).create_new(true);
237    #[cfg(unix)]
238    {
239        use std::os::unix::fs::OpenOptionsExt;
240        options.mode(0o600);
241    }
242    let mut attempt = 0;
243    let (temp, mut file) = loop {
244        let temp = parent.join(format!(".{name}.tmp-{}", proto::random_hex(4)));
245        match options.open(&temp) {
246            Ok(file) => break (temp, file),
247            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists && attempt < 8 => attempt += 1,
248            Err(e) => return Err(e),
249        }
250    };
251    let written = file.write_all(contents).and_then(|()| file.sync_all());
252    drop(file);
253    if let Err(e) = written {
254        let _ = std::fs::remove_file(&temp);
255        return Err(e);
256    }
257    if let Err(e) = std::fs::rename(&temp, path) {
258        let _ = std::fs::remove_file(&temp);
259        return Err(e);
260    }
261    // The new content is in place; a directory sync failure is not a failed write.
262    #[cfg(unix)]
263    if let Ok(dir) = std::fs::File::open(parent) {
264        let _ = dir.sync_all();
265    }
266    Ok(())
267}
268
269#[cfg(test)]
270mod write_atomic_tests {
271    use super::write_atomic;
272
273    fn dir(tag: &str) -> std::path::PathBuf {
274        let d = std::env::temp_dir().join(format!("rk-send-atomic-{tag}-{}", proto::random_hex(4)));
275        std::fs::create_dir_all(&d).unwrap();
276        d
277    }
278    use super::proto;
279
280    #[test]
281    fn replaces_existing_content_and_leaves_no_temp() {
282        let d = dir("replace");
283        let target = d.join("nearby_devices.json");
284        write_atomic(&target, b"one").unwrap();
285        write_atomic(&target, b"two").unwrap();
286        assert_eq!(std::fs::read(&target).unwrap(), b"two");
287        let names: Vec<_> = std::fs::read_dir(&d)
288            .unwrap()
289            .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
290            .collect();
291        assert_eq!(names, vec!["nearby_devices.json".to_string()]);
292        let _ = std::fs::remove_dir_all(&d);
293    }
294
295    #[test]
296    fn creates_missing_parents() {
297        let d = dir("parents");
298        let target = d.join("a").join("b").join("store.json");
299        write_atomic(&target, b"x").unwrap();
300        assert_eq!(std::fs::read(&target).unwrap(), b"x");
301        let _ = std::fs::remove_dir_all(&d);
302    }
303
304    #[cfg(unix)]
305    #[test]
306    fn the_result_is_owner_only() {
307        use std::os::unix::fs::PermissionsExt;
308        let d = dir("mode");
309        let target = d.join("store.json");
310        write_atomic(&target, b"x").unwrap();
311        assert_eq!(
312            std::fs::metadata(&target).unwrap().permissions().mode() & 0o777,
313            0o600
314        );
315        // Replacing a broader file narrows it: the new inode is 0600.
316        std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o644)).unwrap();
317        write_atomic(&target, b"y").unwrap();
318        assert_eq!(
319            std::fs::metadata(&target).unwrap().permissions().mode() & 0o777,
320            0o600
321        );
322        let _ = std::fs::remove_dir_all(&d);
323    }
324
325    #[test]
326    fn a_sibling_differing_only_by_extension_is_untouched() {
327        let d = dir("sibling");
328        std::fs::write(d.join("store.bak"), b"keep").unwrap();
329        write_atomic(&d.join("store.json"), b"x").unwrap();
330        assert_eq!(std::fs::read(d.join("store.bak")).unwrap(), b"keep");
331        let _ = std::fs::remove_dir_all(&d);
332    }
333
334    #[test]
335    fn a_failed_rename_removes_the_temp_and_keeps_the_target() {
336        let d = dir("fail");
337        // A directory at the target makes the rename fail on every platform.
338        let target = d.join("store.json");
339        std::fs::create_dir(&target).unwrap();
340        assert!(write_atomic(&target, b"x").is_err());
341        assert!(target.is_dir());
342        let names: Vec<_> = std::fs::read_dir(&d)
343            .unwrap()
344            .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
345            .collect();
346        assert_eq!(
347            names,
348            vec!["store.json".to_string()],
349            "no stray temp: {names:?}"
350        );
351        let _ = std::fs::remove_dir_all(&d);
352    }
353
354    #[cfg(unix)]
355    #[test]
356    fn a_symlink_at_the_target_is_replaced_not_followed() {
357        let d = dir("link");
358        let outside = d.join("outside.txt");
359        std::fs::write(&outside, b"precious").unwrap();
360        let target = d.join("store.json");
361        std::os::unix::fs::symlink(&outside, &target).unwrap();
362        write_atomic(&target, b"x").unwrap();
363        assert_eq!(std::fs::read(&outside).unwrap(), b"precious");
364        assert_eq!(std::fs::read(&target).unwrap(), b"x");
365        assert!(
366            !std::fs::symlink_metadata(&target)
367                .unwrap()
368                .file_type()
369                .is_symlink()
370        );
371        let _ = std::fs::remove_dir_all(&d);
372    }
373}
374
375pub(crate) struct Seen {
376    pub device: Device,
377    pub last_seen: Instant,
378    pub misses: u32,
379}
380
381pub(crate) struct Pending {
382    pub incoming: Incoming,
383    pub decision: Mutex<Option<bool>>,
384    pub changed: Condvar,
385}
386
387pub(crate) struct CancelHandle {
388    pub flag: AtomicBool,
389    pub stream: Mutex<Option<TcpStream>>,
390}
391
392pub(crate) struct SessionFile {
393    pub meta: proto::FileMeta,
394    pub token: String,
395    pub done: bool,
396}
397
398pub(crate) struct Session {
399    pub transfer_id: String,
400    pub peer_ip: IpAddr,
401    pub peer_fingerprint: String,
402    pub files: HashMap<String, SessionFile>,
403    pub cancel: Arc<AtomicBool>,
404    pub activity: Instant,
405    pub remaining: usize,
406    pub saved: Vec<PathBuf>,
407    pub save_dir: PathBuf,
408    /// The sender proved its key by certificate (not only by callback).
409    pub proven: bool,
410}
411
412pub(crate) struct Inner {
413    pub cfg: Mutex<Config>,
414    pub me: DeviceInfo,
415    pub tls: Arc<rustls::ServerConfig>,
416    pub stop: AtomicBool,
417    /// Wakes the threads that wait with a timeout (liveness, accept retry) when the service stops.
418    pub park: Stopper,
419    pub devices: Mutex<Vec<Seen>>,
420    pub transfers: Mutex<Vec<Transfer>>,
421    pub pending: Mutex<HashMap<String, Arc<Pending>>>,
422    pub sessions: Mutex<HashMap<String, Session>>,
423    pub cancels: Mutex<HashMap<String, Arc<CancelHandle>>>,
424    pub trust: Mutex<trust::TrustStore>,
425    pub on_event: Arc<dyn Fn(Event) + Send + Sync>,
426    pub last_progress: Mutex<Instant>,
427    pub connections: AtomicUsize,
428    pub warnings: Mutex<Vec<Error>>,
429    /// 0 unknown, 1 reachable, 2 macOS is refusing local network access.
430    pub local_network: std::sync::atomic::AtomicU8,
431    /// A sweep of the local networks is running.
432    pub scanning: AtomicBool,
433    /// Someone asked for a sweep; the scanner thread takes it.
434    #[cfg(all(feature = "multicast", not(target_os = "ios")))]
435    pub scan_requested: AtomicBool,
436    /// This Mac's own addresses and when they were read.
437    pub own_ips: Mutex<(Instant, Vec<Ipv4Addr>)>,
438    /// Bonjour instance name to the fingerprint it resolved to, so a Remove can be
439    /// mapped back to a device.
440    #[cfg(all(feature = "bonjour", target_os = "macos"))]
441    pub bonjour_names: Mutex<HashMap<String, String>>,
442}
443
444pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
445    mutex
446        .lock()
447        .unwrap_or_else(|poisoned| poisoned.into_inner())
448}
449
450pub(crate) fn now_ms() -> u64 {
451    SystemTime::now()
452        .duration_since(UNIX_EPOCH)
453        .map(|d| d.as_millis() as u64)
454        .unwrap_or(0)
455}
456
457impl Inner {
458    pub fn emit(&self, event: Event) {
459        (self.on_event)(event);
460    }
461
462    fn config(&self) -> Config {
463        lock(&self.cfg).clone()
464    }
465
466    /// Whether `ip` is one of this Mac's own addresses.
467    fn is_own_ip(&self, ip: IpAddr) -> bool {
468        let IpAddr::V4(v4) = ip else {
469            return ip.is_loopback();
470        };
471        if v4.is_loopback() {
472            return true;
473        }
474        let mut own = lock(&self.own_ips);
475        if own.0.elapsed() > Duration::from_secs(30) {
476            *own = (Instant::now(), local_ipv4s());
477        }
478        own.1.contains(&v4)
479    }
480
481    /// Record a device heard from `ip`, unless it is this Mac itself. Emits
482    /// `Devices` when the list changed. Returns whether the device was taken.
483    pub fn upsert(&self, info: &DeviceInfo, ip: IpAddr) -> bool {
484        let fingerprint = normalize(&info.fingerprint);
485        if fingerprint.is_empty() || fingerprint == self.me.fingerprint || self.is_own_ip(ip) {
486            return false;
487        }
488        // An announcement is nobody's proof: it never sets a record's proof.
489        lock(&self.trust).observe(
490            &fingerprint,
491            &info.alias,
492            info.device_model.as_deref(),
493            info.device_type.as_deref(),
494            None,
495        );
496        let device = Device {
497            fingerprint,
498            alias: info.alias.clone(),
499            device_model: info.device_model.clone(),
500            device_type: info.device_type.clone(),
501            ip: ip.to_string(),
502            port: info.port,
503            protocol: info.protocol.clone(),
504            px: false,
505        };
506        let changed = {
507            let mut devices = lock(&self.devices);
508            match devices
509                .iter_mut()
510                .find(|s| s.device.fingerprint == device.fingerprint)
511            {
512                Some(seen) => {
513                    // An announcement can move a device (ip, port) but never takes
514                    // it from https back to http.
515                    let mut device = device;
516                    device.px = seen.device.px || device.px;
517                    if seen.device.protocol == "https" {
518                        device.protocol = "https".to_string();
519                    }
520                    let changed = seen.device.alias != device.alias
521                        || seen.device.ip != device.ip
522                        || seen.device.port != device.port
523                        || seen.device.protocol != device.protocol;
524                    seen.device = device;
525                    seen.last_seen = Instant::now();
526                    seen.misses = 0;
527                    changed
528                }
529                None => {
530                    devices.push(Seen {
531                        device,
532                        last_seen: Instant::now(),
533                        misses: 0,
534                    });
535                    true
536                }
537            }
538        };
539        if changed {
540            self.emit(Event::Devices);
541        }
542        true
543    }
544
545    /// Sweep the local networks for devices multicast didn't find.
546    #[cfg(all(feature = "multicast", not(target_os = "ios")))]
547    fn scan(&self) {
548        self.scanning.store(true, Ordering::Relaxed);
549        self.emit(Event::Changed);
550        discovery::sweep(&self.me, &self.stop, &|heard| {
551            self.upsert(&heard.info, heard.ip);
552        });
553        self.scanning.store(false, Ordering::Relaxed);
554        self.emit(Event::Changed);
555    }
556
557    pub fn device_list(&self) -> Vec<Device> {
558        let mut list: Vec<Device> = lock(&self.devices)
559            .iter()
560            .map(|s| s.device.clone())
561            .collect();
562        list.sort_by_key(|d| d.alias.to_lowercase());
563        list
564    }
565
566    /// The port the device with this fingerprint announced, if it is on the list.
567    pub fn announced_port(&self, fingerprint: &str) -> Option<u16> {
568        let fingerprint = normalize(fingerprint);
569        lock(&self.devices)
570            .iter()
571            .find(|s| s.device.fingerprint == fingerprint)
572            .map(|s| s.device.port)
573    }
574
575    pub fn trust_state(&self, fingerprint: &str) -> TrustState {
576        lock(&self.trust).state_of(fingerprint)
577    }
578
579    pub fn add_transfer(&self, transfer: Transfer) {
580        let mut transfers = lock(&self.transfers);
581        transfers.push(transfer);
582        while transfers.len() > 12 {
583            match transfers.iter().position(|t| !t.is_open()) {
584                Some(index) => {
585                    transfers.remove(index);
586                }
587                None => break,
588            }
589        }
590    }
591
592    /// Change one transfer. A progress event goes out at most ten times a
593    /// second unless `force` is set.
594    pub fn update_transfer(&self, id: &str, force: bool, change: impl FnOnce(&mut Transfer)) {
595        let updated = {
596            let mut transfers = lock(&self.transfers);
597            transfers.iter_mut().find(|t| t.id == id).map(|t| {
598                change(t);
599                t.clone()
600            })
601        };
602        let Some(transfer) = updated else { return };
603        let due = {
604            let mut last = lock(&self.last_progress);
605            if force || last.elapsed() >= Duration::from_millis(100) {
606                *last = Instant::now();
607                true
608            } else {
609                false
610            }
611        };
612        if due {
613            self.emit(Event::Progress(transfer));
614        }
615    }
616
617    /// Put a transfer in an end state, once.
618    pub fn end_transfer(&self, id: &str, state: &str, error: Option<Error>) {
619        let ended = {
620            let mut transfers = lock(&self.transfers);
621            transfers
622                .iter_mut()
623                .find(|t| t.id == id && t.is_open())
624                .map(|t| {
625                    t.state = state.to_string();
626                    t.error = error;
627                    t.finished = Some(now_ms());
628                    if state == "done" {
629                        t.done_bytes = t.total_bytes;
630                        t.files_done = t.files_total;
631                    }
632                    t.current = None;
633                    t.clone()
634                })
635        };
636        if let Some(transfer) = ended {
637            self.emit(Event::Finished(transfer));
638        }
639    }
640}
641
642/// Runtime figures of a running service, computed on call by [`Service::status`].
643#[derive(Clone, Debug)]
644pub struct SendStatus {
645    /// Devices on the nearby list.
646    pub devices: usize,
647    /// Transfers not yet finished.
648    pub transfers_open: usize,
649    /// Transfers held in the list, finished ones included until dismissed.
650    pub transfers_listed: usize,
651    /// Incoming requests waiting for an answer.
652    pub pending_incoming: usize,
653    /// Connections being served now (capped at 64).
654    pub connections: usize,
655    /// Typed warnings currently raised.
656    pub warnings: usize,
657    /// A sweep of the local networks is running.
658    pub scanning: bool,
659    /// Resident memory, CPU time and I/O counters of the whole process (the acceptor, discovery and
660    /// liveness threads run in it), or `None` when the host cannot report them. See
661    /// `rightkit_child_process::ProcessUsage`.
662    pub process: Option<rightkit_child_process::ProcessUsage>,
663}
664
665/// A running sharing service. Dropping it stops it.
666pub struct Service {
667    inner: Arc<Inner>,
668    /// The acceptor and liveness threads, ended and joined by [`Service::stop`].
669    workers: Mutex<Vec<Worker>>,
670    #[cfg(all(feature = "bonjour", target_os = "macos"))]
671    bonjour: Mutex<Option<(bonjour::Advertiser, bonjour::Browser)>>,
672}
673
674impl Service {
675    /// Start sharing with `config`, presenting `identity`: the host's stored
676    /// certificate and key. Fails before anything is announced when the identity,
677    /// the trust store or the listen port cannot be used.
678    pub fn start(
679        config: Config,
680        identity: Identity,
681        on_event: Arc<dyn Fn(Event) + Send + Sync>,
682    ) -> Result<Service, Error> {
683        let tls = net::server_config(&identity).map_err(|e| Error::Identity {
684            detail: e.to_string(),
685        })?;
686        // Windows lets a second program bind a port another one holds in some
687        // cases (a dual-stack listener); a connection that succeeds first says
688        // somebody is already listening, so say so instead of failing silently.
689        #[cfg(windows)]
690        {
691            if config.port != 0 {
692                let local = std::net::SocketAddr::from((Ipv4Addr::LOCALHOST, config.port));
693                if TcpStream::connect_timeout(&local, Duration::from_millis(300)).is_ok() {
694                    return Err(Error::PortInUse { port: config.port });
695                }
696            }
697        }
698        let listener = TcpListener::bind(("0.0.0.0", config.port)).map_err(|e| {
699            if e.kind() == std::io::ErrorKind::AddrInUse
700                || (cfg!(windows) && e.kind() == std::io::ErrorKind::PermissionDenied)
701            {
702                Error::PortInUse { port: config.port }
703            } else {
704                Error::Listen {
705                    port: config.port,
706                    detail: e.to_string(),
707                }
708            }
709        })?;
710        let port = listener
711            .local_addr()
712            .map(|address| address.port())
713            .map_err(|e| Error::Listen {
714                port: config.port,
715                detail: e.to_string(),
716            })?;
717        let mut config = config;
718        config.port = port;
719
720        net::set_client_identity(&identity);
721        net::set_user_agent(&config.user_agent);
722        let trust = trust::TrustStore::load(&config.trust_path);
723        let me = DeviceInfo {
724            alias: config.alias.clone(),
725            version: proto::VERSION.to_string(),
726            device_model: Some(config.device_model.clone()),
727            device_type: Some(config.device_type.clone()),
728            fingerprint: identity.fingerprint.clone(),
729            port,
730            protocol: "https".to_string(),
731            download: false,
732            announce: None,
733        };
734        let inner = Arc::new(Inner {
735            cfg: Mutex::new(config.clone()),
736            me,
737            tls,
738            stop: AtomicBool::new(false),
739            park: Stopper::new(),
740            devices: Mutex::new(Vec::new()),
741            transfers: Mutex::new(Vec::new()),
742            pending: Mutex::new(HashMap::new()),
743            sessions: Mutex::new(HashMap::new()),
744            cancels: Mutex::new(HashMap::new()),
745            trust: Mutex::new(trust),
746            on_event,
747            last_progress: Mutex::new(Instant::now()),
748            connections: AtomicUsize::new(0),
749            warnings: Mutex::new(Vec::new()),
750            local_network: std::sync::atomic::AtomicU8::new(0),
751            scanning: AtomicBool::new(false),
752            #[cfg(all(feature = "multicast", not(target_os = "ios")))]
753            scan_requested: AtomicBool::new(false),
754            own_ips: Mutex::new((Instant::now(), local_ipv4s())),
755            #[cfg(all(feature = "bonjour", target_os = "macos"))]
756            bonjour_names: Mutex::new(HashMap::new()),
757        });
758
759        // The listener stays blocking: the acceptor sleeps in `accept` until a peer arrives or
760        // `stop` connects to it.
761        let acceptor =
762            spawn_acceptor(inner.clone(), listener, port).map_err(|e| Error::Listen {
763                port,
764                detail: e.to_string(),
765            })?;
766        #[cfg(all(feature = "multicast", not(target_os = "ios")))]
767        {
768            match discovery::listen(port) {
769                Ok(socket) => spawn_discovery(inner.clone(), socket),
770                Err(e) => lock(&inner.warnings).push(Error::DiscoveryUnavailable {
771                    detail: e.to_string(),
772                }),
773            }
774            spawn_announcer(inner.clone());
775            spawn_scanner(inner.clone());
776        }
777        #[cfg(all(feature = "bonjour", target_os = "macos"))]
778        let bonjour = start_bonjour(&inner, &config, &identity.fingerprint);
779        let service = Service {
780            inner: inner.clone(),
781            workers: Mutex::new(vec![acceptor]),
782            #[cfg(all(feature = "bonjour", target_os = "macos"))]
783            bonjour: Mutex::new(bonjour),
784        };
785        match spawn_liveness(inner) {
786            Ok(liveness) => lock(&service.workers).push(liveness),
787            // Dropping `service` on the way out stops what has started.
788            Err(e) => {
789                return Err(Error::Listen {
790                    port,
791                    detail: e.to_string(),
792                });
793            }
794        }
795        Ok(service)
796    }
797
798    /// The port the service listens on (the chosen one when `Config::port` was 0).
799    pub fn bound_port(&self) -> u16 {
800        lock(&self.inner.cfg).port
801    }
802
803    /// Add a device a host-run resolver found (on iOS, Swift's NWBrowser feeds
804    /// this). `txt` holds the TXT keys (`fp`, `alias`, `type`, `model`, `px`).
805    /// The fingerprint must be 64 hex characters; identity is still proven by
806    /// the handshake certificate, never by this announcement.
807    pub fn peer_add_resolved(
808        &self,
809        fingerprint: &str,
810        alias: &str,
811        host: IpAddr,
812        port: u16,
813        txt: &[(String, String)],
814    ) -> Result<(), Error> {
815        let fingerprint = normalize(fingerprint);
816        if !net::valid_pin(&fingerprint) {
817            return Err(Error::InvalidFingerprint);
818        }
819        let info = resolved_info(&fingerprint, alias, port, txt);
820        let px = txt_value(txt, "px").as_deref() == Some("1");
821        self.inner.add_resolved(&info, host, px);
822        Ok(())
823    }
824
825    /// Remove a device a host-run resolver reported gone. Returns whether it was
826    /// on the list.
827    pub fn peer_remove(&self, fingerprint: &str) -> bool {
828        self.inner.remove_device(&normalize(fingerprint))
829    }
830
831    /// Counts and process resource usage, read now. No polling thread runs for this.
832    pub fn status(&self) -> SendStatus {
833        let inner = &self.inner;
834        let transfers = lock(&inner.transfers);
835        SendStatus {
836            devices: lock(&inner.devices).len(),
837            transfers_open: transfers.iter().filter(|t| t.is_open()).count(),
838            transfers_listed: transfers.len(),
839            pending_incoming: lock(&inner.pending).len(),
840            connections: inner.connections.load(Ordering::Relaxed),
841            warnings: lock(&inner.warnings).len(),
842            scanning: inner.scanning.load(Ordering::Relaxed),
843            process: rightkit_child_process::process_usage().ok(),
844        }
845    }
846
847    pub fn snapshot(&self) -> Snapshot {
848        let inner = &self.inner;
849        let cfg = inner.config();
850        let mut incoming: Vec<Incoming> = lock(&inner.pending)
851            .values()
852            .map(|p| p.incoming.clone())
853            .collect();
854        incoming.sort_by(|a, b| a.id.cmp(&b.id));
855        Snapshot {
856            product: cfg.product_name.clone(),
857            alias: cfg.alias,
858            fingerprint: inner.me.fingerprint.clone(),
859            port: cfg.port,
860            save_dir: cfg.save_dir.to_string_lossy().into_owned(),
861            devices: inner.device_list(),
862            incoming,
863            transfers: lock(&inner.transfers).clone(),
864            warnings: lock(&inner.warnings).clone(),
865            local_network: match inner.local_network.load(Ordering::Relaxed) {
866                1 => "granted",
867                2 => "blocked",
868                _ => "unknown",
869            }
870            .to_string(),
871            scanning: inner.scanning.load(Ordering::Relaxed),
872        }
873    }
874
875    /// Look for devices again now: announce on multicast and sweep the local
876    /// networks (the sweep is skipped while one is already running).
877    pub fn refresh(&self) {
878        #[cfg(all(feature = "multicast", not(target_os = "ios")))]
879        {
880            let inner = self.inner.clone();
881            thread::spawn(move || {
882                let _ = discovery::announce(&inner.me, inner.config().port, true);
883            });
884            self.inner.scan_requested.store(true, Ordering::Relaxed);
885        }
886    }
887
888    pub fn devices(&self) -> Vec<Device> {
889        self.inner.device_list()
890    }
891
892    /// The fingerprint of this Mac's certificate.
893    pub fn fingerprint(&self) -> String {
894        self.inner.me.fingerprint.clone()
895    }
896
897    /// Every device with a record, newest sighting first.
898    pub fn devices_trust(&self) -> Vec<TrustEntry> {
899        lock(&self.inner.trust).list()
900    }
901
902    /// What a never-seen device is subject to: Ask or Deny.
903    pub fn default_state(&self) -> TrustState {
904        lock(&self.inner.trust).default_state()
905    }
906
907    /// Set a device to Allow, Ask or Deny. Deny also withdraws what the device
908    /// has pending or in flight.
909    pub fn set_trust(&self, fingerprint: &str, state: TrustState) -> Result<(), Error> {
910        lock(&self.inner.trust).set_state(fingerprint, state)?;
911        if state == TrustState::Deny {
912            self.revoke(fingerprint);
913        }
914        self.inner.emit(Event::Changed);
915        Ok(())
916    }
917
918    /// The owner's own name for a device (empty clears it).
919    pub fn set_label(&self, fingerprint: &str, label: &str) -> Result<(), Error> {
920        lock(&self.inner.trust).set_label(fingerprint, label)?;
921        self.inner.emit(Event::Changed);
922        Ok(())
923    }
924
925    /// Drop the record of a device that is not on the network now.
926    pub fn forget(&self, fingerprint: &str) -> Result<(), Error> {
927        if lock(&self.inner.devices)
928            .iter()
929            .any(|s| s.device.fingerprint == normalize(fingerprint))
930        {
931            return Err(Error::DeviceNearby);
932        }
933        lock(&self.inner.trust).forget(fingerprint)?;
934        self.inner.emit(Event::Changed);
935        Ok(())
936    }
937
938    /// How devices seen for the first time start: Ask or Deny.
939    pub fn set_default_state(&self, state: TrustState) -> Result<(), Error> {
940        lock(&self.inner.trust).set_default(state)?;
941        self.inner.emit(Event::Changed);
942        Ok(())
943    }
944
945    /// Decline this device's waiting requests and cancel its receives in flight.
946    fn revoke(&self, fingerprint: &str) {
947        let fingerprint = normalize(fingerprint);
948        let fingerprint = fingerprint.as_str();
949        let waiting: Vec<Arc<Pending>> = lock(&self.inner.pending)
950            .values()
951            .filter(|p| p.incoming.fingerprint == fingerprint)
952            .cloned()
953            .collect();
954        for p in waiting {
955            *lock(&p.decision) = Some(false);
956            p.changed.notify_all();
957        }
958        let ended: Vec<String> = {
959            let mut sessions = lock(&self.inner.sessions);
960            let ids: Vec<String> = sessions
961                .iter()
962                .filter(|(_, s)| s.peer_fingerprint == fingerprint)
963                .map(|(id, _)| id.clone())
964                .collect();
965            ids.iter()
966                .filter_map(|id| sessions.remove(id))
967                .map(|session| {
968                    session.cancel.store(true, Ordering::Relaxed);
969                    session.transfer_id
970                })
971                .collect()
972        };
973        for transfer_id in ended {
974            self.inner.end_transfer(&transfer_id, "cancelled", None);
975        }
976    }
977
978    pub fn set_save_dir(&self, directory: PathBuf) {
979        lock(&self.inner.cfg).save_dir = directory;
980    }
981
982    /// The nearby device with this fingerprint (or, failing that, alias).
983    fn find_device(&self, target: &str) -> Option<Device> {
984        let devices = lock(&self.inner.devices);
985        let wanted = normalize(target);
986        devices
987            .iter()
988            .find(|s| s.device.fingerprint == wanted)
989            .or_else(|| {
990                devices
991                    .iter()
992                    .find(|s| s.device.alias.eq_ignore_ascii_case(target))
993            })
994            .map(|s| s.device.clone())
995    }
996
997    /// This computer's name as other devices list it.
998    pub fn alias(&self) -> String {
999        self.inner.config().alias
1000    }
1001
1002    /// Send to the device with this fingerprint (or, failing that, alias).
1003    /// Returns the transfer id; progress arrives as events.
1004    pub fn send(
1005        &self,
1006        target: &str,
1007        items: Vec<SendItem>,
1008        clipboard: bool,
1009    ) -> Result<String, Error> {
1010        let device = self.find_device(target).ok_or(Error::DeviceNotNearby)?;
1011        if self.inner.trust_state(&device.fingerprint) == TrustState::Deny {
1012            return Err(Error::DeviceBlocked);
1013        }
1014        if device.protocol != "https" {
1015            return Err(Error::PeerPlaintext {
1016                peer: device.alias.clone(),
1017            });
1018        }
1019        if !net::valid_pin(&device.fingerprint) {
1020            return Err(Error::InvalidIdentity {
1021                peer: device.alias.clone(),
1022            });
1023        }
1024        let ip: IpAddr = device.ip.parse().map_err(|_| Error::NoUsableAddress)?;
1025        let peer = Peer {
1026            ip,
1027            port: device.port,
1028            https: device.protocol == "https",
1029            fingerprint: device.fingerprint.clone(),
1030            alias: device.alias.clone(),
1031        };
1032        let id = proto::random_hex(8);
1033        self.inner.add_transfer(Transfer {
1034            id: id.clone(),
1035            direction: "send".into(),
1036            peer: device.alias.clone(),
1037            peer_fingerprint: device.fingerprint.clone(),
1038            state: "waiting".into(),
1039            total_bytes: 0,
1040            done_bytes: 0,
1041            files_total: 0,
1042            files_done: 0,
1043            current: None,
1044            saved_to: None,
1045            saved_files: Vec::new(),
1046            error: None,
1047            message: None,
1048            clipboard,
1049            started: now_ms(),
1050            finished: None,
1051        });
1052        let handle = Arc::new(CancelHandle {
1053            flag: AtomicBool::new(false),
1054            stream: Mutex::new(None),
1055        });
1056        lock(&self.inner.cancels).insert(id.clone(), handle.clone());
1057        self.inner.emit(Event::Changed);
1058        let inner = self.inner.clone();
1059        let transfer_id = id.clone();
1060        thread::spawn(move || run_send(inner, transfer_id, peer, items, clipboard, handle));
1061        Ok(id)
1062    }
1063
1064    /// Answer a waiting request.
1065    pub fn respond(&self, id: &str, accept: bool) -> bool {
1066        let pending = lock(&self.inner.pending).get(id).cloned();
1067        let Some(pending) = pending else { return false };
1068        *lock(&pending.decision) = Some(accept);
1069        pending.changed.notify_all();
1070        true
1071    }
1072
1073    /// Cancel a transfer, either direction.
1074    pub fn cancel(&self, transfer_id: &str) -> bool {
1075        if let Some(handle) = lock(&self.inner.cancels).get(transfer_id).cloned() {
1076            handle.flag.store(true, Ordering::Relaxed);
1077            if let Some(stream) = lock(&handle.stream).as_ref() {
1078                let _ = stream.shutdown(std::net::Shutdown::Both);
1079            }
1080            return true;
1081        }
1082        receive::cancel_receive(&self.inner, transfer_id)
1083    }
1084
1085    /// Forget a finished transfer.
1086    pub fn dismiss(&self, transfer_id: &str) {
1087        lock(&self.inner.transfers).retain(|t| t.id != transfer_id || t.is_open());
1088        self.inner.emit(Event::Changed);
1089    }
1090
1091    /// Stop sharing. Pending requests are declined, transfers in flight are cancelled, and the
1092    /// acceptor and liveness threads are ended and joined: when this returns the listening socket
1093    /// is closed and its port can be bound again, and no thread of the service's own is left
1094    /// running. It returns promptly (a few milliseconds), never waiting on a peer. Connection
1095    /// handlers and liveness probes already running are not joined; they finish on their own
1096    /// within their socket timeouts. Calling it again, or dropping the `Service`, does nothing more.
1097    pub fn stop(&self) {
1098        self.shutdown();
1099    }
1100
1101    /// [`Service::stop`], reporting whether every worker thread was joined.
1102    fn shutdown(&self) -> bool {
1103        self.inner.stop.store(true, Ordering::Relaxed);
1104        self.inner.park.stop();
1105        #[cfg(all(feature = "bonjour", target_os = "macos"))]
1106        {
1107            let handles = lock(&self.bonjour).take();
1108            drop(handles);
1109        }
1110        let pending: Vec<Arc<Pending>> = lock(&self.inner.pending).values().cloned().collect();
1111        for p in pending {
1112            *lock(&p.decision) = Some(false);
1113            p.changed.notify_all();
1114        }
1115        let handles: Vec<Arc<CancelHandle>> = lock(&self.inner.cancels).values().cloned().collect();
1116        for handle in handles {
1117            handle.flag.store(true, Ordering::Relaxed);
1118            if let Some(stream) = lock(&handle.stream).as_ref() {
1119                let _ = stream.shutdown(std::net::Shutdown::Both);
1120            }
1121        }
1122        for session in lock(&self.inner.sessions).values() {
1123            session.cancel.store(true, Ordering::Relaxed);
1124        }
1125        // No lock is held here: the threads being joined never take one that this thread holds.
1126        let workers = std::mem::take(&mut *lock(&self.workers));
1127        let mut all_ended = true;
1128        for worker in workers {
1129            all_ended &= worker.stop();
1130        }
1131        all_ended
1132    }
1133}
1134
1135impl Drop for Service {
1136    fn drop(&mut self) {
1137        self.stop();
1138    }
1139}
1140
1141// ---- background threads ----------------------------------------------------
1142
1143struct ConnectionGuard(Arc<Inner>);
1144
1145impl Drop for ConnectionGuard {
1146    fn drop(&mut self) {
1147        self.0.connections.fetch_sub(1, Ordering::Relaxed);
1148    }
1149}
1150
1151/// Start the acceptor on a thread of its own. `port` is where `stop` connects to wake it.
1152fn spawn_acceptor(inner: Arc<Inner>, listener: TcpListener, port: u16) -> std::io::Result<Worker> {
1153    Worker::spawn(
1154        "rk-send-accept",
1155        move || wake_listener(port),
1156        move || accept_loop(&inner, listener),
1157    )
1158}
1159
1160/// End a blocked `accept` by connecting to the listener from this process (see `workers.rs` for
1161/// why this and not `shutdown` or `close`). The connection is dropped at once; the acceptor sees
1162/// the stop flag and drops it too. A failed connect is harmless: `Worker::stop` tries again.
1163fn wake_listener(port: u16) {
1164    let address = std::net::SocketAddr::from((Ipv4Addr::LOCALHOST, port));
1165    let _ = TcpStream::connect_timeout(&address, Duration::from_millis(250));
1166}
1167
1168/// Connections this service serves at once; the next one is closed unread.
1169const MAX_CONNECTIONS: usize = 64;
1170
1171/// The acceptor: blocks in `accept` and hands each connection to a handler thread of its own.
1172/// Owns the listener, so the port is released when this returns.
1173fn accept_loop(inner: &Arc<Inner>, listener: TcpListener) {
1174    while !inner.stop.load(Ordering::Relaxed) {
1175        let accepted = listener.accept();
1176        #[cfg(test)]
1177        inner.park.note_wake();
1178        match accepted {
1179            Ok((socket, address)) => {
1180                // `Service::stop` ends the blocking accept with a connection of its own.
1181                if inner.stop.load(Ordering::Relaxed) {
1182                    break;
1183                }
1184                let _ = socket.set_nonblocking(false);
1185                if inner.connections.load(Ordering::Relaxed) >= MAX_CONNECTIONS {
1186                    continue;
1187                }
1188                inner.connections.fetch_add(1, Ordering::Relaxed);
1189                // Counted down however the handler ends, panics included.
1190                let guard = ConnectionGuard(inner.clone());
1191                thread::spawn(move || {
1192                    let guard = guard;
1193                    receive::handle_connection(&guard.0, socket, address);
1194                });
1195            }
1196            // A failed accept (descriptor exhaustion, a peer that reset before it was taken):
1197            // back off so a persistent fault cannot spin, and let a stop end the wait.
1198            Err(_) => {
1199                if inner.park.wait(Duration::from_millis(250)) {
1200                    break;
1201                }
1202            }
1203        }
1204    }
1205}
1206
1207#[cfg(all(feature = "multicast", not(target_os = "ios")))]
1208fn spawn_discovery(inner: Arc<Inner>, socket: std::net::UdpSocket) {
1209    thread::spawn(move || {
1210        let _ = socket.set_read_timeout(Some(Duration::from_millis(500)));
1211        let mut buffer = vec![0u8; 64 * 1024];
1212        let mut rejoined = Instant::now();
1213        while !inner.stop.load(Ordering::Relaxed) {
1214            if rejoined.elapsed() >= Duration::from_secs(30) {
1215                rejoined = Instant::now();
1216                discovery::rejoin(&socket);
1217            }
1218            let Ok((n, from)) = socket.recv_from(&mut buffer) else {
1219                continue;
1220            };
1221            let Some(heard) = discovery::parse(&buffer[..n], from) else {
1222                continue;
1223            };
1224            if !inner.upsert(&heard.info, heard.ip) {
1225                continue;
1226            }
1227            if heard.info.announce.unwrap_or(false) {
1228                let inner = inner.clone();
1229                thread::spawn(move || answer_announcement(&inner, &heard));
1230            }
1231        }
1232    });
1233}
1234
1235#[cfg(all(feature = "multicast", not(target_os = "ios")))]
1236/// Reply to an announcement the way the protocol asks: register with the
1237/// sender over HTTP, and fall back to a multicast reply.
1238fn answer_announcement(inner: &Arc<Inner>, heard: &discovery::Heard) {
1239    let mut me = inner.me.clone();
1240    me.announce = None;
1241    let registered = serde_json::to_vec(&me).ok().is_some_and(|body| {
1242        net::request_json(
1243            heard.ip,
1244            heard.info.port,
1245            heard.info.protocol == "https",
1246            &heard.info.fingerprint,
1247            "POST",
1248            &format!("{}/register", proto::API),
1249            Some(&body),
1250            Duration::from_secs(3),
1251        )
1252        .is_ok_and(|reply| reply.status == 200)
1253    });
1254    if !registered {
1255        let _ = discovery::announce(&inner.me, inner.config().port, false);
1256    }
1257}
1258
1259#[cfg(all(feature = "multicast", not(target_os = "ios")))]
1260fn spawn_announcer(inner: Arc<Inner>) {
1261    thread::spawn(move || {
1262        let mut waits = vec![100u64, 400, 1500].into_iter();
1263        while !inner.stop.load(Ordering::Relaxed) {
1264            let sent = discovery::announce(&inner.me, inner.config().port, true);
1265            inner.local_network.store(
1266                match &sent {
1267                    Ok(()) => 1,
1268                    // EHOSTUNREACH: what a denied Local Network permission looks like.
1269                    #[cfg(target_os = "macos")]
1270                    Err(e) if e.raw_os_error() == Some(65) => 2,
1271                    // WSAEACCES: Windows Firewall (or policy) refusing the multicast send.
1272                    #[cfg(windows)]
1273                    Err(e) if e.raw_os_error() == Some(10013) => 2,
1274                    Err(_) => 0,
1275                },
1276                Ordering::Relaxed,
1277            );
1278            inner.emit(Event::Changed);
1279            let wait = waits.next().unwrap_or(30_000);
1280            let until = Instant::now() + Duration::from_millis(wait);
1281            while Instant::now() < until && !inner.stop.load(Ordering::Relaxed) {
1282                thread::sleep(Duration::from_millis(100));
1283            }
1284        }
1285    });
1286}
1287
1288/// Sweep the local networks shortly after start (multicast gets the first
1289/// word), when asked, and every minute while no device is known.
1290#[cfg(all(feature = "multicast", not(target_os = "ios")))]
1291fn spawn_scanner(inner: Arc<Inner>) {
1292    thread::spawn(move || {
1293        let mut due = Instant::now() + Duration::from_secs(2);
1294        let mut first = true;
1295        while !inner.stop.load(Ordering::Relaxed) {
1296            thread::sleep(Duration::from_millis(100));
1297            let asked = inner.scan_requested.load(Ordering::Relaxed);
1298            let waited = Instant::now() >= due;
1299            if !asked && !waited {
1300                continue;
1301            }
1302            if asked || first || lock(&inner.devices).is_empty() {
1303                inner.scan();
1304                inner.scan_requested.store(false, Ordering::Relaxed);
1305                first = false;
1306            }
1307            due = Instant::now() + Duration::from_secs(60);
1308        }
1309    });
1310}
1311
1312/// How often devices not heard from are probed.
1313const LIVENESS_PERIOD: Duration = Duration::from_secs(20);
1314
1315/// Drop devices that stop answering `GET /info`.
1316fn spawn_liveness(inner: Arc<Inner>) -> std::io::Result<Worker> {
1317    // The wait is on `park`, which `Service::stop` has already stopped: nothing more to wake.
1318    Worker::spawn("rk-send-liveness", || {}, move || liveness_loop(&inner))
1319}
1320
1321/// One timed wait per period, ended at once by a stop.
1322fn liveness_loop(inner: &Arc<Inner>) {
1323    while !inner.park.wait(LIVENESS_PERIOD) {
1324        let stale: Vec<Device> = lock(&inner.devices)
1325            .iter()
1326            .filter(|s| s.last_seen.elapsed() > Duration::from_secs(20))
1327            .map(|s| s.device.clone())
1328            .collect();
1329        for device in stale {
1330            let inner = inner.clone();
1331            thread::spawn(move || {
1332                let alive = device.ip.parse::<IpAddr>().ok().is_some_and(|ip| {
1333                    net::request_json(
1334                        ip,
1335                        device.port,
1336                        device.protocol == "https",
1337                        &device.fingerprint,
1338                        "GET",
1339                        &format!("{}/info", proto::API),
1340                        None,
1341                        Duration::from_secs(2),
1342                    )
1343                    .is_ok_and(|reply| reply.status == 200)
1344                });
1345                let removed = {
1346                    let mut devices = lock(&inner.devices);
1347                    match devices
1348                        .iter()
1349                        .position(|s| s.device.fingerprint == device.fingerprint)
1350                    {
1351                        Some(index) if alive => {
1352                            devices[index].last_seen = Instant::now();
1353                            devices[index].misses = 0;
1354                            false
1355                        }
1356                        Some(index) => {
1357                            devices[index].misses += 1;
1358                            if devices[index].misses >= 2 {
1359                                devices.remove(index);
1360                                true
1361                            } else {
1362                                false
1363                            }
1364                        }
1365                        None => false,
1366                    }
1367                };
1368                if removed {
1369                    inner.emit(Event::Devices);
1370                }
1371            });
1372        }
1373    }
1374}
1375
1376fn run_send(
1377    inner: Arc<Inner>,
1378    id: String,
1379    peer: Peer,
1380    items: Vec<SendItem>,
1381    clipboard: bool,
1382    handle: Arc<CancelHandle>,
1383) {
1384    let finish = |state: &str, error: Option<Error>| {
1385        inner.end_transfer(&id, state, error);
1386        lock(&inner.cancels).remove(&id);
1387    };
1388    let entries = match send::expand(&items) {
1389        Ok(entries) => entries,
1390        Err(message) => return finish("failed", Some(message)),
1391    };
1392    inner.update_transfer(&id, true, |t| {
1393        t.files_total = entries.len();
1394        t.total_bytes = entries.iter().map(|e| e.size).sum();
1395    });
1396    let register = |stream: Option<TcpStream>| {
1397        *lock(&handle.stream) = stream;
1398    };
1399    // A message counts as sent once it is on the other screen.
1400    let delivered = std::cell::Cell::new(false);
1401    let result = send::deliver(
1402        &inner.me,
1403        &peer,
1404        &entries,
1405        clipboard,
1406        &handle.flag,
1407        &register,
1408        &mut |p| {
1409            if p.phase == send::Phase::Delivered {
1410                delivered.set(true);
1411                inner.end_transfer(&id, "done", None);
1412                return;
1413            }
1414            let waiting = p.phase == send::Phase::Waiting;
1415            inner.update_transfer(&id, p.total > 0 && p.done >= p.total, |t| {
1416                t.state = if waiting { "waiting" } else { "active" }.to_string();
1417                t.total_bytes = p.total;
1418                t.done_bytes = p.done;
1419                t.files_total = p.files_total;
1420                t.files_done = p.files_done;
1421                t.current = p.current.clone();
1422            });
1423        },
1424    );
1425    if delivered.get() {
1426        lock(&inner.cancels).remove(&id);
1427        return;
1428    }
1429    match result {
1430        Ok(send::Outcome::Done) => finish("done", None),
1431        Ok(send::Outcome::Declined) => finish("declined", None),
1432        Ok(send::Outcome::Cancelled) => finish("cancelled", None),
1433        Err(_) if handle.flag.load(Ordering::Relaxed) => finish("cancelled", None),
1434        Err(message) => finish("failed", Some(message)),
1435    }
1436}
1437
1438// ---- resolved peers, host-fed or Bonjour ----------------------------------
1439
1440/// The TXT value for `key`, if present.
1441fn txt_value(txt: &[(String, String)], key: &str) -> Option<String> {
1442    txt.iter()
1443        .find(|(k, _)| k.as_str() == key)
1444        .map(|(_, v)| v.clone())
1445}
1446
1447/// A resolved device as an announcement would describe it. An empty alias
1448/// falls back to the start of the fingerprint.
1449fn resolved_info(
1450    fingerprint: &str,
1451    alias: &str,
1452    port: u16,
1453    txt: &[(String, String)],
1454) -> DeviceInfo {
1455    let alias = if alias.is_empty() {
1456        fingerprint.chars().take(8).collect()
1457    } else {
1458        alias.to_string()
1459    };
1460    DeviceInfo {
1461        alias,
1462        version: proto::VERSION.to_string(),
1463        device_model: txt_value(txt, "model").filter(|m| !m.is_empty()),
1464        device_type: txt_value(txt, "type").filter(|t| !t.is_empty()),
1465        fingerprint: fingerprint.to_string(),
1466        port,
1467        protocol: "https".to_string(),
1468        download: false,
1469        announce: None,
1470    }
1471}
1472
1473/// This device's IPv4 addresses, for telling its own announcements apart. Empty
1474/// where multicast discovery is not compiled in.
1475#[cfg(all(feature = "multicast", not(target_os = "ios")))]
1476fn local_ipv4s() -> Vec<Ipv4Addr> {
1477    discovery::local_ipv4s()
1478}
1479
1480#[cfg(not(all(feature = "multicast", not(target_os = "ios"))))]
1481fn local_ipv4s() -> Vec<Ipv4Addr> {
1482    Vec::new()
1483}
1484
1485impl Inner {
1486    /// Record a device a resolver found, with its `px` flag. Returns whether it
1487    /// was taken (not this device itself).
1488    fn add_resolved(&self, info: &DeviceInfo, ip: IpAddr, px: bool) -> bool {
1489        if !self.upsert(info, ip) {
1490            return false;
1491        }
1492        let fingerprint = normalize(&info.fingerprint);
1493        let changed = {
1494            let mut devices = lock(&self.devices);
1495            match devices
1496                .iter_mut()
1497                .find(|s| s.device.fingerprint == fingerprint)
1498            {
1499                Some(seen) if seen.device.px != px => {
1500                    seen.device.px = px;
1501                    true
1502                }
1503                _ => false,
1504            }
1505        };
1506        if changed {
1507            self.emit(Event::Devices);
1508        }
1509        true
1510    }
1511
1512    /// Drop a device from the list. Returns whether it was listed.
1513    fn remove_device(&self, fingerprint: &str) -> bool {
1514        let removed = {
1515            let mut devices = lock(&self.devices);
1516            let before = devices.len();
1517            devices.retain(|s| s.device.fingerprint != fingerprint);
1518            devices.len() != before
1519        };
1520        if removed {
1521            self.emit(Event::Devices);
1522        }
1523        removed
1524    }
1525}
1526
1527#[cfg(all(feature = "bonjour", target_os = "macos"))]
1528impl Inner {
1529    fn bonjour_found(&self, found: bonjour::Found) {
1530        let Some(fingerprint) = txt_value(&found.txt, "fp")
1531            .map(|f| normalize(&f))
1532            .filter(|f| net::valid_pin(f))
1533        else {
1534            return;
1535        };
1536        let alias = txt_value(&found.txt, "alias").unwrap_or_default();
1537        let info = resolved_info(&fingerprint, &alias, found.port, &found.txt);
1538        let px = txt_value(&found.txt, "px").as_deref() == Some("1");
1539        if self.add_resolved(&info, found.host, px) {
1540            lock(&self.bonjour_names).insert(found.instance, fingerprint);
1541        }
1542    }
1543
1544    fn bonjour_lost(&self, instance: &str) {
1545        let fingerprint = lock(&self.bonjour_names).remove(instance);
1546        if let Some(fingerprint) = fingerprint {
1547            self.remove_device(&fingerprint);
1548        }
1549    }
1550}
1551
1552#[cfg(all(feature = "bonjour", target_os = "macos"))]
1553fn start_bonjour(
1554    inner: &Arc<Inner>,
1555    config: &Config,
1556    fingerprint: &str,
1557) -> Option<(bonjour::Advertiser, bonjour::Browser)> {
1558    let model = Some(config.device_model.as_str()).filter(|m| !m.is_empty());
1559    let txt = bonjour::txt_encode(fingerprint, &config.alias, &config.device_type, model);
1560    let advertiser = match bonjour::Advertiser::start(&config.alias, config.port, &txt) {
1561        Ok(advertiser) => advertiser,
1562        Err(e) => {
1563            lock(&inner.warnings).push(Error::DiscoveryUnavailable {
1564                detail: e.to_string(),
1565            });
1566            return None;
1567        }
1568    };
1569    let found = inner.clone();
1570    let lost = inner.clone();
1571    match bonjour::Browser::start(
1572        config.alias.clone(),
1573        Box::new(move |peer| found.bonjour_found(peer)),
1574        Box::new(move |instance| lost.bonjour_lost(&instance)),
1575    ) {
1576        Ok(browser) => Some((advertiser, browser)),
1577        Err(e) => {
1578            lock(&inner.warnings).push(Error::DiscoveryUnavailable {
1579                detail: e.to_string(),
1580            });
1581            None
1582        }
1583    }
1584}
1585
1586#[cfg(test)]
1587mod lifecycle_tests {
1588    use super::*;
1589    use std::io::{ErrorKind, Read};
1590    use std::net::SocketAddr;
1591
1592    /// `Service::start` sets the client identity and User-Agent for the whole process, so these
1593    /// tests run one at a time.
1594    static SERIAL: Mutex<()> = Mutex::new(());
1595
1596    fn serial() -> MutexGuard<'static, ()> {
1597        lock(&SERIAL)
1598    }
1599
1600    fn config(port: u16) -> Config {
1601        let root = std::env::temp_dir().join(format!("rk-send-life-{}", proto::random_hex(4)));
1602        Config {
1603            product_name: "Test".into(),
1604            alias: "Lifecycle".into(),
1605            port,
1606            device_type: "desktop".into(),
1607            device_model: "Test".into(),
1608            save_dir: root.join("save"),
1609            trust_path: root.join("nearby_devices.json"),
1610            user_agent: "LifecycleTest/1".into(),
1611            quarantine_agent: "LifecycleTest".into(),
1612            temp_suffix: ".test-part".into(),
1613            log_prefix: "test sharing:".into(),
1614            policy: Policy::default(),
1615        }
1616    }
1617
1618    fn start_on(port: u16) -> Result<Service, Error> {
1619        let identity = Identity::generate("Lifecycle").expect("identity");
1620        Service::start(config(port), identity, Arc::new(|_: Event| {}))
1621    }
1622
1623    #[test]
1624    fn an_idle_service_wakes_its_threads_about_once_a_second_at_most() {
1625        let _serial = serial();
1626        let service = start_on(0).expect("service starts");
1627        let (began, woke_before) = (Instant::now(), service.inner.park.wakes());
1628        thread::sleep(Duration::from_secs(2));
1629        let (secs, woke) = (
1630            began.elapsed().as_secs_f64(),
1631            service.inner.park.wakes() - woke_before,
1632        );
1633        // The window can stretch on a loaded machine and the allowance stretches with it. The 50 ms
1634        // accept poll and 100 ms liveness tick this replaces woke about 60 times in two seconds.
1635        let allowed = 3u64.max(secs.ceil() as u64 + 1);
1636        assert!(
1637            woke <= allowed,
1638            "the service woke {woke} times in {secs:.1} s"
1639        );
1640        assert!(service.shutdown());
1641    }
1642
1643    #[test]
1644    fn stop_returns_promptly_having_joined_every_service_thread() {
1645        let _serial = serial();
1646        let service = start_on(0).expect("service starts");
1647        // Let both threads reach their waits; stopping before they do ends them at once too.
1648        thread::sleep(Duration::from_millis(100));
1649        assert_eq!(lock(&service.workers).len(), 2, "acceptor and liveness");
1650        let began = Instant::now();
1651        assert!(service.shutdown(), "a service thread did not end");
1652        assert!(
1653            began.elapsed() < Duration::from_secs(1),
1654            "stop took {:?}",
1655            began.elapsed()
1656        );
1657        assert!(lock(&service.workers).is_empty());
1658        // Stopping again, and the drop that follows, change nothing.
1659        assert!(service.shutdown());
1660        service.stop();
1661    }
1662
1663    #[test]
1664    fn a_second_start_on_the_same_port_succeeds_after_stop() {
1665        let _serial = serial();
1666        let first = start_on(0).expect("service starts");
1667        let port = first.bound_port();
1668        assert!(first.shutdown());
1669        for round in 0..3 {
1670            let again = start_on(port)
1671                .unwrap_or_else(|e| panic!("restart {round} on port {port} failed: {e:?}"));
1672            assert_eq!(again.bound_port(), port);
1673            assert!(again.shutdown());
1674        }
1675    }
1676
1677    #[test]
1678    fn the_64_connection_cap_still_closes_the_next_connection_unread() {
1679        let _serial = serial();
1680        let service = start_on(0).expect("service starts");
1681        let address = SocketAddr::from((Ipv4Addr::LOCALHOST, service.bound_port()));
1682        let mut byte = [0u8; 1];
1683
1684        // At the cap the connection is closed without a byte exchanged: end of stream or a reset,
1685        // never a timeout.
1686        service
1687            .inner
1688            .connections
1689            .store(MAX_CONNECTIONS, Ordering::Relaxed);
1690        let mut full = TcpStream::connect(address).expect("connect");
1691        full.set_read_timeout(Some(Duration::from_secs(10)))
1692            .unwrap();
1693        match full.read(&mut byte) {
1694            Ok(0) => {}
1695            Err(e) if !matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
1696            other => panic!("a connection over the cap must be closed, got {other:?}"),
1697        }
1698
1699        // Below the cap it is kept: the handler waits for a TLS hello (30 s timeout), so a short
1700        // read here times out.
1701        service.inner.connections.store(0, Ordering::Relaxed);
1702        let mut open = TcpStream::connect(address).expect("connect");
1703        open.set_read_timeout(Some(Duration::from_millis(300)))
1704            .unwrap();
1705        match open.read(&mut byte) {
1706            Err(e) if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
1707            other => panic!("a connection under the cap must be kept open, got {other:?}"),
1708        }
1709        drop(open);
1710        assert!(service.shutdown());
1711    }
1712}