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, RelayManager, RelayManagerOptions},
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 transfer_limits::{Direction, PendingTransfer, TransferBudget, TransferLimits, TransferQueue},
29};
30
31use crate::node::iroh_node::discovery_scope;
32
33use super::{
34 DaemonHandle, DaemonState, DaemonStatus, FolderEntry, IpcServer, ManagedPool, PidLock, WatchConfig,
35 current_timestamp, daemon_socket_path,
36 state::{BandwidthSnapshot, DaemonStatusReport, ScheduleStatus, load_filter_engine},
37 supervisor::{BandwidthAccountant, DEFAULT_RETRY_WINDOW, IntentControls, IntentSupervisor, SupervisionOptions},
38};
39
40#[derive(Clone, Debug)]
42#[non_exhaustive]
43pub struct DaemonConfig {
44 pub data_dir: PathBuf,
45 pub network: Option<String>,
46 pub sync_interval: Duration,
47 pub observation_ttl: Duration,
48 pub retry_window: Duration,
50 pub backoff_base: Duration,
51 pub backoff_max: Duration,
52 pub rayon_threads: usize,
53 pub log_level: String,
54 pub log_file: Option<PathBuf>,
55 pub watch_debounce: Duration,
56 pub relay_mode: crate::node::iroh_node::RelayMode,
57 pub media_listen: Option<SocketAddr>,
58 pub discovery: crate::node::iroh_node::DiscoveryConfig,
59 pub address_lookup: Option<iroh::address_lookup::memory::MemoryLookup>,
61 pub bep: crate::storage::config::BepConfig,
64}
65
66impl Default for DaemonConfig {
67 fn default() -> Self {
68 Self {
69 data_dir: PathBuf::from("."),
70 network: None,
71 sync_interval: Duration::from_mins(1),
72 observation_ttl: Duration::from_hours(1),
73 retry_window: DEFAULT_RETRY_WINDOW,
74 backoff_base: Duration::from_secs(1),
75 backoff_max: Duration::from_mins(1),
76 rayon_threads: std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get),
77 log_level: "info".to_owned(),
78 log_file: None,
79 watch_debounce: Duration::from_millis(500),
80 relay_mode: crate::node::iroh_node::RelayMode::Default,
81 media_listen: None,
82 discovery: crate::node::iroh_node::DiscoveryConfig::default(),
83 address_lookup: None,
84 bep: crate::storage::config::BepConfig::default(),
85 }
86 }
87}
88
89impl DaemonConfig {
90 #[must_use]
91 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
92 Self {
93 data_dir: data_dir.into(),
94 ..Self::default()
95 }
96 }
97}
98
99pub struct Daemon {
102 config: DaemonConfig,
103 node_db: NodeDatabase,
104 stats_db: StatsDatabase,
105 pid_lock: PidLock,
106 ipc_server: IpcServer,
107 intent_supervisor: IntentSupervisor,
108 folder_manager: FolderManager,
109 sync_engine: SyncEngine,
110 schedule_manager: tokio::sync::RwLock<Option<ScheduleManager>>,
111 transfer_budget: Arc<TransferBudget>,
112 transfer_queue: Arc<TransferQueue>,
113 deferred_downloads: Mutex<HashSet<NamespaceId>>,
116 node: Arc<IrohNode>,
117 handle: DaemonHandle,
118 sync_receiver: tokio::sync::Mutex<mpsc::UnboundedReceiver<Option<String>>>,
119 intent_tasks: Mutex<HashMap<NamespaceId, Option<JoinHandle<()>>>>,
120 intent_controls: IntentControls,
121 watchers: Mutex<HashMap<String, FsWatcher>>,
122 pending_watch_events: Mutex<HashMap<String, PendingWatch>>,
123 filter_engine: tokio::sync::RwLock<Option<FilterEngine>>,
124 archive_pool: Arc<ManagedPool>,
125 network_logger: NetworkLogger,
126 network_manager: tokio::sync::RwLock<NetworkManager>,
127 maintenance_task: tokio::sync::Mutex<Option<JoinHandle<()>>>,
128 indexing: Option<IndexingService>,
129}
130
131struct PendingWatch {
132 paths: HashMap<PathBuf, bool>,
133 ready_at: Instant,
134}
135
136#[derive(Clone, Copy)]
138struct WatchEvaluation<'a> {
139 filter: Option<&'a FilterEngine>,
140 exclude: &'a [String],
141}
142
143type ServerTasks = (
146 JoinHandle<Result<()>>,
147 Option<JoinHandle<Result<()>>>,
148 Option<JoinHandle<Result<()>>>,
149);
150
151type SignalTask<'a> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>>;
152
153impl Daemon {
154 #[must_use]
157 pub fn endpoint(&self) -> iroh::Endpoint {
158 self.node.endpoint().clone()
159 }
160
161 #[expect(
162 clippy::too_many_arguments,
163 reason = "daemon bootstrap passes each subsystem its config"
164 )]
165 async fn open_identity_and_node(
166 data_dir: &Path,
167 node_db: &NodeDatabase,
168 pid_lock: &PidLock,
169 relay_mode: crate::node::iroh_node::RelayMode,
170 discovery: crate::node::iroh_node::DiscoveryConfig,
171 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
172 address_lookup: Option<iroh::address_lookup::memory::MemoryLookup>,
173 bep: &crate::storage::config::BepConfig,
174 ) -> Result<(Arc<IrohNode>, FolderManager, SyncEngine)> {
175 let identity = IdentityManager::new(data_dir.join("identity.key")).inspect_err(|_| {
176 let _ = node_db.remove_lifecycle();
177 let _ = pid_lock.release();
178 })?;
179 let err_cleanup = |_: &_| {
180 let _ = node_db.remove_lifecycle();
181 let _ = pid_lock.release();
182 };
183 let relay = if bep.enabled {
184 let options = RelayManagerOptions::new(bep.relay_config(), data_dir.join("relay"));
185 Some(RelayManager::start(options, identity.node_id()).inspect_err(err_cleanup)?)
186 } else {
187 None
188 };
189 let node = Arc::new(match address_lookup {
190 Some(lookup) => IrohNode::new_with_address_lookup_and_relay(
191 identity,
192 data_dir.join("data"),
193 relay_mode,
194 lookup,
195 discovery,
196 member_keys,
197 relay,
198 )
199 .await
200 .inspect_err(err_cleanup)?,
201 None => IrohNode::new_with_relay(
202 identity,
203 data_dir.join("data"),
204 relay_mode,
205 member_keys,
206 discovery,
207 relay,
208 )
209 .await
210 .inspect_err(err_cleanup)?,
211 });
212 let folder_manager = FolderManager::new(&node);
213 let sync_engine = SyncEngine::new(
214 folder_manager.clone(),
215 node.blob_store().clone(),
216 node.docs_engine().clone(),
217 Some(node.topic_tracker().clone()),
218 );
219 Ok((node, folder_manager, sync_engine))
220 }
221
222 fn init_databases(config: &DaemonConfig) -> Result<(NodeDatabase, StatsDatabase, PidLock)> {
229 let node_db = NodeDatabase::open(config.data_dir.join("node.db"))?;
230 let stats_db = StatsDatabase::open(config.data_dir.join("stats.db"))?;
231 let pid_lock = PidLock::new(&config.data_dir);
232 if !pid_lock.try_acquire()? {
233 return Err(SyncwebError::operation(
234 "daemon already running",
235 config.data_dir.display(),
236 ));
237 }
238 Ok((node_db, stats_db, pid_lock))
239 }
240
241 fn load_app_state(
247 node_db: &NodeDatabase,
248 pid_lock: &PidLock,
249 data_dir: &Path,
250 ) -> Result<(
251 Option<ScheduleManager>,
252 Option<FilterEngine>,
253 DaemonState,
254 TransferLimits,
255 )> {
256 let app_config = node_db.load_app_config().inspect_err(|_error| {
257 let _ = pid_lock.release();
258 })?;
259 let use_schedule = app_config.schedule != crate::schedule::ScheduleConfig::default();
260 let schedule_manager = match ScheduleManager::from_config(&app_config.schedule) {
261 Ok(s) if use_schedule => Some(s),
262 Ok(_) => None,
263 Err(error) => {
264 let _ = pid_lock.release();
265 return Err(error);
266 }
267 };
268 let transfer_limits = TransferLimits::from_config(&app_config.transfer_limits).inspect_err(|_error| {
269 let _ = pid_lock.release();
270 })?;
271 let filter_engine = load_filter_engine(node_db, data_dir).inspect_err(|_error| {
272 let _ = pid_lock.release();
273 })?;
274 let initial_state = DaemonState::new(
275 std::process::id(),
276 String::new(),
277 current_timestamp(),
278 data_dir,
279 DaemonStatus::Starting,
280 );
281 node_db.save_lifecycle(&initial_state).inspect_err(|_error| {
282 let _ = pid_lock.release();
283 })?;
284 Ok((schedule_manager, filter_engine, initial_state, transfer_limits))
285 }
286
287 pub async fn new(config: DaemonConfig) -> Result<Self> {
295 std::fs::create_dir_all(&config.data_dir)?;
296 let (node_db, stats_db, pid_lock) = Self::init_databases(&config)?;
297 node_db.recover_transfer_jobs()?;
298 let (schedule_manager, filter_engine, initial_state, transfer_limits) =
299 Self::load_app_state(&node_db, &pid_lock, &config.data_dir)?;
300
301 let member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>> = {
302 let networks = node_db.list_networks()?;
303 let keys: HashSet<iroh::PublicKey> = networks.iter().flat_map(|n| n.members.iter().copied()).collect();
304 Arc::new(RwLock::new(keys))
305 };
306 let mut node_discovery = config.discovery.clone();
307 node_discovery.scope = discovery_scope(config.network.as_deref());
308 let (node, folder_manager, mut sync_engine) = match Self::open_identity_and_node(
309 &config.data_dir,
310 &node_db,
311 &pid_lock,
312 config.relay_mode.clone(),
313 node_discovery,
314 member_keys.clone(),
315 config.address_lookup.clone(),
316 &config.bep,
317 )
318 .await
319 {
320 Ok(result) => result,
321 Err(error) => {
322 let _ = node_db.remove_lifecycle();
323 let _ = pid_lock.release();
324 return Err(error);
325 }
326 };
327 sync_engine = sync_engine
328 .with_node_db(node_db.clone())
329 .with_reconciler(build_reconciler(node.clone(), folder_manager.clone(), node_db.clone()));
330
331 let archive_pool = match ManagedPool::new("syncweb-archive", config.rayon_threads) {
332 Ok(value) => Arc::new(value),
333 Err(error) => {
334 let _ = node.stop().await;
335 let _ = node_db.remove_lifecycle();
336 let _ = pid_lock.release();
337 return Err(SyncwebError::operation("failed to create daemon thread pool", error));
338 }
339 };
340
341 let initial_handle = DaemonHandle::new(initial_state);
342 {
343 let mut state = initial_handle.state.write().await;
344 state.node_id = node.endpoint().id().to_string();
345 state.status = DaemonStatus::Running;
346 let running_state = state.clone();
347 drop(state);
348 if let Err(error) = node_db.save_lifecycle(&running_state) {
349 let _ = node.stop().await;
350 let _ = node_db.remove_lifecycle();
351 let _ = pid_lock.release();
352 return Err(error);
353 }
354 }
355 let (sync_sender, sync_receiver) = mpsc::unbounded_channel();
356 let handle = DaemonHandle::with_channels_and_reload(
357 initial_handle.state.clone(),
358 initial_handle.folder_registry.clone(),
359 initial_handle.shutdown_sender.clone(),
360 sync_sender,
361 initial_handle.reload_requested.clone(),
362 );
363 let indexing = match IndexingService::new(config.data_dir.join("indexing.sqlite")) {
364 Ok(indexing) => Some(indexing),
365 Err(error) => {
366 tracing::warn!(%error, "failed to open indexing database; catalog channels disabled");
367 None
368 }
369 };
370 let intent_supervisor = IntentSupervisor::new(config.retry_window, config.backoff_base, config.backoff_max);
371 let network_logger = NetworkLogger::new(stats_db.clone());
372 let local_node_id = node.endpoint().id();
373 let nm_result =
374 Self::create_network_manager(&node_db, &local_node_id, &network_logger, Arc::clone(&member_keys));
375 let network_manager = match nm_result {
376 Ok(nm) => nm,
377 Err(error) => {
378 tracing::warn!("failed to create network manager with logger: {error}");
379 NetworkManager::new(node_db.clone(), local_node_id, member_keys)?
380 }
381 };
382 let mut ipc_server = Self::build_ipc_server(
383 &config,
384 &handle,
385 &node,
386 &archive_pool,
387 &folder_manager,
388 &node_db,
389 indexing.clone(),
390 );
391 ipc_server = ipc_server.with_network_manager(Arc::new(tokio::sync::RwLock::new(network_manager.clone())));
392
393 Ok(Self {
394 config,
395 node_db,
396 stats_db,
397 pid_lock,
398 ipc_server,
399 intent_supervisor,
400 folder_manager,
401 sync_engine,
402 schedule_manager: tokio::sync::RwLock::new(schedule_manager),
403 transfer_budget: Arc::new(TransferBudget::new(transfer_limits)),
404 transfer_queue: Arc::new(TransferQueue::new()),
405 deferred_downloads: Mutex::new(HashSet::new()),
406 node,
407 handle,
408 sync_receiver: tokio::sync::Mutex::new(sync_receiver),
409 intent_tasks: Mutex::new(HashMap::new()),
410 intent_controls: Arc::new(Mutex::new(HashMap::new())),
411 watchers: Mutex::new(HashMap::new()),
412 pending_watch_events: Mutex::new(HashMap::new()),
413 filter_engine: tokio::sync::RwLock::new(filter_engine),
414 archive_pool,
415 network_logger,
416 network_manager: tokio::sync::RwLock::new(network_manager),
417 maintenance_task: tokio::sync::Mutex::new(None),
418 indexing,
419 })
420 }
421
422 fn create_network_manager(
423 node_db: &NodeDatabase,
424 local_node_id: &iroh::PublicKey,
425 network_logger: &NetworkLogger,
426 member_keys: Arc<RwLock<HashSet<iroh::PublicKey>>>,
427 ) -> Result<NetworkManager> {
428 NetworkManager::with_logger(node_db.clone(), *local_node_id, network_logger.clone(), member_keys)
429 }
430
431 fn build_ipc_server(
432 config: &DaemonConfig,
433 handle: &DaemonHandle,
434 node: &Arc<IrohNode>,
435 archive_pool: &Arc<ManagedPool>,
436 folder_manager: &FolderManager,
437 node_db: &NodeDatabase,
438 indexing: Option<IndexingService>,
439 ) -> IpcServer {
440 IpcServer::with_archive_context(
441 daemon_socket_path(&config.data_dir),
442 config.data_dir.clone(),
443 handle.clone(),
444 node.clone(),
445 archive_pool.clone(),
446 indexing,
447 )
448 .with_folder_manager(folder_manager.clone())
449 .with_node_db(node_db.clone())
450 }
451
452 pub async fn run(&self) -> Result<()> {
459 let run_result = self.run_inner().await;
460 let cleanup_result = self.shutdown_resources().await;
461 match run_result {
462 Err(error) => {
463 cleanup_result?;
464 Err(error)
465 }
466 Ok(()) => cleanup_result,
467 }
468 }
469
470 pub async fn state(&self) -> DaemonState {
472 self.handle.state.read().await.clone()
473 }
474
475 #[must_use]
477 pub fn archive_pool(&self) -> &ManagedPool {
478 self.archive_pool.as_ref()
479 }
480
481 async fn run_inner(&self) -> Result<()> {
482 tracing::debug!(
483 rayon_threads = self.archive_pool.thread_count(),
484 "daemon runtime initialized"
485 );
486 self.run_initial_setup().await?;
487 let (mut server_task, bridge_task, media_task) = self.spawn_server_tasks()?;
488 let (mut shutdown, mut signal_task, mut interval, mut watch_interval, shutdown_sender) = self.runtime_timers();
489 if let Err(error) = self.run_cycle().await {
490 tracing::error!(%error, "initial daemon cycle failed");
491 }
492 self.spawn_listeners().await;
493 let result = self
494 .run_event_loop(
495 &mut server_task,
496 &mut signal_task,
497 &mut shutdown,
498 &shutdown_sender,
499 &mut interval,
500 &mut watch_interval,
501 )
502 .await;
503 send_shutdown(&shutdown_sender);
504 self.wait_for_server_tasks(server_task, bridge_task, media_task).await?;
505 result
506 }
507
508 async fn run_initial_setup(&self) -> Result<()> {
509 self.load_folders().await?;
510 self.reannounce_folders().await;
511 self.load_subscriptions().await?;
512 self.start_watching().await?;
513 self.spawn_membership_listeners().await;
514 self.subscribe_channels().await;
515 Ok(())
516 }
517
518 fn runtime_timers(
519 &self,
520 ) -> (
521 broadcast::Receiver<()>,
522 SignalTask<'_>,
523 tokio::time::Interval,
524 tokio::time::Interval,
525 broadcast::Sender<()>,
526 ) {
527 let shutdown = self.handle.shutdown_sender.subscribe();
528 let shutdown_sender = self.handle.shutdown_sender.clone();
529 let signal_task: SignalTask<'_> = Box::pin(self.handle_signals(shutdown_sender.clone()));
530 let interval_duration = self.config.sync_interval.max(Duration::from_millis(1));
531 let interval = tokio::time::interval(interval_duration);
532 let watch_interval = tokio::time::interval(Duration::from_millis(100));
533 (shutdown, signal_task, interval, watch_interval, shutdown_sender)
534 }
535
536 async fn spawn_listeners(&self) {
537 self.spawn_maintenance_task().await;
538 }
539
540 fn spawn_server_tasks(&self) -> Result<ServerTasks> {
541 let server = self.ipc_server.clone();
542 let server_task = tokio::spawn(async move { server.serve().await });
543
544 let bridge =
545 crate::media::BridgeServer::new(crate::media::bridge::DEFAULT_BRIDGE_LISTEN, self.ipc_server.clone())?;
546 let bridge_shutdown = self.handle.shutdown_sender.clone();
547 let bridge_task = tokio::spawn(async move { bridge.run(bridge_shutdown).await });
548
549 let media_task = self.config.media_listen.map(|addr| {
550 let media_srv = crate::media::MediaServer::new(addr, self.node.blob_store().clone());
551 let media_shutdown = self.handle.shutdown_sender.clone();
552 tokio::spawn(async move { media_srv.run(media_shutdown).await })
553 });
554
555 Ok((server_task, Some(bridge_task), media_task))
556 }
557
558 async fn wait_for_server_tasks(
559 &self,
560 server_task: JoinHandle<Result<()>>,
561 bridge_task: Option<JoinHandle<Result<()>>>,
562 media_task: Option<JoinHandle<Result<()>>>,
563 ) -> Result<()> {
564 if !server_task.is_finished() {
565 match server_task.await {
566 Ok(server_result) => server_result?,
567 Err(error) => return Err(SyncwebError::operation("daemon IPC task failed", error)),
568 }
569 }
570 if let Some(task) = bridge_task
571 && !task.is_finished()
572 && let Err(error) = task.await
573 {
574 tracing::warn!(%error, "websocket bridge task failed");
575 }
576 if let Some(task) = media_task
577 && !task.is_finished()
578 && let Err(error) = task.await
579 {
580 tracing::warn!(%error, "media server task failed");
581 }
582 Ok(())
583 }
584
585 async fn run_event_loop(
586 &self,
587 server_task: &mut JoinHandle<Result<()>>,
588 signal_task: &mut SignalTask<'_>,
589 shutdown: &mut broadcast::Receiver<()>,
590 shutdown_sender: &broadcast::Sender<()>,
591 interval: &mut tokio::time::Interval,
592 watch_interval: &mut tokio::time::Interval,
593 ) -> Result<()> {
594 loop {
595 tokio::select! {
596 signal_result = &mut *signal_task => {
597 signal_result?;
598 send_shutdown(shutdown_sender);
599 break Ok(());
600 }
601 shutdown_result = shutdown.recv() => {
602 if matches!(shutdown_result, Ok(()) | Err(broadcast::error::RecvError::Closed)) {
603 break Ok(());
604 }
605 }
606 server_result = &mut *server_task => {
607 match server_result {
608 Ok(result) => break result,
609 Err(error) => break Err(SyncwebError::operation("daemon IPC task failed", error)),
610 }
611 }
612 trigger = self.receive_sync_trigger() => {
613 match trigger {
614 Some(namespace) => self.run_trigger(namespace).await?,
615 None => break Ok(()),
616 }
617 }
618 _ = watch_interval.tick() => self.handle_watch_events().await?,
619 _ = interval.tick() => self.run_cycle().await?,
620 }
621 }
622 }
623
624 async fn run_cycle(&self) -> Result<()> {
625 self.reload_if_requested().await?;
626 if let Err(error) = self.load_folders().await {
627 tracing::warn!(%error, "folder load cycle failed — continuing");
628 }
629 self.start_watching().await?;
630 self.refresh_transfer_limits();
631 let live_folders = self.enabled_subscribe_filters();
632 let statuses = self.handle.folder_registry.read().await.statuses();
633 for folder in statuses {
634 let namespace = folder
635 .namespace
636 .parse::<NamespaceId>()
637 .map_err(|error| SyncwebError::operation("invalid managed folder namespace", error))?;
638 let folder_name = (!folder.path.as_os_str().is_empty()).then(|| folder.path.to_string_lossy().into_owned());
639 let Some(filters) = live_folders.get(&namespace) else {
640 if is_active(namespace) && !cancel_session(namespace) {
641 tracing::warn!(%namespace, "live syncing is disabled; intent did not accept cancellation");
642 }
643 continue;
644 };
645 self.apply_folder_schedule(namespace, filters, folder_name.as_deref())
646 .await?;
647 }
648 self.enforce_transfer_limits().await?;
649 self.save_status_report().await?;
650 Ok(())
651 }
652
653 async fn apply_folder_schedule(
655 &self,
656 namespace: NamespaceId,
657 filters: &SubscribeFilters,
658 folder_name: Option<&str>,
659 ) -> Result<()> {
660 let active = {
661 let schedule_manager = self.schedule_manager.read().await;
662 schedule_manager
663 .as_ref()
664 .is_none_or(|manager| manager.is_active(folder_name))
665 };
666 if active {
667 self.start_supervision(namespace, filters).await?;
668 self.set_intent_active(namespace, true)
669 } else if cancel_session(namespace) {
670 tracing::debug!(%namespace, "outside scheduled active window; pausing intent");
671 Ok(())
672 } else {
673 Ok(())
674 }
675 }
676
677 fn refresh_transfer_limits(&self) {
686 if let Ok(config) = self.node_db.load_app_config()
687 && let Ok(limits) = TransferLimits::from_config(&config.transfer_limits)
688 {
689 self.transfer_budget.set_limits(limits);
690 }
691 }
692
693 async fn enforce_transfer_limits(&self) -> Result<()> {
694 let download_limited = !self.transfer_budget.limits().download.is_unlimited();
695 let upload_limited = !self.transfer_budget.limits().upload.is_unlimited();
696 if !download_limited && !upload_limited {
697 return Ok(());
698 }
699
700 if upload_limited && self.transfer_budget.available(Direction::Upload) == 0 {
701 self.pause_sessions_for_upload_limit().await;
702 }
703
704 if !download_limited {
705 return self.restore_deferred_downloads().await;
706 }
707
708 let statuses = self.handle.folder_registry.read().await.statuses();
709 let mut deferred_now = Vec::new();
710 for status in &statuses {
711 if let Ok(namespace) = status.namespace.parse::<NamespaceId>() {
712 self.defer_folder_downloads(namespace, &mut deferred_now).await;
713 }
714 }
715 self.record_deferred_downloads(deferred_now)?;
716 self.drain_transfer_queue().await;
717 Ok(())
718 }
719
720 async fn pause_sessions_for_upload_limit(&self) {
722 let statuses = self.handle.folder_registry.read().await.statuses();
723 for status in &statuses {
724 if let Ok(namespace) = status.namespace.parse::<NamespaceId>()
725 && cancel_session(namespace)
726 {
727 tracing::info!(%namespace, "upload budget exhausted; pausing live session");
728 }
729 }
730 }
731
732 async fn restore_deferred_downloads(&self) -> Result<()> {
734 let to_clear: Vec<NamespaceId> = {
735 let mut deferred = self
736 .deferred_downloads
737 .lock()
738 .map_err(|error| SyncwebError::operation("deferred download mutex is poisoned", error))?;
739 deferred.drain().collect()
740 };
741 for namespace in to_clear {
742 if let Ok(folder) = self.folder_manager.get(namespace).await {
743 let _ = folder.clear_metadata_only().await;
744 }
745 }
746 Ok(())
747 }
748
749 async fn defer_folder_downloads(&self, namespace: NamespaceId, deferred: &mut Vec<NamespaceId>) {
751 let Ok(folder) = self.folder_manager.get(namespace).await else {
752 return;
753 };
754 if let Err(error) = folder.set_metadata_only().await {
755 tracing::debug!(%namespace, %error, "could not defer content downloads for transfer limits");
756 return;
757 }
758 deferred.push(namespace);
759 let Ok(entries) = folder.content_entries().await else {
760 return;
761 };
762 for entry in entries {
763 let hash = entry.content_hash();
764 if folder.has_local(hash).await.unwrap_or(false) {
765 continue;
766 }
767 let path = PathBuf::from(String::from_utf8_lossy(entry.key()).into_owned());
768 self.transfer_queue
769 .enqueue(PendingTransfer::new(namespace, hash, entry.content_len(), path));
770 }
771 }
772
773 fn record_deferred_downloads(&self, deferred_now: Vec<NamespaceId>) -> Result<()> {
774 let mut deferred = self
775 .deferred_downloads
776 .lock()
777 .map_err(|error| SyncwebError::operation("deferred download mutex is poisoned", error))?;
778 deferred.extend(deferred_now);
779 drop(deferred);
780 Ok(())
781 }
782
783 async fn drain_transfer_queue(&self) {
785 while let Some(item) = self.transfer_queue.pop_fitting(&self.transfer_budget) {
786 let peers = self
787 .node
788 .topic_tracker()
789 .find_peers(item.namespace)
790 .await
791 .unwrap_or_default();
792 let mut fetched = false;
793 for peer in peers {
794 if self
795 .node
796 .blob_store()
797 .force_fetch_from_peer(self.node.endpoint(), &peer, item.hash)
798 .await
799 .is_ok()
800 {
801 fetched = true;
802 break;
803 }
804 }
805 if fetched {
806 self.transfer_budget.record(Direction::Download, item.size);
807 let network_id = self
808 .network_manager
809 .read()
810 .await
811 .network_for_folder(&item.namespace)
812 .ok()
813 .flatten();
814 let folder = item.namespace.to_string();
815 if let Err(error) = self.network_logger.record_bandwidth_download(
816 item.size,
817 1,
818 Some(&folder),
819 None,
820 network_id.as_deref(),
821 ) {
822 tracing::warn!(%error, "failed to record queued download");
823 }
824 tracing::debug!(
825 namespace = %item.namespace,
826 hash = %item.hash,
827 size = item.size,
828 queued = self.transfer_queue.len(),
829 "queued transfer fetched within budget"
830 );
831 } else {
832 self.transfer_queue.enqueue(item);
834 break;
835 }
836 }
837 }
838
839 async fn run_trigger(&self, namespace: Option<String>) -> Result<()> {
840 match namespace {
841 Some(value) => self.trigger_namespace(value).await,
842 None => self.run_cycle().await,
843 }
844 }
845
846 async fn trigger_namespace(&self, value: String) -> Result<()> {
847 let parsed_namespace = value
848 .parse::<NamespaceId>()
849 .map_err(|error| SyncwebError::operation("invalid sync namespace", error))?;
850 let tracked = self
851 .handle
852 .folder_registry
853 .read()
854 .await
855 .statuses()
856 .iter()
857 .any(|status| status.namespace == value);
858 if !tracked {
859 tracing::warn!(%parsed_namespace, "not a syncweb folder; nothing to synchronize");
860 return Ok(());
861 }
862 let Some(filters) = self.enabled_subscribe_filters().get(&parsed_namespace).cloned() else {
863 tracing::warn!(
864 %parsed_namespace,
865 "live syncing is disabled for this folder; enable it with `join --subscribe` \
866 or `config set <folder>.subscribe on`"
867 );
868 return Ok(());
869 };
870 if !self.schedule_active_for(parsed_namespace).await {
871 tracing::debug!(%parsed_namespace, "outside scheduled active window; not starting sync");
872 if cancel_session(parsed_namespace) {
873 self.save_status_report().await?;
874 }
875 return Ok(());
876 }
877 self.start_supervision(parsed_namespace, &filters).await?;
878 self.set_intent_active(parsed_namespace, true)?;
879 self.save_status_report().await?;
880 Ok(())
881 }
882
883 async fn schedule_active_for(&self, namespace: NamespaceId) -> bool {
885 let folder_name = self
886 .handle
887 .folder_registry
888 .read()
889 .await
890 .statuses()
891 .into_iter()
892 .find(|status| status.namespace == namespace.to_string())
893 .and_then(|status| {
894 (!status.path.as_os_str().is_empty()).then(|| status.path.to_string_lossy().into_owned())
895 });
896 let schedule_manager = self.schedule_manager.read().await;
897 schedule_manager
898 .as_ref()
899 .is_none_or(|manager| manager.is_active(folder_name.as_deref()))
900 }
901
902 fn enabled_subscribe_filters(&self) -> std::collections::BTreeMap<NamespaceId, SubscribeFilters> {
904 let Ok(config) = self.node_db.load_app_config() else {
905 return std::collections::BTreeMap::new();
906 };
907 config
908 .subscribe
909 .folders
910 .into_iter()
911 .filter(|(_, entry)| entry.enabled)
912 .filter_map(|(namespace, entry)| namespace.parse::<NamespaceId>().ok().map(|id| (id, entry.filters)))
913 .collect()
914 }
915
916 async fn start_supervision(&self, namespace: NamespaceId, filters: &SubscribeFilters) -> Result<()> {
917 self.refresh_transfer_limits();
918 {
919 let mut tasks = self
920 .intent_tasks
921 .lock()
922 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?;
923 tasks.retain(|_, task| task.as_ref().is_none_or(|handle| !handle.is_finished()));
924 if tasks.contains_key(&namespace) || is_active(namespace) {
925 return Ok(());
926 }
927 tasks.insert(namespace, None);
928 }
929 if !self.transfer_budget.limits().download.is_unlimited()
932 && let Ok(folder) = self.folder_manager.get(namespace).await
933 && let Err(error) = folder.set_metadata_only().await
934 {
935 tracing::debug!(%namespace, %error, "could not defer downloads before supervising");
936 }
937 let network_id = {
938 let guard = self.network_manager.read().await;
939 guard.network_for_folder(&namespace)?
940 };
941 let session_id = network_id.as_ref().and_then(|net_id| {
942 self.network_logger
943 .record_sync_start(net_id, &namespace.to_string())
944 .ok()
945 });
946 let sync = self.sync_engine.clone();
947 let supervisor = self.intent_supervisor;
948 let shutdown = self.handle.shutdown_sender.subscribe();
949 let controls = self.intent_controls.clone();
950 let filter = self.filter_engine.read().await.clone();
951 let network_logger = self.network_logger.clone();
952 let params = match SubscribeParams::from_filters(filters) {
953 Ok(params) => params,
954 Err(error) => {
955 tracing::warn!(%namespace, %error, "invalid subscription filters; supervising without them");
956 SubscribeParams::default()
957 }
958 };
959 let (ready_sender, ready_receiver) = oneshot::channel();
960 let accountant = BandwidthAccountant::new(network_logger.clone(), network_id.clone(), namespace.to_string());
961 let task = tokio::spawn(async move {
962 let options =
963 SupervisionOptions::with_ready(controls, filter, ready_sender).with_bandwidth(accountant.clone());
964 let result = supervisor
965 .supervise_with_controls_and_ready(&sync, namespace, params, shutdown, options)
966 .await;
967 if network_id.is_some()
968 && let Some(sid) = session_id
969 {
970 match &result {
971 Ok(supervised) => {
972 let has_error = supervised.last_error.is_some();
973 let bytes = accountant.total();
974 let files = supervised.files_transferred;
975 let errors = u64::from(has_error);
976 let status = if has_error { "failed" } else { "completed" };
977 let _ = network_logger.record_sync_finish(sid, files, bytes, errors, status);
978 }
979 Err(_) => {
980 let _ = network_logger.record_sync_finish(sid, 0, 0, 1, "failed");
981 }
982 }
983 }
984 match &result {
985 Ok(supervised) => {
986 if let Some(error) = &supervised.last_error {
987 tracing::warn!(%namespace, retry_count = supervised.retry_count, %error, "supervised intent stopped");
988 }
989 }
990 Err(error) => tracing::error!(%namespace, %error, "supervised intent failed"),
991 }
992 });
993 self.intent_tasks
994 .lock()
995 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?
996 .insert(namespace, Some(task));
997 ready_receiver
998 .await
999 .map_err(|error| SyncwebError::operation("daemon intent startup notification failed", error))?;
1000 Ok(())
1001 }
1002
1003 fn set_intent_active(&self, namespace: NamespaceId, active: bool) -> Result<()> {
1004 let control = self
1005 .intent_controls
1006 .lock()
1007 .map_err(|error| SyncwebError::operation("daemon intent control mutex is poisoned", error))?
1008 .get(&namespace)
1009 .cloned();
1010 let Some(intent_control) = control else {
1011 return Ok(());
1012 };
1013 let result = if active {
1014 intent_control.resume()
1015 } else {
1016 intent_control.pause()
1017 };
1018 result.map_err(|error| SyncwebError::operation("failed to update scheduled intent", error))
1019 }
1020
1021 pub async fn is_in_active_window(&self) -> bool {
1023 self.schedule_manager
1024 .read()
1025 .await
1026 .as_ref()
1027 .is_none_or(|manager| manager.is_active(None))
1028 }
1029
1030 async fn start_watching(&self) -> Result<()> {
1031 let statuses = self.handle.folder_registry.read().await.statuses();
1032 let wanted: HashMap<_, _> = statuses
1033 .iter()
1034 .filter(|status| !status.path.as_os_str().is_empty())
1035 .map(|status| (status.namespace.clone(), status.path.clone()))
1036 .collect();
1037 let candidates = self.watcher_candidates(&wanted)?;
1038 let mut to_add = Vec::new();
1039 for (namespace, path) in candidates {
1040 if self.watcher_allowed(&namespace, &path).await {
1041 to_add.push((namespace, path));
1042 }
1043 }
1044 self.install_watchers(to_add)
1045 }
1046
1047 fn watcher_candidates(&self, wanted: &HashMap<String, PathBuf>) -> Result<Vec<(String, PathBuf)>> {
1051 let mut watchers = self
1052 .watchers
1053 .lock()
1054 .map_err(|error| SyncwebError::operation("daemon watcher mutex is poisoned", error))?;
1055 watchers.retain(|namespace, _| wanted.contains_key(namespace));
1056 let candidates = wanted
1057 .iter()
1058 .filter(|(namespace, _)| !watchers.contains_key(*namespace))
1059 .map(|(namespace, path)| (namespace.clone(), path.clone()))
1060 .collect();
1061 drop(watchers);
1062 Ok(candidates)
1063 }
1064
1065 async fn watcher_allowed(&self, namespace: &str, path: &Path) -> bool {
1071 let Ok(namespace_id) = namespace.parse::<NamespaceId>() else {
1072 return false;
1073 };
1074 match self.folder_manager.get(namespace_id).await {
1075 Ok(folder) if !folder.is_writable() => {
1076 tracing::debug!(%namespace, "read-only folder; not watching for local changes");
1077 false
1078 }
1079 Ok(_) if path.exists() => true,
1080 Ok(_) => {
1081 tracing::warn!(%namespace, path = %path.display(), "managed folder path does not exist; watcher deferred");
1082 false
1083 }
1084 Err(error) => {
1085 tracing::debug!(%namespace, %error, "folder capability unavailable; watcher deferred");
1086 false
1087 }
1088 }
1089 }
1090
1091 fn install_watchers(&self, to_add: Vec<(String, PathBuf)>) -> Result<()> {
1092 let mut watchers = self
1093 .watchers
1094 .lock()
1095 .map_err(|error| SyncwebError::operation("daemon watcher mutex is poisoned", error))?;
1096 for (namespace, path) in to_add {
1097 if let std::collections::hash_map::Entry::Vacant(slot) = watchers.entry(namespace) {
1098 slot.insert(FsWatcher::new(&path)?);
1099 }
1100 }
1101 Ok(())
1102 }
1103
1104 async fn handle_watch_events(&self) -> Result<()> {
1105 self.start_watching().await?;
1109 let mut observed = Vec::new();
1110 {
1111 let mut watchers = self
1112 .watchers
1113 .lock()
1114 .map_err(|error| SyncwebError::operation("daemon watcher mutex is poisoned", error))?;
1115 for (namespace, watcher) in watchers.iter_mut() {
1116 loop {
1117 match watcher.try_recv() {
1118 Ok(Some(event)) => observed.push((namespace.clone(), event)),
1119 Ok(None) => break,
1120 Err(error) => {
1121 tracing::warn!(%namespace, %error, "filesystem watcher event channel failed");
1122 break;
1123 }
1124 }
1125 }
1126 }
1127 }
1128
1129 if !observed.is_empty() {
1130 let mut pending = self
1131 .pending_watch_events
1132 .lock()
1133 .map_err(|error| SyncwebError::operation("daemon watch queue mutex is poisoned", error))?;
1134 let ready_at = Instant::now()
1135 .checked_add(self.config.watch_debounce)
1136 .unwrap_or_else(Instant::now);
1137 for (namespace, event) in observed {
1138 let removed = matches!(event.event.kind, notify::EventKind::Remove(_));
1139 let entry = pending.entry(namespace).or_insert_with(|| PendingWatch {
1140 paths: HashMap::new(),
1141 ready_at,
1142 });
1143 entry.ready_at = ready_at;
1144 for path in event.paths {
1145 entry.paths.insert(path, removed);
1146 }
1147 }
1148 drop(pending);
1149 }
1150
1151 let ready = {
1152 let mut pending = self
1153 .pending_watch_events
1154 .lock()
1155 .map_err(|error| SyncwebError::operation("daemon watch queue mutex is poisoned", error))?;
1156 let now = Instant::now();
1157 pending
1158 .iter_mut()
1159 .filter_map(|(namespace, batch)| {
1160 (batch.ready_at <= now).then(|| (namespace.clone(), std::mem::take(&mut batch.paths)))
1161 })
1162 .filter(|(_, paths)| !paths.is_empty())
1163 .collect::<Vec<_>>()
1164 };
1165 if ready.is_empty() {
1166 return Ok(());
1167 }
1168
1169 let (roots, watch_configs): (HashMap<_, _>, HashMap<_, _>) = {
1170 let registry = self.handle.folder_registry.read().await;
1171 let statuses = registry.statuses();
1172 let roots = statuses
1173 .iter()
1174 .map(|status| (status.namespace.clone(), status.path.clone()))
1175 .collect();
1176 let watch_configs = statuses
1177 .iter()
1178 .map(|status| (status.namespace.clone(), registry.watch_for(&status.namespace)))
1179 .collect();
1180 drop(registry);
1181 (roots, watch_configs)
1182 };
1183 let live_folders = self.enabled_subscribe_filters();
1184 let global_engine = self.filter_engine.read().await.clone();
1185 for (namespace, paths) in ready {
1186 let Some(root) = roots.get(&namespace) else {
1187 continue;
1188 };
1189 let namespace_id = namespace
1190 .parse::<NamespaceId>()
1191 .map_err(|error| SyncwebError::operation("invalid watched folder namespace", error))?;
1192 let watch = watch_configs.get(&namespace).cloned().unwrap_or_default();
1193 let folder_engine = watch.compiled_filter()?;
1194 let evaluation = WatchEvaluation {
1195 filter: folder_engine.as_ref().or(global_engine.as_ref()),
1196 exclude: &watch.exclude,
1197 };
1198 for (path, removed) in paths {
1199 self.process_watch_event(
1200 namespace_id,
1201 root,
1202 &path,
1203 removed,
1204 live_folders.get(&namespace_id),
1205 evaluation,
1206 )
1207 .await?;
1208 }
1209 }
1210 self.save_status_report().await?;
1211 Ok(())
1212 }
1213
1214 async fn process_watch_event(
1215 &self,
1216 namespace: NamespaceId,
1217 root: &Path,
1218 path: &Path,
1219 removed: bool,
1220 live_filters: Option<&SubscribeFilters>,
1221 evaluation: WatchEvaluation<'_>,
1222 ) -> Result<()> {
1223 let relative = path.strip_prefix(root).unwrap_or(path);
1224 if relative.as_os_str().is_empty() {
1225 return Ok(());
1226 }
1227 let size = std::fs::metadata(path).map_or(0, |metadata| metadata.len());
1228 let accepted = evaluation.filter.is_none_or(|active| {
1229 active.evaluate_for_folder(&namespace.to_string(), &FilterEntry::new(relative.to_path_buf(), size))
1230 != FilterAction::Reject
1231 });
1232 if !accepted {
1233 return Ok(());
1234 }
1235
1236 let folder = self.folder_manager.get(namespace).await?;
1237 if !folder.is_writable() {
1240 tracing::debug!(%namespace, path = %path.display(), "ignoring filesystem change for read-only folder");
1241 return Ok(());
1242 }
1243 let result = if removed || !path.exists() {
1244 folder
1245 .delete_entry(relative.as_os_str().as_encoded_bytes())
1246 .await
1247 .map(|()| 1_u64)
1248 } else if path.is_file() {
1249 let importer = Importer::new(
1250 self.node.blob_store().clone(),
1251 self.node.docs_engine().clone(),
1252 folder.doc().clone(),
1253 folder.author(),
1254 )
1255 .with_root(root)
1256 .with_ignore_patterns(evaluation.exclude.to_vec());
1257 importer
1258 .import_path(path)
1259 .await
1260 .map(|entries| u64::try_from(entries.len()).unwrap_or(u64::MAX))
1261 } else {
1262 Ok(0)
1263 };
1264 match result {
1265 Ok(entries) if entries > 0 => {
1266 self.handle
1267 .folder_registry
1268 .write()
1269 .await
1270 .record_import(namespace, entries, current_timestamp());
1271 if let Some(filters) = live_filters {
1272 self.start_supervision(namespace, filters).await?;
1273 }
1274 }
1275 Ok(_) => {}
1276 Err(error) => {
1277 self.handle
1278 .folder_registry
1279 .write()
1280 .await
1281 .record_error(namespace, error.to_string());
1282 tracing::warn!(%namespace, path = %path.display(), %error, "filesystem change will be retried");
1283 if !removed && is_recoverable_watch_error(&error) {
1284 let mut pending = self.pending_watch_events.lock().map_err(|poisoned| {
1285 SyncwebError::operation("daemon watch queue mutex is poisoned", poisoned)
1286 })?;
1287 let ready_at = Instant::now()
1288 .checked_add(self.config.watch_debounce)
1289 .unwrap_or_else(Instant::now);
1290 let entry = pending.entry(namespace.to_string()).or_insert_with(|| PendingWatch {
1291 paths: HashMap::new(),
1292 ready_at,
1293 });
1294 entry.ready_at = ready_at;
1295 entry.paths.insert(path.to_path_buf(), false);
1296 drop(pending);
1297 }
1298 }
1299 }
1300 Ok(())
1301 }
1302
1303 async fn save_status_report(&self) -> Result<()> {
1304 let state = self.handle.state.read().await.clone();
1305 let statuses = self.handle.folder_registry.read().await.statuses();
1306 let schedule = self.schedule_manager.read().await.clone();
1307 let schedule_report = schedule.as_ref().map(|manager| {
1308 let minute = crate::schedule::current_minute();
1309 let next = manager.next_active_window_start_at(None, minute);
1310 let next_window_start = next.map(|next_minute| {
1311 let offset = if next_minute >= minute {
1312 next_minute.saturating_sub(minute)
1313 } else {
1314 1_440_u16.saturating_sub(minute).saturating_add(next_minute)
1315 };
1316 current_timestamp().saturating_add(u64::from(offset).saturating_mul(60))
1317 });
1318 ScheduleStatus {
1319 in_active_window: manager.is_active(None),
1320 next_window_start,
1321 }
1322 });
1323 let current_stats = self.stats_db.current_stats().unwrap_or_default();
1324 let report = DaemonStatusReport {
1325 pid: state.pid,
1326 node_id: state.node_id,
1327 started_at: state.started_at,
1328 uptime_seconds: current_timestamp().saturating_sub(state.started_at),
1329 folders: statuses.into_iter().collect(),
1330 bandwidth: BandwidthSnapshot {
1331 upload_total: current_stats.total_upload,
1332 download_total: current_stats.total_download,
1333 upload_rate: 0,
1334 download_rate: 0,
1335 },
1336 schedule: schedule_report,
1337 rayon_threads: self.archive_pool.thread_count(),
1338 };
1339 self.node_db.save_status(&report)
1340 }
1341
1342 async fn load_folders(&self) -> Result<()> {
1343 let folders = self.folder_manager.list().await?;
1344 let folder_mounts = self
1345 .node_db
1346 .load_folder_mounts()?
1347 .into_iter()
1348 .collect::<HashMap<_, _>>();
1349 let watch_configs: HashMap<String, WatchConfig> = self
1350 .node_db
1351 .load_folder_watch_configs()?
1352 .into_iter()
1353 .filter_map(|(namespace, encoded)| {
1354 serde_json::from_str::<WatchConfig>(&encoded)
1355 .ok()
1356 .map(|watch| (namespace, watch))
1357 })
1358 .collect();
1359 let mut registry = self.handle.folder_registry.write().await;
1360 for folder in folders {
1361 let namespace_key = folder.namespace_id().to_string();
1362 if registry.is_removed(&namespace_key) {
1363 continue;
1364 }
1365 if !registry
1366 .statuses()
1367 .iter()
1368 .any(|status| status.namespace == namespace_key)
1369 {
1370 let path = folder_mounts.get(&namespace_key).cloned().unwrap_or_default();
1371 let watch = watch_configs.get(&namespace_key).cloned().unwrap_or_default();
1372 registry.add(
1373 FolderEntry::new(folder.namespace_id(), path)
1374 .with_mode(folder.mode().to_string())
1375 .with_watch(watch),
1376 )?;
1377 }
1378 }
1379 drop(registry);
1380 Ok(())
1381 }
1382
1383 async fn reannounce_folders(&self) {
1384 let Ok(folders) = self.folder_manager.list().await else {
1385 return;
1386 };
1387 for folder in &folders {
1388 self.folder_manager.announce_namespace(folder.namespace_id()).await;
1389 }
1390 }
1391
1392 async fn load_subscriptions(&self) -> Result<()> {
1393 let subscriptions = self.node_db.load_subscriptions_with_sizes()?;
1394 if subscriptions.is_empty() {
1395 return Ok(());
1396 }
1397 let hashes: Vec<_> = subscriptions.iter().map(|(hash, _)| *hash).collect();
1398 self.folder_manager.seed_subscriptions(hashes).await;
1399
1400 let mut registry = self.handle.folder_registry.write().await;
1401 for (hash, size) in subscriptions {
1402 let subscription = PublicSubscription::new(hash, None, size);
1403 registry.add_subscription(subscription);
1404 }
1405 drop(registry);
1406 Ok(())
1407 }
1408
1409 async fn reload_if_requested(&self) -> Result<()> {
1410 if !self
1411 .handle
1412 .reload_requested
1413 .swap(false, std::sync::atomic::Ordering::AcqRel)
1414 {
1415 return Ok(());
1416 }
1417 let app_config = self.node_db.load_app_config()?;
1418 let parsed_schedule = ScheduleManager::from_config(&app_config.schedule)?;
1419 let schedule_manager =
1420 (app_config.schedule != crate::schedule::ScheduleConfig::default()).then_some(parsed_schedule);
1421 let filter = load_filter_engine(&self.node_db, &self.config.data_dir)?;
1422 *self.schedule_manager.write().await = schedule_manager;
1423 *self.filter_engine.write().await = filter;
1424 let statuses = self.handle.folder_registry.read().await.statuses();
1425 for status in statuses {
1426 if let Ok(namespace) = status.namespace.parse::<NamespaceId>()
1427 && is_active(namespace)
1428 && !cancel_session(namespace)
1429 {
1430 tracing::warn!(%namespace, "reloaded intent did not accept cancellation");
1431 }
1432 }
1433 tracing::info!("daemon configuration reloaded");
1434 Ok(())
1435 }
1436
1437 async fn receive_sync_trigger(&self) -> Option<Option<String>> {
1438 self.sync_receiver.lock().await.recv().await
1439 }
1440
1441 async fn shutdown_resources(&self) -> Result<()> {
1442 self.handle.set_status(DaemonStatus::Stopping).await;
1443 let stopping_state = self.handle.state.read().await.clone();
1444 self.node_db.save_lifecycle(&stopping_state)?;
1445 self.save_status_report().await?;
1446
1447 self.cancel_active_sessions().await;
1448 self.join_intent_tasks().await?;
1449 self.abort_listeners().await;
1450 self.finalize_node_stop().await
1451 }
1452
1453 async fn cancel_active_sessions(&self) {
1454 let namespaces: Vec<_> = self
1455 .handle
1456 .folder_registry
1457 .read()
1458 .await
1459 .statuses()
1460 .into_iter()
1461 .filter_map(|status| status.namespace.parse::<NamespaceId>().ok())
1462 .collect();
1463 for namespace in namespaces {
1464 if is_active(namespace) && !cancel_session(namespace) {
1465 tracing::warn!(%namespace, "intent did not accept shutdown cancellation");
1466 }
1467 }
1468 }
1469
1470 async fn join_intent_tasks(&self) -> Result<()> {
1471 let tasks = {
1472 let mut task_map = self
1473 .intent_tasks
1474 .lock()
1475 .map_err(|error| SyncwebError::operation("daemon intent task mutex is poisoned", error))?;
1476 std::mem::take(&mut *task_map)
1477 };
1478 for (namespace, task) in tasks {
1479 if let Some(handle) = task {
1480 match tokio::time::timeout(Duration::from_secs(10), handle).await {
1481 Ok(Ok(())) => {}
1482 Ok(Err(error)) => {
1483 tracing::warn!(%namespace, %error, "daemon intent task failed");
1484 }
1485 Err(_) => {
1486 tracing::warn!(%namespace, "daemon intent task did not shut down within 10s timeout");
1487 }
1488 }
1489 }
1490 }
1491 Ok(())
1492 }
1493
1494 async fn abort_listeners(&self) {
1495 let maintenance_handle = self.maintenance_task.lock().await.take();
1496 if let Some(inner) = maintenance_handle {
1497 inner.abort();
1498 }
1499 }
1500
1501 async fn finalize_node_stop(&self) -> Result<()> {
1502 let node_result = tokio::time::timeout(Duration::from_secs(30), self.node.stop()).await;
1503 self.handle.set_status(DaemonStatus::Stopped).await;
1504 let remove_result = self.node_db.remove_lifecycle();
1505 let remove_status_result = self.node_db.remove_status();
1506 let release_result = self.pid_lock.release();
1507 match node_result {
1508 Ok(Ok(())) => {}
1509 Ok(Err(error)) => tracing::warn!(%error, "node stop reported an error"),
1510 Err(_) => tracing::warn!("node stop timed out after 30s"),
1511 }
1512 remove_result?;
1513 remove_status_result?;
1514 release_result?;
1515 Ok(())
1516 }
1517
1518 async fn spawn_maintenance_task(&self) {
1521 let node_db = self.node_db.clone();
1522 let stats_db = self.stats_db.clone();
1523 let mut shutdown = self.handle.shutdown_sender.subscribe();
1524 let handle: tokio::task::JoinHandle<()> = tokio::spawn(async move {
1525 let mut interval = tokio::time::interval(Duration::from_hours(24));
1526 loop {
1527 tokio::select! {
1528 _ = shutdown.recv() => {
1529 tracing::info!("maintenance task shutting down");
1530 break;
1531 }
1532 _ = interval.tick() => {
1533 let dbs: [&dyn crate::storage::Vacuumable; 2] = [&node_db, &stats_db];
1534 for db in dbs {
1535 match db.freelist_count() {
1536 Ok(count) if count > 100 => {
1537 tracing::info!(freelist = %count, "running vacuum");
1538 if let Err(error) = db.vacuum() {
1539 tracing::warn!(%error, "maintenance vacuum failed");
1540 }
1541 }
1542 Ok(_) => {}
1543 Err(error) => tracing::warn!(%error, "maintenance freelist query failed"),
1544 }
1545 }
1546 }
1547 }
1548 }
1549 });
1550 *self.maintenance_task.lock().await = Some(handle);
1551 }
1552
1553 async fn subscribe_channels(&self) {
1554 let Some(ref indexing) = self.indexing else {
1555 tracing::debug!("indexing unavailable; skipping catalog channel subscription");
1556 return;
1557 };
1558 let Some(app_config) = self.load_channel_config() else {
1559 return;
1560 };
1561 let author = match self.node.docs_engine().author().await {
1562 Ok(a) => a,
1563 Err(error) => {
1564 tracing::warn!(%error, "failed to get author for channel subscription");
1565 return;
1566 }
1567 };
1568 let catalog_service = indexing.catalog_service(self.node.docs_engine(), self.node.blob_store(), author);
1569 for (channel_name, channel_config) in &app_config.channels {
1570 self.subscribe_one_channel(indexing, &catalog_service, channel_name, channel_config)
1571 .await;
1572 }
1573 }
1574
1575 fn load_channel_config(&self) -> Option<crate::storage::config::Config> {
1576 let config_path = self.config.data_dir.join("config.toml");
1577 match crate::storage::config::Config::load(&config_path) {
1578 Ok(c) if !c.channels.is_empty() => Some(c),
1579 Ok(_) => None,
1580 Err(error) => {
1581 tracing::warn!(%error, "failed to load config for channel subscription");
1582 None
1583 }
1584 }
1585 }
1586
1587 async fn subscribe_one_channel(
1588 &self,
1589 indexing: &IndexingService,
1590 catalog_service: &crate::indexing::CatalogService,
1591 channel_name: &str,
1592 channel_config: &crate::storage::config::ChannelConfig,
1593 ) {
1594 let Ok(ticket) = channel_config.ticket.parse::<iroh_docs::DocTicket>() else {
1595 tracing::warn!(%channel_name, "invalid channel doc ticket; skipping");
1596 return;
1597 };
1598 let catalog = match catalog_service.subscribe(ticket).await {
1599 Ok(c) => c,
1600 Err(error) => {
1601 tracing::warn!(%channel_name, %error, "failed to subscribe to catalog channel");
1602 return;
1603 }
1604 };
1605 Self::record_channel_namespace(indexing, channel_name, &catalog);
1606 }
1607
1608 fn record_channel_namespace(indexing: &IndexingService, channel_name: &str, catalog: &crate::indexing::Catalog) {
1609 let namespace_id = catalog.namespace_id();
1610 if let Err(error) = indexing.database().set_channel_namespace(channel_name, namespace_id) {
1611 tracing::warn!(%channel_name, %error, "failed to persist channel namespace");
1612 }
1613 tracing::info!(%channel_name, %namespace_id, "subscribed to catalog channel");
1614 }
1615
1616 async fn spawn_membership_listeners(&self) {
1617 let networks: Vec<_> = {
1618 let guard = self.network_manager.read().await;
1619 guard.list().into_iter().cloned().collect::<Vec<_>>()
1620 };
1621 let docs_engine = self.node.docs_engine().clone();
1622 let blob_store = self.node.blob_store().clone();
1623 let shutdown = self.handle.shutdown_sender.subscribe();
1624 let local_pk = self.node.endpoint().id();
1625 let node_db = self.node_db.clone();
1626 for network in networks {
1627 let Some(ref doc_ticket_str) = network.doc_ticket else {
1628 continue;
1629 };
1630 let Ok(doc_ticket) = doc_ticket_str.parse::<iroh_docs::DocTicket>() else {
1631 tracing::warn!(network_id = %network.id, "invalid doc_ticket in network");
1632 continue;
1633 };
1634 let de = docs_engine.clone();
1635 let bs = blob_store.clone();
1636 let mut member_shutdown = shutdown.resubscribe();
1637 let network_id = network.id;
1638 let local_key_str = local_pk.to_string();
1639 let db = node_db.clone();
1640 tokio::spawn(async move {
1641 let doc = match de.import_ticket(doc_ticket).await {
1642 Ok(doc) => doc,
1643 Err(error) => {
1644 tracing::warn!(%network_id, %error, "failed to import membership doc from ticket");
1645 return;
1646 }
1647 };
1648 if let Err(error) = de.start_sync(&doc, Vec::new()).await {
1649 tracing::warn!(%network_id, %error, "failed to start membership doc sync");
1650 }
1651 let mut stream = match de.watch(&doc).await {
1652 Ok(stream) => stream,
1653 Err(error) => {
1654 tracing::warn!(%network_id, %error, "failed to watch membership doc");
1655 return;
1656 }
1657 };
1658 loop {
1659 tokio::select! {
1660 _ = member_shutdown.recv() => {
1661 tracing::debug!(%network_id, "membership listener shutting down");
1662 break;
1663 }
1664 event = stream.next() => {
1665 let Some(event_result) = event else {
1666 break;
1667 };
1668 match event_result {
1669 Ok(iroh_docs::engine::LiveEvent::InsertLocal { entry } | iroh_docs::engine::LiveEvent::InsertRemote { entry, .. }) => {
1670 if entry.key() == NETWORK_MEMBERS_KEY {
1671 let hash = entry.content_hash();
1672 let Ok(content) = bs.get(hash).await else { continue; };
1673 match serde_json::from_slice::<crate::net::membership_doc::SignedMemberList>(&content) {
1674 Ok(member_list) => {
1675 if let Err(error) = member_list.verify() {
1676 tracing::warn!(%network_id, %error, "invalid membership list signature");
1677 continue;
1678 }
1679 let is_member = member_list.members.iter().any(|m| m.key == local_key_str);
1680 if is_member {
1681 tracing::debug!(%network_id, members = member_list.members.len(), "membership updated");
1682 } else {
1683 tracing::warn!(%network_id, "local node was removed from network");
1684 let _ = db.delete_network(network_id);
1685 }
1686 }
1687 Err(error) => {
1688 tracing::warn!(%network_id, %error, "failed to parse membership list");
1689 }
1690 }
1691 }
1692 }
1693 Ok(_) => {}
1694 Err(error) => {
1695 tracing::warn!(%network_id, %error, "membership doc event error");
1696 }
1697 }
1698 }
1699 }
1700 }
1701 });
1702 }
1703 }
1704
1705 async fn handle_signals(&self, shutdown: broadcast::Sender<()>) -> Result<()> {
1706 #[cfg(unix)]
1707 {
1708 let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
1709 tokio::select! {
1710 result = tokio::signal::ctrl_c() => result?,
1711 _ = terminate.recv() => {}
1712 }
1713 }
1714 #[cfg(not(unix))]
1715 {
1716 tokio::signal::ctrl_c().await?;
1717 }
1718 send_shutdown(&shutdown);
1719 Ok(())
1720 }
1721}
1722
1723impl Drop for Daemon {
1724 fn drop(&mut self) {
1725 if let Err(error) = self.node_db.remove_lifecycle() {
1726 tracing::warn!(%error, "failed to remove daemon lifecycle");
1727 }
1728 if let Err(error) = self.node_db.remove_status() {
1729 tracing::warn!(%error, "failed to remove daemon status");
1730 }
1731 }
1732}
1733
1734fn send_shutdown(sender: &broadcast::Sender<()>) {
1735 match sender.send(()) {
1736 Ok(_) | Err(broadcast::error::SendError(())) => {}
1737 }
1738}
1739
1740fn fully_remove(set: &std::sync::Mutex<HashSet<NamespaceId>>, namespace: &NamespaceId) {
1742 let mut guard = set.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
1743 guard.remove(namespace);
1744}
1745
1746fn build_reconciler(
1755 node: Arc<IrohNode>,
1756 manager: FolderManager,
1757 db: NodeDatabase,
1758) -> Arc<dyn Fn(NamespaceId) + Send + Sync> {
1759 const RECONCILE_SETTLE: Duration = Duration::from_millis(750);
1760 let busy = Arc::new(std::sync::Mutex::new(HashSet::<NamespaceId>::new()));
1761 Arc::new(move |namespace| {
1762 let mut busy_guard = match busy.lock() {
1763 Ok(guard) => guard,
1764 Err(poisoned) => poisoned.into_inner(),
1765 };
1766 if !busy_guard.insert(namespace) {
1767 return;
1768 }
1769 drop(busy_guard);
1770 tracing::debug!(%namespace, "folder reconciler triggered");
1771 let node_handle = node.clone();
1772 let manager_handle = manager.clone();
1773 let db_handle = db.clone();
1774 let busy_set = busy.clone();
1775 tokio::spawn(async move {
1776 tokio::time::sleep(RECONCILE_SETTLE).await;
1777 let mounts = match db_handle.load_folder_mounts() {
1778 Ok(mounts) => mounts,
1779 Err(error) => {
1780 tracing::warn!(%namespace, %error, "reconciler could not read folder mounts");
1781 fully_remove(&busy_set, &namespace);
1782 return;
1783 }
1784 };
1785 let Some((_, mount_root)) = mounts
1786 .into_iter()
1787 .find(|(mounted, _)| mounted.parse::<NamespaceId>().is_ok_and(|id| id == namespace))
1788 else {
1789 fully_remove(&busy_set, &namespace);
1790 return;
1791 };
1792 let folder = match manager_handle.get(namespace).await {
1793 Ok(folder) => folder,
1794 Err(error) => {
1795 tracing::warn!(%namespace, %error, "reconciler could not open folder");
1796 fully_remove(&busy_set, &namespace);
1797 return;
1798 }
1799 };
1800 if !folder.is_writable() {
1803 fully_remove(&busy_set, &namespace);
1804 return;
1805 }
1806 match crate::sync::materialize_folder_content(&node_handle, &manager_handle, namespace, &mount_root).await {
1807 Ok(outcome) => {
1808 if outcome.summary.materialized > 0 || outcome.summary.conflicts > 0 {
1809 tracing::info!(
1810 %namespace,
1811 materialized = outcome.summary.materialized,
1812 conflicts = outcome.summary.conflicts,
1813 pending = outcome.summary.pending,
1814 "reconciled folder after live sync"
1815 );
1816 }
1817 if outcome.summary.pending > 0 {
1820 tokio::time::sleep(RECONCILE_SETTLE).await;
1821 let _ = crate::sync::materialize_folder_content(
1822 &node_handle,
1823 &manager_handle,
1824 namespace,
1825 &mount_root,
1826 )
1827 .await;
1828 }
1829 }
1830 Err(error) => {
1831 tracing::warn!(%namespace, %error, "reconciler failed to materialize folder");
1832 }
1833 }
1834 fully_remove(&busy_set, &namespace);
1835 });
1836 })
1837}
1838
1839fn is_recoverable_watch_error(error: &SyncwebError) -> bool {
1840 let message = error.to_string();
1841 message.contains("file changed during import") || message.contains("input path does not exist")
1842}