1#[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#[derive(Clone, Debug)]
46pub struct Config {
47 pub product_name: String,
49 pub alias: String,
51 pub port: u16,
53 pub device_type: String,
55 pub device_model: String,
57 pub save_dir: PathBuf,
59 pub trust_path: PathBuf,
61 pub user_agent: String,
63 pub quarantine_agent: String,
65 pub temp_suffix: String,
67 pub log_prefix: String,
69 pub policy: Policy,
71}
72
73#[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 pub px: bool,
87}
88
89#[derive(Clone, Debug, Serialize)]
90pub struct IncomingFile {
91 pub name: String,
92 pub size: u64,
93}
94
95#[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 pub preview: Option<String>,
109 pub files: Vec<IncomingFile>,
111 pub verified: bool,
114}
115
116#[derive(Clone, Debug, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct Transfer {
120 pub id: String,
121 pub direction: String,
123 pub peer: String,
124 pub peer_fingerprint: String,
125 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 #[serde(skip_serializing_if = "Option::is_none")]
137 pub message: Option<String>,
138 #[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 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 pub warnings: Vec<Error>,
165 pub local_network: String,
167 pub scanning: bool,
169}
170
171#[derive(Clone, Debug)]
172pub enum Event {
173 Devices,
175 Incoming(Incoming),
177 IncomingResolved(String),
179 Progress(Transfer),
181 Finished(Transfer),
183 Changed,
185}
186
187#[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
200fn 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
215fn 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 #[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 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 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 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 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 pub local_network: std::sync::atomic::AtomicU8,
431 pub scanning: AtomicBool,
433 #[cfg(all(feature = "multicast", not(target_os = "ios")))]
435 pub scan_requested: AtomicBool,
436 pub own_ips: Mutex<(Instant, Vec<Ipv4Addr>)>,
438 #[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 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 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 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 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 #[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 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 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 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#[derive(Clone, Debug)]
644pub struct SendStatus {
645 pub devices: usize,
647 pub transfers_open: usize,
649 pub transfers_listed: usize,
651 pub pending_incoming: usize,
653 pub connections: usize,
655 pub warnings: usize,
657 pub scanning: bool,
659 pub process: Option<rightkit_child_process::ProcessUsage>,
663}
664
665pub struct Service {
667 inner: Arc<Inner>,
668 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 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 #[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 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 Err(e) => {
789 return Err(Error::Listen {
790 port,
791 detail: e.to_string(),
792 });
793 }
794 }
795 Ok(service)
796 }
797
798 pub fn bound_port(&self) -> u16 {
800 lock(&self.inner.cfg).port
801 }
802
803 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 pub fn peer_remove(&self, fingerprint: &str) -> bool {
828 self.inner.remove_device(&normalize(fingerprint))
829 }
830
831 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 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 pub fn fingerprint(&self) -> String {
894 self.inner.me.fingerprint.clone()
895 }
896
897 pub fn devices_trust(&self) -> Vec<TrustEntry> {
899 lock(&self.inner.trust).list()
900 }
901
902 pub fn default_state(&self) -> TrustState {
904 lock(&self.inner.trust).default_state()
905 }
906
907 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 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 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 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 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 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 pub fn alias(&self) -> String {
999 self.inner.config().alias
1000 }
1001
1002 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 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 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 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 pub fn stop(&self) {
1098 self.shutdown();
1099 }
1100
1101 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 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
1141struct 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
1151fn 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
1160fn 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
1168const MAX_CONNECTIONS: usize = 64;
1170
1171fn 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 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 let guard = ConnectionGuard(inner.clone());
1191 thread::spawn(move || {
1192 let guard = guard;
1193 receive::handle_connection(&guard.0, socket, address);
1194 });
1195 }
1196 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")))]
1236fn 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 #[cfg(target_os = "macos")]
1270 Err(e) if e.raw_os_error() == Some(65) => 2,
1271 #[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#[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
1312const LIVENESS_PERIOD: Duration = Duration::from_secs(20);
1314
1315fn spawn_liveness(inner: Arc<Inner>) -> std::io::Result<Worker> {
1317 Worker::spawn("rk-send-liveness", || {}, move || liveness_loop(&inner))
1319}
1320
1321fn 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 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 ®ister,
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
1438fn 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
1447fn 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#[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 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 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 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 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 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 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 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 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}