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
#[cfg(feature = "rpc")]
14
use tor_config::ConfigurationTree;
15
use tor_config::file_watcher::{
16
    self, FileEventReceiver, FileEventSender, FileWatcher, FileWatcherBuilder,
17
};
18
use tor_config::load::{ConfigResolveOptions, DisfavouredKey};
19
use tor_config::{ConfigurationSource, ConfigurationSources, sources::FoundConfigFiles};
20
use tor_rtcompat::Runtime;
21
use tor_rtcompat::SpawnExt;
22
use tracing::{debug, error, info, instrument, warn};
23

            
24
#[cfg(target_family = "unix")]
25
use crate::process::sighup_stream;
26

            
27
#[cfg(not(target_family = "unix"))]
28
use futures::stream;
29

            
30
use crate::{ArtiCombinedConfig, ArtiConfig};
31

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

            
35
/// An object that can be reconfigured when our configuration changes.
36
///
37
/// We use this trait so that we can represent abstract modules in our
38
/// application, and pass the configuration to each of them.
39
//
40
// TODO: It is very likely we will want to refactor this even further once we
41
// have a notion of what our modules truly are.
42
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
43
pub(crate) trait ReconfigurableModule: Send + Sync {
44
    /// Try to reconfigure this module according to a newly loaded configuration.
45
    ///
46
    /// By convention, this should only return fatal errors; any such error
47
    /// should cause the program to exit.  For other cases, we should just warn.
48
    //
49
    // TODO: This should probably take "how: Reconfigure" as an argument, and
50
    // pass it down as appropriate. See issue #1156.
51
    fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()>;
52
}
53

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

            
60
    /// The sources from which we read our configuration.
61
    sources: ConfigurationSources,
62

            
63
    /// A sender to use when constructing new [`FileWatcher`]s.
64
    tx: FileEventSender,
65

            
66
    /// Mutable state.
67
    inner: Mutex<CfgMgrInner>,
68
}
69

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

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

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

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

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

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

            
101
    /// A stream on which we will get alerts about SIGHUP events.
102
    sighup_stream: BoxStream<'static, ()>,
103

            
104
    /// A stream that will tell us when our files are changed.
105
    watcher_rx: FileEventReceiver,
106

            
107
    /// An interval that we wait to debounce events from watcher_rx or sighup_stream.
108
    debounce_interval: Option<Duration>,
109

            
110
    /// If true, we start watching for file changes immediately at launch.
111
    watch_files_at_start: bool,
112
}
113

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

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

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

            
162
        Ok((mgr, watcher))
163
    }
164

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

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

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

            
200
6
        match reconfigure(found_files, &mut inner) {
201
6
            Ok(watch) => {
202
6
                info!("Successfully reloaded configuration.");
203
6
                if watch && inner.watcher.is_none() {
204
                    info!("Starting watching over configuration.");
205
                    let (watcher, _files) = self
206
                        .launch_file_watcher()
207
                        .context("Starting to watch over config")?;
208
                    inner.watcher = Some(watcher);
209
6
                } else if !watch && inner.watcher.is_some() {
210
                    info!("Stopped watching over configuration.");
211
                    inner.watcher = None;
212
6
                }
213
            }
214
            // TODO: warn_report does not work on anyhow::Error.
215
            Err(e) => warn!("Couldn't reload configuration: {}", tor_error::Report(e)),
216
        }
217

            
218
6
        Ok(())
219
6
    }
220
}
221

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

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

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

            
265
        Ok(())
266
    }
267

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

            
276
        let Some(mgr) = self.weak_mgr.upgrade() else {
277
            return Err(anyhow::anyhow!(
278
                "CfgMgr disappeared before launching watcher task."
279
            ));
280
        };
281

            
282
        let mut inner = mgr.inner.lock().expect("poisoned lock");
283
        inner.modules.push(weak_module);
284
        Ok(())
285
    }
