mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-03-15 15:05:27 +08:00
Port macOS universal notifiers to Rust
This ports the notifyd-based universal notifier to Rust.
This commit is contained in:
parent
0f29e0de2f
commit
38d198a83a
@ -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'");
|
||||
|
||||
|
@ -94,6 +94,7 @@ mod tinyexpr;
|
||||
mod tokenizer;
|
||||
mod topic_monitor;
|
||||
mod trace;
|
||||
mod universal_notifier;
|
||||
mod util;
|
||||
mod wait_handle;
|
||||
mod wchar;
|
||||
|
124
fish-rust/src/universal_notifier/mod.rs
Normal file
124
fish-rust/src/universal_notifier/mod.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
149
fish-rust/src/universal_notifier/notifyd.rs
Normal file
149
fish-rust/src/universal_notifier/notifyd.rs
Normal 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(¬ifiers);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user