mirror of
https://github.com/fish-shell/fish-shell.git
synced 2025-03-27 14:45:13 +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::flog::{FLOG, FLOGF};
|
||||||
use crate::path::path_get_config;
|
use crate::path::path_get_config;
|
||||||
use crate::path::{path_get_config_remoteness, DirRemoteness};
|
use crate::path::{path_get_config_remoteness, DirRemoteness};
|
||||||
|
use crate::universal_notifier::{default_notifier, UniversalNotifier};
|
||||||
use crate::wchar::prelude::*;
|
use crate::wchar::prelude::*;
|
||||||
use crate::wchar::{wstr, WString};
|
use crate::wchar::{wstr, WString};
|
||||||
use crate::wcstringutil::{join_strings, split_string, string_suffixes_string, LineIterator};
|
use crate::wcstringutil::{join_strings, split_string, string_suffixes_string, LineIterator};
|
||||||
@ -839,67 +840,6 @@ fn default_vars_path() -> WString {
|
|||||||
WString::new()
|
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.
|
/// Error message.
|
||||||
const PARSE_ERR: &wstr = L!("Unable to parse universal variable message: '%ls'");
|
const PARSE_ERR: &wstr = L!("Unable to parse universal variable message: '%ls'");
|
||||||
|
|
||||||
|
@ -94,6 +94,7 @@ mod tinyexpr;
|
|||||||
mod tokenizer;
|
mod tokenizer;
|
||||||
mod topic_monitor;
|
mod topic_monitor;
|
||||||
mod trace;
|
mod trace;
|
||||||
|
mod universal_notifier;
|
||||||
mod util;
|
mod util;
|
||||||
mod wait_handle;
|
mod wait_handle;
|
||||||
mod wchar;
|
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