Skip to main content

syncweb_core/daemon/
daemon.rs

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