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