mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 04:43:42 +01:00
feat(backend): Registry, a thread-safe table from WindowId to a platform handle
Step 11(ii), the pure part. macOS cannot act on a window from its id: it needs the Accessibility element, the pid and the observer. Registry<H> is that table, generic over the handle so that its rules run on any host. Rules, written at the top of registry.rs and tested: thread-safe; a lookup clones the handle and releases the lock before anything is done to it; a removed or replaced handle is handed back to the caller instead of being dropped under the lock (dropping a macOS handle can make an AX round trip, which must never happen with a lock held: seam.md 3.3); a stale or unknown id is Error::WindowGone, never a panic; an entry ends on remove, remove_pid (application quit) or a failed call; re-inserting replaces. Nothing uses it yet; no daemon changes. Verified: native cargo test -p komorebi-backend: 38 unit (9 new) + 7 doc (1 new) + 1 compile_fail passed, 0 failed. Not run: the Windows, KDE and macOS checks (the new module is std-only and no other file changed).
This commit is contained in:
1 parent
f157aa34e9
commit
4c9094d776
2 files changed
+314
No files matched your search
@@ -31,6 +31,7 @@ mod error;
|
||||
pub mod fake;
|
||||
mod id;
|
||||
mod info;
|
||||
mod registry;
|
||||
|
||||
pub use backend::Backend;
|
||||
pub use backend::Batch;
|
||||
@@ -41,4 +42,5 @@ pub use id::MonitorId;
|
||||
pub use id::WindowId;
|
||||
pub use info::MonitorInfo;
|
||||
pub use info::WindowInfo;
|
||||
pub use registry::Registry;
|
||||
pub use komorebi_layouts::Rect;
|
||||
@@ -0,0 +1,312 @@
|
||||
//! A thread-safe table from a [`WindowId`] to whatever a platform needs in order to act on that
|
||||
//! window from nothing but the id.
|
||||
//!
|
||||
//! macOS is the platform that needs it: a window there is an Accessibility element with the pid
|
||||
//! of its application and the observer that reports its events, and nothing can be done to a
|
||||
//! window without them. The table is generic over the handle (`H`) so that its rules can be
|
||||
//! tested on any host with a stand-in handle; macOS instantiates it with its Accessibility types.
|
||||
//!
|
||||
//! # Rules
|
||||
//!
|
||||
//! 1. **Thread-safe.** Any thread may call any method.
|
||||
//! 2. **Nothing of the platform runs under the lock.** [`Registry::get`] clones the handle (the
|
||||
//! handle must be cheap to clone: a retained reference) and returns; the caller acts on the
|
||||
//! clone with the lock released. A method that removes an entry hands the removed handle back
|
||||
//! to the caller instead of dropping it inside, because dropping a macOS handle can make an
|
||||
//! Accessibility round trip. This is what keeps a registry call from ever waiting on the main
|
||||
//! queue, or on an application, while holding the lock (`docs/dev/seam.md` section 3.3).
|
||||
//! 3. **A stale id is an error, not a panic.** Looking up an id that was never registered, was
|
||||
//! removed, or was removed with its application answers [`Error::WindowGone`].
|
||||
//! 4. **Entries end** when the window is destroyed ([`Registry::remove`]), when its application
|
||||
//! quits ([`Registry::remove_pid`]), and when a call on the window shows that it is gone. An
|
||||
//! entry that nobody removes is a leak of a retained element; the platform removes at every
|
||||
//! place it learns of a death.
|
||||
//! 5. **Re-registering an id replaces the entry** and returns the old handle (a tab that is
|
||||
//! now shown by another element keeps the window id).
|
||||
//!
|
||||
//! ```
|
||||
//! use komorebi_backend::Registry;
|
||||
//! use komorebi_backend::WindowId;
|
||||
//!
|
||||
//! let registry: Registry<&'static str> = Registry::new();
|
||||
//! let id = WindowId::from_u32(7);
|
||||
//! registry.insert(id, 100, "element");
|
||||
//! assert_eq!(registry.get(id).unwrap(), "element");
|
||||
//!
|
||||
//! registry.remove(id);
|
||||
//! assert!(registry.get(id).unwrap_err().is_window_gone());
|
||||
//! ```
|
||||
|
||||
use crate::Error;
|
||||
use crate::Result;
|
||||
use crate::WindowId;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::MutexGuard;
|
||||
|
||||
struct Entry<H> {
|
||||
pid: i32,
|
||||
handle: H,
|
||||
}
|
||||
|
||||
/// The table. See the [module documentation](self) for its rules.
|
||||
pub struct Registry<H> {
|
||||
entries: Mutex<HashMap<WindowId, Entry<H>>>,
|
||||
}
|
||||
|
||||
impl<H> Default for Registry<H> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<H> Registry<H> {
|
||||
/// Returns an empty registry.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
// a thread that panicked while holding the lock cannot have left an entry half written (every
|
||||
// method is a single map operation), so a poisoned lock is still good
|
||||
fn lock(&self) -> MutexGuard<'_, HashMap<WindowId, Entry<H>>> {
|
||||
self.entries.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Registers the window, owned by the process `pid`. Returns the handle that this replaces,
|
||||
/// if the id was registered: the caller drops it, with no lock held.
|
||||
#[must_use = "drop the replaced handle yourself: it is not dropped under the lock"]
|
||||
pub fn insert(&self, id: WindowId, pid: i32, handle: H) -> Option<H> {
|
||||
self.lock().insert(id, Entry { pid, handle }).map(|e| e.handle)
|
||||
}
|
||||
|
||||
/// Replaces the handle of a window that is registered, keeping its pid. Returns the old
|
||||
/// handle, or [`Error::WindowGone`] (and registers nothing) if the id is not registered.
|
||||
pub fn replace(&self, id: WindowId, handle: H) -> Result<H> {
|
||||
match self.lock().get_mut(&id) {
|
||||
Some(entry) => Ok(std::mem::replace(&mut entry.handle, handle)),
|
||||
None => Err(Error::WindowGone(id)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Forgets the window. Returns its handle (for the caller to drop with no lock held), or
|
||||
/// `None` if it was not registered, which is not an error: a death is usually seen twice.
|
||||
#[must_use = "drop the removed handle yourself: it is not dropped under the lock"]
|
||||
pub fn remove(&self, id: WindowId) -> Option<H> {
|
||||
self.lock().remove(&id).map(|e| e.handle)
|
||||
}
|
||||
|
||||
/// Forgets every window of the process, as when its application quits. Returns the ids and
|
||||
/// handles removed, sorted by id.
|
||||
#[must_use = "drop the removed handles yourself: they are not dropped under the lock"]
|
||||
pub fn remove_pid(&self, pid: i32) -> Vec<(WindowId, H)> {
|
||||
let mut entries = self.lock();
|
||||
let ids: Vec<WindowId> = entries
|
||||
.iter()
|
||||
.filter(|(_, e)| e.pid == pid)
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
let mut removed: Vec<(WindowId, H)> = ids
|
||||
.into_iter()
|
||||
.filter_map(|id| entries.remove(&id).map(|e| (id, e.handle)))
|
||||
.collect();
|
||||
drop(entries);
|
||||
removed.sort_by_key(|(id, _)| *id);
|
||||
removed
|
||||
}
|
||||
|
||||
/// Whether the id is registered.
|
||||
pub fn contains(&self, id: WindowId) -> bool {
|
||||
self.lock().contains_key(&id)
|
||||
}
|
||||
|
||||
/// The pid of the registered window, or [`Error::WindowGone`].
|
||||
pub fn pid(&self, id: WindowId) -> Result<i32> {
|
||||
self.lock()
|
||||
.get(&id)
|
||||
.map(|e| e.pid)
|
||||
.ok_or(Error::WindowGone(id))
|
||||
}
|
||||
|
||||
/// How many windows are registered.
|
||||
pub fn len(&self) -> usize {
|
||||
self.lock().len()
|
||||
}
|
||||
|
||||
/// Whether nothing is registered.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.lock().is_empty()
|
||||
}
|
||||
|
||||
/// The registered ids, sorted.
|
||||
pub fn ids(&self) -> Vec<WindowId> {
|
||||
let mut ids: Vec<_> = self.lock().keys().copied().collect();
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: Clone> Registry<H> {
|
||||
/// A clone of the handle of the window, or [`Error::WindowGone`] for an id that is not
|
||||
/// registered. The lock is released before this returns.
|
||||
pub fn get(&self, id: WindowId) -> Result<H> {
|
||||
self.lock()
|
||||
.get(&id)
|
||||
.map(|e| e.handle.clone())
|
||||
.ok_or(Error::WindowGone(id))
|
||||
}
|
||||
|
||||
/// Clones of every handle with their ids, sorted by id.
|
||||
pub fn snapshot(&self) -> Vec<(WindowId, H)> {
|
||||
let mut all: Vec<_> = self
|
||||
.lock()
|
||||
.iter()
|
||||
.map(|(id, e)| (*id, e.handle.clone()))
|
||||
.collect();
|
||||
all.sort_by_key(|(id, _)| *id);
|
||||
all
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
fn id(n: u32) -> WindowId {
|
||||
WindowId::from_u32(n)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_registered_window_is_found_by_its_id() {
|
||||
let r = Registry::new();
|
||||
assert!(r.insert(id(1), 10, "a").is_none());
|
||||
assert!(matches!(r.get(id(1)), Ok("a")));
|
||||
assert!(matches!(r.pid(id(1)), Ok(10)));
|
||||
assert!(r.contains(id(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_id_is_window_gone_and_not_a_panic() {
|
||||
let r: Registry<&str> = Registry::new();
|
||||
assert!(matches!(r.get(id(9)), Err(Error::WindowGone(w)) if w == id(9)));
|
||||
assert!(matches!(r.pid(id(9)), Err(Error::WindowGone(_))));
|
||||
assert!(r.get(id(9)).unwrap_err().is_window_gone());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_removed_window_is_stale() {
|
||||
let r = Registry::new();
|
||||
let _ = r.insert(id(1), 10, "a");
|
||||
assert_eq!(r.remove(id(1)), Some("a"));
|
||||
assert!(r.get(id(1)).unwrap_err().is_window_gone());
|
||||
// a death seen twice is not an error
|
||||
assert_eq!(r.remove(id(1)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinserting_replaces_and_hands_back_the_old_handle() {
|
||||
let r = Registry::new();
|
||||
let _ = r.insert(id(1), 10, "tab one");
|
||||
assert_eq!(r.insert(id(1), 10, "tab two"), Some("tab one"));
|
||||
assert!(matches!(r.get(id(1)), Ok("tab two")));
|
||||
assert_eq!(r.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replace_keeps_the_pid_and_refuses_a_stale_id() {
|
||||
let r = Registry::new();
|
||||
let _ = r.insert(id(1), 10, "a");
|
||||
assert!(matches!(r.replace(id(1), "b"), Ok("a")));
|
||||
assert!(matches!(r.pid(id(1)), Ok(10)));
|
||||
assert!(r.replace(id(2), "c").unwrap_err().is_window_gone());
|
||||
assert!(!r.contains(id(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_application_quitting_takes_all_its_windows_and_only_them() {
|
||||
let r = Registry::new();
|
||||
let _ = r.insert(id(3), 10, "c");
|
||||
let _ = r.insert(id(1), 10, "a");
|
||||
let _ = r.insert(id(2), 20, "b");
|
||||
|
||||
assert_eq!(r.remove_pid(10), vec![(id(1), "a"), (id(3), "c")]);
|
||||
assert_eq!(r.ids(), vec![id(2)]);
|
||||
assert!(r.get(id(1)).unwrap_err().is_window_gone());
|
||||
assert_eq!(r.remove_pid(10), vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_is_sorted_and_independent_of_later_changes() {
|
||||
let r = Registry::new();
|
||||
let _ = r.insert(id(2), 1, "b");
|
||||
let _ = r.insert(id(1), 1, "a");
|
||||
let snap = r.snapshot();
|
||||
let _ = r.remove(id(1));
|
||||
assert_eq!(snap, vec![(id(1), "a"), (id(2), "b")]);
|
||||
}
|
||||
|
||||
/// A handle whose drop would, on macOS, make a round trip: it must not run under the lock.
|
||||
struct Probe {
|
||||
registry: Arc<Registry<Arc<Probe>>>,
|
||||
drops: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Drop for Probe {
|
||||
fn drop(&mut self) {
|
||||
// would deadlock if the registry dropped the handle while holding its lock
|
||||
let _ = self.registry.len();
|
||||
self.drops.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_are_never_dropped_under_the_lock() {
|
||||
let registry = Arc::new(Registry::new());
|
||||
let drops = Arc::new(AtomicUsize::new(0));
|
||||
let probe = |_: u32| {
|
||||
Arc::new(Probe {
|
||||
registry: registry.clone(),
|
||||
drops: drops.clone(),
|
||||
})
|
||||
};
|
||||
|
||||
let _ = registry.insert(id(1), 10, probe(1));
|
||||
// the replaced and the removed handles come back to us, so their drop runs here
|
||||
drop(registry.insert(id(1), 10, probe(2)));
|
||||
drop(registry.remove(id(1)));
|
||||
let _ = registry.insert(id(2), 10, probe(3));
|
||||
drop(registry.remove_pid(10));
|
||||
|
||||
assert_eq!(drops.load(Ordering::SeqCst), 3);
|
||||
assert!(registry.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_threads_can_register_look_up_and_remove() {
|
||||
let r = Arc::new(Registry::new());
|
||||
let threads: Vec<_> = (0..8u32)
|
||||
.map(|t| {
|
||||
let r = r.clone();
|
||||
std::thread::spawn(move || {
|
||||
for n in 0..200u32 {
|
||||
let wid = id(t * 1000 + n);
|
||||
let _ = r.insert(wid, t as i32, n);
|
||||
assert!(matches!(r.get(wid), Ok(v) if v == n));
|
||||
assert_eq!(r.remove(wid), Some(n));
|
||||
assert!(r.get(wid).unwrap_err().is_window_gone());
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for t in threads {
|
||||
t.join().unwrap();
|
||||
}
|
||||
|
||||
assert!(r.is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user