1
//! Code to watch configuration files for any changes.
2

            
3
use std::collections::HashSet;
4
use std::sync::{Arc, Mutex, Weak};
5
use std::time::Duration;
6

            
7
use anyhow::Context;
8
use arti_client::TorClient;
9
use arti_client::config::Reconfigure;
10
use futures::StreamExt;
11
use futures::stream::BoxStream;
12
use futures::{FutureExt as _, Stream, select_biased};
13
use tor_basic_utils::error_sources::ErrorSources;
14
use tor_config::ConfigurationTree;
15
use tor_config::ReconfigureError;
16
use tor_config::file_watcher::{
17
    self, FileEventReceiver, FileEventSender, FileWatcher, FileWatcherBuilder,
18
};
19
use tor_config::load::{ConfigResolveOptions, DisfavouredKey};
20
use tor_config::{ConfigurationSource, ConfigurationSources, sources::FoundConfigFiles};
21
use tor_error::into_internal;
22
use tor_error::warn_report;
23
use tor_rtcompat::Runtime;
24
use tor_rtcompat::SpawnExt;
25
use tracing::{debug, error, info, instrument, warn};
26

            
27
#[cfg(target_family = "unix")]
28
use crate::process::sighup_stream;
29

            
30
#[cfg(not(target_family = "unix"))]
31
use futures::stream;
32

            
33
use crate::{ArtiCombinedConfig, ArtiConfig};
34

            
35
/// How long to wait after an event got received, before we try to process it.
36
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
37

            
38
/// An object that can be reconfigured when our configuration changes.
39
///
40
/// We use this trait so that we can represent abstract modules in our
41
/// application, and pass the configuration to each of them.
42
//
43
// TODO: It is very likely we will want to refactor this even further once we
44
// have a notion of what our modules truly are.
45
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
46
pub(crate) trait ReconfigurableModule: Send + Sync {
47
    /// Try to reconfigure this module according to a newly loaded configuration.
48
    ///
49
    /// See [`Reconfigure`] for a description of error-handling behavior.
50
    fn reconfigure(
51
        &self,
52
        new: &ArtiCombinedConfig,
53
        how: Reconfigure,
54
    ) -> Result<(), ReconfigureError>;
55
}
56

            
57
/// Structure to reload configuration as necessary.
58
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
59
pub(crate) struct CfgMgr<R> {
60
    /// A runtime that we use when constructing [`FileWatcher`]s.
61
    runtime: R,
62

            
63
    /// The sources from which we read our configuration.
64
    sources: ConfigurationSources,
65

            
66
    /// A sender to use when constructing new [`FileWatcher`]s.
67
    tx: FileEventSender,
68

            
69
    /// Mutable state.
70
    inner: Mutex<CfgMgrInner>,
71
}
72

            
73
/// Mutable part of a CfgMgr.
74
#[derive(Default)]
75
struct CfgMgrInner {
76
    /// A list of modules to alert whenever the configuration has changed.
77
    modules: Vec<Weak<dyn ReconfigurableModule>>,
78

            
79
    /// If present, a [`FileWatcher`] that is currently watching for changes
80
    /// in the configuration files and directories.
81
    watcher: Option<FileWatcher>,
82

            
83
    /// RPC only: a fully populated, normalized configuration tree, based on the most recent time
84
    /// that we called [`CfgMgr::reload_configuration`].
85
    #[cfg(feature = "rpc")]
86
    normalized_cfg: ConfigurationTree,
87

            
88
    /// RPC only: a set of unrecognized options from the configuration.
89
    #[cfg(feature = "rpc")]
90
    unrecognized_keys: HashSet<DisfavouredKey>,
91

            
92
    /// RPC only: a set of deprecated options from the configuration
93
    #[cfg(feature = "rpc")]
94
    deprecated_keys: HashSet<DisfavouredKey>,
95
}
96

            
97
/// A watcher process that we have not yet launched.
98
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
99
#[must_use = "UnlaunchedWatcher does nothing unless you launch it."]
100
pub(crate) struct UnlaunchedWatcher<R> {
101
    /// The related [`CfgMgr`] that we should tell about reconfiguration events.
102
    weak_mgr: Weak<CfgMgr<R>>,
103

            
104
    /// A stream on which we will get alerts about SIGHUP events.
105
    sighup_stream: BoxStream<'static, ()>,
106

            
107
    /// A stream that will tell us when our files are changed.
108
    watcher_rx: FileEventReceiver,
109

            
110
    /// An interval that we wait to debounce events from watcher_rx or sighup_stream.
111
    debounce_interval: Option<Duration>,
112

            
113
    /// If true, we start watching for file changes immediately at launch.
114
    watch_files_at_start: bool,
115
}
116

            
117
impl<R: Runtime> CfgMgr<R> {
118
    /// Construct a new CfgMgr, and launch a task to watch for any events
119
    /// that mean we have to reload our configuration.
120
    ///
121
    /// If the provided configuration requires it, watch for changes in `sources`
122
    /// and try to reload our configuration. On unix platforms, also watch
123
    /// for SIGHUP and reload configuration then.
124
    ///
125
    /// The modules are `Weak` references to prevent this background task
126
    /// from keeping them alive.
127
    ///
128
    /// See the [`FileWatcher`](FileWatcher#Limitations) docs for limitations.
129
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
130
    #[instrument(level = "trace", skip_all)]
131
    pub(crate) fn new(
132
        runtime: R,
133
        sources: ConfigurationSources,
134
        config: &ArtiConfig,
135
        modules: Vec<Weak<dyn ReconfigurableModule>>,
136
    ) -> anyhow::Result<(Arc<Self>, UnlaunchedWatcher<R>)> {
137
        let (tx, rx) = file_watcher::channel();
138
        let mgr = Arc::new(CfgMgr {
139
            runtime,
140
            sources,
141
            tx,
142
            inner: Mutex::new(CfgMgrInner {
143
                modules,
144
                ..Default::default()
145
            }),
146
        });
147

            
148
        cfg_if::cfg_if! {
149
            if #[cfg(target_family = "unix")] {
150
                let sighup_stream = sighup_stream()?;
151
            } else {
152
                let sighup_stream = stream::pending();
153
            }
154
        }
155
        let sighup_stream = sighup_stream.boxed();
156

            
157
        let watcher = UnlaunchedWatcher {
158
            weak_mgr: Arc::downgrade(&mgr),
159
            sighup_stream,
160
            watcher_rx: rx,
161
            debounce_interval: Some(DEBOUNCE_INTERVAL),
162
            watch_files_at_start: config.application().watch_configuration,
163
        };
164

            
165
        Ok((mgr, watcher))
166
    }
