feat: honor async-backend with epoll/io_uring, fix last RV13 items
Resolves the 4 P3 items in docs/rewrite/notes/review-rv13-codex.md (final-Z16.md): - async-backend now takes effect like Ghostty: ghostty_os::xev ports libxev's backend detection (io_uring, then epoll); global init detects and logs it, the GTK startup applies the config with Zig's messages, and +version reports the real backend. The termio IO loop runs on a new reactor with epoll, io_uring (io-uring crate) and poll(2) (kept for macOS, where libxev uses kqueue). The 21 exec integration tests run under every backend; io_uring was exercised here. - auto-update-channel defaults to the build's release channel. - Windows error names: sharing violation -> AccessDenied, plus ERROR_FILE_EXISTS -> PathAlreadyExists and virus errors -> AntivirusInterference, mirroring std.os.windows.OpenFile's NTSTATUS mapping in Zig 0.15.2 (verified against its source by the orchestrator). - Exact ports of std.fs.path.basenameWindows/basenamePosix for theme name validation (drive, UNC and device roots). Test Plan: - cargo test --workspace --no-fail-fast (3555 passed, exit 0) - just check-macos (both), just check-windows, just check-wasm - +show-config --default --docs vs /usr/bin/ghostty (only the build-specific gtk-opengl-debug line differs)
This commit is contained in:
1 parent
3ce3225642
commit
1bd918159a
18 files changed
+2012
-742
No files matched your search
@@ -32,6 +32,10 @@ windows-sys = { version = "0.61", features = [
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
||||
# libxev's `IO_Uring.available()` probe (`os/xev.rs`).
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
io-uring = "0.7"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.190"
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ pub mod file_type;
|
||||
#[cfg(unix)]
|
||||
pub mod flatpak;
|
||||
pub mod osbytes;
|
||||
pub mod xev;
|
||||
pub use ghostty_zigfmt as zig_fmt;
|
||||
#[cfg(windows)]
|
||||
pub mod flatpak {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Runtime selection of the async event-loop backend (libxev's
|
||||
//! `Dynamic` API: `detect`, `prefer`, `backend`; `Backend.candidates`).
|
||||
//!
|
||||
//! On Linux the candidates are `io_uring` then `epoll`; every other platform
|
||||
//! has exactly one backend (kqueue / IOCP / wasi poll), for which libxev's
|
||||
//! dynamic API collapses to the static one: nothing can be selected.
|
||||
//!
|
||||
//! The selection is process-global, like the `pub var backend` in libxev's
|
||||
//! `dynamic.zig`. It defaults to the *last* candidate (`epoll`) without
|
||||
//! testing availability; call [`detect`] at startup (`global.zig`) and
|
||||
//! optionally [`prefer`] (GTK `Application.startupXev`) before any loop is
|
||||
//! created.
|
||||
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
/// Zig: `xev.Backend`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Backend {
|
||||
IoUring,
|
||||
Epoll,
|
||||
Kqueue,
|
||||
WasiPoll,
|
||||
Iocp,
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
/// `@tagName`.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Backend::IoUring => "io_uring",
|
||||
Backend::Epoll => "epoll",
|
||||
Backend::Kqueue => "kqueue",
|
||||
Backend::WasiPoll => "wasi_poll",
|
||||
Backend::Iocp => "iocp",
|
||||
}
|
||||
}
|
||||
|
||||
fn to_u8(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
fn from_u8(v: u8) -> Backend {
|
||||
match v {
|
||||
0 => Backend::IoUring,
|
||||
1 => Backend::Epoll,
|
||||
2 => Backend::Kqueue,
|
||||
3 => Backend::WasiPoll,
|
||||
_ => Backend::Iocp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Backend {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig: `Backend.candidates()` in priority order.
|
||||
pub fn candidates() -> &'static [Backend] {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
&[Backend::IoUring, Backend::Epoll]
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
&[Backend::Iocp]
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
&[Backend::WasiPoll]
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows", target_arch = "wasm32")))]
|
||||
{
|
||||
&[Backend::Kqueue]
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig: `xev.dynamic` (more than one candidate).
|
||||
pub fn is_dynamic() -> bool {
|
||||
candidates().len() > 1
|
||||
}
|
||||
|
||||
static BACKEND: AtomicU8 = AtomicU8::new(0);
|
||||
static INIT: std::sync::Once = std::sync::Once::new();
|
||||
|
||||
fn init_default() {
|
||||
INIT.call_once(|| {
|
||||
let last = *candidates().last().unwrap_or(&Backend::Epoll);
|
||||
BACKEND.store(last.to_u8(), Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
/// The backend in use (Zig: `xev.backend`).
|
||||
pub fn backend() -> Backend {
|
||||
init_default();
|
||||
Backend::from_u8(BACKEND.load(Ordering::SeqCst))
|
||||
}
|
||||
|
||||
/// Zig: `Api().available()`.
|
||||
pub fn available(be: Backend) -> bool {
|
||||
match be {
|
||||
Backend::Epoll => cfg!(target_os = "linux"),
|
||||
Backend::IoUring => io_uring_available(),
|
||||
Backend::Kqueue => cfg!(any(target_vendor = "apple", target_os = "freebsd")),
|
||||
Backend::WasiPoll => cfg!(target_arch = "wasm32"),
|
||||
Backend::Iocp => cfg!(windows),
|
||||
}
|
||||
}
|
||||
|
||||
/// libxev performs a harmless `io_uring_setup(256)` to see if we have
|
||||
/// io_uring support. We additionally require what our loop needs
|
||||
/// (`IORING_OP_POLL_ADD`, `IORING_OP_POLL_REMOVE` and the `EXT_ARG` timeout),
|
||||
/// so that "available" never selects a ring that cannot run the IO loop.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn io_uring_available() -> bool {
|
||||
let Ok(ring) = io_uring::IoUring::new(256) else {
|
||||
return false;
|
||||
};
|
||||
if !ring.params().is_feature_ext_arg() {
|
||||
return false;
|
||||
}
|
||||
let mut probe = io_uring::Probe::new();
|
||||
if ring.submitter().register_probe(&mut probe).is_err() {
|
||||
return false;
|
||||
}
|
||||
probe.is_supported(io_uring::opcode::PollAdd::CODE)
|
||||
&& probe.is_supported(io_uring::opcode::PollRemove::CODE)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn io_uring_available() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// There is no available backend (Zig: `error.NoAvailableBackends`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("NoAvailableBackends")]
|
||||
pub struct NoAvailableBackends;
|
||||
|
||||
/// Detect the preferred backend: the first available candidate.
|
||||
pub fn detect() -> Result<(), NoAvailableBackends> {
|
||||
init_default();
|
||||
for &be in candidates() {
|
||||
if available(be) {
|
||||
BACKEND.store(be.to_u8(), Ordering::SeqCst);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(NoAvailableBackends)
|
||||
}
|
||||
|
||||
/// Manually set the backend to use. If the backend is not a candidate or not
|
||||
/// available the backend in use is unchanged and false is returned.
|
||||
pub fn prefer(be: Backend) -> bool {
|
||||
init_default();
|
||||
if !candidates().contains(&be) || !available(be) {
|
||||
return false;
|
||||
}
|
||||
BACKEND.store(be.to_u8(), Ordering::SeqCst);
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The process-global is shared between tests.
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn names_match_zig_tags() {
|
||||
assert_eq!(Backend::IoUring.name(), "io_uring");
|
||||
assert_eq!(Backend::Epoll.to_string(), "epoll");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_picks_first_available_candidate() {
|
||||
let _g = LOCK.lock().unwrap();
|
||||
detect().unwrap();
|
||||
let expected = candidates()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|b| available(*b))
|
||||
.unwrap();
|
||||
assert_eq!(backend(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefer_rejects_non_candidates() {
|
||||
let _g = LOCK.lock().unwrap();
|
||||
detect().unwrap();
|
||||
let before = backend();
|
||||
assert!(!prefer(Backend::Iocp));
|
||||
assert_eq!(backend(), before);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn prefer_epoll_then_detect() {
|
||||
let _g = LOCK.lock().unwrap();
|
||||
assert!(prefer(Backend::Epoll));
|
||||
assert_eq!(backend(), Backend::Epoll);
|
||||
if available(Backend::IoUring) {
|
||||
assert!(prefer(Backend::IoUring));
|
||||
assert_eq!(backend(), Backend::IoUring);
|
||||
} else {
|
||||
let before = backend();
|
||||
assert!(!prefer(Backend::IoUring));
|
||||
assert_eq!(backend(), before);
|
||||
}
|
||||
detect().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -61,63 +61,67 @@ pub fn errno_name(errno: i32) -> &'static str {
|
||||
}
|
||||
|
||||
/// See the unix variant; this is the Windows (Win32 error code) table.
|
||||
///
|
||||
/// Zig's Windows file opens go through `std.os.windows.OpenFile`
|
||||
/// (`NtCreateFile`), which switches on the NTSTATUS. Rust opens with
|
||||
/// `CreateFileW`, whose error is `RtlNtStatusToDosError(status)`, so each arm
|
||||
/// below is the Win32 code of the NTSTATUS arm named in its comment (codes
|
||||
/// that `OpenFile` does not name are `error.Unexpected`):
|
||||
///
|
||||
/// | NTSTATUS | Win32 | Zig error |
|
||||
/// |---|---|---|
|
||||
/// | `OBJECT_NAME_INVALID` | `ERROR_INVALID_NAME` | `BadPathName` |
|
||||
/// | `OBJECT_NAME_NOT_FOUND` / `OBJECT_PATH_NOT_FOUND` | `ERROR_FILE_NOT_FOUND` / `ERROR_PATH_NOT_FOUND` | `FileNotFound` |
|
||||
/// | `BAD_NETWORK_PATH` / `BAD_NETWORK_NAME` | `ERROR_BAD_NETPATH` / `ERROR_BAD_NET_NAME` | `NetworkNotFound` |
|
||||
/// | `NO_MEDIA_IN_DEVICE` | `ERROR_NOT_READY` | `NoDevice` |
|
||||
/// | `SHARING_VIOLATION`, `ACCESS_DENIED`, `USER_MAPPED_FILE`, `FILE_IS_A_DIRECTORY` | `ERROR_SHARING_VIOLATION`, `ERROR_ACCESS_DENIED`, `ERROR_USER_MAPPED_FILE` | `AccessDenied` |
|
||||
/// | `PIPE_BUSY` / `PIPE_NOT_AVAILABLE` | `ERROR_PIPE_BUSY` | `PipeBusy` |
|
||||
/// | `OBJECT_NAME_COLLISION` | `ERROR_ALREADY_EXISTS` | `PathAlreadyExists` |
|
||||
/// | `NOT_A_DIRECTORY` | `ERROR_DIRECTORY` | `NotDir` |
|
||||
///
|
||||
/// plus `wToPrefixedFileW`'s `NameTooLong` (`ERROR_FILENAME_EXCED_RANGE`).
|
||||
#[must_use]
|
||||
#[cfg(windows)]
|
||||
#[allow(non_upper_case_globals)] // windows-sys spells two constants `..._64k`
|
||||
pub fn errno_name(code: i32) -> &'static str {
|
||||
use windows_sys::Win32::Foundation::{
|
||||
ERROR_ACCESS_DENIED,
|
||||
ERROR_AUTODATASEG_EXCEEDS_64k,
|
||||
ERROR_BAD_EXE_FORMAT,
|
||||
ERROR_BAD_FORMAT,
|
||||
ERROR_COMMITMENT_LIMIT,
|
||||
ERROR_DIRECTORY,
|
||||
ERROR_DISK_FULL,
|
||||
ERROR_DYNLINK_FROM_INVALID_RING,
|
||||
ERROR_EXE_MACHINE_TYPE_MISMATCH,
|
||||
ERROR_EXE_MARKED_INVALID,
|
||||
ERROR_FILE_NOT_FOUND,
|
||||
ERROR_FILENAME_EXCED_RANGE,
|
||||
ERROR_INFLOOP_IN_RELOC_CHAIN,
|
||||
ERROR_INVALID_EXE_SIGNATURE,
|
||||
ERROR_INVALID_MINALLOCSIZE,
|
||||
ERROR_INVALID_MODULETYPE,
|
||||
ERROR_INVALID_NAME,
|
||||
ERROR_INVALID_SEGDPL,
|
||||
ERROR_INVALID_STACKSEG,
|
||||
ERROR_INVALID_STARTING_CODESEG,
|
||||
ERROR_IOPL_NOT_ENABLED,
|
||||
ERROR_ITERATED_DATA_EXCEEDS_64k,
|
||||
ERROR_PATH_NOT_FOUND,
|
||||
ERROR_RELOC_CHAIN_XEEDS_SEGLIM,
|
||||
ERROR_RING2SEG_MUST_BE_MOVABLE,
|
||||
ERROR_SHARING_VIOLATION,
|
||||
};
|
||||
match code as u32 {
|
||||
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND | ERROR_DIRECTORY => "FileNotFound",
|
||||
ERROR_ACCESS_DENIED => "AccessDenied",
|
||||
ERROR_INVALID_NAME => "InvalidName",
|
||||
windows_errno_name(code as u32)
|
||||
}
|
||||
|
||||
/// The Windows table on plain numbers (so it is testable on every host).
|
||||
#[must_use]
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
fn windows_errno_name(code: u32) -> &'static str {
|
||||
// Win32 error codes (winerror.h). Mirrors the NTSTATUS mapping of
|
||||
// `std.os.windows.OpenFile` in Zig 0.15.2 (verified against its source).
|
||||
const ERROR_FILE_NOT_FOUND: u32 = 2;
|
||||
const ERROR_PATH_NOT_FOUND: u32 = 3;
|
||||
const ERROR_ACCESS_DENIED: u32 = 5;
|
||||
const ERROR_NOT_READY: u32 = 21;
|
||||
const ERROR_SHARING_VIOLATION: u32 = 32;
|
||||
const ERROR_BAD_NETPATH: u32 = 53;
|
||||
const ERROR_BAD_NET_NAME: u32 = 67;
|
||||
const ERROR_FILE_EXISTS: u32 = 80;
|
||||
const ERROR_INVALID_NAME: u32 = 123;
|
||||
const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
const ERROR_FILENAME_EXCED_RANGE: u32 = 206;
|
||||
const ERROR_VIRUS_INFECTED: u32 = 225;
|
||||
const ERROR_VIRUS_DELETED: u32 = 226;
|
||||
const ERROR_PIPE_BUSY: u32 = 231;
|
||||
const ERROR_DIRECTORY: u32 = 267;
|
||||
const ERROR_USER_MAPPED_FILE: u32 = 1224;
|
||||
match code {
|
||||
ERROR_INVALID_NAME => "BadPathName",
|
||||
ERROR_FILE_NOT_FOUND | ERROR_PATH_NOT_FOUND => "FileNotFound",
|
||||
ERROR_BAD_NETPATH | ERROR_BAD_NET_NAME => "NetworkNotFound",
|
||||
ERROR_NOT_READY => "NoDevice",
|
||||
// A sharing violation is `AccessDenied` in `OpenFile` (the
|
||||
// running-executable `FileBusy` retry logic is only in Zig 0.16).
|
||||
ERROR_SHARING_VIOLATION | ERROR_ACCESS_DENIED | ERROR_USER_MAPPED_FILE => "AccessDenied",
|
||||
ERROR_PIPE_BUSY => "PipeBusy",
|
||||
// STATUS_OBJECT_NAME_COLLISION surfaces as either code.
|
||||
ERROR_FILE_EXISTS | ERROR_ALREADY_EXISTS => "PathAlreadyExists",
|
||||
ERROR_VIRUS_INFECTED | ERROR_VIRUS_DELETED => "AntivirusInterference",
|
||||
ERROR_DIRECTORY => "NotDir",
|
||||
ERROR_FILENAME_EXCED_RANGE => "NameTooLong",
|
||||
ERROR_SHARING_VIOLATION => "FileBusy",
|
||||
ERROR_COMMITMENT_LIMIT => "SystemResources",
|
||||
ERROR_DISK_FULL => "NoSpaceLeft",
|
||||
ERROR_BAD_FORMAT
|
||||
| ERROR_INVALID_STARTING_CODESEG
|
||||
| ERROR_INVALID_STACKSEG
|
||||
| ERROR_INVALID_MODULETYPE
|
||||
| ERROR_INVALID_EXE_SIGNATURE
|
||||
| ERROR_EXE_MARKED_INVALID
|
||||
| ERROR_BAD_EXE_FORMAT
|
||||
| ERROR_ITERATED_DATA_EXCEEDS_64k
|
||||
| ERROR_INVALID_MINALLOCSIZE
|
||||
| ERROR_DYNLINK_FROM_INVALID_RING
|
||||
| ERROR_IOPL_NOT_ENABLED
|
||||
| ERROR_INVALID_SEGDPL
|
||||
| ERROR_AUTODATASEG_EXCEEDS_64k
|
||||
| ERROR_RING2SEG_MUST_BE_MOVABLE
|
||||
| ERROR_RELOC_CHAIN_XEEDS_SEGLIM
|
||||
| ERROR_INFLOOP_IN_RELOC_CHAIN
|
||||
| ERROR_EXE_MACHINE_TYPE_MISMATCH => "InvalidExe",
|
||||
_ => "Unexpected",
|
||||
}
|
||||
}
|
||||
@@ -209,25 +213,38 @@ mod tests {
|
||||
assert_eq!(errno_name(40), "Unexpected");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_table() {
|
||||
for (code, name) in [
|
||||
(2, "FileNotFound"),
|
||||
(2u32, "FileNotFound"),
|
||||
(3, "FileNotFound"),
|
||||
(5, "AccessDenied"),
|
||||
(32, "FileBusy"),
|
||||
(112, "NoSpaceLeft"),
|
||||
(123, "InvalidName"),
|
||||
(193, "InvalidExe"),
|
||||
// ERROR_SHARING_VIOLATION: AccessDenied like OpenFile.
|
||||
(32, "AccessDenied"),
|
||||
(1224, "AccessDenied"),
|
||||
(21, "NoDevice"),
|
||||
(53, "NetworkNotFound"),
|
||||
(67, "NetworkNotFound"),
|
||||
(123, "BadPathName"),
|
||||
(183, "PathAlreadyExists"),
|
||||
(80, "PathAlreadyExists"),
|
||||
(225, "AntivirusInterference"),
|
||||
(226, "AntivirusInterference"),
|
||||
(206, "NameTooLong"),
|
||||
(267, "FileNotFound"),
|
||||
(1455, "SystemResources"),
|
||||
(231, "PipeBusy"),
|
||||
(267, "NotDir"),
|
||||
(87, "Unexpected"),
|
||||
(112, "Unexpected"),
|
||||
(193, "Unexpected"),
|
||||
] {
|
||||
assert_eq!(errno_name(code), name);
|
||||
assert_eq!(windows_errno_name(code), name, "code {code}");
|
||||
}
|
||||
let e = std::io::Error::from_raw_os_error(5);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_io_error_name() {
|
||||
let e = std::io::Error::from_raw_os_error(32);
|
||||
assert_eq!(io_error_name(&e), "error.AccessDenied");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user