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

            
3
use std::collections::HashSet;
4
use std::convert::TryInto;
5
use std::path::{Path, PathBuf};
6
use std::sync::mpsc::channel as std_channel;
7
use std::time::Duration;
8

            
9
use arti_client::config::Reconfigure;
10
use arti_client::TorClient;
11
use arti_config::ArtiConfig;
12
use notify::Watcher;
13
use tor_rtcompat::Runtime;
14
use tracing::{debug, info, warn};
15

            
16
/// How long (worst case) should we take to learn about configuration changes?
17
const POLL_INTERVAL: Duration = Duration::from_secs(10);
18

            
19
/// Launch a thread to watch our configuration files.
20
///
21
/// Whenever one or more files in `files` changes, try to reload our
22
/// configuration from them and tell TorClient about it.
23
pub fn watch_for_config_changes<R: Runtime>(
24
    sources: arti_config::ConfigurationSources,
25
    original: ArtiConfig,
26
    client: TorClient<R>,
27
) -> anyhow::Result<()> {
28
    let (tx, rx) = std_channel();
29
    let mut watcher = FileWatcher::new(tx, POLL_INTERVAL)?;
30

            
31
    for file in sources.files() {
32
        watcher.watch_file(file)?;
33
    }
34

            
35
    std::thread::spawn(move || {
36
        // TODO: If someday we make this facility available outside of the
37
        // `arti` application, we probably don't want to have this thread own
38
        // the FileWatcher.
39
        debug!("Waiting for FS events");
40
        while let Ok(event) = rx.recv() {
41
            if !watcher.event_matched(&event) {
42
                // NOTE: Sadly, it's not safe to log in this case.  If the user
43
                // has put a configuration file and a logfile in the same
44
                // directory, logging about discarded events will make us log
45
                // every time we log, and fill up the filesystem.
46
                continue;
47
            }
48
            while let Ok(_ignore) = rx.try_recv() {
49
                // Discard other events, so that we only reload once.
50
                //
51
                // We can afford to treat both error cases from try_recv [Empty
52
                // and Disconnected] as meaning that we've discarded other
53
                // events: if we're disconnected, we'll notice it when we next
54
                // call recv() in the outer loop.
55
            }
56
            debug!("FS event {:?}: reloading configuration.", event);
57
            match reconfigure(&sources, &original, &client) {
58
                Ok(exit) => {
59
                    info!("Successfully reloaded configuration.");
60
                    if exit {
61
                        break;
62
                    }
63
                }
64
                Err(e) => warn!("Couldn't reload configuration: {}", e),
65
            }
66
        }
67
        debug!("Thread exiting");
68
    });
69

            
70
    // Dropping the thread handle here means that we don't get any special
71
    // notification about a panic.  TODO: We should change that at some point in
72
    // the future.
73

            
74
    Ok(())
75
}
76

            
77
/// Reload the configuration files, apply the runtime configuration, and
78
/// reconfigure the client as much as we can.
79
///
80
/// Return true if we should stop watching for configuration changes.
81
fn reconfigure<R: Runtime>(
82
    sources: &arti_config::ConfigurationSources,
83
    original: &ArtiConfig,
84
    client: &TorClient<R>,
85
) -> anyhow::Result<bool> {
86
    let config = sources.load()?;
87
    let config: ArtiConfig = config.try_into()?;
88
    if config.proxy() != original.proxy() {
89
        warn!("Can't (yet) reconfigure proxy settings while arti is running.");
90
    }
91
    if config.logging() != original.logging() {
92
        warn!("Can't (yet) reconfigure logging settings while arti is running.");
93
    }
94
    let client_config = config.tor_client_config()?;
95
    client.reconfigure(&client_config, Reconfigure::WarnOnFailures)?;
96

            
97
    if !config.application().watch_configuration() {
98
        // Stop watching for configuration changes.
99
        return Ok(true);
100
    }
101

            
102
    Ok(false)
103
}
104

            
105
/// A wrapper around `notify::RecommendedWatcher` to watch a set of parent
106
/// directories in order to learn about changes in some specific files that they
107
/// contain.
108
///
109
/// The `Watcher` implementation in `notify` has a weakness: it gives sensible
110
/// results when you're watching directories, but if you start watching
111
/// non-directory files, it won't notice when those files get replaced.  That's
112
/// a problem for users who want to change their configuration atomically by
113
/// making new files and then moving them into place over the old ones.
114
///
115
/// For more background on the issues with `notify`, see
116
/// <https://github.com/notify-rs/notify/issues/165> and
117
/// <https://github.com/notify-rs/notify/pull/166>.
118
///
119
/// TODO: Someday we might want to make this code exported someplace.  If we do,
120
/// we should test it, and improve its API a lot.  Right now, the caller needs
121
/// to mess around with `std::sync::mpsc` and filter out the events they want
122
/// using `FileWatcher::event_matched`.
123
struct FileWatcher {
124
    /// An underlying `notify` watcher that tells us about directory changes.
125
    watcher: notify::RecommendedWatcher,
126
    /// The list of directories that we're currently watching.
127
    watching_dirs: HashSet<PathBuf>,
128
    /// The list of files we actually care about.
129
    watching_files: HashSet<PathBuf>,
130
}
131

            
132
impl FileWatcher {
133
    /// Like `notify::watcher`, but create a FileWatcher instead.
134
    fn new(
135
        tx: std::sync::mpsc::Sender<notify::DebouncedEvent>,
136
        interval: Duration,
137
    ) -> anyhow::Result<Self> {
138
        let watcher = notify::watcher(tx, interval)?;
139
        Ok(Self {
140
            watcher,
141
            watching_dirs: HashSet::new(),
142
            watching_files: HashSet::new(),
143
        })
144
    }
145

            
146
    /// Watch a single file (not a directory).  Does nothing if we're already watching that file.
147
    fn watch_file<P: AsRef<Path>>(&mut self, path: P) -> anyhow::Result<()> {
148
        // Make the path absolute (without necessarily making it canonical).
149
        //
150
        // We do this because `notify` reports all of its events in terms of
151
        // absolute paths, so if we were to tell it to watch a directory by its
152
        // relative path, we'd get reports about the absolute paths of the files
153
        // in that directory.
154
        let cwd = std::env::current_dir()?;
155
        let path = cwd.join(path.as_ref());
156
        debug_assert!(path.is_absolute());
157

            
158
        // See what directory we should watch in order to watch this file.
159
        let watch_target = match path.parent() {
160
            // The file has a parent, so watch that.
161
            Some(parent) => parent,
162
            // The file has no parent.  Given that it's absolute, that means
163
            // that we're looking at the root directory.  There's nowhere to go
164
            // "up" from there.
165
            None => path.as_ref(),
166
        };
167

            
168
        // Start watching this directory, if we're not already watching it.
169
        if !self.watching_dirs.contains(watch_target) {
170
            self.watcher
171
                .watch(watch_target, notify::RecursiveMode::NonRecursive)?;
172

            
173
            self.watching_dirs.insert(watch_target.into());
174
        }
175

            
176
        // Note this file as one that we're watching, so that we can see changes
177
        // to it later on.
178
        self.watching_files.insert(path);
179

            
180
        Ok(())
181
    }
182

            
183
    /// Return true if the provided event describes a change affecting one of
184
    /// the files that we care about.
185
    fn event_matched(&self, event: &notify::DebouncedEvent) -> bool {
186
        let watching = |f| self.watching_files.contains(f);
187

            
188
        match event {
189
            notify::DebouncedEvent::NoticeWrite(f) => watching(f),
190
            notify::DebouncedEvent::NoticeRemove(f) => watching(f),
191
            notify::DebouncedEvent::Create(f) => watching(f),
192
            notify::DebouncedEvent::Write(f) => watching(f),
193
            notify::DebouncedEvent::Chmod(f) => watching(f),
194
            notify::DebouncedEvent::Remove(f) => watching(f),
195
            notify::DebouncedEvent::Rename(f1, f2) => watching(f1) || watching(f2),
196
            notify::DebouncedEvent::Rescan => {
197
                // We've missed some events: no choice but to reload.
198
                true
199
            }
200
            notify::DebouncedEvent::Error(_, Some(f)) => watching(f),
201
            notify::DebouncedEvent::Error(_, _) => false,
202
        }
203
    }
204
}