167

            
168
    /// Create a new [`FileWatcher`] for the files in this configuration.
169
    ///
170
    /// Return it, along with the set of files we found.
171
    ///
172
    /// The caller is responsible for storing the `FileWatcher`; when it is dropped,
173
    /// it stops watching.
174
8
    fn launch_file_watcher(&self) -> anyhow::Result<(FileWatcher, FoundConfigFiles<'_>)> {
175
8
        let mut watcher = FileWatcher::builder(self.runtime.clone());
176
8
        let found_files = prepare(&mut watcher, &self.sources)?;
177
8
        let watcher = watcher.start_watching(self.tx.clone())?;
178
8
        Ok((watcher, found_files))
179
8
    }
180

            
181
    /// Reload the configuration.
182
    #[instrument(level = "trace", skip_all)]
183
6
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
184
6
    pub(crate) fn reload_configuration(&self, how: Reconfigure) -> anyhow::Result<()> {
185
6
        let mut inner = self.inner.lock().expect("Lock poisoned");
186

            
187
        // Question: I do not understand why we are making a new file watcher unconditionally
188
        // at this point. -nm
189
6
        let (found_files, new_watcher) = if inner.watcher.is_some() {
190
6
            let (watcher, files) = self
191
6
                .launch_file_watcher()
192
6
                .context("Failed to re-scan config")?;
193
6
            (files, Some(watcher))
194
        } else {
195
            let files = self
196
                .sources
197
                .scan()
198
                .context("FS watch: failed to rescan config")?;
199
            (files, None)
200
        };
201

            
202
6
        let config = found_files.load()?;
203

            
204
6
        match reconfigure(config, &mut inner, how) {
205
6
            Ok(watch) => {
206
6
                info!("Successfully reloaded configuration.");
207
6
                if how != Reconfigure::CheckAllOrNothing {
208
6
                    if watch && inner.watcher.is_none() {
209
                        info!("Starting watching over configuration.");
210
                        let (watcher, _files) = self
211
                            .launch_file_watcher()
212
                            .context("Starting to watch over config")?;
213
                        inner.watcher = Some(watcher);
214
6
                    } else if !watch && inner.watcher.is_some() {
215
                        info!("Stopped watching over configuration.");
216
                        inner.watcher = None;
217
6
                    } else {
218
6
                        inner.watcher = new_watcher;
219
6
                    }
220
                }
221
            }
222
            Err(e) => warn_report!(e, "Couldn't reload configuration"),
223
        }
224

            
225
6
        Ok(())
226
6
    }