286
}
287

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

            
301
    loop {
302
        select_biased! {
303
            event = sighup_stream.next().fuse() => {
304
                let Some(()) = event else {
305
                    break;
306
                };
307

            
308
                info!("Received SIGHUP");
309
            },
310
            event = rx.next().fuse() => {
311
2
                if let Some(debounce_interval) = debounce_interval {
312
                    runtime.sleep(debounce_interval).await;
313
                }
314

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

            
327
        if let Some(mgr) = weak_mgr.upgrade() {
328
            mgr.reload_configuration()?;
329
            drop(mgr);
330
        } else {
331
            debug!("Configuration mgr disappeared; exiting loop");
332
            break;
333
        }
334
    }
335

            
336
    Ok(())
337
2
}
338

            
339
/// A TorClient that we may or may not have told to start bootstrapping.
340
pub(crate) struct LaunchableTorClient<R: Runtime> {
341
    /// Original value of defer_bootstrap.
342
    orig_defer_bootstrap: bool,
343

            
344
    /// True if we have launched bootstrapping on the the client.
345
    have_launched: Mutex<bool>,
346

            
347
    /// The client itself.
348
    client: Arc<TorClient<R>>,
349
}
350

            
351
impl<R: Runtime> ReconfigurableModule for LaunchableTorClient<R> {
352
    #[instrument(level = "trace", skip_all)]
353
    fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
354
        // TODO RPC: Take 'how' as an argument.
355

            
356
        if new.0.application().defer_bootstrap && !self.orig_defer_bootstrap {
357
            warn!("Cannot enable defer_bootstrap while arti is running.");
358
        }
359
        if !new.0.application().defer_bootstrap {
360
            self.ensure_bootstrap_launched()?;
361
        }
362

            
363
        TorClient::reconfigure(&self.client, &new.1, Reconfigure::WarnOnFailures)?;
364
        Ok(())
365
    }
366
}
367

            
368
impl<R: Runtime> LaunchableTorClient<R> {
369
    /// Create a new LaunchableTorClient.
370
    ///
371
    /// We assume that it has (or has not) been told to bootstrap itself based on `cfg`.
372
    pub(crate) fn new(client: Arc<TorClient<R>>, cfg: &crate::ApplicationConfig) -> Self {
373
        Self {
374
            orig_defer_bootstrap: cfg.defer_bootstrap,
375
            have_launched: Mutex::new(!cfg.defer_bootstrap),
376
            client,
377
        }
378
    }
379

            
380
    /// If we have not already told this LaunchableTorClient to bootstrap itself, do so.
381
    fn ensure_bootstrap_launched(&self) -> anyhow::Result<()> {
382
        let mut have_launched = self.have_launched.lock().expect("lock poisoned");
383

            
384
        if *have_launched {
385
            return Ok(());
386
        }
387

            
388
        let client = Arc::clone(&self.client);
389
        // We spawn this as a new task since `bootstrap` is very much async,
390
        // but this needs to be called from `reconfigure`, which is not.
391
        self.client
392
            .runtime()
393
            .spawn(async move {
394
                let _outcome = client.bootstrap().await;
395
            })
396
            .context("Launching bootstrap")?;
397

            
398
        *have_launched = true;
399
        Ok(())
400
    }
401

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

            
407
        self.client.bootstrap().await
408
    }
409
}
410

            
411
/// Internal type to represent the Arti application as a `ReconfigurableModule`.
412
pub(crate) struct Application {
413
    /// The configuration that Arti had at startup.
414
    ///
415
    /// We use this to check whether the user is asking for any impermissible
416
    /// transitions.
417
    original_config: ArtiConfig,
418
}
419

            
420
impl Application {
421
    /// Construct a new `Application` to receive configuration changes for the
422
    /// arti application.
423
    pub(crate) fn new(cfg: ArtiConfig) -> Self {
424
        Self {
425
            original_config: cfg,
426
        }
427
    }
428
}
429

            
430
impl ReconfigurableModule for Application {
431
    // TODO: This should probably take "how: Reconfigure" as an argument, and
432
    // pass it down as appropriate. See issue #1156.
433
    #[instrument(level = "trace", skip_all)]
434
    fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
435
        let original = &self.original_config;
436
        let config = &new.0;
437

            
438
        if config.proxy() != original.proxy() {
439
            warn!("Can't (yet) reconfigure proxy settings while arti is running.");
440
        }
441
        if config.logging() != original.logging() {
442
            warn!("Can't (yet) reconfigure logging settings while arti is running.");
443
        }
444
        #[cfg(feature = "rpc")]
445
        if config.rpc != original.rpc {
446
            warn!("Can't (yet) change RPC settings while arti is running.");
447
        }
448
        if config.application().permit_debugging && !original.application().permit_debugging {
449
            warn!("Cannot disable application hardening when it has already been enabled.");
450
        }
451
        // Note that this is the only config transition we actually perform so far.
452
        if !config.application().permit_debugging {
453
            #[cfg(feature = "harden")]
454
            crate::process::enable_process_hardening()?;
455
        }
456

            
457
        Ok(())
458
    }
