From 9b54e243b18302387cd744147262b86789cef25f Mon Sep 17 00:00:00 2001 From: ridiculousfish Date: Sun, 26 Nov 2023 15:08:04 -0800 Subject: [PATCH] Add FFI bindings to universal notifiers and adopt in input_common This removes the C++ bits for universal notifiers. --- CMakeLists.txt | 1 - fish-rust/build.rs | 2 +- fish-rust/src/env/environment.rs | 3 +- fish-rust/src/env_universal_common.rs | 39 -- fish-rust/src/ffi.rs | 1 - fish-rust/src/universal_notifier/mod.rs | 36 ++ src/env_universal_common.cpp | 598 ------------------------ src/env_universal_common.h | 74 +-- src/fish_tests.cpp | 97 ---- src/input_common.cpp | 15 +- 10 files changed, 44 insertions(+), 822 deletions(-) delete mode 100644 src/env_universal_common.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef3117bda..bab8275f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,7 +110,6 @@ set(FISH_SRCS src/color.cpp src/common.cpp src/env.cpp - src/env_universal_common.cpp src/event.cpp src/expand.cpp src/fallback.cpp diff --git a/fish-rust/build.rs b/fish-rust/build.rs index ec1b832ba..0f77f6ed5 100644 --- a/fish-rust/build.rs +++ b/fish-rust/build.rs @@ -70,7 +70,6 @@ fn main() { "fish-rust/src/complete.rs", "fish-rust/src/env_dispatch.rs", "fish-rust/src/env/env_ffi.rs", - "fish-rust/src/env_universal_common.rs", "fish-rust/src/event.rs", "fish-rust/src/exec.rs", "fish-rust/src/expand.rs", @@ -106,6 +105,7 @@ fn main() { "fish-rust/src/tokenizer.rs", "fish-rust/src/trace.rs", "fish-rust/src/util.rs", + "fish-rust/src/universal_notifier/mod.rs", "fish-rust/src/wildcard.rs", ]; cxx_build::bridges(&source_files) diff --git a/fish-rust/src/env/environment.rs b/fish-rust/src/env/environment.rs index cd1ce6c69..8da18c3a0 100644 --- a/fish-rust/src/env/environment.rs +++ b/fish-rust/src/env/environment.rs @@ -21,6 +21,7 @@ use crate::path::{ }; use crate::proc::is_interactive_session; use crate::termsize; +use crate::universal_notifier::default_notifier; use crate::wchar::prelude::*; use crate::wcstringutil::join_strings; use crate::wutil::{fish_wcstol, wgetcwd, wgettext}; @@ -322,7 +323,7 @@ impl EnvStack { let mut callbacks = CallbackDataList::new(); let changed = uvars().sync(&mut callbacks); if changed { - ffi::env_universal_notifier_t_default_notifier_post_notification_ffi(); + default_notifier().post_notification(); } // React internally to changes to special variables like LANG, and populate on-variable events. let mut result = Vec::new(); diff --git a/fish-rust/src/env_universal_common.rs b/fish-rust/src/env_universal_common.rs index 651bdebb8..6a797cc66 100644 --- a/fish-rust/src/env_universal_common.rs +++ b/fish-rust/src/env_universal_common.rs @@ -10,7 +10,6 @@ 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}; @@ -1042,41 +1041,3 @@ fn skip_spaces(mut s: &wstr) -> &wstr { } s } - -pub struct UniversalNotifierFFI(pub &'static dyn UniversalNotifier); - -#[cxx::bridge] -mod env_universal_common_ffi { - extern "Rust" { - type UniversalNotifierFFI; - #[cxx_name = "default_notifier"] - fn ffi_default_notifier() -> Box; - fn poll(&self) -> bool; - fn post_notification(&self); - fn usec_delay_between_polls(&self) -> u64; - fn notification_fd(&self) -> i32; - fn notification_fd_became_readable(&self, fd: i32) -> bool; - } -} - -fn ffi_default_notifier() -> Box { - Box::new(UniversalNotifierFFI(default_notifier())) -} - -impl UniversalNotifierFFI { - fn poll(&self) -> bool { - todo!("universal notifier") - } - fn post_notification(&self) { - todo!("universal notifier") - } - fn usec_delay_between_polls(&self) -> u64 { - todo!("universal notifier") - } - fn notification_fd(&self) -> RawFd { - todo!("universal notifier") - } - fn notification_fd_became_readable(&self, _fd: RawFd) -> bool { - todo!("universal notifier") - } -} diff --git a/fish-rust/src/ffi.rs b/fish-rust/src/ffi.rs index 7f396dffe..4856ea83e 100644 --- a/fish-rust/src/ffi.rs +++ b/fish-rust/src/ffi.rs @@ -68,7 +68,6 @@ include_cpp! { generate!("save_term_foreground_process_group") generate!("restore_term_foreground_process_group_for_exit") generate!("set_cloexec") - generate!("env_universal_notifier_t_default_notifier_post_notification_ffi") generate!("builtin_bind") generate!("builtin_commandline") diff --git a/fish-rust/src/universal_notifier/mod.rs b/fish-rust/src/universal_notifier/mod.rs index 1b4e811bb..10421a4c9 100644 --- a/fish-rust/src/universal_notifier/mod.rs +++ b/fish-rust/src/universal_notifier/mod.rs @@ -71,3 +71,39 @@ static DEFAULT_NOTIFIER: OnceCell> = OnceCell::new(); pub fn default_notifier() -> &'static dyn UniversalNotifier { DEFAULT_NOTIFIER.get_or_init(create_notifier).as_ref() } + +struct UniversalNotifierFFI(&'static dyn UniversalNotifier); +fn default_notifier_ffi() -> Box { + Box::new(UniversalNotifierFFI(default_notifier())) +} + +impl UniversalNotifierFFI { + fn post_notification(&self) { + self.0.post_notification(); + } + + fn notification_fd_ffi(&self) -> i32 { + self.0.notification_fd().unwrap_or(-1) + } + + fn notification_fd_became_readable_ffi(&self, fd: i32) -> bool { + self.0.notification_fd_became_readable(fd) + } +} + +#[cxx::bridge] +mod ffi { + extern "Rust" { + type UniversalNotifierFFI; + #[cxx_name = "default_notifier"] + fn default_notifier_ffi() -> Box; + + fn post_notification(&self); + + #[cxx_name = "notification_fd"] + fn notification_fd_ffi(&self) -> i32; + + #[cxx_name = "notification_fd_became_readable"] + fn notification_fd_became_readable_ffi(&self, fd: i32) -> bool; + } +} diff --git a/src/env_universal_common.cpp b/src/env_universal_common.cpp deleted file mode 100644 index 01d9ea31c..000000000 --- a/src/env_universal_common.cpp +++ /dev/null @@ -1,598 +0,0 @@ -// The utility library for universal variables. Used both by the client library and by the daemon. -#include "config.h" // IWYU pragma: keep - -#include // IWYU pragma: keep -#include -#include -#include -// We need the ioctl.h header so we can check if SIOCGIFHWADDR is defined by it so we know if we're -// on a Linux system. -#include // IWYU pragma: keep -#include // IWYU pragma: keep -#ifdef __CYGWIN__ -#include -#endif -#ifdef HAVE_SYS_SELECT_H -#include // IWYU pragma: keep -#endif -#if !defined(__APPLE__) && !defined(__CYGWIN__) -#include -#endif -#include -#include // IWYU pragma: keep -#include // IWYU pragma: keep - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common.h" -#include "env.h" -#include "env_universal_common.h" -#include "fallback.h" // IWYU pragma: keep -#include "fd_readable_set.rs.h" -#include "flog.h" -#include "path.h" -#include "util.h" // IWYU pragma: keep -#include "wcstringutil.h" -#include "wutil.h" - -#ifdef __APPLE__ -#define FISH_NOTIFYD_AVAILABLE -#include -#endif - -#ifdef __HAIKU__ -#define _BSD_SOURCE -#include -#endif // Haiku - -namespace { -class universal_notifier_shmem_poller_t final : public universal_notifier_t { -#ifdef __CYGWIN__ - // This is what our shared memory looks like. Everything here is stored in network byte order - // (big-endian). - struct universal_notifier_shmem_t { - uint32_t magic; - uint32_t version; - uint32_t universal_variable_seed; - }; - -#define SHMEM_MAGIC_NUMBER 0xF154 -#define SHMEM_VERSION_CURRENT 1000 - - private: - long long last_change_time{0}; - uint32_t last_seed{0}; - volatile universal_notifier_shmem_t *region{nullptr}; - - void open_shmem() { - assert(region == nullptr); - - // Use a path based on our uid to avoid collisions. - char path[NAME_MAX]; - snprintf(path, sizeof path, "/%ls_shmem_%d", program_name ? program_name : L"fish", - getuid()); - - autoclose_fd_t fd{shm_open(path, O_RDWR | O_CREAT, 0600)}; - if (!fd.valid()) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to open shared memory with path '%s': %s"), path, error); - return; - } - - // Get the size. - off_t size = 0; - struct stat buf = {}; - if (fstat(fd.fd(), &buf) < 0) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to fstat shared memory object with path '%s': %s"), path, - error); - return; - } - size = buf.st_size; - - // Set the size, if it's too small. - if (size < (off_t)sizeof(universal_notifier_shmem_t)) { - if (ftruncate(fd.fd(), sizeof(universal_notifier_shmem_t)) < 0) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to truncate shared memory object with path '%s': %s"), path, - error); - return; - } - } - - // Memory map the region. - void *addr = mmap(nullptr, sizeof(universal_notifier_shmem_t), PROT_READ | PROT_WRITE, - MAP_SHARED, fd.fd(), 0); - if (addr == MAP_FAILED) { - const char *error = std::strerror(errno); - FLOGF(error, _(L"Unable to memory map shared memory object with path '%s': %s"), path, - error); - this->region = nullptr; - return; - } - this->region = static_cast(addr); - - // Read the current seed. - this->poll(); - } - - public: - // Our notification involves changing the value in our shared memory. In practice, all clients - // will be in separate processes, so it suffices to set the value to a pid. For testing - // purposes, however, it's useful to keep them in the same process, so we increment the value. - // This isn't "safe" in the sense that multiple simultaneous increments may result in one being - // lost, but it should always result in the value being changed, which is sufficient. - void post_notification() override { - if (region != nullptr) { - /* Read off the seed */ - uint32_t seed = ntohl(region->universal_variable_seed); //!OCLINT(constant cond op) - - // Increment it. Don't let it wrap to zero. - do { - seed++; - } while (seed == 0); - - // Write out our data. - region->magic = htonl(SHMEM_MAGIC_NUMBER); //!OCLINT(constant cond op) - region->version = htonl(SHMEM_VERSION_CURRENT); //!OCLINT(constant cond op) - region->universal_variable_seed = htonl(seed); //!OCLINT(constant cond op) - - FLOGF(uvar_notifier, "posting notification: seed %u -> %u", last_seed, seed); - last_seed = seed; - } - } - - universal_notifier_shmem_poller_t() { open_shmem(); } - - ~universal_notifier_shmem_poller_t() { - if (region != nullptr) { - void *address = const_cast(static_cast(region)); - if (munmap(address, sizeof(universal_notifier_shmem_t)) < 0) { - wperror(L"munmap"); - } - } - } - - bool poll() override { - bool result = false; - if (region != nullptr) { - uint32_t seed = ntohl(region->universal_variable_seed); //!OCLINT(constant cond op) - if (seed != last_seed) { - result = true; - FLOGF(uvar_notifier, "polled true: shmem seed change %u -> %u", last_seed, seed); - last_seed = seed; - last_change_time = get_time(); - } - } - return result; - } - - unsigned long usec_delay_between_polls() const override { - // If it's been less than five seconds since the last change, we poll quickly Otherwise we - // poll more slowly. Note that a poll is a very cheap shmem read. The bad part about making - // this high is the process scheduling/wakeups it produces. - long long usec_per_sec = 1000000; - if (get_time() - last_change_time < 5LL * usec_per_sec) { - return usec_per_sec / 10; // 10 times a second - } - return usec_per_sec / 3; // 3 times a second - } -#else // this class isn't valid on this system - public: - [[noreturn]] universal_notifier_shmem_poller_t() { - DIE("universal_notifier_shmem_poller_t cannot be used on this system"); - } -#endif -}; - -/// A notifyd-based notifier. Very straightforward. -class universal_notifier_notifyd_t final : public universal_notifier_t { -#ifdef FISH_NOTIFYD_AVAILABLE - // Note that we should not use autoclose_fd_t, as notify_cancel() takes responsibility for - // closing it. - int notify_fd{-1}; - int token{-1}; // NOTIFY_TOKEN_INVALID - std::string name{}; - - void setup_notifyd() { - // Per notify(3), the user.uid.%d style is only accessible to processes with that uid. - char local_name[256]; - snprintf(local_name, sizeof local_name, "user.uid.%d.%ls.uvars", getuid(), - program_name ? program_name : L"fish"); - name.assign(local_name); - - uint32_t status = - notify_register_file_descriptor(name.c_str(), &this->notify_fd, 0, &this->token); - - if (status != NOTIFY_STATUS_OK) { - FLOGF(warning, "notify_register_file_descriptor() failed with status %u.", status); - FLOGF(warning, "Universal variable notifications may not be received."); - } - if (notify_fd >= 0) { - // Mark us for non-blocking reads, and CLO_EXEC. - int flags = fcntl(notify_fd, F_GETFL, 0); - if (flags >= 0 && !(flags & O_NONBLOCK)) { - fcntl(notify_fd, F_SETFL, flags | O_NONBLOCK); - } - - (void)set_cloexec(notify_fd); - // 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. - (void)set_cloexec(notify_fd + 1); - } - } - - public: - universal_notifier_notifyd_t() { setup_notifyd(); } - - ~universal_notifier_notifyd_t() { - if (token != -1 /* NOTIFY_TOKEN_INVALID */) { - // Note this closes notify_fd. - notify_cancel(token); - } - } - - int notification_fd() const override { return notify_fd; } - - bool notification_fd_became_readable(int fd) override { - // 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 == notify_fd); - bool read_something = false; - unsigned char buff[64]; - ssize_t amt_read; - do { - amt_read = read(notify_fd, buff, sizeof buff); - read_something = (read_something || amt_read > 0); - } while (amt_read == sizeof buff); - FLOGF(uvar_notifier, "notify fd %s readable", read_something ? "was" : "was not"); - return read_something; - } - - void post_notification() override { - FLOG(uvar_notifier, "posting notification"); - uint32_t status = notify_post(name.c_str()); - if (status != NOTIFY_STATUS_OK) { - FLOGF(warning, - "notify_post() failed with status %u. Uvar notifications may not be sent.", - status); - } - } -#else // this class isn't valid on this system - public: - [[noreturn]] universal_notifier_notifyd_t() { - DIE("universal_notifier_notifyd_t cannot be used on this system"); - } -#endif -}; - -/// Returns a "variables" file in the appropriate runtime directory. This is called infrequently and -/// so does not need to be cached. -static wcstring default_named_pipe_path() { - wcstring result = env_get_runtime_path(); - if (!result.empty()) { - result.append(L"/fish_universal_variables"); - } - return result; -} - -/// Create a fifo (named pipe) at \p test_path if non-null, or a default runtime path if null. -/// Open the fifo for both reading and writing, in non-blocking mode. -/// \return the fifo, or an invalid fd on failure. -static autoclose_fd_t make_fifo(const wchar_t *test_path, const wchar_t *suffix) { - wcstring vars_path = test_path ? wcstring(test_path) : default_named_pipe_path(); - vars_path.append(suffix); - const std::string narrow_path = wcs2zstring(vars_path); - - int mkfifo_status = mkfifo(narrow_path.c_str(), 0600); - if (mkfifo_status == -1 && errno != EEXIST) { - const char *error = std::strerror(errno); - const wchar_t *errmsg = _(L"Unable to make a pipe for universal variables using '%ls': %s"); - FLOGF(error, errmsg, vars_path.c_str(), error); - return autoclose_fd_t{}; - } - - autoclose_fd_t res{wopen_cloexec(vars_path, O_RDWR | O_NONBLOCK, 0600)}; - if (!res.valid()) { - const char *error = std::strerror(errno); - const wchar_t *errmsg = _(L"Unable to open a pipe for universal variables using '%ls': %s"); - FLOGF(error, errmsg, vars_path.c_str(), error); - } - return res; -} - -// Named-pipe based notifier. All clients open the same named pipe for reading and writing. The -// pipe's readability status is a trigger to enter polling mode. -// -// To post a notification, write some data to the pipe, wait a little while, and then read it back. -// -// To receive a notification, watch for the pipe to become readable. When it does, enter a polling -// mode until the pipe is no longer readable, where we poll based on the modification date of the -// pipe. To guard against the possibility of a shell exiting when there is data remaining in the -// pipe, if the pipe is kept readable too long, clients will attempt to read data out of it (to -// render it no longer readable). -class universal_notifier_named_pipe_t final : public universal_notifier_t { -#if !defined(__CYGWIN__) - // We operate a state machine. - enum state_t{ - // The pipe is not yet readable. There is nothing to do in poll. - // If the pipe becomes readable we will enter the polling state. - waiting_for_readable, - - // The pipe is readable. In poll, check if the pipe is still readable, - // and whether its timestamp has changed. - polling_during_readable, - - // We have written to the pipe (so we expect it to be readable). - // We may read back from it in poll(). - waiting_to_drain, - }; - - // The state we are currently in. - state_t state{waiting_for_readable}; - - // When we entered that state, in microseconds since epoch. - long long state_start_usec{-1}; - - // The pipe itself; this is opened read/write. - autoclose_fd_t pipe_fd; - - // The pipe's file ID containing the last modified timestamp. - file_id_t pipe_timestamps{}; - - // If we are in waiting_to_drain state, how much we have written and therefore are responsible - // for draining. - size_t drain_amount{0}; - - // We "flash" the pipe to make it briefly readable, for this many usec. - static constexpr long long k_flash_duration_usec = 1e4; - - // If the pipe remains readable for this many usec, we drain it. - static constexpr long long k_readable_too_long_duration_usec = 1e6; - - /// \return the name of a state. - static const char *state_name(state_t s) { - switch (s) { - case waiting_for_readable: - return "waiting"; - case polling_during_readable: - return "polling"; - case waiting_to_drain: - return "draining"; - } - DIE("Unreachable"); - } - - // Switch to a state (may or may not be new). - void set_state(state_t new_state) { - FLOGF(uvar_notifier, "changing from %s to %s", state_name(state), state_name(new_state)); - state = new_state; - state_start_usec = get_time(); - } - - // Called when the pipe has been readable for too long. - void drain_excess() const { - // The pipe seems to have data on it, that won't go away. Read a big chunk out of it. We - // don't read until it's exhausted, because if someone were to pipe say /dev/null, that - // would cause us to hang! - FLOG(uvar_notifier, "pipe was full, draining it"); - char buff[512]; - ignore_result(read(pipe_fd.fd(), buff, sizeof buff)); - } - - // Called when we want to read back data we have written, to mark the pipe as non-readable. - void drain_written() { - while (this->drain_amount > 0) { - char buff[64]; - size_t amt = std::min(this->drain_amount, sizeof buff); - ignore_result(read(this->pipe_fd.fd(), buff, amt)); - this->drain_amount -= amt; - } - } - - /// Check if the pipe's file ID (aka struct stat) is different from what we have stored. - /// If it has changed, it indicates that someone has modified the pipe; update our stored id. - /// \return true if changed, false if not. - bool update_pipe_timestamps() { - if (!pipe_fd.valid()) return false; - file_id_t timestamps = file_id_for_fd(pipe_fd.fd()); - if (timestamps == this->pipe_timestamps) { - return false; - } - this->pipe_timestamps = timestamps; - return true; - } - - public: - explicit universal_notifier_named_pipe_t(const wchar_t *test_path) - : pipe_fd(make_fifo(test_path, L".notifier")) {} - - ~universal_notifier_named_pipe_t() override = default; - - int notification_fd() const override { - if (!pipe_fd.valid()) return -1; - // If we are waiting for the pipe to be readable, return it for select. - // Otherwise we expect it to be readable already; return invalid. - switch (state) { - case waiting_for_readable: - return pipe_fd.fd(); - case polling_during_readable: - case waiting_to_drain: - return -1; - } - DIE("unreachable"); - } - - bool notification_fd_became_readable(int fd) override { - assert(fd == pipe_fd.fd() && "Wrong fd"); - UNUSED(fd); - switch (state) { - case waiting_for_readable: - // We are now readable. - // Grab the timestamp and return true indicating that we received a notification. - set_state(polling_during_readable); - update_pipe_timestamps(); - return true; - - case polling_during_readable: - case waiting_to_drain: - // We did not return an fd to wait on, so should not be called. - DIE("should not be called in this state"); - } - DIE("unreachable"); - } - - void post_notification() override { - if (!pipe_fd.valid()) return; - // We need to write some data (any data) to the pipe, then wait for a while, then read - // it back. Nobody is expected to read it except us. - FLOGF(uvar_notifier, "writing to pipe (written %lu)", (unsigned long)drain_amount); - char c[1] = {'\0'}; - ssize_t amt_written = write(pipe_fd.fd(), c, sizeof c); - if (amt_written < 0 && (errno == EWOULDBLOCK || errno == EAGAIN)) { - // Very unusual: the pipe is full! Try to read some and repeat once. - drain_excess(); - amt_written = write(pipe_fd.fd(), c, sizeof c); - if (amt_written < 0) { - FLOG(uvar_notifier, "pipe could not be drained, skipping notification"); - return; - } - FLOG(uvar_notifier, "pipe drained"); - } - assert(amt_written >= 0 && "Amount should not be negative"); - this->drain_amount += amt_written; - // We unconditionally set our state to waiting to drain. - set_state(waiting_to_drain); - update_pipe_timestamps(); - } - - unsigned long usec_delay_between_polls() const override { - if (!pipe_fd.valid()) return 0; - switch (state) { - case waiting_for_readable: - // No polling necessary until it becomes readable. - return 0; - - case polling_during_readable: - case waiting_to_drain: - return k_flash_duration_usec; - } - DIE("Unreachable"); - } - - bool poll() override { - if (!pipe_fd.valid()) return false; - switch (state) { - case waiting_for_readable: - // Nothing to do until the fd is readable. - return false; - - case polling_during_readable: { - // If we're no longer readable, go back to wait mode. - // Conversely, if we have been readable too long, perhaps some fish died while its - // written data was still on the pipe; drain some. - if (!poll_fd_readable(pipe_fd.fd())) { - set_state(waiting_for_readable); - } else if (get_time() >= state_start_usec + k_readable_too_long_duration_usec) { - drain_excess(); - } - // Sync if the pipe's timestamp is different, meaning someone modified the pipe - // since we last saw it. - if (update_pipe_timestamps()) { - FLOG(uvar_notifier, "pipe changed, will sync uvars"); - return true; - } - return false; - } - - case waiting_to_drain: { - // We wrote data to the pipe. Maybe read it back. - // If we are still readable, then there is still data on the pipe; maybe another - // change occurred with ours. - if (get_time() >= state_start_usec + k_flash_duration_usec) { - drain_written(); - if (!poll_fd_readable(pipe_fd.fd())) { - set_state(waiting_for_readable); - } else { - set_state(polling_during_readable); - } - } - return update_pipe_timestamps(); - } - } - DIE("Unreachable"); - } - -#else // this class isn't valid on this system - public: - universal_notifier_named_pipe_t(const wchar_t *test_path) { - static_cast(test_path); - DIE("universal_notifier_named_pipe_t cannot be used on this system"); - } -#endif -}; -} // namespace - -universal_notifier_t::notifier_strategy_t universal_notifier_t::resolve_default_strategy() { -#ifdef FISH_NOTIFYD_AVAILABLE - return strategy_notifyd; -#elif defined(__CYGWIN__) - return strategy_shmem_polling; -#else - return strategy_named_pipe; -#endif -} - -universal_notifier_t &universal_notifier_t::default_notifier() { - static std::unique_ptr result = - new_notifier_for_strategy(universal_notifier_t::resolve_default_strategy()); - return *result; -} - -std::unique_ptr universal_notifier_t::new_notifier_for_strategy( - universal_notifier_t::notifier_strategy_t strat, const wchar_t *test_path) { - switch (strat) { - case strategy_notifyd: { - return make_unique(); - } - case strategy_shmem_polling: { - return make_unique(); - } - case strategy_named_pipe: { - return make_unique(test_path); - } - } - DIE("should never reach this statement"); - return nullptr; -} - -// Default implementations. -universal_notifier_t::universal_notifier_t() = default; - -universal_notifier_t::~universal_notifier_t() = default; - -int universal_notifier_t::notification_fd() const { return -1; } - -bool universal_notifier_t::poll() { return false; } - -void universal_notifier_t::post_notification() {} - -unsigned long universal_notifier_t::usec_delay_between_polls() const { return 0; } - -bool universal_notifier_t::notification_fd_became_readable(int fd) { - UNUSED(fd); - return false; -} - -void env_universal_notifier_t_default_notifier_post_notification_ffi() { - return universal_notifier_t::default_notifier().post_notification(); -} diff --git a/src/env_universal_common.h b/src/env_universal_common.h index d68baadb2..d8739914f 100644 --- a/src/env_universal_common.h +++ b/src/env_universal_common.h @@ -16,79 +16,7 @@ #include "wutil.h" #if INCLUDE_RUST_HEADERS -#include "env_universal_common.rs.h" +#include "universal_notifier/mod.rs.h" #endif -/// 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. -class universal_notifier_t { - public: - enum notifier_strategy_t { - // 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, - }; - - universal_notifier_t(const universal_notifier_t &) = delete; - universal_notifier_t &operator=(const universal_notifier_t &) = delete; - - protected: - universal_notifier_t(); - - public: - static notifier_strategy_t resolve_default_strategy(); - virtual ~universal_notifier_t(); - - // Factory constructor. - static std::unique_ptr new_notifier_for_strategy( - notifier_strategy_t strat, const wchar_t *test_path = nullptr); - - // Default instance. Other instances are possible for testing. - static universal_notifier_t &default_notifier(); - - // FFI helper so autocxx can "deduce" the lifetime. - static universal_notifier_t &default_notifier_ffi(int &) { return default_notifier(); } - - // Does a fast poll(). Returns true if changed. - virtual bool poll(); - - // Triggers a notification. - virtual void post_notification(); - - // Recommended delay between polls. A value of 0 means no polling required (so no timeout). - virtual unsigned long usec_delay_between_polls() const; - - // Returns the fd from which to watch for events, or -1 if none. - virtual int notification_fd() const; - - // The notification_fd is readable; drain it. Returns true if a notification is considered to - // have been posted. - virtual bool notification_fd_became_readable(int fd); -}; - -wcstring get_runtime_path(); - -void env_universal_notifier_t_default_notifier_post_notification_ffi(); - #endif diff --git a/src/fish_tests.cpp b/src/fish_tests.cpp index 1ae39f2a5..dc7e28566 100644 --- a/src/fish_tests.cpp +++ b/src/fish_tests.cpp @@ -1692,102 +1692,6 @@ static void test_undo() { do_test(line.text() == L"abc"); } -#define UVARS_TEST_PATH L"test/fish_uvars_test/varsfile.txt" - -// todo!("port this") -bool poll_notifier(const std::unique_ptr ¬e) { - if (note->poll()) return true; - - bool result = false; - int fd = note->notification_fd(); - if (fd >= 0 && poll_fd_readable(fd)) { - result = note->notification_fd_became_readable(fd); - } - return result; -} - -// todo!("port this") -static void test_notifiers_with_strategy(universal_notifier_t::notifier_strategy_t strategy) { - say(L"Testing universal notifiers with strategy %d", (int)strategy); - constexpr size_t notifier_count = 16; - std::unique_ptr notifiers[notifier_count]; - - // Populate array of notifiers. - for (auto ¬ifier : notifiers) { - notifier = universal_notifier_t::new_notifier_for_strategy(strategy, UVARS_TEST_PATH); - } - - // Nobody should poll yet. - for (const auto ¬ifier : notifiers) { - if (poll_notifier(notifier)) { - err(L"Universal variable notifier polled true before any changes, with strategy %d", - (int)strategy); - } - } - - // Tweak each notifier. Verify that others see it. - for (size_t post_idx = 0; post_idx < notifier_count; post_idx++) { - notifiers[post_idx]->post_notification(); - - if (strategy == universal_notifier_t::strategy_notifyd) { - // 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. - usleep(40000); - } - - for (size_t i = 0; i < notifier_count; i++) { - bool polled = poll_notifier(notifiers[i]); - - // We aren't concerned with the one who posted. Poll from it (to drain it), and then - // skip it. - if (i == post_idx) { - continue; - } - - if (!polled) { - err(L"Universal variable notifier (%lu) %p polled failed to notice changes, with " - L"strategy %d", - i, notifiers[i].get(), (int)strategy); - continue; - } - // It should not poll again immediately. - if (poll_notifier(notifiers[i])) { - err(L"Universal variable notifier (%lu) %p polled twice in a row with strategy %d", - i, notifiers[i].get(), (int)strategy); - } - } - - // Named pipes have special cleanup requirements. - if (strategy == universal_notifier_t::strategy_named_pipe) { - usleep(1000000 / 10); // corresponds to NAMED_PIPE_FLASH_DURATION_USEC - // Have to clean up the posted one first, so that the others see the pipe become no - // longer readable. - poll_notifier(notifiers[post_idx]); - for (const auto ¬ifier : notifiers) { - poll_notifier(notifier); - } - } - } - - // Nobody should poll now. - for (const auto ¬ifier : notifiers) { - if (poll_notifier(notifier)) { - err(L"Universal variable notifier polled true after all changes, with strategy %d", - (int)strategy); - } - } -} - -// todo!("port this") -static void test_universal_notifiers() { - if (system("mkdir -p test/fish_uvars_test/ && touch test/fish_uvars_test/varsfile.txt")) { - err(L"mkdir failed"); - } - - auto strategy = universal_notifier_t::resolve_default_strategy(); - test_notifiers_with_strategy(strategy); -} - // todo!("port this") static void test_new_parser_correctness() { say(L"Testing parser correctness"); @@ -2769,7 +2673,6 @@ static const test_t s_tests[]{ {TEST_GROUP("colors"), test_colors}, {TEST_GROUP("input"), test_input}, {TEST_GROUP("undo"), test_undo}, - {TEST_GROUP("universal"), test_universal_notifiers}, {TEST_GROUP("completion_insertions"), test_completion_insertions}, {TEST_GROUP("illegal_command_exit_code"), test_illegal_command_exit_code}, {TEST_GROUP("maybe"), test_maybe}, diff --git a/src/input_common.cpp b/src/input_common.cpp index 27094ce71..333501588 100644 --- a/src/input_common.cpp +++ b/src/input_common.cpp @@ -61,7 +61,8 @@ using readb_result_t = int; static readb_result_t readb(int in_fd) { assert(in_fd >= 0 && "Invalid in fd"); - universal_notifier_t& notifier = universal_notifier_t::default_notifier(); + auto notifier_box = default_notifier(); + const auto& notifier = *notifier_box; auto fdset_box = new_fd_readable_set(); fd_readable_set_t& fdset = *fdset_box; for (;;) { @@ -76,15 +77,8 @@ static readb_result_t readb(int in_fd) { int notifier_fd = notifier.notification_fd(); fdset.add(notifier_fd); - // Get its suggested delay (possibly none). - // Note a 0 here means do not poll. - uint64_t timeout = kNoTimeout; - if (uint64_t usecs_delay = notifier.usec_delay_between_polls()) { - timeout = usecs_delay; - } - // Here's where we call select(). - int select_res = fdset.check_readable(timeout); + int select_res = fdset.check_readable(kNoTimeout); if (select_res < 0) { if (errno == EINTR || errno == EAGAIN) { // A signal. @@ -99,8 +93,7 @@ static readb_result_t readb(int in_fd) { // The priority order is: uvars, stdin, ioport. // Check to see if we want a universal variable barrier. // This may come about through readability, or through a call to poll(). - if ((fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)) || - notifier.poll()) { + if (fdset.test(notifier_fd) && notifier.notification_fd_became_readable(notifier_fd)) { return readb_uvar_notified; }