Debounce config reloads only after filesystem notifications
This commit is contained in:
1 parent
b469929847
commit
4da59f3906
2 files changed
+54
-19
No files matched your search
+27
-18
@@ -78,30 +78,16 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
||||||
overview::init_font_loader(&mut event_loop, &mut state)?;
|
overview::init_font_loader(&mut event_loop, &mut state)?;
|
||||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||||
let mut monitor =
|
let mut monitor = config::config_path().and_then(
|
||||||
config::config_path().and_then(|path| match reload::ConfigMonitor::new(path) {
|
|path| match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) {
|
||||||
Ok(monitor) => Some(monitor),
|
Ok(monitor) => Some(monitor),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!(%error, "automatic config watching unavailable; use feresectl reload-config");
|
warn!(%error, "automatic config watching unavailable; use feresectl reload-config");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
|
||||||
use calloop::timer::{TimeoutAction, Timer};
|
|
||||||
event_loop.handle().insert_source(
|
|
||||||
Timer::from_duration(Duration::from_millis(150)),
|
|
||||||
move |_, _, state: &mut Ferese| {
|
|
||||||
if let Some(result) = monitor
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|monitor| monitor.poll(std::time::Instant::now()))
|
|
||||||
{
|
|
||||||
if let Err(error) = result.and_then(|source| state.reload_config_source(source)) {
|
|
||||||
warn!(%error, "config reload rejected; retaining last working config");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
TimeoutAction::ToDuration(Duration::from_millis(150))
|
|
||||||
},
|
},
|
||||||
)?;
|
);
|
||||||
|
use calloop::timer::{TimeoutAction, Timer};
|
||||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||||
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||||
let runner = std::rc::Rc::new(std::cell::RefCell::new(daemon::Runner::new(
|
let runner = std::rc::Rc::new(std::cell::RefCell::new(daemon::Runner::new(
|
||||||
@@ -117,7 +103,30 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
TimeoutAction::ToDuration(Duration::from_secs(1))
|
TimeoutAction::ToDuration(Duration::from_secs(1))
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
let reload_timer_pending = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||||
|
let reload_handle = event_loop.handle();
|
||||||
let result = event_loop.run(None, &mut state, |state| {
|
let result = event_loop.run(None, &mut state, |state| {
|
||||||
|
if let Some(monitor) = &mut monitor {
|
||||||
|
if let Some(result) = monitor.poll(std::time::Instant::now()) {
|
||||||
|
if let Err(error) = result.and_then(|source| state.reload_config_source(source)) {
|
||||||
|
warn!(%error, "config reload rejected; retaining last working config");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At most one timer exists. Edits arriving during debounce move the
|
||||||
|
// deadline; the old timer wakes once and is replaced if necessary.
|
||||||
|
if !reload_timer_pending.get()
|
||||||
|
&& let Some(deadline) = monitor.next_deadline()
|
||||||
|
{
|
||||||
|
let pending = reload_timer_pending.clone();
|
||||||
|
match reload_handle.insert_source(Timer::from_deadline(deadline), move |_, _, _| {
|
||||||
|
pending.set(false);
|
||||||
|
TimeoutAction::Drop
|
||||||
|
}) {
|
||||||
|
Ok(_) => reload_timer_pending.set(true),
|
||||||
|
Err(error) => warn!(%error, "could not schedule config debounce"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// The decoder wakes the loop after publishing its result, including
|
// The decoder wakes the loop after publishing its result, including
|
||||||
// when no output has a pending frame.
|
// when no output has a pending frame.
|
||||||
let wallpaper_changed = state.wallpaper.poll();
|
let wallpaper_changed = state.wallpaper.poll();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Filesystem events only: idle timer ticks never read/stat the config.
|
//! Filesystem events wake the loop; only pending edits need a debounce timer.
|
||||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use std::{
|
use std::{
|
||||||
io::Read,
|
io::Read,
|
||||||
@@ -28,9 +28,18 @@ pub(crate) struct ConfigMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigMonitor {
|
impl ConfigMonitor {
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn new(path: PathBuf) -> notify::Result<Self> {
|
pub(crate) fn new(path: PathBuf) -> notify::Result<Self> {
|
||||||
|
Self::with_wakeup(path, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_wakeup(
|
||||||
|
path: PathBuf,
|
||||||
|
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
|
||||||
|
) -> notify::Result<Self> {
|
||||||
let (sender, events) = mpsc::sync_channel(1);
|
let (sender, events) = mpsc::sync_channel(1);
|
||||||
let target = path.clone();
|
let target = path.clone();
|
||||||
|
let event_wakeup = wakeup.clone();
|
||||||
let mut watcher =
|
let mut watcher =
|
||||||
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
||||||
if let Ok(event) = event
|
if let Ok(event) = event
|
||||||
@@ -41,6 +50,9 @@ impl ConfigMonitor {
|
|||||||
.any(|p| p == &target || target.starts_with(p))
|
.any(|p| p == &target || target.starts_with(p))
|
||||||
{
|
{
|
||||||
let _ = sender.try_send(());
|
let _ = sender.try_send(());
|
||||||
|
if let Some(wakeup) = &event_wakeup {
|
||||||
|
wakeup.wakeup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
// Watch the directory, not the inode: atomic editor saves replace it.
|
// Watch the directory, not the inode: atomic editor saves replace it.
|
||||||
@@ -57,6 +69,11 @@ impl ConfigMonitor {
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
let initial_dirty = path.is_file().then(Instant::now);
|
let initial_dirty = path.is_file().then(Instant::now);
|
||||||
|
if initial_dirty.is_some()
|
||||||
|
&& let Some(wakeup) = wakeup
|
||||||
|
{
|
||||||
|
wakeup.wakeup();
|
||||||
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_watcher: watcher,
|
_watcher: watcher,
|
||||||
events,
|
events,
|
||||||
@@ -66,6 +83,10 @@ impl ConfigMonitor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_deadline(&self) -> Option<Instant> {
|
||||||
|
self.dirty.map(|since| since + Duration::from_millis(120))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn poll(&mut self, now: Instant) -> Option<Result<String, String>> {
|
pub(crate) fn poll(&mut self, now: Instant) -> Option<Result<String, String>> {
|
||||||
if self.events.try_iter().next().is_some() {
|
if self.events.try_iter().next().is_some() {
|
||||||
self.dirty = Some(now);
|
self.dirty = Some(now);
|
||||||
@@ -221,6 +242,10 @@ mod tests {
|
|||||||
let mut monitor = ConfigMonitor::new(path).unwrap();
|
let mut monitor = ConfigMonitor::new(path).unwrap();
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
monitor.dirty = Some(now);
|
monitor.dirty = Some(now);
|
||||||
|
assert_eq!(
|
||||||
|
monitor.next_deadline(),
|
||||||
|
Some(now + Duration::from_millis(120))
|
||||||
|
);
|
||||||
assert!(monitor.poll(now + Duration::from_millis(119)).is_none());
|
assert!(monitor.poll(now + Duration::from_millis(119)).is_none());
|
||||||
assert!(
|
assert!(
|
||||||
monitor
|
monitor
|
||||||
@@ -228,5 +253,6 @@ mod tests {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.is_ok()
|
.is_ok()
|
||||||
);
|
);
|
||||||
|
assert_eq!(monitor.next_deadline(), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user