1use std::sync::{Arc, Mutex, Weak};
4use std::time::Duration;
5
6use anyhow::Context;
7use arti_client::TorClient;
8use arti_client::config::Reconfigure;
9use futures::StreamExt;
10use futures::{FutureExt as _, Stream, select_biased};
11use tor_config::file_watcher::{self, FileEventSender, FileWatcher, FileWatcherBuilder};
12use tor_config::{ConfigurationSource, ConfigurationSources, sources::FoundConfigFiles};
13use tor_rtcompat::Runtime;
14use tor_rtcompat::SpawnExt;
15use tracing::{debug, error, info, instrument, warn};
16
17#[cfg(target_family = "unix")]
18use crate::process::sighup_stream;
19
20#[cfg(not(target_family = "unix"))]
21use futures::stream;
22
23use crate::{ArtiCombinedConfig, ArtiConfig};
24
25const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
27
28#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
36pub(crate) trait ReconfigurableModule: Send + Sync {
37 fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()>;
45}
46
47#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
58#[instrument(level = "trace", skip_all)]
59pub(crate) fn watch_for_config_changes<R: Runtime>(
60 runtime: &R,
61 sources: ConfigurationSources,
62 config: &ArtiConfig,
63 modules: Vec<Weak<dyn ReconfigurableModule>>,
64) -> anyhow::Result<()> {
65 let watch_file = config.application().watch_configuration;
66
67 cfg_if::cfg_if! {
68 if #[cfg(target_family = "unix")] {
69 let sighup_stream = sighup_stream()?;
70 } else {
71 let sighup_stream = stream::pending();
72 }
73 }
74
75 let rt = runtime.clone();
76 let () = runtime
77 .clone()
78 .spawn(async move {
79 let res: anyhow::Result<()> = run_watcher(
80 rt,
81 sources,
82 modules,
83 watch_file,
84 sighup_stream,
85 Some(DEBOUNCE_INTERVAL),
86 )
87 .await;
88
89 match res {
90 Ok(()) => debug!("Config watcher task exiting"),
91 Err(e) => error!("Config watcher task exiting: {}", tor_error::Report(e)),
93 }
94 })
95 .context("failed to spawn task")?;
96
97 Ok(())
98}
99
100#[allow(clippy::cognitive_complexity)] #[instrument(level = "trace", skip_all)]
105async fn run_watcher<R: Runtime>(
106 runtime: R,
107 sources: ConfigurationSources,
108 modules: Vec<Weak<dyn ReconfigurableModule>>,
109 watch_file: bool,
110 mut sighup_stream: impl Stream<Item = ()> + Unpin,
111 debounce_interval: Option<Duration>,
112) -> anyhow::Result<()> {
113 let (tx, mut rx) = file_watcher::channel();
114 let mut watcher = if watch_file {
115 let mut watcher = FileWatcher::builder(runtime.clone());
116 prepare(&mut watcher, &sources)?;
117 Some(watcher.start_watching(tx.clone())?)
118 } else {
119 None
120 };
121
122 debug!("Entering FS event loop");
123
124 loop {
125 select_biased! {
126 event = sighup_stream.next().fuse() => {
127 let Some(()) = event else {
128 break;
129 };
130
131 info!("Received SIGHUP");
132 },
133 event = rx.next().fuse() => {
134 if let Some(debounce_interval) = debounce_interval {
135 runtime.sleep(debounce_interval).await;
136 }
137
138 while let Some(_ignore) = rx.try_recv() {
139 }
146 debug!("Config reload event {:?}: reloading configuration.", event);
147 },
148 }
149
150 watcher =
151 reload_configuration(runtime.clone(), watcher, &sources, &modules, tx.clone()).await?;
152 }
153
154 Ok(())
155}
156
157#[allow(clippy::cognitive_complexity)] #[instrument(level = "trace", skip_all)]
160async fn reload_configuration<R: Runtime>(
161 runtime: R,
162 mut watcher: Option<FileWatcher>,
163 sources: &ConfigurationSources,
164 modules: &[Weak<dyn ReconfigurableModule>],
165 tx: FileEventSender,
166) -> anyhow::Result<Option<FileWatcher>> {
167 let found_files = if watcher.is_some() {
169 let mut new_watcher = FileWatcher::builder(runtime.clone());
170 let found_files = prepare(&mut new_watcher, sources)
171 .context("FS watch: failed to rescan config and re-establish watch")?;
172 let new_watcher = new_watcher
173 .start_watching(tx.clone())
174 .context("FS watch: failed to start watching config")?;
175 watcher = Some(new_watcher);
176 found_files
177 } else {
178 sources
179 .scan()
180 .context("FS watch: failed to rescan config")?
181 };
182
183 match reconfigure(found_files, modules) {
184 Ok(watch) => {
185 info!("Successfully reloaded configuration.");
186 if watch && watcher.is_none() {
187 info!("Starting watching over configuration.");
188 let mut new_watcher = FileWatcher::builder(runtime.clone());
189 let _found_files = prepare(&mut new_watcher, sources)
190 .context("FS watch: failed to rescan config and re-establish watch: {}")?;
191 let new_watcher = new_watcher
192 .start_watching(tx.clone())
193 .context("FS watch: failed to rescan config and re-establish watch: {}")?;
194 watcher = Some(new_watcher);
195 } else if !watch && watcher.is_some() {
196 info!("Stopped watching over configuration.");
197 watcher = None;
198 }
199 }
200 Err(e) => warn!("Couldn't reload configuration: {}", tor_error::Report(e)),
202 }
203
204 Ok(watcher)
205}
206
207pub(crate) struct LaunchableTorClient<R: Runtime> {
209 orig_defer_bootstrap: bool,
211
212 have_launched: Mutex<bool>,
214
215 client: Arc<TorClient<R>>,
217}
218
219impl<R: Runtime> ReconfigurableModule for LaunchableTorClient<R> {
220 #[instrument(level = "trace", skip_all)]
221 fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
222 if new.0.application().defer_bootstrap && !self.orig_defer_bootstrap {
225 warn!("Cannot enable defer_bootstrap while arti is running.");
226 }
227 if !new.0.application().defer_bootstrap {
228 self.ensure_bootstrap_launched()?;
229 }
230
231 TorClient::reconfigure(&self.client, &new.1, Reconfigure::WarnOnFailures)?;
232 Ok(())
233 }
234}
235
236impl<R: Runtime> LaunchableTorClient<R> {
237 pub(crate) fn new(client: Arc<TorClient<R>>, cfg: &crate::ApplicationConfig) -> Self {
241 Self {
242 orig_defer_bootstrap: cfg.defer_bootstrap,
243 have_launched: Mutex::new(!cfg.defer_bootstrap),
244 client,
245 }
246 }
247
248 fn ensure_bootstrap_launched(&self) -> anyhow::Result<()> {
250 let mut have_launched = self.have_launched.lock().expect("lock poisoned");
251
252 if *have_launched {
253 return Ok(());
254 }
255
256 let client = Arc::clone(&self.client);
257 self.client
260 .runtime()
261 .spawn(async move {
262 let _outcome = client.bootstrap().await;
263 })
264 .context("Launching bootstrap")?;
265
266 *have_launched = true;
267 Ok(())
268 }
269
270 pub(crate) async fn bootstrap(&self) -> arti_client::Result<()> {
273 *self.have_launched.lock().expect("lock poisoned") = true;
274
275 self.client.bootstrap().await
276 }
277}
278
279pub(crate) struct Application {
281 original_config: ArtiConfig,
286}
287
288impl Application {
289 pub(crate) fn new(cfg: ArtiConfig) -> Self {
292 Self {
293 original_config: cfg,
294 }
295 }
296}
297
298impl ReconfigurableModule for Application {
299 #[allow(clippy::cognitive_complexity)]
302 #[instrument(level = "trace", skip_all)]
303 fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
304 let original = &self.original_config;
305 let config = &new.0;
306
307 if config.proxy() != original.proxy() {
308 warn!("Can't (yet) reconfigure proxy settings while arti is running.");
309 }
310 if config.logging() != original.logging() {
311 warn!("Can't (yet) reconfigure logging settings while arti is running.");
312 }
313 #[cfg(feature = "rpc")]
314 if config.rpc != original.rpc {
315 warn!("Can't (yet) change RPC settings while arti is running.");
316 }
317 if config.application().permit_debugging && !original.application().permit_debugging {
318 warn!("Cannot disable application hardening when it has already been enabled.");
319 }
320 if !config.application().permit_debugging {
322 #[cfg(feature = "harden")]
323 crate::process::enable_process_hardening()?;
324 }
325
326 Ok(())
327 }
328}
329
330fn prepare<'a, R: Runtime>(
332 watcher: &mut FileWatcherBuilder<R>,
333 sources: &'a ConfigurationSources,
334) -> anyhow::Result<FoundConfigFiles<'a>> {
335 let sources = sources.scan()?;
336 for source in sources.iter() {
337 match source {
338 ConfigurationSource::Dir(dir) => watcher.watch_dir(dir, "toml")?,
339 ConfigurationSource::File(file) => watcher.watch_path(file)?,
340 ConfigurationSource::Verbatim(_) => {}
341 }
342 }
343 Ok(sources)
344}
345
346#[instrument(level = "trace", skip_all)]
354fn reconfigure(
355 found_files: FoundConfigFiles<'_>,
356 reconfigurable: &[Weak<dyn ReconfigurableModule>],
357) -> anyhow::Result<bool> {
358 let _ = reconfigurable;
359 let config = found_files.load()?;
360 let config = tor_config::resolve::<ArtiCombinedConfig>(config)?;
361
362 let reconfigurable = reconfigurable.iter().flat_map(Weak::upgrade);
364 let mut has_modules = false;
366
367 for module in reconfigurable {
368 has_modules = true;
369 module.reconfigure(&config)?;
370 }
371
372 Ok(has_modules && config.0.application().watch_configuration)
373}
374
375#[cfg(test)]
376mod test {
377 #![allow(clippy::bool_assert_comparison)]
379 #![allow(clippy::clone_on_copy)]
380 #![allow(clippy::dbg_macro)]
381 #![allow(clippy::mixed_attributes_style)]
382 #![allow(clippy::print_stderr)]
383 #![allow(clippy::print_stdout)]
384 #![allow(clippy::single_char_pattern)]
385 #![allow(clippy::unwrap_used)]
386 #![allow(clippy::unchecked_time_subtraction)]
387 #![allow(clippy::useless_vec)]
388 #![allow(clippy::needless_pass_by_value)]
389 #![allow(clippy::string_slice)] use crate::ArtiConfigBuilder;
393
394 use super::*;
395 use futures::SinkExt as _;
396 use futures::channel::mpsc;
397 use postage::watch;
398 use std::path::PathBuf;
399 use std::sync::{Arc, Mutex};
400 use test_temp_dir::{TestTempDir, test_temp_dir};
401 use tor_async_utils::PostageWatchSenderExt;
402 use tor_config::sources::MustRead;
403
404 const CONFIG_NAME1: &str = "config1.toml";
406 const CONFIG_NAME2: &str = "config2.toml";
408 const CONFIG_NAME3: &str = "config3.toml";
410
411 struct TestModule {
412 tx: Arc<Mutex<watch::Sender<ArtiCombinedConfig>>>,
414 }
415
416 impl ReconfigurableModule for TestModule {
417 fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
418 let config = new.clone();
419 self.tx.lock().unwrap().maybe_send(|_| config);
420
421 Ok(())
422 }
423 }
424
425 async fn create_module() -> (
429 Arc<dyn ReconfigurableModule>,
430 watch::Receiver<ArtiCombinedConfig>,
431 ) {
432 let (tx, mut rx) = watch::channel();
433 let _: ArtiCombinedConfig = rx.next().await.unwrap();
436
437 (
438 Arc::new(TestModule {
439 tx: Arc::new(Mutex::new(tx)),
440 }),
441 rx,
442 )
443 }
444
445 fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
447 let tmp = dir.as_path_untracked().join("tmp");
448 std::fs::write(&tmp, data).unwrap();
449 let path = dir.as_path_untracked().join(name);
450 std::fs::rename(tmp, &path).unwrap();
452 path
453 }
454
455 fn write_config(dir: &TestTempDir, name: &str, config: &ArtiConfigBuilder) -> PathBuf {
457 let s = toml::to_string(&config).unwrap();
458 write_file(dir, name, s.as_bytes())
459 }
460
461 #[test]
462 fn watch_single_file() {
463 tor_rtcompat::test_with_one_runtime!(|rt| async move {
464 let temp_dir = test_temp_dir!();
465 let mut config_builder = ArtiConfigBuilder::default();
466 config_builder.application().watch_configuration(true);
467
468 let cfg_file = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
469 let mut cfg_sources = ConfigurationSources::new_empty();
470 cfg_sources.push_source(ConfigurationSource::File(cfg_file), MustRead::MustRead);
471
472 let (module, mut rx) = create_module().await;
473
474 config_builder.logging().log_sensitive_information(true);
475 let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
476
477 let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
480 let runtime = rt.clone();
481 let () = rt
482 .spawn(async move {
483 run_watcher(
484 runtime,
485 cfg_sources,
486 vec![Arc::downgrade(&module)],
487 true,
488 sighup_rx,
489 None,
490 )
491 .await
492 .unwrap();
493 })
494 .unwrap();
495
496 sighup_tx.send(()).await.unwrap();
497
498 let config = rx.next().await.unwrap();
500
501 assert_eq!(config.0, config_builder.build().unwrap());
502
503 config_builder.logging().log_sensitive_information(false);
505 let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
506 let config = rx.next().await.unwrap();
508 assert_eq!(config.0, config_builder.build().unwrap());
509 });
510 }
511
512 #[test]
514 #[ignore]
515 fn watch_multiple() {
516 tor_rtcompat::test_with_one_runtime!(|rt| async move {
517 let temp_dir = test_temp_dir!();
518 let mut config_builder1 = ArtiConfigBuilder::default();
519 config_builder1.application().watch_configuration(true);
520
521 let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
522 let mut cfg_sources = ConfigurationSources::new_empty();
523 cfg_sources.push_source(
524 ConfigurationSource::Dir(temp_dir.as_path_untracked().to_path_buf()),
525 MustRead::MustRead,
526 );
527
528 let (module, mut rx) = create_module().await;
529 let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
532 let runtime = rt.clone();
533 let () = rt
534 .spawn(async move {
535 run_watcher(
536 runtime,
537 cfg_sources,
538 vec![Arc::downgrade(&module)],
539 true,
540 sighup_rx,
541 None,
542 )
543 .await
544 .unwrap();
545 })
546 .unwrap();
547
548 config_builder1.logging().log_sensitive_information(true);
549 let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
550 sighup_tx.send(()).await.unwrap();
551 let config = rx.next().await.unwrap();
553 assert_eq!(config.0, config_builder1.build().unwrap());
554
555 let mut config_builder2 = ArtiConfigBuilder::default();
556 config_builder2.application().watch_configuration(true);
557 config_builder2.system().max_files(0_u64);
559 let _: PathBuf = write_config(&temp_dir, CONFIG_NAME2, &config_builder2);
560 let mut config_builder_combined = config_builder1.clone();
562 config_builder_combined.system().max_files(0_u64);
563 let config = rx.next().await.unwrap();
564 assert_eq!(config.0, config_builder_combined.build().unwrap());
565 config_builder2.logging().console("foo".to_string());
567 let mut config_builder_combined2 = config_builder_combined.clone();
568 config_builder_combined2
569 .logging()
570 .console("foo".to_string());
571 let config3: PathBuf = write_config(&temp_dir, CONFIG_NAME3, &config_builder2);
572 let config = rx.next().await.unwrap();
573 assert_eq!(config.0, config_builder_combined2.build().unwrap());
574
575 std::fs::remove_file(config3).unwrap();
577 let config = rx.next().await.unwrap();
578 assert_eq!(config.0, config_builder_combined.build().unwrap());
579 });
580 }
581}