aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2024-05-03 22:20:45 +0200
committerGitHub <noreply@github.com>2024-05-03 20:20:45 +0000
commita77f77c48fca298caab3a4834b2d7ab1a98cae88 (patch)
tree308699bf8619487597307d22b699d642f632b80d
parent82f41ed65ce0d3207d56d6877fd0ed898d7dce0c (diff)
downloadalacritty-a77f77c48fca298caab3a4834b2d7ab1a98cae88.tar.gz
alacritty-a77f77c48fca298caab3a4834b2d7ab1a98cae88.zip
Fix shutdown of config monitor
This implements a coordinated shutdown of the config monitor by sending an event to its thread and waiting for the thread to terminate.
-rw-r--r--alacritty/src/config/monitor.rs240
-rw-r--r--alacritty/src/main.rs13
2 files changed, 143 insertions, 110 deletions
diff --git a/alacritty/src/config/monitor.rs b/alacritty/src/config/monitor.rs
index f4b39a22..53cff1c9 100644
--- a/alacritty/src/config/monitor.rs
+++ b/alacritty/src/config/monitor.rs
@@ -1,9 +1,13 @@
use std::path::PathBuf;
-use std::sync::mpsc::{self, RecvTimeoutError};
+use std::sync::mpsc::{self, RecvTimeoutError, Sender};
+use std::thread::JoinHandle;
use std::time::{Duration, Instant};
-use log::{debug, error};
-use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
+use log::{debug, error, warn};
+use notify::{
+ Config, Error as NotifyError, Event as NotifyEvent, EventKind, RecommendedWatcher,
+ RecursiveMode, Watcher,
+};
use winit::event_loop::EventLoopProxy;
use alacritty_terminal::thread;
@@ -15,115 +19,139 @@ 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;
- }
+/// Config file update monitor.
+pub struct ConfigMonitor {
+ thread: JoinHandle<()>,
+ shutdown_tx: Sender<Result<NotifyEvent, NotifyError>>,
+}
- // 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,
- }
+impl ConfigMonitor {
+ pub fn new(mut paths: Vec<PathBuf>, event_proxy: EventLoopProxy<Event>) -> Option<Self> {
+ // Don't monitor config if there is no path to watch.
+ if paths.is_empty() {
+ return None;
}
- }
- // 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);
+ // 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 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);
+ // The Duration argument is a debouncing period.
+ let (tx, rx) = mpsc::channel();
+ let mut watcher = match RecommendedWatcher::new(
+ tx.clone(),
+ Config::default().with_poll_interval(FALLBACK_POLLING_TIMEOUT),
+ ) {
+ Ok(watcher) => watcher,
+ Err(err) => {
+ error!("Unable to watch config file: {}", err);
+ return None;
+ },
+ };
+
+ let join_handle = 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::Other if event.info() == Some("shutdown") => break,
+ 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);
+ }
},
- _ => (),
- },
- 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;
- },
- };
+ Ok(Err(err)) => {
+ debug!("Config watcher errors: {:?}", err);
+ },
+ Err(err) => {
+ debug!("Config watcher channel dropped unexpectedly: {}", err);
+ break;
+ },
+ };
+ }
+ });
+
+ Some(Self { thread: join_handle, shutdown_tx: tx })
+ }
+
+ /// Synchronously shut down the monitor.
+ pub fn shutdown(self) {
+ // Request shutdown.
+ let mut event = NotifyEvent::new(EventKind::Other);
+ event = event.set_info("shutdown");
+ let _ = self.shutdown_tx.send(Ok(event));
+
+ // Wait for thread to terminate.
+ if let Err(err) = self.thread.join() {
+ warn!("config monitor shutdown failed: {err:?}");
}
- });
+ }
}
diff --git a/alacritty/src/main.rs b/alacritty/src/main.rs
index 2a60961a..f9301767 100644
--- a/alacritty/src/main.rs
+++ b/alacritty/src/main.rs
@@ -56,7 +56,8 @@ mod gl {
#[cfg(unix)]
use crate::cli::MessageOptions;
use crate::cli::{Options, Subcommands};
-use crate::config::{monitor, UiConfig};
+use crate::config::monitor::ConfigMonitor;
+use crate::config::UiConfig;
use crate::event::{Event, Processor};
#[cfg(target_os = "macos")]
use crate::macos::locale;
@@ -165,8 +166,10 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
//
// The monitor watches the config file for changes and reloads it. Pending
// config changes are processed in the main loop.
+ let mut config_monitor = None;
if config.live_config_reload {
- monitor::watch(config.config_paths.clone(), window_event_loop.create_proxy());
+ config_monitor =
+ ConfigMonitor::new(config.config_paths.clone(), window_event_loop.create_proxy());
}
// Create the IPC socket listener.
@@ -205,8 +208,10 @@ fn alacritty(mut options: Options) -> Result<(), Box<dyn Error>> {
// FIXME: Change PTY API to enforce the correct drop order with the typesystem.
drop(processor);
- // FIXME patch notify library to have a shutdown method.
- // config_reloader.join().ok();
+ // Terminate the config monitor.
+ if let Some(config_monitor) = config_monitor.take() {
+ config_monitor.shutdown();
+ }
// Without explicitly detaching the console cmd won't redraw it's prompt.
#[cfg(windows)]