use std::cell::RefCell;
use std::collections::HashMap;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use nix::errno::Errno;
use nix::sys::inotify::{AddWatchFlags, Inotify, WatchDescriptor};
use crate::ApplicationEntry;
pub(crate) struct AppsCache {
pub(crate) apps: HashMap<Rc<str>, ApplicationEntry>,
pub(crate) inotify: Inotify,
pub(crate) watched: HashMap<WatchDescriptor, PathBuf>,
}
thread_local! {
pub(crate) static APPS_CACHE: RefCell<Option<AppsCache>> = const { RefCell::new(None) };
}
pub(crate) fn drain_inotify_events(cache: &mut AppsCache) -> bool {
let mut dirty = false;
loop {
match cache.inotify.read_events() {
Ok(events) => {
for ev in events {
if ev.mask.contains(AddWatchFlags::IN_Q_OVERFLOW) {
dirty = true;
continue;
}
if ev.mask.contains(AddWatchFlags::IN_IGNORED) {
cache.watched.remove(&ev.wd);
dirty = true;
continue;
}
if let Some(name) = ev.name.as_ref() {
if name.as_bytes().ends_with(b".desktop") {
dirty = true;
}
}
}
}
Err(Errno::EAGAIN) => break,
Err(e) => {
log::warn!("inotify read_events failed ({e}); invalidating apps cache");
dirty = true;
break;
}
}
}
dirty
}
pub(crate) fn ensure_watches(cache: &mut AppsCache, xdg_data_dirs: &[String]) {
const FLAGS: AddWatchFlags = AddWatchFlags::IN_CREATE
.union(AddWatchFlags::IN_DELETE)
.union(AddWatchFlags::IN_MODIFY)
.union(AddWatchFlags::IN_MOVED_FROM)
.union(AddWatchFlags::IN_MOVED_TO)
.union(AddWatchFlags::IN_CLOSE_WRITE)
.union(AddWatchFlags::IN_ONLYDIR);
for dir in xdg_data_dirs {
let path = Path::new(dir).join("applications");
if !path.exists() {
continue;
}
if cache.watched.values().any(|p| p == &path) {
continue;
}
match cache.inotify.add_watch(&path, FLAGS) {
Ok(wd) => {
cache.watched.insert(wd, path);
}
Err(e) => {
log::warn!("inotify add_watch failed for {}: {}", path.display(), e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::desktop::installed_apps_impl;
use nix::sys::inotify::InitFlags;
#[test]
fn cache_invalidates_on_desktop_file_change() {
let test_root =
std::env::temp_dir().join(format!("app_rummage_cache_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&test_root);
let apps_dir = test_root.join("applications");
std::fs::create_dir_all(&apps_dir).unwrap();
let dirs = vec![test_root.to_string_lossy().into_owned()];
let inotify = Inotify::init(InitFlags::IN_NONBLOCK | InitFlags::IN_CLOEXEC).unwrap();
let initial = installed_apps_impl(&dirs, &[]);
let mut cache = AppsCache {
apps: initial,
inotify,
watched: HashMap::new(),
};
ensure_watches(&mut cache, &dirs);
assert_eq!(cache.watched.len(), 1, "should have watched applications/");
assert!(cache.apps.is_empty(), "no desktop files yet");
let desktop_path = apps_dir.join("org.example.New.desktop");
std::fs::write(
&desktop_path,
"[Desktop Entry]\nType=Application\nName=New\nExec=/usr/bin/true\n",
)
.unwrap();
assert!(
drain_inotify_events(&mut cache),
"creating a .desktop file must dirty the cache",
);
cache.apps = installed_apps_impl(&dirs, &[]);
assert!(cache.apps.contains_key("org.example.New"));
std::fs::write(apps_dir.join("README.txt"), b"ignore me").unwrap();
assert!(
!drain_inotify_events(&mut cache),
"non-.desktop file changes must not dirty the cache",
);
let _ = std::fs::remove_dir_all(&test_root);
}
}