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::{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/// Configuration used to construct and run a daemon.
45#[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
95/// Owns the Iroh node, synchronization intents, IPC server, and daemon
96/// lifecycle state.
97pub 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    /// Open node and stats databases and acquire the process lock.
163    ///
164    /// # Errors
165    ///
166    /// Returns an error if a database cannot be opened or the lock is held by
167    /// another process.
168    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    /// Load app config, schedule manager, filter engine, and write initial lifecycle state.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if loading config, filters, or saving the lifecycle fails.
186    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    /// Create a daemon, acquire its process lock, and persist its running
220    /// state.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if another daemon owns the data directory, the node
225    /// cannot be opened, or configuration cannot be parsed.
226    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    /// Run the daemon until IPC or operating-system shutdown is requested.
373    ///
374    /// # Errors
375    ///
376    /// Returns an error if an IPC listener, sync cycle, or cleanup operation
377    /// fails.
378    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    /// Return a snapshot of the daemon lifecycle state.
391    pub async fn state(&self) -> DaemonState {
392        self.handle.state.read().await.clone()
393    }
394
395    /// Return the daemon's fixed archive pool.
396    #[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    /// The `subscribe-changes` folders with live syncing enabled, mapped to their filters.
608    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    /// Return whether the global schedule is currently active.
741    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    /// Return the configured download limit at the current wall-clock time.
750    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    /// Spawn a background task that periodically vacuums databases with
1194    /// excessive freelist pages.
1195    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}