1use std::{
2 collections::{HashMap, HashSet},
3 net::SocketAddr,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex, RwLock},
6 time::{Duration, Instant},
7};
8
9use iroh_docs::NamespaceId;
10use n0_future::StreamExt;
11use tokio::{
12 sync::{broadcast, mpsc, oneshot},
13 task::JoinHandle,
14};
15
16use crate::{
17 error::{Result, SyncwebError},
18 filter::{FilterAction, FilterEngine, FilterEntry},
19 folder::{FolderManager, PublicSubscription},
20 fs::{FsWatcher, Importer},
21 gossip::TopicChannel,
22 indexing::{
23 IndexingDatabase, IndexingService, REPORT_GOSSIP_TOPIC, ReportRecord,
24 links::{MutablePointer, PrivateLink, REVOCATION_GOSSIP_TOPIC},
25 resilience::{ReplicationBudget, ResilienceConfig},
26 wot::{ATTESTATION_GOSSIP_TOPIC, Attestation},
27 },
28 net::{NetworkLogger, NetworkManager},
29 node::{gossip_service::GossipService, identity::IdentityManager, iroh_node::IrohNode},
30 schedule::ScheduleManager,
31 storage::{node_db::NodeDatabase, stats_db::StatsDatabase},
32 sync::{SubscribeParams, SyncEngine, cancel_session, is_active},
33};
34
35use super::{
36 DaemonHandle, DaemonState, DaemonStatus, FolderEntry, IpcServer, ManagedPool, PidLock, current_timestamp,
37 daemon_socket_path,
38 state::{BandwidthSnapshot, DaemonStatusReport, FolderStatusReport, ScheduleStatus},
39 supervisor::{IntentControls, IntentSupervisor, SupervisionOptions},
40};
41
42#[derive(Clone, Debug)]
44#[non_exhaustive]
45pub struct DaemonConfig {
46 pub data_dir: PathBuf,
47 pub network: Option<String>,
48 pub sync_interval: Duration,
49 pub observation_ttl: Duration,
50 pub max_retries: u32,
51 pub backoff_base: Duration,
52 pub backoff_max: Duration,
53 pub rayon_threads: usize,
54 pub log_level: String,
55 pub log_file: Option<PathBuf>,
56 pub watch_debounce: Duration,
57 pub relay_mode: crate::node::iroh_node::RelayMode,
58 pub bridge_listen: Option<SocketAddr>,
59 pub media_listen: Option<SocketAddr>,
60}
61
62impl Default for DaemonConfig {
63 fn default() -> Self {
64 Self {
65 data_dir: PathBuf::from("."),
66 network: None,
67 sync_interval: Duration::from_mins(1),
68 observation_ttl: Duration::from_hours(1),
69 max_retries: 3,
70 backoff_base: Duration::from_secs(1),
71 backoff_max: Duration::from_mins(1),
72 rayon_threads: std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get),
73 log_level: "info".to_owned(),
74 log_file: None,
75 watch_debounce: Duration::from_millis(500),
76 relay_mode: crate::node::iroh_node::RelayMode::Default,
77 bridge_listen: None,
78 media_listen: None,
79 }
80 }
81}
82
83impl DaemonConfig {
84 #[must_use]
85 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
86 Self {
87 data_dir: data_dir.into(),
88 ..Self::default()
89 }
90 }
91}
92
93pub struct Daemon {
96 config: DaemonConfig,
97 node_db: NodeDatabase,
98 stats_db: StatsDatabase,
99 pid_lock: PidLock,
100 ipc_server: IpcServer,
101 intent_supervisor: IntentSupervisor,
102 folder_manager: FolderManager,
103 sync_engine: SyncEngine,
104 schedule_manager: tokio::sync::RwLock<Option<ScheduleManager>>,
105 node: Arc<IrohNode>,
106 handle: DaemonHandle,
107 sync_receiver: tokio::sync::Mutex<mpsc::UnboundedReceiver<Option<String>>>,
108 intent_tasks: Mutex<HashMap<NamespaceId, JoinHandle<()>>>,
109 intent_controls: IntentControls,
110 watchers: Mutex<HashMap<String, FsWatcher>>,
111 pending_watch_events: Mutex<HashMap<String, PendingWatch>>,
112 filter_engine: tokio::sync::RwLock<Option<FilterEngine>>,
113 archive_pool: Arc<ManagedPool>,
114 network_logger: NetworkLogger,
115 network_manager: tokio::sync::RwLock<NetworkManager>,
116 attestation_listener: tokio::sync::Mutex<Option<JoinHandle<()>>>,
117 report_listener: tokio::sync::Mutex<Option<JoinHandle<()>>>,
118 revocation_listener: tokio::sync::Mutex<Option<JoinHandle<()>>>,
119}
120
121struct PendingWatch {
122 paths: HashMap<PathBuf, bool>,
123 ready_at: Instant,
124}
125
126type SignalTask<'a> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>>;
127type ServerTasks = (
128 JoinHandle<Result<()>>,
129 Option<JoinHandle<Result<()>>>,
130 Option<JoinHandle<Result<()>>>,
131);
132
133impl Daemon {
134 async fn open_identity_and_node(
135 data_dir: &Path,
136 node_db: &NodeDatabase,
137 pid_lock: &PidLock,
138 relay_mode: crate::node::iroh_node::RelayMode,
139 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
140 ) -> Result<(Arc<IrohNode>, FolderManager, SyncEngine)> {
141 let identity = IdentityManager::new(data_dir.join("identity.key")).inspect_err(|_| {
142 let _ = node_db.remove_lifecycle();
143 let _ = pid_lock.release();
144 })?;
145 let node = Arc::new(
146 IrohNode::new(identity, data_dir.join("data"), relay_mode, member_keys)
147 .await
148 .inspect_err(|_| {
149 let _ = node_db.remove_lifecycle();
150 let _ = pid_lock.release();
151 })?,
152 );
153 let folder_manager = FolderManager::new(&node);
154 let sync_engine = SyncEngine::new(
155 folder_manager.clone(),
156 node.blob_store().clone(),
157 node.docs_engine().clone(),
158 node.gossip_service().clone(),
159 Some(node.topic_tracker().clone()),
160 );
161 Ok((node, folder_manager, sync_engine))
162 }
163
164 fn init_databases(config: &DaemonConfig) -> Result<(NodeDatabase, StatsDatabase, PidLock)> {
171 let node_db = NodeDatabase::open(config.data_dir.join("node.db"))?;
172 let stats_db = StatsDatabase::open(config.data_dir.join("stats.db"))?;
173 let pid_lock = PidLock::new(&config.data_dir);
174 if !pid_lock.try_acquire()? {
175 return Err(SyncwebError::operation(
176 "daemon already running",
177 config.data_dir.display(),
178 ));
179 }
180 Ok((node_db, stats_db, pid_lock))
181 }
182
183 fn load_app_state(
189 node_db: &NodeDatabase,
190 pid_lock: &PidLock,
191 data_dir: &Path,
192 ) -> Result<(Option<ScheduleManager>, Option<FilterEngine>, DaemonState)> {
193 let app_config = node_db.load_app_config().inspect_err(|_error| {
194 let _ = pid_lock.release();
195 })?;
196 let use_schedule = app_config.schedule != crate::schedule::ScheduleConfig::default();
197 let schedule_manager = match ScheduleManager::from_config(&app_config.schedule) {
198 Ok(s) if use_schedule => Some(s),
199 Ok(_) => None,
200 Err(error) => {
201 let _ = pid_lock.release();
202 return Err(error);
203 }
204 };
205 let filter_engine = node_db.load_filter_engine().inspect_err(|_error| {
206 let _ = pid_lock.release();
207 })?;
208 let initial_state = DaemonState::new(
209 std::process::id(),
210 String::new(),
211 current_timestamp(),
212 data_dir,
213 DaemonStatus::Starting,
214 );
215 node_db.save_lifecycle(&initial_state).inspect_err(|_error| {
216 let _ = pid_lock.release();
217 })?;
218 Ok((schedule_manager, filter_engine, initial_state))
219 }
220
221 pub async fn new(config: DaemonConfig) -> Result<Self> {
229 std::fs::create_dir_all(&config.data_dir)?;
230 let (node_db, stats_db, pid_lock) = Self::init_databases(&config)?;
231 let (schedule_manager, filter_engine, initial_state) =
232 Self::load_app_state(&node_db, &pid_lock, &config.data_dir)?;
233
234 let member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>> = {
235 let networks = node_db.list_networks()?;
236 let keys: HashSet<iroh::PublicKey> = networks.iter().flat_map(|n| n.members.iter().copied()).collect();
237 Arc::new(RwLock::new(keys))
238 };
239 let (node, folder_manager, mut sync_engine) = Self::open_identity_and_node(
240 &config.data_dir,
241 &node_db,
242 &pid_lock,
243 config.relay_mode.clone(),
244 member_keys.clone(),
245 )
246 .await?;
247 sync_engine = sync_engine.with_node_db(node_db.clone());
248
249 let archive_pool = match ManagedPool::new("syncweb-archive", config.rayon_threads) {
250 Ok(value) => Arc::new(value),
251 Err(error) => {
252 let _ = node.stop().await;
253 let _ = node_db.remove_lifecycle();
254 let _ = pid_lock.release();
255 return Err(SyncwebError::operation("failed to create daemon thread pool", error));
256 }
257 };
258
259 let initial_handle = DaemonHandle::new(initial_state);
260 {
261 let mut state = initial_handle.state.write().await;
262 state.node_id = node.endpoint().id().to_string();
263 state.status = DaemonStatus::Running;
264 let running_state = state.clone();
265 drop(state);
266 if let Err(error) = node_db.save_lifecycle(&running_state) {
267 let _ = node.stop().await;
268 let _ = node_db.remove_lifecycle();
269 let _ = pid_lock.release();
270 return Err(error);
271 }
272 }
273 let (sync_sender, sync_receiver) = mpsc::unbounded_channel();
274 let handle = DaemonHandle::with_channels_and_reload(
275 initial_handle.state.clone(),
276 initial_handle.folder_registry.clone(),
277 initial_handle.shutdown_sender.clone(),
278 sync_sender,
279 initial_handle.reload_requested.clone(),
280 );
281 let resilience = IndexingService::new(config.data_dir.join("indexing.sqlite"))
282 .ok()
283 .map(|indexing| indexing.resilience_service(ResilienceConfig::new(ReplicationBudget::default())));
284 let intent_supervisor = IntentSupervisor::new(config.max_retries, config.backoff_base, config.backoff_max);
285 let network_logger = NetworkLogger::new(stats_db.clone());
286 let local_node_id = node.endpoint().id();
287 let nm_result =
288 Self::create_network_manager(&node_db, &local_node_id, &network_logger, Arc::clone(&member_keys));
289 let network_manager = match nm_result {
290 Ok(nm) => nm,
291 Err(error) => {
292 tracing::warn!("failed to create network manager with logger: {error}");
293 NetworkManager::new(node_db.clone(), local_node_id, member_keys)?
294 }
295 };
296 let mut ipc_server = Self::build_ipc_server(&config, &handle, &node, &archive_pool, &folder_manager, &node_db);
297 ipc_server = match resilience {
298 Some(service) => ipc_server.with_resilience(service),
299 None => ipc_server,
300 };
301 ipc_server = ipc_server.with_network_manager(Arc::new(tokio::sync::RwLock::new(network_manager.clone())));
302
303 Ok(Self {
304 config,
305 node_db,
306 stats_db,
307 pid_lock,
308 ipc_server,
309 intent_supervisor,
310 folder_manager,
311 sync_engine,
312 schedule_manager: tokio::sync::RwLock::new(schedule_manager),
313 node,
314 handle,
315 sync_receiver: tokio::sync::Mutex::new(sync_receiver),
316 intent_tasks: Mutex::new(HashMap::new()),
317 intent_controls: Arc::new(Mutex::new(HashMap::new())),
318 watchers: Mutex::new(HashMap::new()),
319 pending_watch_events: Mutex::new(HashMap::new()),
320 filter_engine: tokio::sync::RwLock::new(filter_engine),
321 archive_pool,
322 network_logger,
323 network_manager: tokio::sync::RwLock::new(network_manager),
324 attestation_listener: tokio::sync::Mutex::new(None),
325 report_listener: tokio::sync::Mutex::new(None),
326 revocation_listener: tokio::sync::Mutex::new(None),
327 })
328 }
329
330 fn create_network_manager(
331 node_db: &NodeDatabase,
332 local_node_id: &iroh::PublicKey,
333 network_logger: &NetworkLogger,
334 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
335 ) -> Result<NetworkManager> {
336 NetworkManager::with_logger(node_db.clone(), *local_node_id, network_logger.clone(), member_keys)
337 }
338
339 fn build_ipc_server(
340 config: &DaemonConfig,
341 handle: &DaemonHandle,
342 node: &Arc<IrohNode>,
343 archive_pool: &Arc<ManagedPool>,
344 folder_manager: &FolderManager,
345 node_db: &NodeDatabase,
346 ) -> IpcServer {
347 IpcServer::with_archive_context(
348 daemon_socket_path(&config.data_dir),
349 handle.clone(),
350 node.clone(),
351 archive_pool.clone(),
352 )
353 .with_folder_manager(folder_manager.clone())
354 .with_node_db(node_db.clone())
355 }
356
357 pub async fn run(&self) -> Result<()> {
364 let run_result = self.run_inner().await;
365 let cleanup_result = self.shutdown_resources().await;
366 match run_result {
367 Err(error) => {
368 cleanup_result?;
369 Err(error)
370 }
371 Ok(()) => cleanup_result,
372 }
373 }
374
375 pub async fn state(&self) -> DaemonState {
377 self.handle.state.read().await.clone()
378 }
379
380 #[must_use]
382 pub fn archive_pool(&self) -> &ManagedPool {
383 self.archive_pool.as_ref()
384 }
385
386 async fn run_inner(&self) -> Result<()> {
387 tracing::debug!(
388 rayon_threads = self.archive_pool.thread_count(),
389 "daemon runtime initialized"
390 );
391 self.load_folders().await?;
392 self.reannounce_folders().await;
393 self.load_subscriptions().await?;
394 self.start_watching().await?;
395 self.subscribe_network_gossip().await;
396 self.spawn_membership_listeners().await;
397
398 let (mut server_task, bridge_task, media_task) = self.spawn_server_tasks();
399
400 let mut shutdown = self.handle.shutdown_sender.subscribe();
401 let shutdown_sender = self.handle.shutdown_sender.clone();
402 let mut signal_task: SignalTask<'_> = Box::pin(self.handle_signals(shutdown_sender.clone()));
403 let interval_duration = self.config.sync_interval.max(Duration::from_millis(1));
404 let mut interval = tokio::time::interval(interval_duration);
405 let mut watch_interval = tokio::time::interval(Duration::from_millis(100));
406 if let Err(error) = self.run_cycle().await {
407 tracing::error!(%error, "initial daemon cycle failed");
408 }
409 self.spawn_attestation_listener().await;
410 self.spawn_report_listener().await;
411 self.spawn_revocation_listener().await;
412 self.spawn_maintenance_task();
413 let result = self
414 .run_event_loop(
415 &mut server_task,
416 &mut signal_task,
417 &mut shutdown,
418 &shutdown_sender,
419 &mut interval,
420 &mut watch_interval,
421 )
422 .await;
423 send_shutdown(&shutdown_sender);
424 self.wait_for_server_tasks(server_task, bridge_task, media_task).await?;
425 result
426 }
427
428 fn spawn_server_tasks(&self) -> ServerTasks {
429 let server = self.ipc_server.clone();
430 let server_task = tokio::spawn(async move { server.serve().await });
431
432 let bridge_task = self.config.bridge_listen.map(|addr| {
433 let bridge = crate::bridge::WsBridgeServer::new(self.node.clone(), addr, self.config.data_dir.clone());
434 let shutdown = self.handle.shutdown_sender.clone();
435 tokio::spawn(async move { bridge.run(shutdown).await })
436 });
437
438 let media_task = self.config.media_listen.map(|addr| {
439 let media_srv = crate::media::MediaServer::new(addr, self.node.blob_store().clone());
440 let shutdown = self.handle.shutdown_sender.clone();
441 tokio::spawn(async move { media_srv.run(shutdown).await })
442 });
443
444 (server_task, bridge_task, media_task)
445 }
446
447 async fn wait_for_server_tasks(
448 &self,
449 server_task: JoinHandle<Result<()>>,
450 bridge_task: Option<JoinHandle<Result<()>>>,
451 media_task: Option<JoinHandle<Result<()>>>,
452 ) -> Result<()> {
453 if !server_task.is_finished() {
454 match server_task.await {
455 Ok(server_result) => server_result?,
456 Err(error) => return Err(SyncwebError::operation("daemon IPC task failed", error)),
457 }
458 }
459 if let Some(task) = bridge_task
460 && !task.is_finished()
461 && let Err(error) = task.await
462 {
463 tracing::warn!(%error, "bridge server task failed");
464 }
465 if let Some(task) = media_task
466 && !task.is_finished()
467 && let Err(error) = task.await
468 {
469 tracing::warn!(%error, "media server task failed");
470 }
471 Ok(())
472 }
473
474 async fn run_event_loop(
475 &self,
476 server_task: &mut JoinHandle<Result<()>>,
477 signal_task: &mut SignalTask<'_>,
478 shutdown: &mut broadcast::Receiver<()>,
479 shutdown_sender: &broadcast::Sender<()>,
480 interval: &mut tokio::time::Interval,
481 watch_interval: &mut tokio::time::Interval,
482 ) -> Result<()> {
483 loop {
484 tokio::select! {
485 signal_result = &mut *signal_task => {
486 signal_result?;
487 send_shutdown(shutdown_sender);
488 break Ok(());
489 }
490 shutdown_result = shutdown.recv() => {
491 if matches!(shutdown_result, Ok(()) | Err(broadcast::error::RecvError::Closed)) {
492 break Ok(());
493 }
494 }
495 server_result = &mut *server_task => {
496 match server_result {
497 Ok(result) => break result,
498 Err(error) => break Err(SyncwebError::operation("daemon IPC task failed", error)),
499 }
500 }
501 trigger = self.receive_sync_trigger() => {
502 match trigger {
503 Some(namespace) => self.run_trigger(namespace).await?,
504 None => break Ok(()),
505 }
506 }
507 _ = watch_interval.tick() => self.handle_watch_events().await?,
508 _ = interval.tick() => self.run_cycle().await?,
509 }
510 }
511 }
512
513 async fn run_cycle(&self) -> Result<()> {
514 self.reload_if_requested().await?;
515 self.load_folders().await?;
516 self.start_watching().await?;
517 let statuses = self.handle.folder_registry.read().await.statuses();
518 for folder in statuses {
519 let namespace = folder
520 .namespace
521 .parse::<NamespaceId>()
522 .map_err(|error| SyncwebError::operation("invalid managed folder namespace", error))?;
523 let folder_name = (!folder.path.as_os_str().is_empty()).then(|| folder.path.to_string_lossy().into_owned());
524 let active = {
525 let schedule_manager = self.schedule_manager.read().await;
526 schedule_manager
527 .as_ref()
528 .is_none_or(|manager| manager.is_active(folder_name.as_deref()))
529 };
530 self.start_supervision(namespace).await?;
531 if active {
532 self.set_intent_active(namespace, true)?;
533 } else {
534 self.set_intent_active(namespace, false)?;
535 }
536 }
537 self.save_status_report().await?;
538 Ok(())
539 }
540
541 async fn run_trigger(&self, namespace: Option<String>) -> Result<()> {
542 match namespace {
543 Some(value) => {
544 let parsed_namespace = value
545 .parse::<NamespaceId>()
546 .map_err(|error| SyncwebError::operation("invalid sync namespace", error))?;
547 self.start_supervision(parsed_namespace).await?;
548 self.set_intent_active(parsed_namespace, true)?;
549 self.save_status_report().await?;
550 Ok(())
551 }
552 None => self.run_cycle().await,
553 }
554 }
555
556 async fn start_supervision(&self, namespace: NamespaceId) -> Result<()> {
557 if is_active(namespace) {
558 return Ok(());
559 }
560 {
561 let mut tasks = self
562 .intent_tasks
563 .lock()
564 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?;
565 tasks.retain(|_, task| !task.is_finished());
566 if tasks.contains_key(&namespace) {
567 return Ok(());
568 }
569 }
570 let network_id = {
571 let guard = self.network_manager.read().await;
572 guard.network_for_folder(&namespace)?
573 };
574 let session_id = network_id.as_ref().and_then(|net_id| {
575 self.network_logger
576 .record_sync_start(net_id, &namespace.to_string())
577 .ok()
578 });
579 let sync = self.sync_engine.clone();
580 let supervisor = self.intent_supervisor;
581 let shutdown = self.handle.shutdown_sender.subscribe();
582 let controls = self.intent_controls.clone();
583 let filter = self.filter_engine.read().await.clone();
584 let network_logger = self.network_logger.clone();
585 let folder_name = self
586 .handle
587 .folder_registry
588 .read()
589 .await
590 .statuses()
591 .into_iter()
592 .find(|status| status.namespace == namespace.to_string())
593 .and_then(|status| {
594 (!status.path.as_os_str().is_empty()).then(|| status.path.to_string_lossy().into_owned())
595 });
596 let bandwidth = self
597 .schedule_manager
598 .read()
599 .await
600 .as_ref()
601 .map(|manager| manager.current_limits(folder_name.as_deref()));
602 let params = bandwidth.map_or_else(SubscribeParams::default, |limits| {
603 SubscribeParams::default().with_bandwidth_limits(limits)
604 });
605 let (ready_sender, ready_receiver) = oneshot::channel();
606 let task = tokio::spawn(async move {
607 let result = supervisor
608 .supervise_with_controls_and_ready(
609 &sync,
610 namespace,
611 params,
612 shutdown,
613 SupervisionOptions::with_ready(controls, filter, ready_sender),
614 )
615 .await;
616 if network_id.is_some()
617 && let Some(sid) = session_id
618 {
619 match &result {
620 Ok(supervised) => {
621 let has_error = supervised.last_error.is_some();
622 let files = 0;
623 let bytes = 0;
624 let errors = u64::from(has_error);
625 let status = if has_error { "failed" } else { "completed" };
626 let _ = network_logger.record_sync_finish(sid, files, bytes, errors, status);
627 }
628 Err(_) => {
629 let _ = network_logger.record_sync_finish(sid, 0, 0, 1, "failed");
630 }
631 }
632 }
633 match &result {
634 Ok(supervised) => {
635 if let Some(error) = &supervised.last_error {
636 tracing::warn!(%namespace, retry_count = supervised.retry_count, %error, "supervised intent stopped");
637 }
638 }
639 Err(error) => tracing::error!(%namespace, %error, "supervised intent failed"),
640 }
641 });
642 self.intent_tasks
643 .lock()
644 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?
645 .insert(namespace, task);
646 ready_receiver
647 .await
648 .map_err(|error| SyncwebError::operation("daemon intent startup notification failed", error))?;
649 Ok(())
650 }
651
652 fn set_intent_active(&self, namespace: NamespaceId, active: bool) -> Result<()> {
653 let control = self
654 .intent_controls
655 .lock()
656 .map_err(|error| SyncwebError::operation("daemon intent control mutex is poisoned", error))?
657 .get(&namespace)
658 .cloned();
659 let Some(intent_control) = control else {
660 return Ok(());
661 };
662 let result = if active {
663 intent_control.resume()
664 } else {
665 intent_control.pause()
666 };
667 result.map_err(|error| SyncwebError::operation("failed to update scheduled intent", error))
668 }
669
670 pub async fn is_in_active_window(&self) -> bool {
672 self.schedule_manager
673 .read()
674 .await
675 .as_ref()
676 .is_none_or(|manager| manager.is_active(None))
677 }
678
679 pub async fn current_bandwidth_limit(&self) -> Option<u64> {
681 self.schedule_manager.read().await.as_ref().and_then(|manager| {
682 let limits = manager.current_limits(None);
683 limits.max_download.or(limits.max_upload)
684 })
685 }
686
687 async fn start_watching(&self) -> Result<()> {
688 let statuses = self.handle.folder_registry.read().await.statuses();
689 let wanted: HashMap<_, _> = statuses
690 .iter()
691 .filter(|status| !status.path.as_os_str().is_empty())
692 .map(|status| (status.namespace.clone(), status.path.clone()))
693 .collect();
694 let mut watchers = self
695 .watchers
696 .lock()
697 .map_err(|error| SyncwebError::operation("daemon watcher mutex is poisoned", error))?;
698 watchers.retain(|namespace, _| wanted.contains_key(namespace));
699 for (namespace, path) in wanted {
700 if watchers.contains_key(&namespace) {
701 continue;
702 }
703 if !path.exists() {
704 tracing::warn!(%namespace, path = %path.display(), "managed folder path does not exist; watcher deferred");
705 continue;
706 }
707 watchers.insert(namespace, FsWatcher::new(&path)?);
708 }
709 drop(watchers);
710 Ok(())
711 }
712
713 async fn handle_watch_events(&self) -> Result<()> {
714 let mut observed = Vec::new();
715 {
716 let mut watchers = self
717 .watchers
718 .lock()
719 .map_err(|error| SyncwebError::operation("daemon watcher mutex is poisoned", error))?;
720 for (namespace, watcher) in watchers.iter_mut() {
721 loop {
722 match watcher.try_recv() {
723 Ok(Some(event)) => observed.push((namespace.clone(), event)),
724 Ok(None) => break,
725 Err(error) => {
726 tracing::warn!(%namespace, %error, "filesystem watcher event channel failed");
727 break;
728 }
729 }
730 }
731 }
732 }
733
734 if !observed.is_empty() {
735 let mut pending = self
736 .pending_watch_events
737 .lock()
738 .map_err(|error| SyncwebError::operation("daemon watch queue mutex is poisoned", error))?;
739 let ready_at = Instant::now()
740 .checked_add(self.config.watch_debounce)
741 .unwrap_or_else(Instant::now);
742 for (namespace, event) in observed {
743 let removed = matches!(event.event.kind, notify::EventKind::Remove(_));
744 let entry = pending.entry(namespace).or_insert_with(|| PendingWatch {
745 paths: HashMap::new(),
746 ready_at,
747 });
748 entry.ready_at = ready_at;
749 for path in event.paths {
750 entry.paths.insert(path, removed);
751 }
752 }
753 drop(pending);
754 }
755
756 let ready = {
757 let mut pending = self
758 .pending_watch_events
759 .lock()
760 .map_err(|error| SyncwebError::operation("daemon watch queue mutex is poisoned", error))?;
761 let now = Instant::now();
762 pending
763 .iter_mut()
764 .filter_map(|(namespace, batch)| {
765 (batch.ready_at <= now).then(|| (namespace.clone(), std::mem::take(&mut batch.paths)))
766 })
767 .filter(|(_, paths)| !paths.is_empty())
768 .collect::<Vec<_>>()
769 };
770 if ready.is_empty() {
771 return Ok(());
772 }
773
774 let roots: HashMap<_, _> = self
775 .handle
776 .folder_registry
777 .read()
778 .await
779 .statuses()
780 .into_iter()
781 .map(|status| (status.namespace, status.path))
782 .collect();
783 for (namespace, paths) in ready {
784 let Some(root) = roots.get(&namespace) else {
785 continue;
786 };
787 let namespace_id = namespace
788 .parse::<NamespaceId>()
789 .map_err(|error| SyncwebError::operation("invalid watched folder namespace", error))?;
790 for (path, removed) in paths {
791 self.process_watch_event(namespace_id, root, &path, removed).await?;
792 }
793 }
794 self.save_status_report().await?;
795 Ok(())
796 }
797
798 async fn process_watch_event(&self, namespace: NamespaceId, root: &Path, path: &Path, removed: bool) -> Result<()> {
799 let relative = path.strip_prefix(root).unwrap_or(path);
800 if relative.as_os_str().is_empty() {
801 return Ok(());
802 }
803 let size = std::fs::metadata(path).map_or(0, |metadata| metadata.len());
804 let accepted = self.filter_engine.read().await.as_ref().is_none_or(|filter| {
805 filter.evaluate_for_folder(&namespace.to_string(), &FilterEntry::new(relative.to_path_buf(), size))
806 != FilterAction::Reject
807 });
808 if !accepted {
809 return Ok(());
810 }
811
812 let folder = self.folder_manager.get(namespace).await?;
813 let result = if removed || !path.exists() {
814 folder
815 .delete_entry(relative.as_os_str().as_encoded_bytes())
816 .await
817 .map(|()| 1_u64)
818 } else if path.is_file() {
819 let importer = Importer::new(
820 self.node.blob_store().clone(),
821 self.node.docs_engine().clone(),
822 folder.doc().clone(),
823 folder.author(),
824 )
825 .with_root(root);
826 importer
827 .import_path(path)
828 .await
829 .map(|entries| u64::try_from(entries.len()).unwrap_or(u64::MAX))
830 } else {
831 Ok(0)
832 };
833 match result {
834 Ok(entries) if entries > 0 => {
835 self.handle
836 .folder_registry
837 .write()
838 .await
839 .record_import(namespace, entries, current_timestamp());
840 self.start_supervision(namespace).await?;
841 }
842 Ok(_) => {}
843 Err(error) => {
844 self.handle
845 .folder_registry
846 .write()
847 .await
848 .record_error(namespace, error.to_string());
849 tracing::warn!(%namespace, path = %path.display(), %error, "filesystem change will be retried");
850 if !removed && is_recoverable_watch_error(&error) {
851 let mut pending = self.pending_watch_events.lock().map_err(|poisoned| {
852 SyncwebError::operation("daemon watch queue mutex is poisoned", poisoned)
853 })?;
854 let ready_at = Instant::now()
855 .checked_add(self.config.watch_debounce)
856 .unwrap_or_else(Instant::now);
857 let entry = pending.entry(namespace.to_string()).or_insert_with(|| PendingWatch {
858 paths: HashMap::new(),
859 ready_at,
860 });
861 entry.ready_at = ready_at;
862 entry.paths.insert(path.to_path_buf(), false);
863 drop(pending);
864 }
865 }
866 }
867 Ok(())
868 }
869
870 async fn save_status_report(&self) -> Result<()> {
871 let state = self.handle.state.read().await.clone();
872 let statuses = self.handle.folder_registry.read().await.statuses();
873 let schedule = self.schedule_manager.read().await.clone();
874 let schedule_report = schedule.as_ref().map(|manager| {
875 let minute = current_minute();
876 let next = manager.next_active_window_start_at(None, minute);
877 let next_window_start = next.map(|next_minute| {
878 let offset = if next_minute >= minute {
879 next_minute.saturating_sub(minute)
880 } else {
881 1_440_u16.saturating_sub(minute).saturating_add(next_minute)
882 };
883 current_timestamp().saturating_add(u64::from(offset).saturating_mul(60))
884 });
885 ScheduleStatus {
886 in_active_window: manager.is_active(None),
887 next_window_start,
888 }
889 });
890 let current_stats = self.stats_db.current_stats().unwrap_or_default();
891 let report = DaemonStatusReport {
892 pid: state.pid,
893 node_id: state.node_id,
894 started_at: state.started_at,
895 uptime_seconds: current_timestamp().saturating_sub(state.started_at),
896 folders: statuses
897 .into_iter()
898 .map(|folder| FolderStatusReport {
899 namespace: folder.namespace,
900 path: folder.path,
901 session_active: folder.session_active,
902 last_sync_at: folder.last_sync_at,
903 entries_synced: folder.entries_synced,
904 errors: folder.errors,
905 })
906 .collect(),
907 bandwidth: BandwidthSnapshot {
908 upload_total: current_stats.total_upload,
909 download_total: current_stats.total_download,
910 upload_rate: 0,
911 download_rate: 0,
912 },
913 schedule: schedule_report,
914 rayon_threads: self.archive_pool.thread_count(),
915 };
916 self.node_db.save_status(&report)
917 }
918
919 async fn load_folders(&self) -> Result<()> {
920 let folders = self.folder_manager.list().await?;
921 let mut registry = self.handle.folder_registry.write().await;
922 for folder in folders {
923 let namespace = folder.namespace_id();
924 if !registry
925 .statuses()
926 .iter()
927 .any(|status| status.namespace == namespace.to_string())
928 {
929 registry.add(FolderEntry::new(namespace, PathBuf::new()))?;
930 }
931 }
932 drop(registry);
933 Ok(())
934 }
935
936 async fn reannounce_folders(&self) {
937 let Ok(folders) = self.folder_manager.list().await else {
938 return;
939 };
940 for folder in &folders {
941 self.folder_manager.announce_namespace(folder.namespace_id()).await;
942 }
943 }
944
945 async fn load_subscriptions(&self) -> Result<()> {
946 let subscriptions = self.node_db.load_subscriptions_with_sizes()?;
947 if subscriptions.is_empty() {
948 return Ok(());
949 }
950 let hashes: Vec<_> = subscriptions.iter().map(|(hash, _)| *hash).collect();
951 self.folder_manager.seed_subscriptions(hashes).await;
952
953 let mut registry = self.handle.folder_registry.write().await;
954 for (hash, size) in subscriptions {
955 let subscription = PublicSubscription::new(hash, None, size);
956 registry.add_subscription(subscription);
957 }
958 drop(registry);
959 Ok(())
960 }
961
962 async fn reload_if_requested(&self) -> Result<()> {
963 if !self
964 .handle
965 .reload_requested
966 .swap(false, std::sync::atomic::Ordering::AcqRel)
967 {
968 return Ok(());
969 }
970 let app_config = self.node_db.load_app_config()?;
971 let parsed_schedule = ScheduleManager::from_config(&app_config.schedule)?;
972 let schedule_manager =
973 (app_config.schedule != crate::schedule::ScheduleConfig::default()).then_some(parsed_schedule);
974 let filter = self.node_db.load_filter_engine()?;
975 *self.schedule_manager.write().await = schedule_manager;
976 *self.filter_engine.write().await = filter;
977 let statuses = self.handle.folder_registry.read().await.statuses();
978 for status in statuses {
979 if let Ok(namespace) = status.namespace.parse::<NamespaceId>()
980 && is_active(namespace)
981 && !cancel_session(namespace)
982 {
983 tracing::warn!(%namespace, "reloaded intent did not accept cancellation");
984 }
985 }
986 tracing::info!("daemon configuration reloaded");
987 Ok(())
988 }
989
990 async fn receive_sync_trigger(&self) -> Option<Option<String>> {
991 self.sync_receiver.lock().await.recv().await
992 }
993
994 async fn shutdown_resources(&self) -> Result<()> {
995 self.handle.set_status(DaemonStatus::Stopping).await;
996 let stopping_state = self.handle.state.read().await.clone();
997 self.node_db.save_lifecycle(&stopping_state)?;
998 self.save_status_report().await?;
999
1000 let namespaces: Vec<_> = self
1001 .handle
1002 .folder_registry
1003 .read()
1004 .await
1005 .statuses()
1006 .into_iter()
1007 .filter_map(|status| status.namespace.parse::<NamespaceId>().ok())
1008 .collect();
1009 for namespace in namespaces {
1010 if is_active(namespace) && !cancel_session(namespace) {
1011 tracing::warn!(%namespace, "intent did not accept shutdown cancellation");
1012 }
1013 }
1014
1015 let tasks = {
1016 let mut task_map = self
1017 .intent_tasks
1018 .lock()
1019 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?;
1020 std::mem::take(&mut *task_map)
1021 };
1022 for (_, task) in tasks {
1023 task.await
1024 .map_err(|error| SyncwebError::operation("daemon intent task failed", error))?;
1025 }
1026
1027 let attestation_handle = self.attestation_listener.lock().await.take();
1028 if let Some(handle) = attestation_handle {
1029 handle.abort();
1030 }
1031
1032 let report_handle = self.report_listener.lock().await.take();
1033 if let Some(handle) = report_handle {
1034 handle.abort();
1035 }
1036
1037 let revocation_handle = self.revocation_listener.lock().await.take();
1038 if let Some(handle) = revocation_handle {
1039 handle.abort();
1040 }
1041
1042 let node_result = self.node.stop().await;
1043 self.handle.set_status(DaemonStatus::Stopped).await;
1044 let remove_result = self.node_db.remove_lifecycle();
1045 let remove_status_result = self.node_db.remove_status();
1046 let release_result = self.pid_lock.release();
1047 node_result?;
1048 remove_result?;
1049 remove_status_result?;
1050 release_result?;
1051 Ok(())
1052 }
1053
1054 async fn spawn_attestation_listener(&self) {
1055 let gossip_service = self.node.gossip_service().clone();
1056 let data_dir = self.config.data_dir.clone();
1057 let shutdown = self.handle.shutdown_sender.subscribe();
1058 let handle = tokio::spawn(async move {
1059 if let Err(error) = listen_for_attestations(gossip_service, data_dir, shutdown).await {
1060 tracing::error!(%error, "attestation gossip listener failed");
1061 }
1062 });
1063 *self.attestation_listener.lock().await = Some(handle);
1064 }
1065
1066 async fn spawn_report_listener(&self) {
1067 let gossip_service = self.node.gossip_service().clone();
1068 let data_dir = self.config.data_dir.clone();
1069 let shutdown = self.handle.shutdown_sender.subscribe();
1070 let handle = tokio::spawn(async move {
1071 if let Err(error) = listen_for_reports(gossip_service, data_dir, shutdown).await {
1072 tracing::error!(%error, "report gossip listener failed");
1073 }
1074 });
1075 *self.report_listener.lock().await = Some(handle);
1076 }
1077
1078 async fn spawn_revocation_listener(&self) {
1079 let gossip_service = self.node.gossip_service().clone();
1080 let data_dir = self.config.data_dir.clone();
1081 let shutdown = self.handle.shutdown_sender.subscribe();
1082 let handle = tokio::spawn(async move {
1083 if let Err(error) = listen_for_revocations(gossip_service, data_dir, shutdown).await {
1084 tracing::error!(%error, "revocation gossip listener failed");
1085 }
1086 });
1087 *self.revocation_listener.lock().await = Some(handle);
1088 }
1089
1090 fn spawn_maintenance_task(&self) {
1093 let node_db = self.node_db.clone();
1094 let stats_db = self.stats_db.clone();
1095 let mut shutdown = self.handle.shutdown_sender.subscribe();
1096 tokio::spawn(async move {
1097 let mut interval = tokio::time::interval(Duration::from_hours(24));
1098 loop {
1099 tokio::select! {
1100 _ = shutdown.recv() => {
1101 tracing::info!("maintenance task shutting down");
1102 break;
1103 }
1104 _ = interval.tick() => {
1105 let dbs: [&dyn crate::storage::Vacuumable; 2] = [&node_db, &stats_db];
1106 for db in dbs {
1107 match db.freelist_count() {
1108 Ok(count) if count > 100 => {
1109 tracing::info!(freelist = %count, "running vacuum");
1110 if let Err(error) = db.vacuum() {
1111 tracing::warn!(%error, "maintenance vacuum failed");
1112 }
1113 }
1114 Ok(_) => {}
1115 Err(error) => tracing::warn!(%error, "maintenance freelist query failed"),
1116 }
1117 }
1118 }
1119 }
1120 }
1121 });
1122 }
1123
1124 async fn subscribe_network_gossip(&self) {
1125 let networks: Vec<_> = {
1126 let guard = self.network_manager.read().await;
1127 guard.list().into_iter().cloned().collect::<Vec<_>>()
1128 };
1129 for network in &networks {
1130 let network_id = network.id;
1131 let topic = network.topic;
1132 let members: Vec<_> = network
1133 .members
1134 .iter()
1135 .copied()
1136 .filter(|m| *m != self.node.endpoint().id())
1137 .collect();
1138 match self.node.gossip_service().subscribe(topic, members).await {
1139 Ok(_topic) => {
1140 tracing::debug!(%network_id, "subscribed to network gossip topic");
1141 }
1142 Err(error) => {
1143 tracing::warn!(%network_id, %error, "failed to subscribe to network gossip topic");
1144 }
1145 }
1146 }
1147 }
1148
1149 async fn spawn_membership_listeners(&self) {
1150 let networks: Vec<_> = {
1151 let guard = self.network_manager.read().await;
1152 guard.list().into_iter().cloned().collect::<Vec<_>>()
1153 };
1154 let docs_engine = self.node.docs_engine().clone();
1155 let blob_store = self.node.blob_store().clone();
1156 let shutdown = self.handle.shutdown_sender.subscribe();
1157 let local_pk = self.node.endpoint().id();
1158 let node_db = self.node_db.clone();
1159 for network in networks {
1160 let Some(ref doc_ticket_str) = network.doc_ticket else {
1161 continue;
1162 };
1163 let Ok(doc_ticket) = doc_ticket_str.parse::<iroh_docs::DocTicket>() else {
1164 tracing::warn!(network_id = %network.id, "invalid doc_ticket in network");
1165 continue;
1166 };
1167 let de = docs_engine.clone();
1168 let bs = blob_store.clone();
1169 let mut member_shutdown = shutdown.resubscribe();
1170 let network_id = network.id;
1171 let local_key_str = local_pk.to_string();
1172 let db = node_db.clone();
1173 tokio::spawn(async move {
1174 let doc = match de.import_ticket(doc_ticket).await {
1175 Ok(doc) => doc,
1176 Err(error) => {
1177 tracing::warn!(%network_id, %error, "failed to import membership doc from ticket");
1178 return;
1179 }
1180 };
1181 if let Err(error) = de.start_sync(&doc, Vec::new()).await {
1182 tracing::warn!(%network_id, %error, "failed to start membership doc sync");
1183 }
1184 let mut stream = match de.watch(&doc).await {
1185 Ok(stream) => stream,
1186 Err(error) => {
1187 tracing::warn!(%network_id, %error, "failed to watch membership doc");
1188 return;
1189 }
1190 };
1191 loop {
1192 tokio::select! {
1193 _ = member_shutdown.recv() => {
1194 tracing::debug!(%network_id, "membership listener shutting down");
1195 break;
1196 }
1197 event = stream.next() => {
1198 let Some(event_result) = event else {
1199 break;
1200 };
1201 match event_result {
1202 Ok(iroh_docs::engine::LiveEvent::InsertLocal { entry } | iroh_docs::engine::LiveEvent::InsertRemote { entry, .. }) => {
1203 if entry.key() == b"sys/network/members" {
1204 let hash = entry.content_hash();
1205 let Ok(content) = bs.get(hash).await else { continue; };
1206 match serde_json::from_slice::<crate::net::membership_doc::SignedMemberList>(&content) {
1207 Ok(member_list) => {
1208 if let Err(error) = member_list.verify() {
1209 tracing::warn!(%network_id, %error, "invalid membership list signature");
1210 continue;
1211 }
1212 let is_member = member_list.members.iter().any(|m| m.key == local_key_str);
1213 if is_member {
1214 tracing::debug!(%network_id, members = member_list.members.len(), "membership updated");
1215 } else {
1216 tracing::warn!(%network_id, "local node was removed from network");
1217 let _ = db.delete_network(network_id);
1218 }
1219 }
1220 Err(error) => {
1221 tracing::warn!(%network_id, %error, "failed to parse membership list");
1222 }
1223 }
1224 }
1225 }
1226 Ok(_) => {}
1227 Err(error) => {
1228 tracing::warn!(%network_id, %error, "membership doc event error");
1229 }
1230 }
1231 }
1232 }
1233 }
1234 });
1235 }
1236 }
1237
1238 async fn handle_signals(&self, shutdown: broadcast::Sender<()>) -> Result<()> {
1239 #[cfg(unix)]
1240 {
1241 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
1242 tokio::select! {
1243 result = tokio::signal::ctrl_c() => result?,
1244 _ = terminate.recv() => {}
1245 }
1246 }
1247 #[cfg(not(unix))]
1248 {
1249 tokio::signal::ctrl_c().await?;
1250 }
1251 send_shutdown(&shutdown);
1252 Ok(())
1253 }
1254}
1255
1256impl Drop for Daemon {
1257 fn drop(&mut self) {
1258 if let Err(error) = self.node_db.remove_lifecycle() {
1259 tracing::warn!(%error, "failed to remove daemon lifecycle");
1260 }
1261 if let Err(error) = self.node_db.remove_status() {
1262 tracing::warn!(%error, "failed to remove daemon status");
1263 }
1264 }
1265}
1266
1267fn send_shutdown(sender: &broadcast::Sender<()>) {
1268 match sender.send(()) {
1269 Ok(_) | Err(broadcast::error::SendError(())) => {}
1270 }
1271}
1272
1273fn current_minute() -> u16 {
1274 let seconds = std::time::SystemTime::now()
1275 .duration_since(std::time::UNIX_EPOCH)
1276 .map_or(0, |duration| duration.as_secs());
1277 u16::try_from((seconds % 86_400).div_euclid(60)).unwrap_or(0)
1278}
1279
1280fn is_recoverable_watch_error(error: &SyncwebError) -> bool {
1281 let message = error.to_string();
1282 message.contains("file changed during import") || message.contains("input path does not exist")
1283}
1284
1285fn attestation_topic_id() -> iroh_gossip::TopicId {
1286 iroh_gossip::TopicId::from_bytes(*blake3::hash(ATTESTATION_GOSSIP_TOPIC).as_bytes())
1287}
1288
1289fn persist_incoming_attestation(att: &Attestation, db: &IndexingDatabase, existing: &mut Vec<Attestation>) {
1290 if existing.contains(att) {
1291 return;
1292 }
1293 existing.push(att.clone());
1294 if let Err(error) = db.save_attestations(existing) {
1295 tracing::warn!(%error, "failed to persist incoming attestation");
1296 }
1297 tracing::debug!(
1298 content = %att.content,
1299 issuer = %att.issuer,
1300 kind = %att.kind,
1301 "attestation received via gossip"
1302 );
1303}
1304
1305async fn listen_for_attestations(
1306 gossip_service: GossipService,
1307 data_dir: PathBuf,
1308 mut shutdown: broadcast::Receiver<()>,
1309) -> Result<()> {
1310 let topic = gossip_service
1311 .subscribe_and_join(attestation_topic_id(), Vec::new())
1312 .await?;
1313 let (sender, receiver) = GossipService::split(topic);
1314 let topic_channel = TopicChannel::<Attestation>::new(
1315 Arc::new(gossip_service.inner().clone()),
1316 ATTESTATION_GOSSIP_TOPIC,
1317 sender,
1318 );
1319 let mut stream = topic_channel.receive_from(receiver);
1320 let db = IndexingDatabase::open(data_dir.join("indexing.sqlite"))?;
1321 let mut existing = db.load_attestations()?;
1322
1323 loop {
1324 tokio::select! {
1325 _ = shutdown.recv() => {
1326 tracing::info!("attestation listener shutting down");
1327 break Ok(());
1328 }
1329 msg = stream.next() => {
1330 let Some(att) = msg else {
1331 break Ok(());
1332 };
1333 persist_incoming_attestation(&att, &db, &mut existing);
1334 }
1335 }
1336 }
1337}
1338
1339fn report_topic_id() -> iroh_gossip::TopicId {
1340 iroh_gossip::TopicId::from_bytes(*blake3::hash(REPORT_GOSSIP_TOPIC).as_bytes())
1341}
1342
1343fn revocation_topic_id() -> iroh_gossip::TopicId {
1344 iroh_gossip::TopicId::from_bytes(*blake3::hash(REVOCATION_GOSSIP_TOPIC).as_bytes())
1345}
1346
1347async fn listen_for_revocations(
1348 gossip_service: GossipService,
1349 data_dir: PathBuf,
1350 mut shutdown: broadcast::Receiver<()>,
1351) -> Result<()> {
1352 let topic = gossip_service
1353 .subscribe_and_join(revocation_topic_id(), Vec::new())
1354 .await?;
1355 let (sender, receiver) = GossipService::split(topic);
1356 let topic_channel = TopicChannel::<PrivateLink>::new(
1357 Arc::new(gossip_service.inner().clone()),
1358 REVOCATION_GOSSIP_TOPIC,
1359 sender,
1360 );
1361 let mut stream = topic_channel.receive_from(receiver);
1362 let db = IndexingDatabase::open(data_dir.join("indexing.sqlite"))?;
1363 let (pointers, mirrors, mut revoked) = db.load_links()?;
1364
1365 loop {
1366 tokio::select! {
1367 _ = shutdown.recv() => {
1368 tracing::info!("revocation listener shutting down");
1369 break Ok(());
1370 }
1371 msg = stream.next() => {
1372 handle_revocation_message(msg, &mut revoked, &pointers, &mirrors, &db);
1373 }
1374 }
1375 }
1376}
1377
1378fn handle_revocation_message(
1379 msg: Option<PrivateLink>,
1380 revoked: &mut Vec<PrivateLink>,
1381 pointers: &[MutablePointer],
1382 mirrors: &[String],
1383 db: &IndexingDatabase,
1384) {
1385 let Some(revocation) = msg else {
1386 return;
1387 };
1388 if !revoked.contains(&revocation) {
1389 revoked.push(revocation.clone());
1390 tracing::debug!(
1391 manifest = %revocation.manifest,
1392 "revocation received via gossip"
1393 );
1394 if let Err(error) = db.save_links(pointers, mirrors, revoked) {
1395 tracing::warn!(%error, "failed to persist incoming revocation");
1396 }
1397 }
1398}
1399
1400async fn listen_for_reports(
1401 gossip_service: GossipService,
1402 data_dir: PathBuf,
1403 mut shutdown: broadcast::Receiver<()>,
1404) -> Result<()> {
1405 let topic = gossip_service.subscribe_and_join(report_topic_id(), Vec::new()).await?;
1406 let (sender, receiver) = GossipService::split(topic);
1407 let topic_channel =
1408 TopicChannel::<ReportRecord>::new(Arc::new(gossip_service.inner().clone()), REPORT_GOSSIP_TOPIC, sender);
1409 let mut stream = topic_channel.receive_from(receiver);
1410 let db = IndexingDatabase::open(data_dir.join("indexing.sqlite"))?;
1411 let mut existing = db.load_content_reports()?;
1412
1413 loop {
1414 tokio::select! {
1415 _ = shutdown.recv() => {
1416 tracing::info!("report listener shutting down");
1417 break Ok(());
1418 }
1419 msg = stream.next() => {
1420 if !handle_report_message(msg, &mut existing, &db) {
1421 break Ok(());
1422 }
1423 }
1424 }
1425 }
1426}
1427
1428fn handle_report_message(msg: Option<ReportRecord>, existing: &mut Vec<ReportRecord>, db: &IndexingDatabase) -> bool {
1429 let Some(report) = msg else {
1430 return false;
1431 };
1432 tracing::debug!(
1433 content = %report.content,
1434 reason = %report.reason,
1435 "report received via gossip"
1436 );
1437 if !existing.contains(&report) {
1438 existing.push(report);
1439 if let Err(error) = db.save_content_reports(existing) {
1440 tracing::warn!(%error, "failed to persist incoming report");
1441 }
1442 }
1443 true
1444}