aboutsummaryrefslogtreecommitdiff
path: root/alacritty/src/config/monitor.rs
blob: f4b39a227e1668a460d85c46be3c4f3ef38f1d9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use std::path::PathBuf;
use std::sync::mpsc::{self, RecvTimeoutError};
use std::time::{Duration, Instant};

use log::{debug, error};
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use winit::event_loop::EventLoopProxy;

use alacritty_terminal::thread;

use crate::event::{Event, EventType};

const DEBOUNCE_DELAY: Duration = Duration::from_millis(10);

/// The fallback for `RecommendedWatcher` polling.
const FALLBACK_POLLING_TIMEOUT: Duration = Duration::from_secs(1);

pub fn watch(mut paths: Vec<PathBuf>, event_proxy: EventLoopProxy<Event>) {
    // Don't monitor config if there is no path to watch.
    if paths.is_empty() {
        return;
    }

    // Exclude char devices like `/dev/null`, sockets, and so on, by checking that file type is a
    // regular file.
    paths.retain(|path| {
        // Call `metadata` to resolve symbolic links.
        path.metadata().map_or(false, |metadata| metadata.file_type().is_file())
    });

    // Canonicalize paths, keeping the base paths for symlinks.
    for i in 0..paths.len() {
        if let Ok(canonical_path) = paths[i].canonicalize() {
            match paths[i].symlink_metadata() {
                Ok(metadata) if metadata.file_type().is_symlink() => paths.push(canonical_path),
                _ => paths[i] = canonical_path,
            }
        }
    }

    // The Duration argument is a debouncing period.
    let (tx, rx) = mpsc::channel();
    let mut watcher = match RecommendedWatcher::new(
        tx,
        Config::default().with_poll_interval(FALLBACK_POLLING_TIMEOUT),
    ) {
        Ok(watcher) => watcher,
        Err(err) => {
            error!("Unable to watch config file: {}", err);
            return;
        },
    };

    thread::spawn_named("config watcher", move || {
        // Get all unique parent directories.
        let mut parents = paths
            .iter()
            .map(|path| {
                let mut path = path.clone();
                path.pop();
                path
            })
            .collect::<Vec<PathBuf>>();
        parents.sort_unstable();
        parents.dedup();

        // Watch all configuration file directories.
        for parent in &parents {
            if let Err(err) = watcher.watch(parent, RecursiveMode::NonRecursive) {
                debug!("Unable to watch config directory {:?}: {}", parent, err);
            }
        }

        // The current debouncing time.
        let mut debouncing_deadline: Option<Instant> = None;

        // The events accumulated during the debounce period.
        let mut received_events = Vec::new();

        loop {
            // We use `recv_timeout` to debounce the events coming from the watcher and reduce
            // the amount of config reloads.
            let event = match debouncing_deadline.as_ref() {
                Some(debouncing_deadline) => {
                    rx.recv_timeout(debouncing_deadline.saturating_duration_since(Instant::now()))
                },
                None => {
                    let event = rx.recv().map_err(Into::into);
                    // Set the debouncing deadline after receiving the event.
                    debouncing_deadline = Some(Instant::now() + DEBOUNCE_DELAY);
                    event
                },
            };

            match event {
                Ok(Ok(event)) => match event.kind {
                    EventKind::Any
                    | EventKind::Create(_)
                    | EventKind::Modify(_)
                    | EventKind::Other => {
                        received_events.push(event);
                    },
                    _ => (),
                },
                Err(RecvTimeoutError::Timeout) => {
                    // Go back to polling the events.
                    debouncing_deadline = None;

                    if received_events
                        .drain(..)
                        .flat_map(|event| event.paths.into_iter())
                        .any(|path| paths.contains(&path))
                    {
                        // Always reload the primary configuration file.
                        let event = Event::new(EventType::ConfigReload(paths[0].clone()), None);
                        let _ = event_proxy.send_event(event);
                    }
                },
                Ok(Err(err)) => {
                    debug!("Config watcher errors: {:?}", err);
                },
                Err(err) => {
                    debug!("Config watcher channel dropped unexpectedly: {}", err);
                    break;
                },
            };
        }
    });
}