Port macOS universal notifiers to Rust

This ports the notifyd-based universal notifier to Rust.
This commit is contained in:
ridiculousfish 2023-11-25 16:35:30 -08:00
parent 0f29e0de2f
commit 38d198a83a
4 changed files with 275 additions and 61 deletions

View File

@ -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'");

View File

@ -94,6 +94,7 @@ mod tinyexpr;
mod tokenizer;
mod topic_monitor;
mod trace;
mod universal_notifier;
mod util;
mod wait_handle;
mod wchar;

View File

@ -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<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;
}
/// A notifier which does nothing.
pub struct NullNotifier;
impl UniversalNotifier for NullNotifier {
fn post_notification(&self) {}
fn notification_fd(&self) -> Option<RawFd> {
None
}
fn notification_fd_became_readable(&self, _fd: RawFd) -> bool {
false
}
}
/// Create a notifier.
pub fn create_notifier() -> Box<dyn UniversalNotifier> {
#[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<Box<dyn UniversalNotifier>> = 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
);
}
}

View File

@ -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<Self> {
// 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<RawFd> {
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::<Vec<_>>();
super::test_notifiers(&notifiers);
}