459
}
460

            
461
/// Find the configuration files and prepare the watcher
462
8
fn prepare<'a, R: Runtime>(
463
8
    watcher: &mut FileWatcherBuilder<R>,
464
8
    sources: &'a ConfigurationSources,
465
8
) -> anyhow::Result<FoundConfigFiles<'a>> {
466
8
    let sources = sources.scan()?;
467
8
    for source in sources.iter() {
468
8
        match source {
469
            ConfigurationSource::Dir(dir) => watcher.watch_dir(dir, "toml")?,
470
8
            ConfigurationSource::File(file) => watcher.watch_path(file)?,
471
            ConfigurationSource::Verbatim(_) => {}
472
        }
473
    }
474
8
    Ok(sources)
475
8
}
476

            
477
/// Reload the configuration files, apply the runtime configuration, and
478
/// reconfigure the client as much as we can.
479
///
480
/// Return true if we should be watching for configuration changes.
481
//
482
// TODO: This should probably take "how: Reconfigure" as an argument, and
483
// pass it down as appropriate. See issue #1156.
484
#[instrument(level = "trace", skip_all)]
485
6
fn reconfigure(
486
6
    found_files: FoundConfigFiles<'_>,
487
6
    mgr_inner: &mut CfgMgrInner,
488
6
) -> anyhow::Result<bool> {
489
6
    let config = found_files.load()?;
490
    #[allow(unused_mut)]
491
6
    let mut resolve_options = ConfigResolveOptions::default();
492
    #[cfg(feature = "rpc")]
493
6
    {
494
6
        resolve_options.want_output_tree = true;
495
6
    }
496

            
497
6
    let rs = tor_config::resolve_return_results::<ArtiCombinedConfig>(config, &resolve_options)?;
498
6
    let config = rs.value;
499
    #[cfg(feature = "rpc")]
500
6
    {
501
6
        mgr_inner.normalized_cfg = rs
502
6
            .output_tree
503
6
            .expect("normalized cfg not exposed as expected!?");
504
6
        mgr_inner.deprecated_keys = rs.deprecated.into_iter().collect();
505
6
        mgr_inner.unrecognized_keys = rs.unrecognized.into_iter().collect();
506
6
    }
507

            
508
    // Filter out the modules that have been dropped
509
6
    let reconfigurable = mgr_inner.modules.iter().flat_map(Weak::upgrade);
510
    // If there are no more modules, we should exit.
511
6
    let mut has_modules = false;
512

            
513
6
    for module in reconfigurable {
514
6
        has_modules = true;
515
6
        module.reconfigure(&config)?;
516
    }
517

            
518
6
    Ok(has_modules && config.0.application().watch_configuration)
519
6
}
520

            
521
#[cfg(test)]
522
mod test {
523
    // @@ begin test lint list maintained by maint/add_warning @@
524
    #![allow(clippy::bool_assert_comparison)]
525
    #![allow(clippy::clone_on_copy)]
526
    #![allow(clippy::dbg_macro)]
527
    #![allow(clippy::mixed_attributes_style)]
528
    #![allow(clippy::print_stderr)]
529
    #![allow(clippy::print_stdout)]
530
    #![allow(clippy::single_char_pattern)]
531
    #![allow(clippy::unwrap_used)]
532
    #![allow(clippy::unchecked_time_subtraction)]
533
    #![allow(clippy::useless_vec)]
534
    #![allow(clippy::needless_pass_by_value)]
535
    #![allow(clippy::string_slice)] // See arti#2571
536
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
537

            
538
    use crate::ArtiConfigBuilder;
539

            
540
    use super::*;
541
    use futures::SinkExt as _;
542
    use futures::channel::mpsc;
543
    use postage::watch;
544
    use std::path::PathBuf;
545
    use std::sync::{Arc, Mutex};
546
    use test_temp_dir::{TestTempDir, test_temp_dir};
547
    use tor_async_utils::PostageWatchSenderExt;
548
    use tor_config::sources::MustRead;
549

            
550
    /// Filename for config1
551
    const CONFIG_NAME1: &str = "config1.toml";
552
    /// Filename for config2
553
    const CONFIG_NAME2: &str = "config2.toml";
554
    /// Filename for config3
555
    const CONFIG_NAME3: &str = "config3.toml";
556

            
557
    struct TestModule {
558
        // A sender for sending the new config to the test function
559
        tx: Arc<Mutex<watch::Sender<ArtiCombinedConfig>>>,
560
    }
561

            
562
    impl ReconfigurableModule for TestModule {
563
        fn reconfigure(&self, new: &ArtiCombinedConfig) -> anyhow::Result<()> {
564
            let config = new.clone();
565
            self.tx.lock().unwrap().maybe_send(|_| config);
566

            
567
            Ok(())
568
        }
569
    }
570

            
571
    /// Create a test reconfigurable module.
572
    ///
573
    /// Returns the module and a channel on which the new configs received by the module are sent.
574
    async fn create_module() -> (
575
        Arc<dyn ReconfigurableModule>,
576
        watch::Receiver<ArtiCombinedConfig>,
577
    ) {
578
        let (tx, mut rx) = watch::channel();
579
        // Read the initial value from the postage::watch stream
580
        // (the first observed value on this test stream is always the default config)
581
        let _: ArtiCombinedConfig = rx.next().await.unwrap();
582

            
583
        (
584
            Arc::new(TestModule {
585
                tx: Arc::new(Mutex::new(tx)),
586
            }),
587
            rx,
588
        )
589
    }
590

            
591
    /// Write `data` to file `name` within `dir`.
592
    fn write_file(dir: &TestTempDir, name: &str, data: &[u8]) -> PathBuf {
593
        let tmp = dir.as_path_untracked().join("tmp");
594
        std::fs::write(&tmp, data).unwrap();
595
        let path = dir.as_path_untracked().join(name);
596
        // Atomically write the config file
597
        std::fs::rename(tmp, &path).unwrap();
598
        path
599
    }
600

            
601
    /// Write an `ArtiConfigBuilder` to a file within `dir`.
602
    fn write_config(dir: &TestTempDir, name: &str, config: &ArtiConfigBuilder) -> PathBuf {
603
        let s = toml::to_string(&config).unwrap();
604
        write_file(dir, name, s.as_bytes())
605
    }
606

            
607
    #[test]
608
    fn watch_single_file() {
609
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
610
            let temp_dir = test_temp_dir!();
611
            let mut config_builder = ArtiConfigBuilder::default();
612
            config_builder.application().watch_configuration(true);
613

            
614
            let cfg_file = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
615
            let mut cfg_sources = ConfigurationSources::new_empty();
616
            cfg_sources.push_source(ConfigurationSource::File(cfg_file), MustRead::MustRead);
617

            
618
            let (module, mut rx) = create_module().await;
619

            
620
            config_builder.logging().log_sensitive_information(true);
621
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
622

            
623
            let (fw_tx, fw_rx) = file_watcher::channel();
624
            let mgr = Arc::new(CfgMgr {
625
                runtime: rt.clone(),
626
                sources: cfg_sources,
627
                tx: fw_tx,
628
                inner: Mutex::new(CfgMgrInner {
629
                    modules: vec![Arc::downgrade(&module)],
630
                    ..Default::default()
631
                }),
632
            });
633

            
634
            let (watcher, _) = mgr.launch_file_watcher().unwrap();
635
            mgr.inner.lock().unwrap().watcher = Some(watcher);
636
            let weak_mgr = Arc::downgrade(&mgr);
637

            
638
            // Use a fake sighup stream to wait until run_watcher()'s select_biased!
639
            // loop is entered
640
            let (mut sighup_tx, sighup_rx) = mpsc::unbounded();
641
            let runtime = rt.clone();
642
            let () = rt
643
                .spawn(async move {
644
                    run_watcher(runtime.clone(), fw_rx, sighup_rx, weak_mgr, None)
645
                        .await
646
                        .unwrap();
647
                })
648
                .unwrap();
649

            
650
            sighup_tx.send(()).await.unwrap();
651

            
652
            // The reconfigurable modules should've been reloaded in response to sighup
653
            let config = rx.next().await.unwrap();
654
            assert_eq!(config.0, config_builder.build().unwrap());
655

            
656
            // Overwrite the config
657
            config_builder.logging().log_sensitive_information(false);
658
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder);
659
            // The reconfigurable modules should've been reloaded in response to the config change
660
            let config = rx.next().await.unwrap();
661
            assert_eq!(config.0, config_builder.build().unwrap());
662
        });