227
}
228

            
229
impl<R: Runtime> UnlaunchedWatcher<R> {
230
    /// Begin running the file watcher task for a given configuration manager.
231
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
232
    #[instrument(level = "trace", skip_all)]
233
    pub(crate) fn launch(self) -> anyhow::Result<()> {
234
        let UnlaunchedWatcher {
235
            weak_mgr,
236
            sighup_stream,
237
            watcher_rx,
238
            debounce_interval,
239
            watch_files_at_start,
240
        } = self;
241
        let Some(mgr) = weak_mgr.upgrade() else {
242
            return Err(anyhow::anyhow!(
243
                "CfgMgr disappeared before we could launch the monitor task"
244
            ));
245
        };
246

            
247
        let rt = mgr.runtime.clone();
248
        let weak_mgr = Arc::downgrade(&mgr);
249
        mgr.runtime
250
            .spawn(async move {
251
                let res: anyhow::Result<()> =
252
                    run_watcher(rt, watcher_rx, sighup_stream, weak_mgr, debounce_interval).await;
253
                match res {
254
                    Ok(()) => debug!("Config watcher task exiting"),
255
                    // TODO: warn_report does not work on anyhow::Error.
256
                    Err(e) => error!("Config watcher task exiting: {}", tor_error::Report(e)),
257
                }
258
            })
259
            .context("failed to spawn task")?;
260

            
261
        if watch_files_at_start {
262
            // Note: You might think that there was a race condition here, where launching the
263
            // watcher _now_ would fail to catch any file changes that had happened between
264
            // reading the configuration initially and now.
265
            //
266
            // You'd be right, except that the [`FileWatcher`] code starts every new FileWatcher
267
            // with a pending `rescan` event.
268
            let (watcher, _files) = mgr.launch_file_watcher()?;
269
            mgr.inner.lock().expect("lock poisoned").watcher = Some(watcher);
270
        }
271

            
272
        Ok(())
273
    }
274

            
275
    /// Add `module` to the set of modules that need to be reconfigured when the configuration changes.
276
    ///
277
    /// This method is on the [`UnlaunchedWatcher`] because is not (yet) meant to be called after
278
    /// the watcher task is launched.
279
    #[cfg_attr(feature = "experimental-api", visibility::make(pub))]
280
    pub(crate) fn add_module(&self, module: &Arc<dyn ReconfigurableModule>) -> anyhow::Result<()> {
281
        let weak_module = Arc::downgrade(module);
282

            
283
        let Some(mgr) = self.weak_mgr.upgrade() else {
284
            return Err(anyhow::anyhow!(
285
                "CfgMgr disappeared before launching watcher task."
286
            ));
287
        };
288

            
289
        let mut inner = mgr.inner.lock().expect("poisoned lock");
290
        inner.modules.push(weak_module);
291
        Ok(())
292
    }
293
}
294

            
295
/// Start watching for configuration changes.
296
///
297
/// Spawned from [`UnlaunchedWatcher::launch`].
298
#[instrument(level = "trace", skip_all)]
299
2
async fn run_watcher<R: Runtime>(
300
2
    runtime: R,
301
2
    mut rx: FileEventReceiver,
302
2
    mut sighup_stream: impl Stream<Item = ()> + Unpin,
303
2
    weak_mgr: Weak<CfgMgr<R>>,
304
2
    debounce_interval: Option<Duration>,
305
2
) -> anyhow::Result<()> {
306
    debug!("Entering FS event loop");
307

            
308
    loop {
309
        select_biased! {
310
            event = sighup_stream.next().fuse() => {
311
                let Some(()) = event else {
312
                    break;
313
                };
314

            
315
                info!("Received SIGHUP");
316
            },
317
            event = rx.next().fuse() => {
318
2
                if let Some(debounce_interval) = debounce_interval {
319
                    runtime.sleep(debounce_interval).await;
320
                }
321

            
322
                while let Some(_ignore) = rx.try_recv() {
323
                    // Discard other events, so that we only reload once.
324
                    //
325
                    // We can afford to treat both error cases from try_recv [Empty
326
                    // and Disconnected] as meaning that we've discarded other
327
                    // events: if we're disconnected, we'll notice it when we next
328
                    // call recv() in the outer loop.
329
                }
330
                debug!("Config reload event {:?}: reloading configuration.", event);
331
            },
332
        }
333

            
334
        if let Some(mgr) = weak_mgr.upgrade() {
335
            mgr.reload_configuration(Reconfigure::WarnOnFailures)?;
336
            drop(mgr);
337
        } else {
338
            debug!("Configuration mgr disappeared; exiting loop");
339
            break;
340
        }
341
    }
342

            
343
    Ok(())
344
2
}
345

            
346
/// A TorClient that we may or may not have told to start bootstrapping.
347
pub(crate) struct LaunchableTorClient<R: Runtime> {
348
    /// Original value of defer_bootstrap.
349
    orig_defer_bootstrap: bool,
350

            
351
    /// True if we have launched bootstrapping on the the client.
352
    have_launched: Mutex<bool>,
353

            
354
    /// The client itself.
355
    client: Arc<TorClient<R>>,
356
}
357

            
358
impl<R: Runtime> ReconfigurableModule for LaunchableTorClient<R> {
359
    #[instrument(level = "trace", skip_all)]
360
    fn reconfigure(
361
        &self,
362
        new: &ArtiCombinedConfig,
363
        how: Reconfigure,
364
    ) -> Result<(), ReconfigureError> {
365
        if how == Reconfigure::AllOrNothing {
366
            // If we're in all-or-nothing mode, we check it first.
367
            self.reconfigure(new, Reconfigure::CheckAllOrNothing)?;
368
        }
369
        let dry_run = how == Reconfigure::CheckAllOrNothing;
370

            
371
        if new.0.application().defer_bootstrap && !self.orig_defer_bootstrap {
372
            how.cannot_change_specific("defer_bootstrap", "from off to on")?;
373
        }
374
        if !dry_run && !new.0.application().defer_bootstrap {
375
            self.ensure_bootstrap_launched()
376
                .map_err(into_internal!("Unable to launch client bootstrap"))?;
377
        }
378

            
379
        TorClient::reconfigure(&self.client, &new.1, how).map_err(extract_reconfigure_error)?;
380
        Ok(())
381
    }
382
}
383

            
384
/// If possible, extract the ReconfigureError from `err`.  Otherwise,
385
/// return `err` as an internal ReconfigureError.
386
//
387
// (We could get rid of this function if arti_client::Error were not opaque,
388
// or if arti_client::reconfigure were to return a ReconfigureError.
389
// But  now is not the time to revisit those decisions.)
390
fn extract_reconfigure_error(err: arti_client::Error) -> ReconfigureError {
391
    for e in ErrorSources::new(&err) {
392
        if let Some(reconfig_error) = e.downcast_ref::<ReconfigureError>() {
393
            return reconfig_error.clone();
394
        };
395
    }
396
    (into_internal!("Failure while reconfiguring")(err)).into()
397
}
398

            
399
impl<R: Runtime> LaunchableTorClient<R> {
400
    /// Create a new LaunchableTorClient.
401
    ///
402
    /// We assume that it has (or has not) been told to bootstrap itself based on `cfg`.
403
    pub(crate) fn new(client: Arc<TorClient<R>>, cfg: &crate::ApplicationConfig) -> Self {
404
        Self {
405
            orig_defer_bootstrap: cfg.defer_bootstrap,
406
            have_launched: Mutex::new(!cfg.defer_bootstrap),
407
            client,
408
        }
409
    }
410

            
411
    /// If we have not already told this LaunchableTorClient to bootstrap itself, do so.
412
    fn ensure_bootstrap_launched(&self) -> Result<(), futures::task::SpawnError> {
413
        let mut have_launched = self.have_launched.lock().expect("lock poisoned");
414

            
415
        if *have_launched {
416
            return Ok(());
417
        }
418

            
419
        let client = Arc::clone(&self.client);
420
        // We spawn this as a new task since `bootstrap` is very much async,
421
        // but this needs to be called from `reconfigure`, which is not.
422
        self.client.runtime().spawn(async move {
423
            let _outcome = client.bootstrap().await;
424
        })?;
425

            
426
        *have_launched = true;
427
        Ok(())
428
    }
429

            
430
    /// As [`TorClient::bootstrap`], but performs necessary bookkeeping to remember
431
    /// that we have launched a bootstrap attempt.
432
    pub(crate) async fn bootstrap(&self) -> arti_client::Result<()> {
433
        *self.have_launched.lock().expect("lock poisoned") = true;
434

            
435
        self.client.bootstrap().await
436
    }
437
}
438

            
439
/// Internal type to represent the Arti application as a `ReconfigurableModule`.
440
pub(crate) struct Application {
441
    /// The configuration that Arti had at startup.
442
    ///
443
    /// We use this to check whether the user is asking for any impermissible
444
    /// transitions.
445
    original_config: ArtiConfig,
446
}
447

            
448
impl Application {
449
    /// Construct a new `Application` to receive configuration changes for the
450
    /// arti application.
451
    pub(crate) fn new(cfg: ArtiConfig) -> Self {
452
        Self {
453
            original_config: cfg,
454
        }
455
    }
456
}
457

            
458
impl ReconfigurableModule for Application {
459
    #[instrument(level = "trace", skip_all)]
460
    fn reconfigure(
461
        &self,
462
        new: &ArtiCombinedConfig,
463
        how: Reconfigure,
464
    ) -> Result<(), ReconfigureError> {
465
        if how == Reconfigure::AllOrNothing {
466
            // If we're in all-or-nothing mode, we check it first.
467
            self.reconfigure(new, Reconfigure::CheckAllOrNothing)?;
468
        }
469
        let dry_run = how == Reconfigure::CheckAllOrNothing;
470

            
471
        let original = &self.original_config;
472
        let config = &new.0;
473

            
474
        if config.proxy() != original.proxy() {
475
            how.cannot_change("proxy settings")?;
476
        }
477
        if config.logging() != original.logging() {
478
            how.cannot_change("logging")?;
479
        }
480
        #[cfg(feature = "rpc")]
481
        if config.rpc != original.rpc {
482
            how.cannot_change("RPC settings")?;
483
        }
484
        if config.application().permit_debugging && !original.application().permit_debugging {
485
            how.cannot_change_specific("application hardening", "from on to off")?;
486
        }
487
        // Note that this is the only config transition we actually perform so far.
488
        if !dry_run && !config.application().permit_debugging {
489
            #[cfg(feature = "harden")]
490
            crate::process::enable_process_hardening()
491
                .map_err(into_internal!("can't disable debugging"))?;
492
        }
493

            
494
        Ok(())
495
    }
496
}
497

            
498
/// Find the configuration files and prepare the watcher
499
8
fn prepare<'a, R: Runtime>(
500
8
    watcher: &mut FileWatcherBuilder<R>,
