From 38d198a83a3a5c8e69ef8d63fbce0d5aaee04ba0 Mon Sep 17 00:00:00 2001 From: ridiculousfish Date: Sat, 25 Nov 2023 16:35:30 -0800 Subject: [PATCH] Port macOS universal notifiers to Rust This ports the notifyd-based universal notifier to Rust. --- fish-rust/src/env_universal_common.rs | 62 +------- fish-rust/src/lib.rs | 1 + fish-rust/src/universal_notifier/mod.rs | 124 ++++++++++++++++ fish-rust/src/universal_notifier/notifyd.rs | 149 ++++++++++++++++++++ 4 files changed, 275 insertions(+), 61 deletions(-) create mode 100644 fish-rust/src/universal_notifier/mod.rs create mode 100644 fish-rust/src/universal_notifier/notifyd.rs diff --git a/fish-rust/src/env_universal_common.rs b/fish-rust/src/env_universal_common.rs index 5e9d4ed48..c70f743b1 100644 --- a/fish-rust/src/env_universal_common.rs +++ b/fish-rust/src/env_universal_common.rs @@ -10,6 +10,7 @@ use crate::fds::{open_cloexec, wopen_cloexec}; use crate::flog::{FLOG, FLOGF}; use crate::path::path_get_config; use crate::path::{path_get_config_remoteness, DirRemoteness}; +use crate::universal_notifier::{default_notifier, UniversalNotifier}; use crate::wchar::prelude::*; use crate::wchar::{wstr, WString}; use crate::wcstringutil::{join_strings, split_string, string_suffixes_string, LineIterator}; @@ -839,67 +840,6 @@ fn default_vars_path() -> WString { WString::new() } -pub enum NotifierStrategy { - // Poll on shared memory. - strategy_shmem_polling, - - // Mac-specific notify(3) implementation. - strategy_notifyd, - - // Strategy that uses a named pipe. Somewhat complex, but portable and doesn't require - // polling most of the time. - strategy_named_pipe, -} - -/// The "universal notifier" is an object responsible for broadcasting and receiving universal -/// variable change notifications. These notifications do not contain the change, but merely -/// indicate that the uvar file has changed. It is up to the uvar subsystem to re-read the file. -/// -/// We support a few notification strategies. Not all strategies are supported on all platforms. -/// -/// Notifiers may request polling, and/or provide a file descriptor to be watched for readability in -/// select(). -/// -/// To request polling, the notifier overrides usec_delay_between_polls() to return a positive -/// value. That value will be used as the timeout in select(). When select returns, the loop invokes -/// poll(). poll() should return true to indicate that the file may have changed. -/// -/// To provide a file descriptor, the notifier overrides notification_fd() to return a non-negative -/// fd. This will be added to the "read" file descriptor list in select(). If the fd is readable, -/// notification_fd_became_readable() will be called; that function should be overridden to return -/// true if the file may have changed. -pub trait UniversalNotifier { - // Does a fast poll(). Returns true if changed. - fn poll(&self) -> bool; - - // Triggers a notification. - fn post_notification(&self); - - // Recommended delay between polls. A value of 0 means no polling required (so no timeout). - fn usec_delay_between_polls(&self) -> u64; - - // Returns the fd from which to watch for events, or -1 if none. - fn notification_fd(&self) -> RawFd; - - // The notification_fd is readable; drain it. Returns true if a notification is considered to - // have been posted. - fn notification_fd_became_readable(&self, fd: RawFd) -> bool; -} - -fn resolve_default_strategy() -> NotifierStrategy { - todo!("universal notifier"); -} - -// Default instance. Other instances are possible for testing. -pub fn default_notifier() -> &'static dyn UniversalNotifier { - todo!("universal notifier"); -} - -/// Factory constructor. -fn new_notifier_for_strategy(_strat: NotifierStrategy, _test_path: Option<&wstr>) { - todo!("universal notifier"); -} - /// Error message. const PARSE_ERR: &wstr = L!("Unable to parse universal variable message: '%ls'"); diff --git a/fish-rust/src/lib.rs b/fish-rust/src/lib.rs index 7350e8dc1..91994fbff 100644 --- a/fish-rust/src/lib.rs +++ b/fish-rust/src/lib.rs @@ -94,6 +94,7 @@ mod tinyexpr; mod tokenizer; mod topic_monitor; mod trace; +mod universal_notifier; mod util; mod wait_handle; mod wchar; diff --git a/fish-rust/src/universal_notifier/mod.rs b/fish-rust/src/universal_notifier/mod.rs new file mode 100644 index 000000000..e634cbe41 --- /dev/null +++ b/fish-rust/src/universal_notifier/mod.rs @@ -0,0 +1,124 @@ +use once_cell::sync::OnceCell; +use std::os::fd::RawFd; + +#[cfg(target_os = "macos")] +mod notifyd; + +/// The "universal notifier" is an object responsible for broadcasting and receiving universal +/// variable change notifications. These notifications do not contain the change, but merely +/// indicate that the uvar file has changed. It is up to the uvar subsystem to re-read the file. +/// +/// Notifiers may provide a file descriptor to be watched for readability in +/// select(). +/// +/// To provide a file descriptor, the notifier overrides notification_fd() to return a non-negative +/// fd. This will be added to the "read" file descriptor list in select(). If the fd is readable, +/// notification_fd_became_readable() will be called; that function should be overridden to return +/// true if the file may have changed. +pub trait UniversalNotifier: Send + Sync { + // Triggers a notification. + fn post_notification(&self); + + // Returns the fd from which to watch for events. + fn notification_fd(&self) -> Option; + + // The notification_fd is readable; drain it. Returns true if a notification is considered to + // have been posted. + fn notification_fd_became_readable(&self, fd: RawFd) -> bool; +} + +/// A notifier which does nothing. +pub struct NullNotifier; + +impl UniversalNotifier for NullNotifier { + fn post_notification(&self) {} + + fn notification_fd(&self) -> Option { + None + } + + fn notification_fd_became_readable(&self, _fd: RawFd) -> bool { + false + } +} + +/// Create a notifier. +pub fn create_notifier() -> Box { + #[cfg(target_os = "macos")] + { + if let Some(notifier) = notifyd::NotifydNotifier::new() { + return Box::new(notifier); + } + } + Box::new(NullNotifier) +} + +// Default instance. Other instances are possible for testing. +static DEFAULT_NOTIFIER: OnceCell> = OnceCell::new(); + +pub fn default_notifier() -> &'static dyn UniversalNotifier { + DEFAULT_NOTIFIER.get_or_init(create_notifier).as_ref() +} + +// Test a slice of notifiers. +#[cfg(test)] +pub fn test_notifiers(notifiers: &[&dyn UniversalNotifier]) { + let poll_notifier = |n: &dyn UniversalNotifier| -> bool { + let Some(fd) = n.notification_fd() else { + return false; + }; + if crate::fd_readable_set::poll_fd_readable(fd) { + n.notification_fd_became_readable(fd) + } else { + false + } + }; + + // Nobody should poll yet. + for (idx, &n) in notifiers.iter().enumerate() { + assert!( + !poll_notifier(n), + "notifier {} polled before notification", + idx + ); + } + + // Tweak each notifier. Verify that others see it. + for (idx1, &n1) in notifiers.iter().enumerate() { + n1.post_notification(); + + // notifyd requires a round trip to the notifyd server, which means we have to wait a + // little bit to receive it. In practice 40 ms seems to be enough. + unsafe { libc::usleep(40000) }; + + for (idx2, &n2) in notifiers.iter().enumerate() { + let mut polled = poll_notifier(n2); + + // We aren't concerned with the one who posted, except we do need to poll to drain it. + if idx1 == idx2 { + continue; + } + assert!( + polled, + "notifier {} did not see notification from {}", + idx2, idx1 + ); + // It should not poll again immediately. + polled = poll_notifier(n2); + assert!( + !polled, + "notifier {} polled twice after notification from {}", + idx2, idx1 + ); + } + } + + // Nobody should poll now. + for (idx, &n) in notifiers.iter().enumerate() { + assert!( + !poll_notifier(n), + "notifier {} polled after all changes", + idx + ); + } +} diff --git a/fish-rust/src/universal_notifier/notifyd.rs b/fish-rust/src/universal_notifier/notifyd.rs new file mode 100644 index 000000000..fb7fc7c31 --- /dev/null +++ b/fish-rust/src/universal_notifier/notifyd.rs @@ -0,0 +1,149 @@ +use crate::common::PROGRAM_NAME; +use crate::fds::{make_fd_nonblocking, set_cloexec}; +use crate::flog::{FLOG, FLOGF}; +use crate::universal_notifier::UniversalNotifier; +use crate::wchar::prelude::*; +use libc::{c_char, c_int}; +use std::ffi::CString; +use std::os::fd::RawFd; + +extern "C" { + fn notify_register_file_descriptor( + name: *const c_char, + fd: *mut c_int, + flags: c_int, + token: *mut c_int, + ) -> u32; + + fn notify_post(name: *const c_char) -> u32; + + fn notify_cancel(token: c_int) -> c_int; +} + +const NOTIFY_STATUS_OK: u32 = 0; +const NOTIFY_TOKEN_INVALID: c_int = -1; + +/// A notifier based on notifyd. +pub struct NotifydNotifier { + // The file descriptor to watch for readability. + // Note that we should NOT use AutocloseFd, as notify_cancel() takes responsibility for + // closing it. + notify_fd: RawFd, + token: c_int, + name: CString, +} + +impl NotifydNotifier { + pub fn new() -> Option { + // Per notify(3), the user.uid.%d style is only accessible to processes with that uid. + let program_name = *PROGRAM_NAME.get().unwrap_or(&L!("fish")); + let local_name = format!( + "user.uid.{}.{}.uvars", + unsafe { libc::getuid() }, + program_name + ); + let name = CString::new(local_name).ok()?; + + let mut notify_fd = -1; + let mut token = -1; + let status = unsafe { + notify_register_file_descriptor(name.as_ptr(), &mut notify_fd, 0, &mut token) + }; + if status != NOTIFY_STATUS_OK || notify_fd < 0 { + FLOGF!( + warning, + "notify_register_file_descriptor() failed with status %u.", + status + ); + FLOG!( + warning, + "Universal variable notifications may not be received." + ); + return None; + } + // Mark us for non-blocking reads, and CLO_EXEC. + let _ = make_fd_nonblocking(notify_fd); + let _ = set_cloexec(notify_fd, true); + + // Serious hack: notify_fd is likely the read end of a pipe. The other end is owned by + // libnotify, which does not mark it as CLO_EXEC (it should!). The next fd is probably + // notify_fd + 1. Do it ourselves. If the implementation changes and some other FD gets + // marked as CLO_EXEC, that's probably a good thing. + let _ = set_cloexec(notify_fd + 1, true); + + Some(Self { + notify_fd, + token, + name, + }) + } +} + +impl Drop for NotifydNotifier { + fn drop(&mut self) { + if self.token != NOTIFY_TOKEN_INVALID { + // Note this closes notify_fd. + unsafe { + notify_cancel(self.token); + } + } + } +} + +impl UniversalNotifier for NotifydNotifier { + fn post_notification(&self) { + FLOG!(uvar_notifier, "posting notification"); + let status = unsafe { notify_post(self.name.as_ptr()) }; + if status != NOTIFY_STATUS_OK { + FLOGF!( + warning, + "notify_post() failed with status %u. Uvar notifications may not be sent.", + status, + ); + } + } + + fn notification_fd(&self) -> Option { + Some(self.notify_fd) + } + + fn notification_fd_became_readable(&self, fd: RawFd) -> bool { + // notifyd notifications come in as 32 bit values. We don't care about the value. We set + // ourselves as non-blocking, so just read until we can't read any more. + assert!(fd == self.notify_fd); + let mut read_something = false; + let mut buff: [u8; 64] = [0; 64]; + loop { + let amt_read = unsafe { + libc::read( + self.notify_fd, + buff.as_mut_ptr() as *mut libc::c_void, + buff.len(), + ) + }; + read_something = read_something || amt_read > 0; + if amt_read != buff.len() as isize { + break; + } + } + FLOGF!( + uvar_notifier, + "notify fd %s readable", + if read_something { "was" } else { "was not" }, + ); + read_something + } +} + +#[test] +fn test_notifyd_notifiers() { + let mut notifiers = Vec::new(); + for _ in 0..16 { + notifiers.push(NotifydNotifier::new().expect("failed to create notifier")); + } + let notifiers = notifiers + .iter() + .map(|n| n as &dyn UniversalNotifier) + .collect::>(); + super::test_notifiers(¬ifiers); +}