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#[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 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
93pub 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 #[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 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 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 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 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 pub async fn state(&self) -> DaemonState {
412 self.handle.state.read().await.clone()
413 }
414
415 #[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 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 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 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 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}