501
8
    sources: &'a ConfigurationSources,
502
8
) -> anyhow::Result<FoundConfigFiles<'a>> {
503
8
    let sources = sources.scan()?;
504
8
    for source in sources.iter() {
505
8
        match source {
506
            ConfigurationSource::Dir(dir) => watcher.watch_dir(dir, "toml")?,
507
8
            ConfigurationSource::File(file) => watcher.watch_path(file)?,
508
            ConfigurationSource::Verbatim(_) => {}
509
        }
510
    }
511
8
    Ok(sources)
512
8
}
513

            
514
/// Reload the configuration files, apply the runtime configuration, and
515
/// reconfigure the client as much as we can.
516
///
517
/// Return true if we should be watching for configuration changes.
518
#[instrument(level = "trace", skip_all)]
519
6
fn reconfigure(
520
6
    config: ConfigurationTree,
521
6
    mgr_inner: &mut CfgMgrInner,
522
6
    how: Reconfigure,
523
6
) -> Result<bool, ChangeConfigurationError> {
524
    #[allow(unused_mut)]
525
6
    let mut resolve_options = ConfigResolveOptions::default();
526
    #[cfg(feature = "rpc")]
527
6
    {
528
6
        resolve_options.want_output_tree = true;
529
6
    }
530

            
531
6
    let rs = tor_config::resolve_return_results::<ArtiCombinedConfig>(config, &resolve_options)?;
532
6
    let config = rs.value;
533

            
534
    // Filter out the modules that have been dropped
535
6
    let reconfigurable: Vec<_> = mgr_inner.modules.iter().flat_map(Weak::upgrade).collect();
536
6
    let has_modules = !reconfigurable.is_empty();
537

            
538
6
    if how == Reconfigure::AllOrNothing {
539
        for module in &reconfigurable {
540
            module.reconfigure(&config, Reconfigure::CheckAllOrNothing)?;
541
        }
542
6
    }
543
6
    for module in &reconfigurable {
544
6
        module.reconfigure(&config, how)?;
545
    }
546

            
547
    #[cfg(feature = "rpc")]
548
6
    {
549
6
        mgr_inner.normalized_cfg = rs
550
6
            .output_tree
551
6
            .expect("normalized cfg not exposed as expected!?");
552
6
        mgr_inner.deprecated_keys = rs.deprecated.into_iter().collect();
553
6
        mgr_inner.unrecognized_keys = rs.unrecognized.into_iter().collect();
554
6
    }
555

            
556
6
    Ok(has_modules && config.0.application().watch_configuration)
557
6
}
558

            
559
/// An error that occurred while trying to reload and/or replace our configuration
560
#[derive(thiserror::Error, Clone, Debug)]
561
pub(crate) enum ChangeConfigurationError {
562
    /// We couldn't turn the configuration tree into the appropriate set of data structures.
563
    #[error("Invalid configuration")]
564
    Resolve(#[from] tor_config::load::ConfigResolveError),
565

            
566
    /// One of the transitions we tried to make was not allowed, or failed as we tried to apply it.
567
    #[error("Configuration transition failed")]
568
    Transition(#[from] ReconfigureError),
569
}
570

            
571
#[cfg(test)]
572
mod test {
573
    // @@ begin test lint list maintained by maint/add_warning @@
574
    #![allow(clippy::bool_assert_comparison)]
575
    #![allow(clippy::clone_on_copy)]
576
    #![allow(clippy::dbg_macro)]
577
    #![allow(clippy::mixed_attributes_style)]
578
    #![allow(clippy::print_stderr)]
579
    #![allow(clippy::print_stdout)]
580
    #![allow(clippy::single_char_pattern)]
581
    #![allow(clippy::unwrap_used)]
582
    #![allow(clippy::unchecked_time_subtraction)]
583
    #![allow(clippy::useless_vec)]
584
    #![allow(clippy::needless_pass_by_value)]
585
    #![allow(clippy::string_slice)] // See arti#2571
586
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
587

            
588
    use crate::ArtiConfigBuilder;
589

            
590
    use super::*;
591
    use futures::SinkExt as _;
592
    use futures::channel::mpsc;
593
    use postage::watch;
594
    use std::path::PathBuf;
595
    use std::sync::{Arc, Mutex};
596
    use test_temp_dir::{TestTempDir, test_temp_dir};
597
    use tor_async_utils::PostageWatchSenderExt;
598
    use tor_config::sources::MustRead;
599

            
600
    /// Filename for config1
601
    const CONFIG_NAME1: &str = "config1.toml";
602
    /// Filename for config2
603
    const CONFIG_NAME2: &str = "config2.toml";
604
    /// Filename for config3
605
    const CONFIG_NAME3: &str = "config3.toml";
606

            
607
    struct TestModule {
608
        // A sender for sending the new config to the test function
609
        tx: Arc<Mutex<watch::Sender<ArtiCombinedConfig>>>,
610
    }
611

            
612
    impl ReconfigurableModule for TestModule {
613
        fn reconfigure(
614
            &self,
615
            new: &ArtiCombinedConfig,
616
            _how: Reconfigure,
617
        ) -> Result<(), ReconfigureError> {
618
            let config = new.clone();
619
            self.tx.lock().unwrap().maybe_send(|_| config);
620

            
621
            Ok(())
622
        }
623
    }
624

            
625
    /// Create a test reconfigurable module.
626
    ///
627
    /// Returns the module and a channel on which the new configs received by the module are sent.
628
    async fn create_module() -> (
629
        Arc<dyn ReconfigurableModule>,
630
        watch::Receiver<ArtiCombinedConfig>,
631
    ) {
632
        let (tx, mut rx) = watch::channel();
633
        // Read the initial value from the postage::watch stream
634
        // (the first observed value on this test stream is always the default config)
635
        let _: ArtiCombinedConfig = rx.next().await.unwrap();
636

            
637
        (
638
            Arc::new(TestModule {
639
                tx: Arc::new(Mutex::new(tx)),
640
            }),
641
            rx,
642
        )
643
    }
644

            
645
    /// Write `data` to file `name` within `dir`.
646
    fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
647
        let tmp = dir.as_path_untracked().join("tmp");
648
        std::fs::write(&tmp, data).unwrap();
649
        let path = dir.as_path_untracked().join(name);
650
        // Atomically write the config file
651
        std::fs::rename(tmp, &path).unwrap();
652
        path
653
    }
654

            
655
    /// Write an `ArtiConfigBuilder` to a file within `dir`.
656
    fn write_config(dir: &TestTempDir, name: &str, config: &ArtiConfigBuilder) -> PathBuf {
657
        let s = toml::to_string(&config).unwrap();
658
        write_file(dir, name, s.as_bytes())
659
    }
660

            
661
    #[test]
662
    fn watch_single_file() {
663
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
664
            let temp_dir = test_temp_dir!();
665
            let mut config_builder = ArtiConfigBuilder::default();
666
            config_builder.application().watch_configuration(true);
667

            
668
            let cfg_file = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
669
            let mut cfg_sources = ConfigurationSources::new_empty();
670
            cfg_sources.push_source(ConfigurationSource::File(cfg_file), MustRead::MustRead);
671

            
672
            let (module, mut rx) = create_module().await;
673

            
674
            config_builder.logging().log_sensitive_information(true);
675
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
676

            
677
            let (fw_tx, fw_rx) = file_watcher::channel();
678
            let mgr = Arc::new(CfgMgr {
679
                runtime: rt.clone(),
680
                sources: cfg_sources,
681
                tx: fw_tx,
682
                inner: Mutex::new(CfgMgrInner {
683
                    modules: vec![Arc::downgrade(&module)],
684
                    ..Default::default()
685
                }),
686
            });
687

            
688
            let (watcher, _) = mgr.launch_file_watcher().unwrap();
689
            mgr.inner.lock().unwrap().watcher = Some(watcher);
690
            let weak_mgr = Arc::downgrade(&mgr);
691

            
692
            // Use a fake sighup stream to wait until run_watcher()'s select_biased!
693
            // loop is entered
694
            let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
695
            let runtime = rt.clone();
696
            let () = rt
697
                .spawn(async move {
698
                    run_watcher(runtime.clone(), fw_rx, sighup_rx, weak_mgr, None)
699
                        .await
700
                        .unwrap();
701
                })
702
                .unwrap();
703

            
704
            sighup_tx.send(()).await.unwrap();
705

            
706
            // The reconfigurable modules should've been reloaded in response to sighup
707
            let config = rx.next().await.unwrap();
708
            assert_eq!(config.0, config_builder.build().unwrap());
709

            
710
            // Overwrite the config
711
            config_builder.logging().log_sensitive_information(false);
712
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
713
            // The reconfigurable modules should've been reloaded in response to the config change
714
            let config = rx.next().await.unwrap();
715
            assert_eq!(config.0, config_builder.build().unwrap());
716
        });
717
    }
