feat(macos): port Ghostty's macOS code paths (Metal, CoreText, Darwin)
Ports the macOS-specific Zig code behind cfg(target_os = "macos"): - renderer: a GraphicsApi trait with generic::Renderer<G>; Metal backend (all renderer/metal/* files, shaders.metal verbatim, runtime library compile, IOSurface layer, CVDisplayLink), shadertoy -> MSL via naga; OpenGL remains the backend elsewhere. - font: CoreText face, shaper and discovery with build.rs backend selection (coretext default; coretext-freetype / coretext-harfbuzz / coretext-noshape features), no fontconfig/freetype/harfbuzz on Apple. - input: KeymapDarwin via UCKeyTranslate, option-as-alt, mac keycodes. - os: macos, mach, cf_release_thread, NSProcessInfo args and macOS branches; config: macOS defaults, keybinds, Application Support paths. - termio/core: login(1) wrapping, openpty/execvpe, Darwin child exit handling, os_log logging. Compile-checked for aarch64/x86_64-apple-darwin (clippy -D warnings clean) using the cross setup in docs/rewrite/notes/macos-M1.md; cannot run here. The embedded runtime/C API (embedded.zig) is the next step. Also includes the cross-platform integration edits shared by the crash, verification, Windows and macOS work (Cargo target-gating, thread crash-context hooks, apprt-gtk surface/IME refactor, termio/os cfg gates), a cfg fix so ghostty-config builds on wasm32, and an allow for the intentional recursion in the crash overflow test. Test Plan: - cargo test --workspace (3224 passed) - cargo clippy --workspace --all-targets -- -D warnings - cargo check --target aarch64-apple-darwin -p ghostty-renderer (with the documented cross env)
This commit is contained in:
1 parent
76771bed0d
commit
bab49ee596
124 files changed
+14936
-1242
No files matched your search
@@ -221,9 +221,37 @@ impl std::fmt::Debug for Command {
|
||||
|
||||
/// Everything that must be allocated before forking.
|
||||
struct Prepared {
|
||||
#[cfg_attr(target_vendor = "apple", allow(dead_code))]
|
||||
path: CString,
|
||||
argv: Vec<CString>,
|
||||
envp: Vec<CString>,
|
||||
/// Darwin has no `execvpe`: the candidate executable paths (the PATH
|
||||
/// search of Zig's `execvpeZ`, precomputed so the child doesn't allocate).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
candidates: Vec<CString>,
|
||||
}
|
||||
|
||||
/// The default PATH used when `PATH` is unset (Zig `std.posix.execvpeZ`:
|
||||
/// `/usr/local/bin:/bin/:/usr/bin`).
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
const DEFAULT_PATH: &str = "/usr/local/bin:/bin/:/usr/bin";
|
||||
|
||||
/// The paths `execvpe` tries for `file` given a PATH value: the file itself
|
||||
/// if it contains a slash, otherwise `<dir>/<file>` per PATH entry (an empty
|
||||
/// entry means the current directory).
|
||||
#[cfg(any(target_vendor = "apple", test))]
|
||||
fn exec_candidates(file: &str, path_env: Option<&str>) -> Vec<String> {
|
||||
if file.contains('/') {
|
||||
return vec![file.to_owned()];
|
||||
}
|
||||
path_env
|
||||
.unwrap_or(DEFAULT_PATH)
|
||||
.split(':')
|
||||
.map(|dir| {
|
||||
let dir = if dir.is_empty() { "." } else { dir };
|
||||
format!("{dir}/{file}")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Command {
|
||||
@@ -266,7 +294,18 @@ impl Command {
|
||||
})
|
||||
.collect::<Result<_, _>>()?,
|
||||
};
|
||||
Ok(Prepared { path, argv, envp })
|
||||
#[cfg(target_vendor = "apple")]
|
||||
let candidates = exec_candidates(&self.path, std::env::var("PATH").ok().as_deref())
|
||||
.into_iter()
|
||||
.map(CString::new)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Prepared {
|
||||
path,
|
||||
argv,
|
||||
envp,
|
||||
#[cfg(target_vendor = "apple")]
|
||||
candidates,
|
||||
})
|
||||
}
|
||||
|
||||
/// Start the subprocess. This returns immediately once the child is
|
||||
@@ -409,6 +448,16 @@ impl Command {
|
||||
fn setup_fd(dup: impl Fn(&OwnedFd) -> rustix::io::Result<()>, src: &OwnedFd) -> Result<(), Errno> {
|
||||
// dup2 (like Zig's dup3 with flags=0) leaves the target without
|
||||
// CLOEXEC: we do NOT want this file descriptor to be closed on exec.
|
||||
//
|
||||
// Mac doesn't support dup3 so Zig uses dup2 there, and purposely
|
||||
// clears CLOEXEC on the source fd first.
|
||||
#[cfg(target_vendor = "apple")]
|
||||
{
|
||||
let flags = rustix::io::fcntl_getfd(src)?;
|
||||
if flags.contains(rustix::io::FdFlags::CLOEXEC) {
|
||||
rustix::io::fcntl_setfd(src, flags - rustix::io::FdFlags::CLOEXEC)?;
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match dup(src) {
|
||||
Err(Errno::INTR) => {}
|
||||
@@ -442,6 +491,7 @@ fn fork() -> Result<i32, Errno> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> Errno {
|
||||
debug_assert!(argv.last().is_some_and(|p| p.is_null()));
|
||||
@@ -455,12 +505,50 @@ fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> E
|
||||
Errno::from_raw_os_error(errno_now())
|
||||
}
|
||||
|
||||
/// Darwin has no `execvpe`; do the PATH search by hand like Zig's
|
||||
/// `execvpeZ` (EACCES is remembered, ENOENT/ENOTDIR continue the search,
|
||||
/// anything else is returned immediately).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
fn execvpe(prep: &Prepared, argv: &[*const c_char], envp: &[*const c_char]) -> Errno {
|
||||
debug_assert!(argv.last().is_some_and(|p| p.is_null()));
|
||||
debug_assert!(envp.last().is_some_and(|p| p.is_null()));
|
||||
let mut seen_eacces = false;
|
||||
for candidate in &prep.candidates {
|
||||
// SAFETY: the candidate is a valid NUL-terminated string; argv/envp
|
||||
// are NULL-terminated arrays of pointers into CStrings (owned by
|
||||
// `prep`) that outlive the call. execve only returns on failure.
|
||||
unsafe {
|
||||
libc::execve(candidate.as_ptr(), argv.as_ptr(), envp.as_ptr());
|
||||
}
|
||||
let err = errno_now();
|
||||
match err {
|
||||
libc::EACCES => seen_eacces = true,
|
||||
libc::ENOENT | libc::ENOTDIR => {}
|
||||
_ => return Errno::from_raw_os_error(err),
|
||||
}
|
||||
}
|
||||
Errno::from_raw_os_error(if seen_eacces {
|
||||
libc::EACCES
|
||||
} else {
|
||||
libc::ENOENT
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
#[allow(unsafe_code)]
|
||||
fn errno_now() -> i32 {
|
||||
// SAFETY: __errno_location returns a valid thread-local pointer.
|
||||
unsafe { *libc::__errno_location() }
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
fn errno_now() -> i32 {
|
||||
// SAFETY: __error returns a valid thread-local pointer.
|
||||
unsafe { *libc::__error() }
|
||||
}
|
||||
|
||||
fn write_stderr(msg: &[u8]) {
|
||||
// Best effort, no allocation.
|
||||
let _ = rustix::io::write(borrow_stderr(), msg);
|
||||
@@ -910,4 +998,18 @@ mod tests {
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| None).is_err());
|
||||
assert!(linux_cgroup_post_fork(&cmd, &hard, |_, _, _| Some(Err("x".into()))).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_candidates_search_path() {
|
||||
assert_eq!(exec_candidates("/bin/sh", Some("/x")), ["/bin/sh"]);
|
||||
assert_eq!(exec_candidates("a/b", None), ["a/b"]);
|
||||
assert_eq!(
|
||||
exec_candidates("sh", Some("/usr/bin::/bin")),
|
||||
["/usr/bin/sh", "./sh", "/bin/sh"]
|
||||
);
|
||||
assert_eq!(
|
||||
exec_candidates("sh", None),
|
||||
["/usr/local/bin/sh", "/bin//sh", "/usr/bin/sh"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ use ghostty_renderer::size::{GridSize, ScreenSize};
|
||||
use ghostty_terminal::terminal::Terminal;
|
||||
use rustix::{
|
||||
event::{PollFd, PollFlags},
|
||||
process::{Pid, PidfdFlags, Signal, WaitOptions},
|
||||
process::{Pid, Signal, WaitOptions},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -254,9 +254,17 @@ impl Exec {
|
||||
set_nonblocking(&read_fd);
|
||||
|
||||
// Start our read thread
|
||||
let read_thread = std::thread::Builder::new()
|
||||
.name("io-reader".into())
|
||||
.spawn(move || read_thread_main(&read_fd, &output, &quit_read))?;
|
||||
// The read thread shares our crash context (surface dimensions).
|
||||
let crash_dims = ghostty_crash::current_dims();
|
||||
let read_thread =
|
||||
std::thread::Builder::new()
|
||||
.name("io-reader".into())
|
||||
.spawn(move || {
|
||||
// Setup our crash metadata
|
||||
let _crash =
|
||||
ghostty_crash::ThreadGuard::new(ghostty_crash::ThreadType::Io, crash_dims);
|
||||
read_thread_main(&read_fd, &output, &quit_read);
|
||||
})?;
|
||||
|
||||
Ok(ThreadData {
|
||||
start,
|
||||
@@ -339,7 +347,9 @@ impl Exec {
|
||||
|
||||
/// How we notice the child exiting.
|
||||
enum ProcessWatch {
|
||||
/// A pidfd that becomes readable when the process exits.
|
||||
/// A pidfd that becomes readable when the process exits (Linux only; no
|
||||
/// equivalent is used on Darwin where libxev uses kqueue, so we poll).
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
PidFd(OwnedFd, i32),
|
||||
/// Fallback: `waitpid(WNOHANG)` every [`PROCESS_POLL_MS`].
|
||||
Poll(i32, Instant),
|
||||
@@ -354,13 +364,19 @@ impl ProcessWatch {
|
||||
let Some(p) = Pid::from_raw(pid) else {
|
||||
return ProcessWatch::Done;
|
||||
};
|
||||
match rustix::process::pidfd_open(p, PidfdFlags::empty()) {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
match rustix::process::pidfd_open(p, rustix::process::PidfdFlags::empty()) {
|
||||
Ok(fd) => ProcessWatch::PidFd(fd, pid),
|
||||
Err(err) => {
|
||||
log::warn!("pidfd_open failed ({err}), falling back to polling waitpid");
|
||||
ProcessWatch::Poll(pid, Instant::now() + Duration::from_millis(PROCESS_POLL_MS))
|
||||
}
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
{
|
||||
let _ = p;
|
||||
ProcessWatch::Poll(pid, Instant::now() + Duration::from_millis(PROCESS_POLL_MS))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,6 +432,7 @@ impl ThreadData {
|
||||
/// An fd that becomes readable when the child exited.
|
||||
pub fn process_fd(&self) -> Option<BorrowedFd<'_>> {
|
||||
match &self.watch {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
ProcessWatch::PidFd(fd, _) => Some(fd.as_fd()),
|
||||
_ => None,
|
||||
}
|
||||
@@ -513,7 +530,9 @@ impl ThreadData {
|
||||
/// reap the child and report the exit.
|
||||
pub fn on_process_ready(&mut self, cmd_pid: Option<i32>) {
|
||||
let pid = match &self.watch {
|
||||
ProcessWatch::PidFd(_, pid) | ProcessWatch::Poll(pid, _) => *pid,
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
ProcessWatch::PidFd(_, pid) => *pid,
|
||||
ProcessWatch::Poll(pid, _) => *pid,
|
||||
_ => return,
|
||||
};
|
||||
let _ = cmd_pid;
|
||||
@@ -802,6 +821,12 @@ impl Subprocess {
|
||||
};
|
||||
env.insert("TERMINFO", terminfo);
|
||||
} else {
|
||||
if cfg!(target_vendor = "apple") {
|
||||
log::warn!("ghostty terminfo not found, using xterm-256color");
|
||||
log::warn!("the terminfo SHOULD exist on macos, please ensure");
|
||||
log::warn!("you're using a valid app bundle.");
|
||||
}
|
||||
|
||||
env.insert("TERM", "xterm-256color");
|
||||
env.insert("COLORTERM", "truecolor");
|
||||
}
|
||||
@@ -809,6 +834,14 @@ impl Subprocess {
|
||||
// Add our binary to the path if we can find it.
|
||||
add_ghostty_path(&mut env);
|
||||
|
||||
// On macOS, export additional data directories from our
|
||||
// application bundle.
|
||||
if cfg!(target_vendor = "apple")
|
||||
&& let Some(resources_dir) = &cfg.resources_dir
|
||||
{
|
||||
darwin_bundle_env(&mut env, resources_dir);
|
||||
}
|
||||
|
||||
// Set environment variables used by some programs (such as neovim) to detect
|
||||
// which terminal emulator and version they're running under.
|
||||
env.insert("TERM_PROGRAM", "ghostty");
|
||||
@@ -1189,6 +1222,34 @@ fn add_ghostty_path(env: &mut EnvMap) {
|
||||
}
|
||||
}
|
||||
|
||||
/// macOS: export additional data directories from our application bundle
|
||||
/// (`XDG_DATA_DIRS` and `MANPATH`).
|
||||
pub fn darwin_bundle_env(env: &mut EnvMap, resources_dir: &str) {
|
||||
let xdg_data_dir_key = "XDG_DATA_DIRS";
|
||||
let data_dir = format!("{resources_dir}/..");
|
||||
let current = env.get(xdg_data_dir_key).map_or_else(
|
||||
|| "/usr/local/share:/usr/share".to_owned(),
|
||||
|v| v.to_string_lossy().into_owned(),
|
||||
);
|
||||
env.insert(
|
||||
xdg_data_dir_key,
|
||||
ghostty_os::append_env(¤t, &data_dir),
|
||||
);
|
||||
|
||||
let manpath_key = "MANPATH";
|
||||
let man_dir = format!("{resources_dir}/../man");
|
||||
// Always append with colon in front, as it mean that if
|
||||
// `MANPATH` is empty, then it should be treated as an extra
|
||||
// path instead of overriding all paths set by OS.
|
||||
let current = env
|
||||
.get(manpath_key)
|
||||
.map_or_else(String::new, |v| v.to_string_lossy().into_owned());
|
||||
env.insert(
|
||||
manpath_key,
|
||||
ghostty_os::append_env_always(¤t, &man_dir),
|
||||
);
|
||||
}
|
||||
|
||||
/// Kill the underlying subprocess. This sends a SIGHUP to the child
|
||||
/// process group. This also waits for the command to exit.
|
||||
fn kill_command(command: &Command) -> Result<(), Error> {
|
||||
@@ -1216,6 +1277,10 @@ fn kill_pid(pid_raw: i32) -> Result<(), Error> {
|
||||
loop {
|
||||
match rustix::process::kill_process_group(pgid, Signal::HUP) {
|
||||
Ok(()) => log::debug!("process group killed pgid={}", pgid.as_raw_nonzero()),
|
||||
// EPERM is expected on Darwin and ignored.
|
||||
Err(rustix::io::Errno::PERM) if cfg!(target_vendor = "apple") => {
|
||||
log::debug!("killpg failed with EPERM, expected on Darwin and ignoring");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"error killing process group pgid={} err={err}",
|
||||
@@ -1286,9 +1351,34 @@ fn getpgid(pid: Pid) -> Option<Pid> {
|
||||
/// configured command. This isn't as straightforward as it seems since
|
||||
/// we deal with shell-wrapping, etc.
|
||||
///
|
||||
/// (The macOS `login(1)` branch of the Zig function is not ported, Linux
|
||||
/// only; neither is the Windows `cmd.exe` branch.)
|
||||
/// On macOS the command is wrapped in `login(1)` (see
|
||||
/// [`exec_command_with`]).
|
||||
pub fn exec_command(command: &ConfigCommand) -> Vec<String> {
|
||||
#[cfg(unix)]
|
||||
let passwd = ghostty_os::passwd::get;
|
||||
#[cfg(not(unix))]
|
||||
let passwd = || Err(std::io::Error::from(std::io::ErrorKind::Unsupported));
|
||||
exec_command_with(command, cfg!(target_vendor = "apple"), &passwd)
|
||||
}
|
||||
|
||||
/// [`exec_command`] with the platform and passwd lookup injected (Zig takes
|
||||
/// the passwd package as a comptime argument and `isDarwin()` at comptime),
|
||||
/// so the macOS `login(1)` logic is testable on every platform.
|
||||
///
|
||||
/// The `passwd` function is used by macOS to determine the username and
|
||||
/// home directory for the login shell. It is unused on other platforms.
|
||||
pub fn exec_command_with(
|
||||
command: &ConfigCommand,
|
||||
darwin: bool,
|
||||
passwd: &dyn Fn() -> std::io::Result<ghostty_os::passwd::Entry>,
|
||||
) -> Vec<String> {
|
||||
// If we're on macOS, we have to use `login(1)` to get all of
|
||||
// the proper environment variables set, a login shell, and proper
|
||||
// hushlogin behavior.
|
||||
if darwin && let Some(args) = darwin_login_command(command, passwd) {
|
||||
return args;
|
||||
}
|
||||
|
||||
match command {
|
||||
// We need to clone the command since there's no guarantee the config remains valid.
|
||||
ConfigCommand::Direct(args) => args.clone(),
|
||||
@@ -1312,6 +1402,124 @@ pub fn exec_command(command: &ConfigCommand) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The macOS `login(1)` wrapper; `None` falls back to POSIX behavior (Zig
|
||||
/// `break :darwin`).
|
||||
fn darwin_login_command(
|
||||
command: &ConfigCommand,
|
||||
passwd: &dyn Fn() -> std::io::Result<ghostty_os::passwd::Entry>,
|
||||
) -> Option<Vec<String>> {
|
||||
let passwd = match passwd() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
log::warn!("failed to read passwd, not using a login shell err={err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(username) = passwd.name else {
|
||||
log::warn!("failed to get username, not using a login shell");
|
||||
return None;
|
||||
};
|
||||
|
||||
let hush = match &passwd.home {
|
||||
Some(home) => {
|
||||
if std::fs::read_dir(home).is_err() {
|
||||
log::warn!("failed to open home dir, not checking for hushlogin");
|
||||
false
|
||||
} else {
|
||||
std::path::Path::new(home).join(".hushlogin").exists()
|
||||
}
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
// If we made it this far we're going to start building
|
||||
// the actual command.
|
||||
//
|
||||
// This capacity is chosen based on what we'd need to
|
||||
// execute a shell command (very common). We can/will
|
||||
// grow if necessary for a longer command (uncommon).
|
||||
let mut args: Vec<String> = Vec::with_capacity(9);
|
||||
|
||||
// The reason for executing login this way is unclear. This
|
||||
// comment will attempt to explain but prepare for a truly
|
||||
// unhinged reality.
|
||||
//
|
||||
// The first major issue is that on macOS, a lot of users
|
||||
// put shell configurations in ~/.bash_profile instead of
|
||||
// ~/.bashrc (or equivalent for another shell). This file is only
|
||||
// loaded for a login shell so macOS users expect all their terminals
|
||||
// to be login shells. No other platform behaves this way and its
|
||||
// totally braindead but somehow the entire dev community on
|
||||
// macOS has cargo culted their way to this reality so we have to
|
||||
// do it...
|
||||
//
|
||||
// To get a login shell, you COULD just prepend argv0 with a `-`
|
||||
// but that doesn't fully work because `getlogin()` C API will
|
||||
// return the wrong value, SHELL won't be set, and various
|
||||
// other login behaviors that macOS users expect.
|
||||
//
|
||||
// The proper way is to use `login(1)`. But login(1) forces
|
||||
// the working directory to change to the home directory,
|
||||
// which we may not want. If we specify "-l" then we can avoid
|
||||
// this behavior but now the shell isn't a login shell.
|
||||
//
|
||||
// There is another issue: `login(1)` on macOS 14.3 and earlier
|
||||
// checked for ".hushlogin" in the working directory. This means
|
||||
// that if we specify "-l" then we won't get hushlogin honored
|
||||
// if its in the home directory (which is standard). To get
|
||||
// around this, we check for hushlogin ourselves and if present
|
||||
// specify the "-q" flag to login(1).
|
||||
//
|
||||
// So to get all the behaviors we want, we specify "-l" but
|
||||
// execute "bash" (which is built-in to macOS). We then use
|
||||
// the bash builtin "exec" to replace the process with a login
|
||||
// shell ("-l" on exec) with the command we really want.
|
||||
//
|
||||
// We use "bash" instead of other shells that ship with macOS
|
||||
// because as of macOS Sonoma, we found with a microbenchmark
|
||||
// that bash can `exec` into the desired command ~2x faster
|
||||
// than zsh.
|
||||
//
|
||||
// To figure out a lot of this logic I read the login.c
|
||||
// source code in the OSS distribution Apple provides for
|
||||
// macOS.
|
||||
//
|
||||
// Awesome.
|
||||
args.push("/usr/bin/login".into());
|
||||
if hush {
|
||||
args.push("-q".into());
|
||||
}
|
||||
args.push("-flp".into());
|
||||
args.push(username);
|
||||
|
||||
match command {
|
||||
// Direct args can be passed directly to login, since
|
||||
// login uses execvp we don't need to worry about PATH
|
||||
// searching.
|
||||
ConfigCommand::Direct(v) => args.extend(v.iter().cloned()),
|
||||
|
||||
ConfigCommand::Shell(v) => {
|
||||
// Use "exec" to replace the bash process with
|
||||
// our intended command so we don't have a parent
|
||||
// process hanging around.
|
||||
let cmd = format!("exec -l {v}");
|
||||
|
||||
// We execute bash with "--noprofile --norc" so that it doesn't
|
||||
// load startup files so that (1) our shell integration doesn't
|
||||
// break and (2) user configuration doesn't mess this process
|
||||
// up.
|
||||
args.push("/bin/bash".into());
|
||||
args.push("--noprofile".into());
|
||||
args.push("--norc".into());
|
||||
args.push("-c".into());
|
||||
args.push(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
Some(args)
|
||||
}
|
||||
|
||||
impl Drop for ThreadData {
|
||||
fn drop(&mut self) {
|
||||
// Join the reader if thread_exit wasn't called (shouldn't happen).
|
||||
@@ -1328,10 +1536,95 @@ impl Drop for ThreadData {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pw(name: Option<&str>) -> impl Fn() -> std::io::Result<ghostty_os::passwd::Entry> {
|
||||
let name = name.map(str::to_owned);
|
||||
move || {
|
||||
Ok(ghostty_os::passwd::Entry {
|
||||
name: name.clone(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_darwin_shell_command() {
|
||||
let result = exec_command_with(
|
||||
&ConfigCommand::Shell("foo bar baz".into()),
|
||||
true,
|
||||
&pw(Some("testuser")),
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
[
|
||||
"/usr/bin/login",
|
||||
"-flp",
|
||||
"testuser",
|
||||
"/bin/bash",
|
||||
"--noprofile",
|
||||
"--norc",
|
||||
"-c",
|
||||
"exec -l foo bar baz"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_darwin_direct_command() {
|
||||
let result = exec_command_with(
|
||||
&ConfigCommand::Direct(vec!["foo".into(), "bar baz".into()]),
|
||||
true,
|
||||
&pw(Some("testuser")),
|
||||
);
|
||||
assert_eq!(
|
||||
result,
|
||||
["/usr/bin/login", "-flp", "testuser", "foo", "bar baz"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_darwin_hushlogin() {
|
||||
let dir = std::env::temp_dir().join(format!("ghostty-hush-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join(".hushlogin"), "").unwrap();
|
||||
let home = dir.to_str().unwrap().to_owned();
|
||||
let passwd = move || {
|
||||
Ok(ghostty_os::passwd::Entry {
|
||||
name: Some("u".into()),
|
||||
home: Some(home.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let result = exec_command_with(&ConfigCommand::Direct(vec!["x".into()]), true, &passwd);
|
||||
assert_eq!(result, ["/usr/bin/login", "-q", "-flp", "u", "x"]);
|
||||
std::fs::remove_dir_all(&dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_darwin_falls_back_without_username() {
|
||||
// Empty passwd entry means we can't construct a macOS login
|
||||
// command and falls back to POSIX behavior.
|
||||
let result =
|
||||
exec_command_with(&ConfigCommand::Shell("foo bar baz".into()), true, &pw(None));
|
||||
assert_eq!(result, ["/bin/sh", "-c", "foo bar baz"]);
|
||||
|
||||
// Failed passwd entry also falls back.
|
||||
let fail = || Err(std::io::Error::other("fail"));
|
||||
let result = exec_command_with(
|
||||
&ConfigCommand::Direct(vec!["foo".into(), "bar baz".into()]),
|
||||
true,
|
||||
&fail,
|
||||
);
|
||||
assert_eq!(result, ["foo", "bar baz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_command_shell_command() {
|
||||
// execCommand: shell command, empty passwd
|
||||
let result = exec_command(&ConfigCommand::Shell("foo bar baz".into()));
|
||||
let result = exec_command_with(
|
||||
&ConfigCommand::Shell("foo bar baz".into()),
|
||||
false,
|
||||
&pw(None),
|
||||
);
|
||||
assert_eq!(result, ["/bin/sh", "-c", "foo bar baz"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! ghostty-termio: see docs/rewrite/ARCHITECTURE.md for the module map.
|
||||
//!
|
||||
//! Linux only. Ported: `pty.zig`, `Command.zig`, the `terminfo/*` source
|
||||
//! Linux, with a Windows (ConPTY) port behind `cfg(windows)` (the `*_windows.rs`
|
||||
//! files, selected with `#[path]`). Ported: `pty.zig`, `Command.zig`, the `terminfo/*` source
|
||||
//! generator and everything in `termio/` (`Options`, `backend`, `message`,
|
||||
//! `mailbox`, `Termio`, `Thread`, `Exec`, `stream_handler`,
|
||||
//! `shell_integration`). See `docs/rewrite/notes/ghostty-termio-IO1.md` and
|
||||
@@ -19,11 +20,14 @@
|
||||
)]
|
||||
|
||||
pub mod backend;
|
||||
#[cfg_attr(windows, path = "command_windows.rs")]
|
||||
pub mod command;
|
||||
#[cfg_attr(windows, path = "exec_windows.rs")]
|
||||
pub mod exec;
|
||||
pub mod mailbox;
|
||||
pub mod message;
|
||||
pub mod options;
|
||||
#[cfg_attr(windows, path = "pty_windows.rs")]
|
||||
pub mod pty;
|
||||
pub mod shell_integration;
|
||||
pub mod sinks;
|
||||
@@ -32,7 +36,10 @@ pub mod stream_handler;
|
||||
pub mod terminfo;
|
||||
pub mod termio;
|
||||
pub mod thread;
|
||||
#[cfg_attr(windows, path = "wakeup_windows.rs")]
|
||||
pub mod wakeup;
|
||||
/// Pure Windows `CreateProcessW` input encoders (tested on every host).
|
||||
pub mod windows_args;
|
||||
|
||||
pub use backend::Backend;
|
||||
pub use command::Command;
|
||||
|
||||
@@ -152,6 +152,31 @@ mod tests {
|
||||
assert!(matches!(back, Message::Focused(true)));
|
||||
}
|
||||
|
||||
/// Block until the wakeup is signaled.
|
||||
#[cfg(unix)]
|
||||
fn wait_readable(w: &Wakeup) {
|
||||
let mut fds = [rustix::event::PollFd::from_borrowed_fd(
|
||||
w.as_fd(),
|
||||
rustix::event::PollFlags::IN,
|
||||
)];
|
||||
rustix::event::poll(&mut fds, None).unwrap();
|
||||
}
|
||||
|
||||
/// Block until the wakeup is signaled (this consumes the auto-reset
|
||||
/// event; the callers' `drain` after it is then a harmless no-op).
|
||||
#[cfg(windows)]
|
||||
#[allow(unsafe_code)]
|
||||
fn wait_readable(w: &Wakeup) {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
// SAFETY: valid event handle owned by the wakeup.
|
||||
unsafe {
|
||||
windows_sys::Win32::System::Threading::WaitForSingleObject(
|
||||
w.as_handle().as_raw_handle().cast(),
|
||||
windows_sys::Win32::System::Threading::INFINITE,
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_send_wakes_consumer_and_completes() {
|
||||
let mb = Mailbox::new().unwrap();
|
||||
@@ -161,11 +186,7 @@ mod tests {
|
||||
let consumer = mb.clone();
|
||||
let t = std::thread::spawn(move || {
|
||||
// Wait for the wakeup the full-queue path posts, then drain.
|
||||
let mut fds = [rustix::event::PollFd::from_borrowed_fd(
|
||||
consumer.wakeup().as_fd(),
|
||||
rustix::event::PollFlags::IN,
|
||||
)];
|
||||
rustix::event::poll(&mut fds, None).unwrap();
|
||||
wait_readable(consumer.wakeup());
|
||||
consumer.wakeup().drain();
|
||||
consumer.drain().count()
|
||||
});
|
||||
@@ -185,11 +206,7 @@ mod tests {
|
||||
let m2 = Arc::clone(&m);
|
||||
// The consumer needs the lock to make progress (like the IO thread).
|
||||
let t = std::thread::spawn(move || {
|
||||
let mut fds = [rustix::event::PollFd::from_borrowed_fd(
|
||||
consumer.wakeup().as_fd(),
|
||||
rustix::event::PollFlags::IN,
|
||||
)];
|
||||
rustix::event::poll(&mut fds, None).unwrap();
|
||||
wait_readable(consumer.wakeup());
|
||||
let mut g = m2.lock().unwrap();
|
||||
*g += 1;
|
||||
consumer.drain().count()
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
|
||||
|
||||
use rustix::{fs, process, pty, termios};
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
use rustix::{fs, pty};
|
||||
use rustix::{process, termios};
|
||||
|
||||
/// Redeclare the winsize struct so we can just use a Rust struct. The
|
||||
/// defaults on this are some reasonable screen size but you should probably
|
||||
@@ -106,25 +108,82 @@ pub struct Pty {
|
||||
pub slave: Option<OwnedFd>,
|
||||
}
|
||||
|
||||
/// Equivalent of glibc openpty(): the master is CLOEXEC (Ghostty sets this
|
||||
/// explicitly: only the slave fd should be inherited by the child), the
|
||||
/// slave is not.
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
fn open_pair(_size: Winsize) -> Result<(OwnedFd, OwnedFd), Error> {
|
||||
let master =
|
||||
pty::openpt(pty::OpenptFlags::RDWR | pty::OpenptFlags::NOCTTY | pty::OpenptFlags::CLOEXEC)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::grantpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::unlockpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
let name = pty::ptsname(&master, Vec::new()).map_err(|_| Error::OpenptyFailed)?;
|
||||
let slave = fs::open(
|
||||
name.as_c_str(),
|
||||
fs::OFlags::RDWR | fs::OFlags::NOCTTY,
|
||||
fs::Mode::empty(),
|
||||
)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
Ok((master, slave))
|
||||
}
|
||||
|
||||
/// Darwin: libc `openpty()` (`util.h`), exactly as Zig does, followed by
|
||||
/// setting CLOEXEC on the master fd (only the slave fd should be inherited
|
||||
/// by the child process).
|
||||
#[cfg(target_vendor = "apple")]
|
||||
#[allow(unsafe_code)]
|
||||
fn open_pair(size: Winsize) -> Result<(OwnedFd, OwnedFd), Error> {
|
||||
use std::os::fd::FromRawFd;
|
||||
|
||||
let mut ws = libc::winsize {
|
||||
ws_row: size.ws_row,
|
||||
ws_col: size.ws_col,
|
||||
ws_xpixel: size.ws_xpixel,
|
||||
ws_ypixel: size.ws_ypixel,
|
||||
};
|
||||
let mut master_fd: libc::c_int = -1;
|
||||
let mut slave_fd: libc::c_int = -1;
|
||||
// SAFETY: out-pointers reference live locals; the name/termios
|
||||
// arguments are allowed to be null; `ws` is only read.
|
||||
let rc = unsafe {
|
||||
libc::openpty(
|
||||
&raw mut master_fd,
|
||||
&raw mut slave_fd,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
&raw mut ws,
|
||||
)
|
||||
};
|
||||
if rc < 0 {
|
||||
return Err(Error::OpenptyFailed);
|
||||
}
|
||||
// SAFETY: openpty succeeded so both fds are valid and exclusively ours.
|
||||
let (master, slave) = unsafe {
|
||||
(
|
||||
OwnedFd::from_raw_fd(master_fd),
|
||||
OwnedFd::from_raw_fd(slave_fd),
|
||||
)
|
||||
};
|
||||
|
||||
// Set CLOEXEC on the master fd, only the slave fd should be inherited
|
||||
// by the child process (shell/command).
|
||||
match rustix::io::fcntl_getfd(&master) {
|
||||
Ok(flags) => {
|
||||
if let Err(err) = rustix::io::fcntl_setfd(&master, flags | rustix::io::FdFlags::CLOEXEC)
|
||||
{
|
||||
log::warn!("error setting CLOEXEC on master fd err={err}");
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("error getting flags for master fd err={err}"),
|
||||
}
|
||||
Ok((master, slave))
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
/// Open a new PTY with the given initial size.
|
||||
pub fn open(size: Winsize) -> Result<Pty, Error> {
|
||||
// Equivalent of glibc openpty(): the master is CLOEXEC (Ghostty sets
|
||||
// this explicitly: only the slave fd should be inherited by the
|
||||
// child), the slave is not.
|
||||
let master = pty::openpt(
|
||||
pty::OpenptFlags::RDWR | pty::OpenptFlags::NOCTTY | pty::OpenptFlags::CLOEXEC,
|
||||
)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::grantpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
pty::unlockpt(&master).map_err(|_| Error::OpenptyFailed)?;
|
||||
let name = pty::ptsname(&master, Vec::new()).map_err(|_| Error::OpenptyFailed)?;
|
||||
let slave = fs::open(
|
||||
name.as_c_str(),
|
||||
fs::OFlags::RDWR | fs::OFlags::NOCTTY,
|
||||
fs::Mode::empty(),
|
||||
)
|
||||
.map_err(|_| Error::OpenptyFailed)?;
|
||||
let (master, slave) = open_pair(size)?;
|
||||
|
||||
termios::tcsetwinsize(&master, size.into()).map_err(|_| Error::OpenptyFailed)?;
|
||||
|
||||
|
||||
@@ -101,13 +101,30 @@ fn basename(path: &str) -> &str {
|
||||
}
|
||||
|
||||
fn detect_shell(command: &Command) -> Option<Shell> {
|
||||
detect_shell_for(command, cfg!(target_vendor = "apple"))
|
||||
}
|
||||
|
||||
/// [`detect_shell`] for an explicit platform (`darwin == isDarwin()`).
|
||||
fn detect_shell_for(command: &Command, darwin: bool) -> Option<Shell> {
|
||||
let mut it = command.arg_iterator();
|
||||
let arg0 = it.next()?;
|
||||
let exe = basename(&arg0);
|
||||
|
||||
match exe {
|
||||
// (The Darwin /bin/bash special case does not apply on Linux.)
|
||||
"bash" => Some(Shell::Bash),
|
||||
"bash" => {
|
||||
// Apple distributes their own patched version of Bash 3.2
|
||||
// on macOS that disables the ENV-based POSIX startup path.
|
||||
// This means we're unable to perform our automatic shell
|
||||
// integration sequence in this specific environment.
|
||||
//
|
||||
// If we're running "/bin/bash" on Darwin, we can assume
|
||||
// we're using Apple's Bash because /bin is non-writable
|
||||
// on modern macOS due to System Integrity Protection.
|
||||
if darwin && arg0 == "/bin/bash" {
|
||||
return None;
|
||||
}
|
||||
Some(Shell::Bash)
|
||||
}
|
||||
"elvish" => Some(Shell::Elvish),
|
||||
"fish" => Some(Shell::Fish),
|
||||
"nu" => Some(Shell::Nushell),
|
||||
@@ -450,6 +467,14 @@ mod tests {
|
||||
assert_eq!(detect_shell(&sh("nu")), Some(Shell::Nushell));
|
||||
assert_eq!(detect_shell(&sh("zsh")), Some(Shell::Zsh));
|
||||
|
||||
// Apple's patched /bin/bash can't use the ENV-based integration.
|
||||
assert_eq!(detect_shell_for(&sh("/bin/bash"), true), None);
|
||||
assert_eq!(detect_shell_for(&sh("/bin/bash"), false), Some(Shell::Bash));
|
||||
assert_eq!(
|
||||
detect_shell_for(&sh("/opt/homebrew/bin/bash"), true),
|
||||
Some(Shell::Bash)
|
||||
);
|
||||
|
||||
assert_eq!(detect_shell(&sh("bash -c 'command'")), Some(Shell::Bash));
|
||||
assert_eq!(detect_shell(&sh("\"/a b/bash\"")), Some(Shell::Bash));
|
||||
assert_eq!(detect_shell(&sh("/usr/bin/zsh")), Some(Shell::Zsh));
|
||||
|
||||
@@ -27,17 +27,22 @@ use ghostty_apprt::surface::Message as SurfaceMessage;
|
||||
use ghostty_datastruct::blocking_queue::Timeout;
|
||||
use ghostty_renderer::size::Size;
|
||||
use ghostty_vt::{csi, modes::Mode};
|
||||
#[cfg(unix)]
|
||||
use rustix::event::{PollFd, PollFlags};
|
||||
|
||||
use crate::{
|
||||
backend::ThreadData as BackendThreadData,
|
||||
exec,
|
||||
message::Message,
|
||||
state::SharedStateExt,
|
||||
termio::{Error, Termio, ThreadData},
|
||||
wakeup::Wakeup,
|
||||
};
|
||||
|
||||
/// The Windows event loop (`WaitForMultipleObjects`).
|
||||
#[cfg(windows)]
|
||||
#[path = "thread_windows.rs"]
|
||||
mod windows_loop;
|
||||
|
||||
/// The number of milliseconds to coalesce certain messages like resize for.
|
||||
/// Not all message types are coalesced.
|
||||
pub const COALESCE_MIN_MS: u64 = 25;
|
||||
@@ -81,6 +86,9 @@ pub struct Thread {
|
||||
sync_reset_deadline: Option<Instant>,
|
||||
|
||||
flags: Flags,
|
||||
|
||||
/// The surface dimensions recorded in crash reports.
|
||||
pub crash_dims: Option<std::sync::Arc<ghostty_crash::SurfaceDims>>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -96,6 +104,7 @@ impl Thread {
|
||||
coalesce_resize: None,
|
||||
sync_reset_deadline: None,
|
||||
flags: Flags::default(),
|
||||
crash_dims: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,6 +118,10 @@ impl Thread {
|
||||
/// once the stop handle was notified; dropping the termio afterwards
|
||||
/// cleans everything up.
|
||||
pub fn thread_main(mut self, mut io: Termio) {
|
||||
// Setup our crash metadata
|
||||
let _crash =
|
||||
ghostty_crash::ThreadGuard::new(ghostty_crash::ThreadType::Io, self.crash_dims.clone());
|
||||
|
||||
// Call child function so we can use errors...
|
||||
let result = self.thread_main_(&mut io);
|
||||
if let Err(err) = result {
|
||||
@@ -140,6 +153,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
/// After an abnormal failure: discard messages until the stop signal.
|
||||
#[cfg(unix)]
|
||||
fn drain_until_stop(&mut self, io: &Termio) {
|
||||
let mailbox = io.mailbox().clone();
|
||||
loop {
|
||||
@@ -183,6 +197,7 @@ impl Thread {
|
||||
}
|
||||
|
||||
/// The event loop. Returns when stopped.
|
||||
#[cfg(unix)]
|
||||
fn run(&mut self, io: &mut Termio, td: &mut ThreadData) {
|
||||
let mailbox = io.mailbox().clone();
|
||||
|
||||
@@ -386,7 +401,8 @@ fn report_start_error(io: &Termio, err: &Error) {
|
||||
t.modes.set(Mode::CursorVisible, false);
|
||||
|
||||
let msg: String = match err {
|
||||
Error::Exec(exec::Error::Pty(crate::pty::Error::OpenptyFailed)) => "\
|
||||
#[cfg(unix)]
|
||||
Error::Exec(crate::exec::Error::Pty(crate::pty::Error::OpenptyFailed)) => "\
|
||||
Your system cannot allocate any more pty devices.
|
||||
|
||||
Ghostty requires a pty device to launch a new terminal.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! A tiny cross-thread wakeup primitive (replaces `xev.Async`).
|
||||
//!
|
||||
//! Backed by a non-blocking `eventfd`, so it can be included in a `poll(2)`
|
||||
//! set next to a pty fd or a pidfd. [`Wakeup::notify`] may be called from any
|
||||
//! Backed by a non-blocking `eventfd` (a non-blocking pipe on Darwin, which
|
||||
//! has no eventfd; libxev uses kqueue there), so it can be included in a
|
||||
//! `poll(2)` set next to a pty fd or a pidfd. [`Wakeup::notify`] may be called from any
|
||||
//! thread, any number of times; the waiter calls [`Wakeup::drain`] after
|
||||
//! `poll` reports the fd readable.
|
||||
|
||||
@@ -11,30 +12,65 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use rustix::event::{EventfdFlags, eventfd};
|
||||
/// The fds backing a wakeup: a single eventfd (read == write) on Linux, the
|
||||
/// two ends of a pipe on Darwin.
|
||||
struct Fds {
|
||||
read: OwnedFd,
|
||||
write: Option<OwnedFd>,
|
||||
}
|
||||
|
||||
/// See the module docs. Cloning shares the same underlying eventfd.
|
||||
impl Fds {
|
||||
#[cfg(not(target_vendor = "apple"))]
|
||||
fn new() -> io::Result<Fds> {
|
||||
use rustix::event::{EventfdFlags, eventfd};
|
||||
Ok(Fds {
|
||||
read: eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?,
|
||||
write: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_vendor = "apple")]
|
||||
fn new() -> io::Result<Fds> {
|
||||
use rustix::{
|
||||
fs::{OFlags, fcntl_getfl, fcntl_setfl},
|
||||
io::{FdFlags, fcntl_setfd},
|
||||
};
|
||||
let (read, write) = rustix::pipe::pipe()?;
|
||||
for fd in [&read, &write] {
|
||||
fcntl_setfd(fd, FdFlags::CLOEXEC)?;
|
||||
fcntl_setfl(fd, fcntl_getfl(fd)? | OFlags::NONBLOCK)?;
|
||||
}
|
||||
Ok(Fds {
|
||||
read,
|
||||
write: Some(write),
|
||||
})
|
||||
}
|
||||
|
||||
fn write_fd(&self) -> &OwnedFd {
|
||||
self.write.as_ref().unwrap_or(&self.read)
|
||||
}
|
||||
}
|
||||
|
||||
/// See the module docs. Cloning shares the same underlying fds.
|
||||
#[derive(Clone)]
|
||||
pub struct Wakeup(Arc<OwnedFd>, Option<Arc<dyn Fn() + Send + Sync>>);
|
||||
pub struct Wakeup(Arc<Fds>, Option<Arc<dyn Fn() + Send + Sync>>);
|
||||
|
||||
impl std::fmt::Debug for Wakeup {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("Wakeup").field(&self.0).finish()
|
||||
f.debug_tuple("Wakeup").field(&self.0.read).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Wakeup {
|
||||
pub fn new() -> io::Result<Wakeup> {
|
||||
let fd = eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?;
|
||||
Ok(Wakeup(Arc::new(fd), None))
|
||||
Ok(Wakeup(Arc::new(Fds::new()?), None))
|
||||
}
|
||||
|
||||
/// A wakeup that calls `hook` instead of signalling an eventfd. Used to
|
||||
/// bridge into waiters that are not `poll(2)`-based (the renderer
|
||||
/// thread's channel). `as_fd`/`drain` are meaningless for these.
|
||||
pub fn with_hook(hook: Arc<dyn Fn() + Send + Sync>) -> io::Result<Wakeup> {
|
||||
let fd = eventfd(0, EventfdFlags::CLOEXEC | EventfdFlags::NONBLOCK)?;
|
||||
Ok(Wakeup(Arc::new(fd), Some(hook)))
|
||||
Ok(Wakeup(Arc::new(Fds::new()?), Some(hook)))
|
||||
}
|
||||
|
||||
/// Wake up the waiter. Multiple notifications coalesce.
|
||||
@@ -44,7 +80,7 @@ impl Wakeup {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
match rustix::io::write(&*self.0, &1u64.to_ne_bytes()) {
|
||||
match rustix::io::write(self.0.write_fd(), &1u64.to_ne_bytes()) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(rustix::io::Errno::INTR) => {}
|
||||
// Counter would overflow: the waiter is already guaranteed
|
||||
@@ -58,19 +94,23 @@ impl Wakeup {
|
||||
/// Reset the notification state. Returns true if a notification was
|
||||
/// pending.
|
||||
pub fn drain(&self) -> bool {
|
||||
let mut buf = [0u8; 8];
|
||||
let mut buf = [0u8; 64];
|
||||
// An eventfd resets with one read; a pipe is read until empty.
|
||||
let mut any = false;
|
||||
loop {
|
||||
match rustix::io::read(&*self.0, &mut buf) {
|
||||
Ok(_) => return true,
|
||||
match rustix::io::read(&self.0.read, &mut buf) {
|
||||
Ok(_) if self.0.write.is_none() => return true,
|
||||
Ok(0) => return any,
|
||||
Ok(_) => any = true,
|
||||
Err(rustix::io::Errno::INTR) => {}
|
||||
Err(_) => return false,
|
||||
Err(_) => return any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The fd to poll for `POLLIN`.
|
||||
pub fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
self.0.as_fd()
|
||||
self.0.read.as_fd()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user