663
    }
664

            
665
    // TODO: Ignored until #1607 is fixed
666
    #[test]
667
    #[ignore]
668
    fn watch_multiple() {
669
        tor_rtcompat::test_with_one_runtime!(|rt| async move {
670
            let temp_dir = test_temp_dir!();
671
            let mut config_builder1 = ArtiConfigBuilder::default();
672
            config_builder1.application().watch_configuration(true);
673

            
674
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
675
            let mut cfg_sources = ConfigurationSources::new_empty();
676
            cfg_sources.push_source(
677
                ConfigurationSource::Dir(temp_dir.as_path_untracked().to_path_buf()),
678
                MustRead::MustRead,
679
            );
680

            
681
            let (module, mut rx) = create_module().await;
682

            
683
            let (fw_tx, fw_rx) = file_watcher::channel();
684
            let mgr = Arc::new(CfgMgr {
685
                runtime: rt.clone(),
686
                sources: cfg_sources,
687
                tx: fw_tx,
688
                inner: Mutex::new(CfgMgrInner {
689
                    modules: vec![Arc::downgrade(&module)],
690
                    ..Default::default()
691
                }),
692
            });
693

            
694
            let (watcher, _) = mgr.launch_file_watcher().unwrap();
695
            mgr.inner.lock().unwrap().watcher = Some(watcher);
696
            let weak_mgr = Arc::downgrade(&mgr);
697

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

            
710
            config_builder1.logging().log_sensitive_information(true);
711
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME1, &config_builder1);
712
            sighup_tx.send(()).await.unwrap();
713
            // The reconfigurable modules should've been reloaded in response to sighup
714
            let config = rx.next().await.unwrap();
715
            assert_eq!(config.0, config_builder1.build().unwrap());
716

            
717
            let mut config_builder2 = ArtiConfigBuilder::default();
718
            config_builder2.application().watch_configuration(true);
719
            // Write another config file...
720
            config_builder2.system().max_files(0_u64);
721
            let _: PathBuf = write_config(&temp_dir, CONFIG_NAME2, &config_builder2);
722
            // Check that the 2 config files are merged
723
            let mut config_builder_combined = config_builder1.clone();
724
            config_builder_combined.system().max_files(0_u64);
725
            let config = rx.next().await.unwrap();
726
            assert_eq!(config.0, config_builder_combined.build().unwrap());
727
            // Now write a new config file to the watched dir
728
            config_builder2.logging().console("foo".to_string());
729
            let mut config_builder_combined2 = config_builder_combined.clone();
730
            config_builder_combined2
731
                .logging()
732
                .console("foo".to_string());
733
            let config3: PathBuf = write_config(&temp_dir, CONFIG_NAME3, &config_builder2);
734
            let config = rx.next().await.unwrap();
735
            assert_eq!(config.0, config_builder_combined2.build().unwrap());
736

            
737
            // Removing the file should also trigger an event
738
            std::fs::remove_file(config3).unwrap();
739
            let config = rx.next().await.unwrap();
740
            assert_eq!(config.0, config_builder_combined.build().unwrap());
741
        });
742
    }
743
}