718

            
719
    // TODO: Ignored until #1607 is fixed
720
    #[test]
721
    #[ignore]
722
    fn watch_multiple() {
723
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
724
            let temp_dir = test_temp_dir!();
725
            let mut config_builder1 = ArtiConfigBuilder::default();
726
            config_builder1.application().watch_configuration(true);
727

            
728
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
729
            let mut cfg_sources = ConfigurationSources::new_empty();
730
            cfg_sources.push_source(
731
                ConfigurationSource::Dir(temp_dir.as_path_untracked().to_path_buf()),
732
                MustRead::MustRead,
733
            );
734

            
735
            let (module, mut rx) = create_module().await;
736

            
737
            let (fw_tx, fw_rx) = file_watcher::channel();
738
            let mgr = Arc::new(CfgMgr {
739
                runtime: rt.clone(),
740
                sources: cfg_sources,
741
                tx: fw_tx,
742
                inner: Mutex::new(CfgMgrInner {
743
                    modules: vec![Arc::downgrade(&module)],
744
                    ..Default::default()
745
                }),
746
            });
747

            
748
            let (watcher, _) = mgr.launch_file_watcher().unwrap();
749
            mgr.inner.lock().unwrap().watcher = Some(watcher);
750
            let weak_mgr = Arc::downgrade(&mgr);
751

            
752
            // Use a fake sighup stream to wait until run_watcher()'s select_biased!
753
            // loop is entered
754
            let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
755
            let runtime = rt.clone();
756
            let () = rt
757
                .spawn(async move {
758
                    run_watcher(runtime.clone(), fw_rx, sighup_rx, weak_mgr, None)
759
                        .await
760
                        .unwrap();
761
                })
762
                .unwrap();
763

            
764
            config_builder1.logging().log_sensitive_information(true);
765
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
766
            sighup_tx.send(()).await.unwrap();
767
            // The reconfigurable modules should've been reloaded in response to sighup
768
            let config = rx.next().await.unwrap();
769
            assert_eq!(config.0, config_builder1.build().unwrap());
770

            
771
            let mut config_builder2 = ArtiConfigBuilder::default();
772
            config_builder2.application().watch_configuration(true);
773
            // Write another config file...
774
            config_builder2.system().max_files(0_u64);
775
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME2, &config_builder2);
776
            // Check that the 2 config files are merged
777
            let mut config_builder_combined = config_builder1.clone();
778
            config_builder_combined.system().max_files(0_u64);
779
            let config = rx.next().await.unwrap();
780
            assert_eq!(config.0, config_builder_combined.build().unwrap());
781
            // Now write a new config file to the watched dir
782
            config_builder2.logging().console("foo".to_string());
783
            let mut config_builder_combined2 = config_builder_combined.clone();
784
            config_builder_combined2
785
                .logging()
786
                .console("foo".to_string());
787
            let config3: PathBuf = write_config(&temp_dir, CONFIG_NAME3, &config_builder2);
788
            let config = rx.next().await.unwrap();
789
            assert_eq!(config.0, config_builder_combined2.build().unwrap());
790

            
791
            // Removing the file should also trigger an event
792
            std::fs::remove_file(config3).unwrap();
793
            let config = rx.next().await.unwrap();
794
            assert_eq!(config.0, config_builder_combined.build().unwrap());
795
        });
796
    }
797
}