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