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