#![deny(unsafe_code)]
//! Unix-like platform primitives for Flash.
//!
//! [`PosixPlatform`] is the concrete macOS/Linux adapter and reports the full
//! capability set for those hosts. The dedicated FlashOS adapter also composes
//! the classified Rust and `relibc` routes implemented here, while owning its
//! target policy and qualification state separately.
use std::any::Any;
use std::collections::BTreeSet;
use std::ffi::{CString, OsStr};
use std::fmt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::process::CommandExt;
use std::path::{Component, Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::Duration;
use flash_platform::{
Capabilities, Capability, ChildProcess, DescriptorEndpoint, DescriptorReadError,
DescriptorWriteError, DirectoryEntry, DirectoryEntryKind, DirectoryReadError,
DirectoryReadRequest, DirectoryStream, FileActionError, FileIoEndpoint, FileOpenMode,
FileOpenRequest, ForegroundTerminalGuard, JobControlSignalGuard, JobSignal, PipeEndpoints,
PipeError, Platform, PlatformError, ProcessGroupId, ProcessStatus, ProcessTransition,
SignalError, SpawnError, SpawnRequest, StandardDirectories, StandardDirectoryEnvironment,
TerminalModeGuard, TerminalModeToken, TerminalSize, TerminateError, WaitError,
WorkingDirectoryError, WorkingDirectoryRequest,
};
/// A uniquely owned POSIX descriptor with close-on-exec discipline.
///
/// The wrapper is intentionally not `Clone`: another owner requires the
/// fallible [`try_clone`](OwnedDescriptor::try_clone) operation. Normal release
/// happens only through `Drop` or an explicit transfer back into [`OwnedFd`].
#[derive(Debug)]
pub struct OwnedDescriptor {
descriptor: File,
}
impl OwnedDescriptor {
/// Take ownership and atomically duplicate the descriptor with close-on-exec.
///
/// The supplied owner is released whether duplication succeeds or fails.
pub fn adopt(descriptor: OwnedFd) -> io::Result<Self> {
let cloexec_descriptor = descriptor.try_clone()?;
Ok(Self {
descriptor: File::from(cloexec_descriptor),
})
}
/// Create another close-on-exec owner of the same open file description.
pub fn try_clone(&self) -> io::Result<Self> {
self.descriptor
.try_clone()
.map(|descriptor| Self { descriptor })
}
/// Borrow the descriptor without transferring ownership.
pub fn as_fd(&self) -> BorrowedFd<'_> {
self.descriptor.as_fd()
}
/// Transfer ownership to a standard-library descriptor owner.
pub fn into_owned_fd(self) -> OwnedFd {
self.descriptor.into()
}
}
/// Open one private file relative to a trusted directory descriptor.
///
/// The final component is never followed through a symlink, the returned
/// descriptor is close-on-exec, and creation is exclusive when requested.
/// Keeping this primitive in the POSIX adapter also supplies Redox's `openat`
/// declaration, which is implemented by relibc but not exposed by `rustix`.
pub fn open_private_file_at(
directory: BorrowedFd<'_>,
name: &OsStr,
create_new: bool,
mode: u32,
) -> io::Result<OwnedFd> {
let mut components = Path::new(name).components();
if !matches!(components.next(), Some(Component::Normal(_))) || components.next().is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"relative file name must contain exactly one normal component",
));
}
let name = CString::new(name.as_bytes()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"relative file name contains a NUL byte",
)
})?;
let mut flags = libc::O_RDWR | libc::O_NOFOLLOW | libc::O_CLOEXEC;
if create_new {
flags |= libc::O_CREAT | libc::O_EXCL;
}
openat_owned(directory, &name, flags, mode)
}
#[allow(unsafe_code)]
fn openat_owned(
directory: BorrowedFd<'_>,
name: &std::ffi::CStr,
flags: libc::c_int,
mode: u32,
) -> io::Result<OwnedFd> {
#[cfg(target_os = "redox")]
// SAFETY: this declaration matches relibc's POSIX `openat` symbol and ABI;
// the call below supplies the pointer and variadic-mode invariants.
unsafe extern "C" {
#[link_name = "openat"]
fn redox_openat(
directory: libc::c_int,
path: *const libc::c_char,
flags: libc::c_int,
mode: libc::mode_t,
) -> libc::c_int;
}
// SAFETY: `directory` remains borrowed for the call, `name` is a live
// NUL-terminated C string, and the variadic openat mode is supplied because
// `O_CREAT` may be present. A nonnegative result transfers one owned fd.
let descriptor = unsafe {
#[cfg(target_os = "redox")]
{
redox_openat(
directory.as_raw_fd(),
name.as_ptr(),
flags,
mode as libc::mode_t,
)
}
#[cfg(not(target_os = "redox"))]
{
libc::openat(
directory.as_raw_fd(),
name.as_ptr(),
flags,
mode as libc::c_uint,
)
}
};
if descriptor < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: successful `openat` returned a new descriptor whose ownership is
// transferred exactly once to `OwnedFd`.
Ok(unsafe { OwnedFd::from_raw_fd(descriptor) })
}
impl DescriptorEndpoint for OwnedDescriptor {
fn as_any(&self) -> &dyn Any {
self
}
fn read(&mut self, buffer: &mut [u8]) -> Result<usize, DescriptorReadError> {
self.descriptor
.read(buffer)
.map_err(|error| DescriptorReadError::Operation {
kind: error.kind(),
message: error.to_string(),
})
}
fn write(&mut self, buffer: &[u8]) -> Result<usize, DescriptorWriteError> {
self.descriptor
.write(buffer)
.map_err(|error| DescriptorWriteError::Operation {
kind: error.kind(),
message: error.to_string(),
})
}
}
impl FileIoEndpoint for OwnedDescriptor {
fn read(&mut self, buffer: &mut [u8]) -> Result<usize, FileActionError> {
self.descriptor.read(buffer).map_err(file_action_error)
}
fn write(&mut self, buffer: &[u8]) -> Result<usize, FileActionError> {
self.descriptor.write(buffer).map_err(file_action_error)
}
}
/// POSIX adapter for process and terminal capabilities.
#[derive(Debug, Default, Clone, Copy)]
pub struct PosixPlatform;
/// Owned POSIX child process handle.
#[derive(Debug)]
pub struct PosixChild {
child: Child,
completed: Option<ProcessStatus>,
process_group: Option<ProcessGroupId>,
}
impl ChildProcess for PosixChild {
fn id(&self) -> u64 {
u64::from(self.child.id())
}
fn process_group(&self) -> Option<ProcessGroupId> {
self.process_group
}
fn wait(&mut self) -> Result<ProcessStatus, WaitError> {
if let Some(status) = self.completed {
return Ok(status);
}
match self.observe(0)? {
ProcessTransition::Completed(status) => Ok(status),
// Unreachable without WUNTRACED or WCONTINUED: a plain wait never
// reports a nonterminal transition. If a host ever did, report an
// adapter failure rather than unwinding or fabricating completion.
ProcessTransition::Stopped { signal } => Err(WaitError::new(
io::ErrorKind::Other,
format!("the host reported a stop by signal {signal} without being asked for one"),
)),
ProcessTransition::Continued => Err(WaitError::new(
io::ErrorKind::Other,
"the host reported a continuation without being asked for one",
)),
}
}
fn wait_for_transition(&mut self) -> Result<ProcessTransition, WaitError> {
if let Some(status) = self.completed {
return Ok(ProcessTransition::Completed(status));
}
#[cfg(target_os = "macos")]
let transition = child_wait::observe_transition(self.pid()?)?;
#[cfg(not(target_os = "macos"))]
let transition = child_wait::observe(self.pid()?, libc::WUNTRACED | libc::WCONTINUED)?;
if let ProcessTransition::Completed(status) = transition {
self.completed = Some(status);
}
Ok(transition)
}
fn try_wait_for_transition(&mut self) -> Result<Option<ProcessTransition>, WaitError> {
if let Some(status) = self.completed {
return Ok(Some(ProcessTransition::Completed(status)));
}
let transition = child_wait::try_observe(self.pid()?, libc::WUNTRACED | libc::WCONTINUED)?;
if let Some(ProcessTransition::Completed(status)) = transition {
self.completed = Some(status);
}
Ok(transition)
}
fn terminate(&mut self) -> Result<(), TerminateError> {
// Once the adapter has reaped, std does not know the child is gone, so
// `Child::kill` would signal a pid the host may have already reused.
if self.completed.is_some() {
return Ok(());
}
self.child
.kill()
.map_err(|error| TerminateError::new(error.kind(), error.to_string()))
}
}
impl PosixChild {
fn pid(&self) -> Result<libc::pid_t, WaitError> {
libc::pid_t::try_from(self.child.id()).map_err(|_| {
WaitError::new(
io::ErrorKind::InvalidInput,
"the child identifier exceeds the POSIX process identifier range",
)
})
}
/// Reap the child for one transition, caching a completion.
///
/// The adapter reaps for itself rather than through `std::process::Child`,
/// so the completion is recorded here and every later call short-circuits on
/// it. Nothing else reaps the pid, so the recorded status is the only one
/// the child ever produces.
fn observe(&mut self, flags: libc::c_int) -> Result<ProcessTransition, WaitError> {
let pid = self.pid()?;
let transition = child_wait::observe(pid, flags)?;
if let ProcessTransition::Completed(status) = transition {
self.completed = Some(status);
}
Ok(transition)
}
}
impl Platform for PosixPlatform {
fn capabilities(&self) -> Capabilities {
Capabilities::full()
}
fn is_terminal(&self) -> bool {
terminal_mode::is_terminal(io::stdin().as_fd())
}
fn is_output_terminal(&self) -> bool {
terminal_mode::is_terminal(io::stdout().as_fd())
}
fn terminal_size(&self) -> Result<TerminalSize, PlatformError> {
self.require(Capability::TerminalInfo)?;
match terminal_mode::window_size(io::stdin().as_fd()) {
// A pty can legitimately report zero before a size is set; the
// documented fallback keeps the renderer's arithmetic valid.
Ok((columns, rows)) if columns > 0 && rows > 0 => Ok(TerminalSize::new(columns, rows)),
Ok(_) | Err(_) => Ok(TerminalSize::new(80, 24)),
}
}
fn enter_raw_mode(&self) -> Result<Box<dyn TerminalModeGuard>, PlatformError> {
self.require(Capability::TerminalInfo)?;
let stdin = io::stdin();
if !terminal_mode::is_terminal(stdin.as_fd()) {
return Err(PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: "standard input is not a terminal".to_owned(),
});
}
let saved = terminal_mode::current_attributes(stdin.as_fd()).map_err(|error| {
PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("reading terminal attributes failed: {error}"),
}
})?;
let raw = terminal_mode::raw_from(&saved);
terminal_mode::apply(stdin.as_fd(), &raw).map_err(|error| PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("entering raw mode failed: {error}"),
})?;
let saved_status = terminal_mode::status_flags(stdin.as_fd()).map_err(|error| {
let _ = terminal_mode::apply(stdin.as_fd(), &saved);
PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("reading terminal status flags failed: {error}"),
}
})?;
terminal_mode::apply_status_flags(stdin.as_fd(), saved_status | libc::O_NONBLOCK).map_err(
|error| {
let _ = terminal_mode::apply(stdin.as_fd(), &saved);
PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("entering nonblocking terminal mode failed: {error}"),
}
},
)?;
Ok(Box::new(PosixTerminalModeGuard {
saved,
saved_status,
restored: false,
}))
}
fn read_terminal_input(
&self,
buffer: &mut [u8],
timeout: Duration,
) -> Result<Option<usize>, PlatformError> {
self.require(Capability::TerminalInfo)?;
terminal_input::read(buffer, timeout).map_err(|error| PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("reading terminal input failed: {error}"),
})
}
fn snapshot_terminal_mode(&self) -> Result<Box<dyn TerminalModeToken>, PlatformError> {
self.require(Capability::TerminalInfo)?;
let stdin = io::stdin();
if !terminal_mode::is_terminal(stdin.as_fd()) {
return Err(PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: "standard input is not a terminal".to_owned(),
});
}
terminal_mode::current_attributes(stdin.as_fd())
.map(|attributes| {
Box::new(PosixTerminalModeToken { attributes }) as Box<dyn TerminalModeToken>
})
.map_err(|error| PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("reading terminal attributes failed: {error}"),
})
}
fn apply_terminal_mode(&self, token: &dyn TerminalModeToken) -> Result<(), PlatformError> {
self.require(Capability::TerminalInfo)?;
let token = token
.as_any()
.downcast_ref::<PosixTerminalModeToken>()
.ok_or_else(|| PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: "terminal mode token belongs to another adapter".to_owned(),
})?;
terminal_mode::apply(io::stdin().as_fd(), &token.attributes).map_err(|error| {
PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("applying terminal attributes failed: {error}"),
}
})
}
fn foreground_process_group(&self) -> Result<Option<ProcessGroupId>, PlatformError> {
self.require(Capability::ForegroundTerminal)?;
let stdin = io::stdin();
if !terminal_mode::is_terminal(stdin.as_fd()) {
return Ok(None);
}
foreground_terminal::owner(stdin.as_fd())
.map_err(foreground_unavailable)
.map(ProcessGroupId::new)
}
fn enter_foreground(
&self,
group: ProcessGroupId,
) -> Result<Box<dyn ForegroundTerminalGuard>, PlatformError> {
self.require(Capability::ForegroundTerminal)?;
let stdin = io::stdin();
if !terminal_mode::is_terminal(stdin.as_fd()) {
return Err(PlatformError::Unavailable {
capability: Capability::ForegroundTerminal,
reason: "standard input is not a terminal".to_owned(),
});
}
let previous = foreground_terminal::owner(stdin.as_fd())
.map_err(foreground_unavailable)
.map(ProcessGroupId::new)?;
foreground_terminal::hand_over(stdin.as_fd(), group).map_err(foreground_unavailable)?;
Ok(Box::new(PosixForegroundTerminalGuard {
previous,
restored: false,
}))
}
fn install_job_control_signals(&self) -> Result<Box<dyn JobControlSignalGuard>, PlatformError> {
self.require(Capability::Signals)?;
job_control_signals::install()
.map(|guard| Box::new(guard) as Box<dyn JobControlSignalGuard>)
}
fn shell_executable(&self) -> Result<std::path::PathBuf, PlatformError> {
self.require(Capability::ShellExecutable)?;
std::env::current_exe().map_err(|error| PlatformError::Unavailable {
capability: Capability::ShellExecutable,
reason: format!("determining the running executable failed: {error}"),
})
}
fn ignore_hangup(&self) -> Result<(), PlatformError> {
self.require(Capability::HangupDisposition)?;
hangup_disposition::ignore()
}
fn standard_directories(
&self,
environment: &dyn StandardDirectoryEnvironment,
) -> Result<StandardDirectories, PlatformError> {
self.require(Capability::StandardDirectories)?;
let home = absolute_environment_path(environment, "HOME").ok_or_else(|| {
PlatformError::Unavailable {
capability: Capability::StandardDirectories,
reason: "HOME does not name an absolute directory".to_owned(),
}
})?;
let config = absolute_environment_path(environment, "XDG_CONFIG_HOME")
.unwrap_or_else(|| host_config_root(&home));
let cache = absolute_environment_path(environment, "XDG_CACHE_HOME")
.unwrap_or_else(|| host_cache_root(&home));
let state = absolute_environment_path(environment, "XDG_STATE_HOME")
.unwrap_or_else(|| host_state_root(&home));
Ok(StandardDirectories::new(home, config, cache, state))
}
fn signal_process_group(
&self,
group: ProcessGroupId,
signal: JobSignal,
) -> Result<(), SignalError> {
self.require(Capability::Signals)?;
process_signals::deliver(group, signal)
}
fn resolve_working_directory(
&self,
request: WorkingDirectoryRequest<'_>,
) -> Result<std::path::PathBuf, WorkingDirectoryError> {
self.require(Capability::WorkingDirectory)?;
let candidate = if request.path().is_absolute() {
request.path().to_owned()
} else {
request.cwd().join(request.path())
};
let resolved = std::fs::canonicalize(candidate).map_err(working_directory_error)?;
let metadata = std::fs::metadata(&resolved).map_err(working_directory_error)?;
if !metadata.is_dir() {
return Err(WorkingDirectoryError::Operation {
kind: io::ErrorKind::NotADirectory,
message: format!("{} is not a directory", resolved.display()),
});
}
Ok(resolved)
}
fn pipe(&self) -> Result<PipeEndpoints, PipeError> {
self.require(Capability::Pipes)?;
let (reader, writer) = io::pipe().map_err(pipe_error)?;
let reader = OwnedDescriptor::adopt(OwnedFd::from(reader)).map_err(pipe_error)?;
let writer = OwnedDescriptor::adopt(OwnedFd::from(writer)).map_err(pipe_error)?;
Ok(PipeEndpoints::new(Box::new(reader), Box::new(writer)))
}
fn open_file(
&self,
request: FileOpenRequest<'_>,
) -> Result<Box<dyn DescriptorEndpoint>, FileActionError> {
open_owned_file(self, request).map(|endpoint| Box::new(endpoint) as _)
}
fn open_file_io(
&self,
request: FileOpenRequest<'_>,
) -> Result<Box<dyn FileIoEndpoint>, FileActionError> {
open_owned_file(self, request).map(|endpoint| Box::new(endpoint) as _)
}
fn inherit_descriptor(
&self,
descriptor: u32,
) -> Result<Box<dyn DescriptorEndpoint>, FileActionError> {
self.require(Capability::FileActions)?;
let descriptor = inherited_descriptor::duplicate(descriptor).map_err(file_action_error)?;
Ok(Box::new(OwnedDescriptor {
descriptor: File::from(descriptor),
}))
}
fn read_directory(
&self,
request: DirectoryReadRequest<'_>,
) -> Result<Box<dyn DirectoryStream>, DirectoryReadError> {
self.require(Capability::DirectoryRead)?;
let cwd = std::path::absolute(request.cwd()).map_err(directory_read_error)?;
let path = if request.path().is_relative() {
cwd.join(request.path())
} else {
request.path().to_owned()
};
// `read_dir` reports a missing or non-directory target here, so an
// unreadable target fails before any entry is handed out.
let entries = fs::read_dir(path).map_err(directory_read_error)?;
Ok(Box::new(PosixDirectoryStream { entries }))
}
fn read_descriptor(
&self,
endpoint: &dyn DescriptorEndpoint,
buffer: &mut [u8],
) -> Result<usize, DescriptorReadError> {
self.require(Capability::Pipes)?;
let endpoint = endpoint
.as_any()
.downcast_ref::<OwnedDescriptor>()
.ok_or(DescriptorReadError::InvalidEndpoint)?;
let mut descriptor = &endpoint.descriptor;
descriptor
.read(buffer)
.map_err(|error| DescriptorReadError::Operation {
kind: error.kind(),
message: error.to_string(),
})
}
fn spawn(&self, request: &SpawnRequest<'_>) -> Result<Box<dyn ChildProcess>, SpawnError> {
self.require(Capability::ProcessSpawn)?;
if request.process_group().requires_capability() {
self.require(Capability::ProcessGroups)?;
}
let cwd = std::path::absolute(request.cwd()).map_err(spawn_error)?;
let executable = if request.executable().is_relative() {
cwd.join(request.executable())
} else {
request.executable().to_owned()
};
let mut command = Command::new(executable);
command
.arg0(&request.argv()[0])
.args(&request.argv()[1..])
.env_clear()
.envs(
request
.environment()
.iter()
.map(|(name, value)| (name, value)),
)
.current_dir(cwd);
let action_targets = request
.descriptors()
.iter()
.map(|mapping| descriptor_number(mapping.target()))
.chain(
request
.closed_descriptors()
.iter()
.copied()
.map(descriptor_number),
)
.collect::<Result<BTreeSet<_>, _>>()?;
let mut extra_mappings = Vec::new();
let mut reservations = Vec::new();
for mapping in request.descriptors() {
let endpoint = mapping
.endpoint()
.as_any()
.downcast_ref::<OwnedDescriptor>()
.ok_or_else(|| SpawnError::Operation {
kind: io::ErrorKind::InvalidInput,
message: "descriptor endpoint belongs to another platform adapter".to_owned(),
})?;
let target = descriptor_number(mapping.target())?;
match mapping.target() {
0 => {
command.stdin(Stdio::from(
endpoint.try_clone().map_err(spawn_error)?.into_owned_fd(),
));
}
1 => {
command.stdout(Stdio::from(
endpoint.try_clone().map_err(spawn_error)?.into_owned_fd(),
));
}
2 => {
command.stderr(Stdio::from(
endpoint.try_clone().map_err(spawn_error)?.into_owned_fd(),
));
}
_ => {
let descriptor =
clone_avoiding_targets(endpoint, &action_targets, &mut reservations)?;
extra_mappings.push((descriptor, target));
}
}
}
let closes = request
.closed_descriptors()
.iter()
.copied()
.map(descriptor_number)
.collect::<Result<Vec<_>, _>>()?;
// The disposition reset is installed first so a child that fails any
// later hook is already killable by a default-disposition interrupt.
child_signal_dispositions::configure(&mut command);
// Group placement is installed before the descriptor hook so a child
// that fails its descriptor setup is already signallable as a member of
// its job's group rather than of the shell's own group.
child_process_group::configure(&mut command, request.process_group())?;
child_descriptors::configure(&mut command, extra_mappings, closes);
command
.spawn()
.map(|child| {
let process_group = child_process_group::adopt(&child, request.process_group());
Box::new(PosixChild {
child,
completed: None,
process_group,
}) as Box<dyn ChildProcess>
})
.map_err(spawn_error)
}
}
#[allow(unsafe_code)]
mod inherited_descriptor {
use std::io;
use std::os::fd::{FromRawFd, OwnedFd};
pub(super) fn duplicate(descriptor: u32) -> io::Result<OwnedFd> {
let descriptor = libc::c_int::try_from(descriptor).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"inherited descriptor exceeds the POSIX descriptor range",
)
})?;
// SAFETY: fcntl validates the caller-supplied descriptor before
// returning a new descriptor. A nonnegative result is a fresh owned
// descriptor with close-on-exec set; no borrowed Rust descriptor is
// constructed from an untrusted raw value.
let duplicated = unsafe { libc::fcntl(descriptor, libc::F_DUPFD_CLOEXEC, 0) };
if duplicated == -1 {
return Err(io::Error::last_os_error());
}
// SAFETY: a successful F_DUPFD_CLOEXEC result is a fresh descriptor
// whose ownership has not been transferred elsewhere.
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
}
}
fn absolute_environment_path(
environment: &dyn StandardDirectoryEnvironment,
name: &str,
) -> Option<PathBuf> {
environment
.value(OsStr::new(name))
.filter(|value| !value.is_empty() && Path::new(value).is_absolute())
.map(PathBuf::from)
}
#[cfg(target_os = "macos")]
fn host_config_root(home: &Path) -> PathBuf {
home.join("Library/Application Support")
}
#[cfg(not(target_os = "macos"))]
fn host_config_root(home: &Path) -> PathBuf {
home.join(".config")
}
#[cfg(target_os = "macos")]
fn host_cache_root(home: &Path) -> PathBuf {
home.join("Library/Caches")
}
#[cfg(not(target_os = "macos"))]
fn host_cache_root(home: &Path) -> PathBuf {
home.join(".cache")
}
#[cfg(target_os = "macos")]
fn host_state_root(home: &Path) -> PathBuf {
home.join("Library/Application Support")
}
#[cfg(not(target_os = "macos"))]
fn host_state_root(home: &Path) -> PathBuf {
home.join(".local/state")
}
fn clone_avoiding_targets(
endpoint: &OwnedDescriptor,
targets: &BTreeSet<i32>,
reservations: &mut Vec<OwnedFd>,
) -> Result<OwnedFd, SpawnError> {
loop {
let descriptor = endpoint.try_clone().map_err(spawn_error)?.into_owned_fd();
if targets.contains(&descriptor.as_raw_fd()) {
reservations.push(descriptor);
} else {
return Ok(descriptor);
}
}
}
fn descriptor_number(descriptor: u32) -> Result<i32, SpawnError> {
i32::try_from(descriptor).map_err(|_| SpawnError::Operation {
kind: io::ErrorKind::InvalidInput,
message: format!("child descriptor {descriptor} exceeds the POSIX descriptor range"),
})
}
/// A lazily advanced walk over one host directory.
///
/// It holds the host iterator rather than a materialized vector, so a caller
/// that stops early stops the walk with it.
#[derive(Debug)]
struct PosixDirectoryStream {
entries: fs::ReadDir,
}
impl DirectoryStream for PosixDirectoryStream {
fn next_entry(&mut self) -> Result<Option<DirectoryEntry>, DirectoryReadError> {
let Some(entry) = self.entries.next() else {
return Ok(None);
};
let entry = entry.map_err(directory_read_error)?;
// `symlink_metadata` does not follow the link, so a link reports itself
// and a dangling link is still enumerable.
let metadata = entry
.path()
.symlink_metadata()
.map_err(directory_read_error)?;
let file_type = metadata.file_type();
let kind = if file_type.is_symlink() {
DirectoryEntryKind::Symlink
} else if file_type.is_dir() {
DirectoryEntryKind::Directory
} else if file_type.is_file() {
DirectoryEntryKind::File
} else {
DirectoryEntryKind::Other
};
let size = matches!(kind, DirectoryEntryKind::File).then(|| metadata.len());
Ok(Some(DirectoryEntry::new(entry.file_name(), kind, size)))
}
}
fn directory_read_error(error: io::Error) -> DirectoryReadError {
DirectoryReadError::Operation {
kind: error.kind(),
message: error.to_string(),
}
}
fn pipe_error(error: io::Error) -> PipeError {
PipeError::Operation {
kind: error.kind(),
message: error.to_string(),
}
}
fn spawn_error(error: io::Error) -> SpawnError {
SpawnError::Operation {
kind: error.kind(),
message: error.to_string(),
}
}
fn file_action_error(error: io::Error) -> FileActionError {
FileActionError::Operation {
kind: error.kind(),
message: error.to_string(),
}
}
fn open_owned_file(
platform: &PosixPlatform,
request: FileOpenRequest<'_>,
) -> Result<OwnedDescriptor, FileActionError> {
platform.require(Capability::FileActions)?;
let cwd = std::path::absolute(request.cwd()).map_err(file_action_error)?;
let path = if request.path().is_relative() {
cwd.join(request.path())
} else {
request.path().to_owned()
};
let mut options = OpenOptions::new();
match request.mode() {
FileOpenMode::Read => {
options.read(true);
}
FileOpenMode::WriteTruncate => {
options.write(true).create(true).truncate(true);
}
FileOpenMode::WriteAppend => {
options.write(true).create(true).append(true);
}
}
let descriptor = options
.open(path)
.map(OwnedFd::from)
.map_err(file_action_error)?;
OwnedDescriptor::adopt(descriptor).map_err(file_action_error)
}
fn working_directory_error(error: io::Error) -> WorkingDirectoryError {
WorkingDirectoryError::Operation {
kind: error.kind(),
message: error.to_string(),
}
}
/// Default signal dispositions and an empty mask for a child that has not
/// executed yet.
///
/// Dispositions and the signal mask are both inherited across `fork`. An
/// interactive shell ignores the job-control signals for its own survival, so
/// without this reset every job it starts would inherit that ignore and stop
/// responding to the keyboard. The mask is cleared for the same reason: `fork`
/// copies the calling thread's mask, and an executed program is entitled to a
/// clear one.
#[allow(unsafe_code)]
mod child_signal_dispositions {
use std::io;
use std::os::unix::process::CommandExt;
use std::process::Command;
/// Signal dispositions a spawned program must not inherit from its shell.
///
/// The interactive shell arranges the five job-control signals for its own
/// survival. A reserved background-chain supervisor additionally ignores
/// hang-up so it can wait for its descendants after the parent signals the
/// group; ordinary external descendants must restore the default instead.
const RESET: [libc::c_int; 6] = [
libc::SIGHUP,
libc::SIGINT,
libc::SIGQUIT,
libc::SIGTSTP,
libc::SIGTTOU,
libc::SIGTTIN,
];
/// Install the pre-exec hook that restores the child's default handling.
///
/// Registered before the process-group and descriptor hooks so a child that
/// fails a later hook is already killable by a default-disposition
/// interrupt.
pub(super) fn configure(command: &mut Command) {
// SAFETY: this runs after `fork` and calls only sigaction, sigemptyset,
// and sigprocmask, each of which POSIX lists among the functions usable
// after `fork` in a multithreaded process. The closure captures nothing,
// `zeroed` is a valid starting pattern for both `sigaction` and
// `sigset_t`, and every pointer targets a live, correctly typed local.
unsafe {
command.pre_exec(|| {
let mut action: libc::sigaction = std::mem::zeroed();
action.sa_sigaction = libc::SIG_DFL;
if libc::sigemptyset(&raw mut action.sa_mask) == -1 {
return Err(io::Error::last_os_error());
}
for signal in RESET {
if libc::sigaction(signal, &raw const action, std::ptr::null_mut()) == -1 {
return Err(io::Error::last_os_error());
}
}
let mut empty: libc::sigset_t = std::mem::zeroed();
if libc::sigemptyset(&raw mut empty) == -1 {
return Err(io::Error::last_os_error());
}
if libc::sigprocmask(libc::SIG_SETMASK, &raw const empty, std::ptr::null_mut())
== -1
{
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
}
/// Process-group placement for a child that has not executed yet.
///
/// `setpgid` is applied twice on purpose: once in the child before `exec` and
/// once in the parent after `fork`. Either call alone leaves a window in which
/// the other side observes the wrong group — the parent may signal the job
/// before the child has moved itself, and the child may `exec` before the
/// parent's call lands. Both calls are idempotent, so the redundant one is a
/// no-op rather than a correction.
#[allow(unsafe_code)]
mod child_process_group {
use std::io;
use std::process::Child;
use flash_platform::{ProcessGroup, ProcessGroupId, SpawnError};
use super::spawn_error;
use std::os::unix::process::CommandExt;
use std::process::Command;
/// Install the pre-exec hook that moves the child into its group.
pub(super) fn configure(
command: &mut Command,
placement: ProcessGroup,
) -> Result<(), SpawnError> {
let target = match placement {
// The child is already in the shell's group; leave it there rather
// than calling setpgid with the shell's own identifier.
ProcessGroup::Inherit => return Ok(()),
// POSIX spells "make the caller its own leader" as pgid zero.
ProcessGroup::New => 0,
ProcessGroup::Join(group) => group_number(group)?,
};
// SAFETY: the hook captures one integer and calls only setpgid, which
// POSIX lists as async-signal-safe.
unsafe {
command.pre_exec(move || {
if libc::setpgid(0, target) == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
Ok(())
}
/// Close the parent-side race and report the group the child now belongs to.
pub(super) fn adopt(child: &Child, placement: ProcessGroup) -> Option<ProcessGroupId> {
let group = match placement {
ProcessGroup::Inherit => return None,
// A new leader names its group after itself, so the identifier is
// knowable only once the child exists.
ProcessGroup::New => ProcessGroupId::new(u64::from(child.id()))?,
ProcessGroup::Join(group) => group,
};
let target = group_number(group).ok()?;
let pid = i32::try_from(child.id()).ok()?;
// SAFETY: setpgid takes two scalars and dereferences nothing.
//
// A failure here is not an error: EACCES means the child already
// executed, and ESRCH means it already exited. In both cases the
// child's own pre-exec call decided the group, so the parent's
// duplicate has nothing left to do.
unsafe {
libc::setpgid(pid, target);
}
Some(group)
}
fn group_number(group: ProcessGroupId) -> Result<libc::pid_t, SpawnError> {
libc::pid_t::try_from(group.get()).map_err(|_| {
spawn_error(io::Error::new(
io::ErrorKind::InvalidInput,
format!("process group {group} exceeds the POSIX process identifier range"),
))
})
}
}
#[allow(unsafe_code)]
mod child_descriptors {
use std::ffi::c_int;
use std::io;
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::process::Command;
// SAFETY: these declarations match the POSIX C signatures. Calls remain
// inside the pre-exec hook below, which supplies only scalar descriptors.
unsafe extern "C" {
fn dup2(source: c_int, target: c_int) -> c_int;
fn close(descriptor: c_int) -> c_int;
}
pub(super) fn configure(
command: &mut Command,
mappings: Vec<(OwnedFd, c_int)>,
closes: Vec<c_int>,
) {
if mappings.is_empty() && closes.is_empty() {
return;
}
// SAFETY: the hook captures only preallocated descriptor owners and
// integers, then calls the async-signal-safe POSIX dup2/close functions.
unsafe {
command.pre_exec(move || {
for (source, target) in &mappings {
if dup2(source.as_raw_fd(), *target) == -1 {
return Err(io::Error::last_os_error());
}
}
for descriptor in &closes {
if close(*descriptor) == -1 {
let error = io::Error::last_os_error();
// EBADF means the requested descriptor was already
// absent, which is the specified close no-op.
if error.raw_os_error() != Some(9) {
return Err(error);
}
}
}
Ok(())
});
}
}
}
/// Foreground terminal ownership.
///
/// `tcsetpgrp` raises `SIGTTOU` at the caller when the calling process is not
/// in the terminal's current foreground group, whose default action stops the
/// shell. That is exactly the situation restoration runs in: the job owns the
/// terminal, and the shell taking it back is a background write. The signal is
/// therefore blocked for the calling thread across the call and the previous
/// mask is restored afterwards.
///
/// The mask is per-thread (`pthread_sigmask`), not a process-wide disposition
/// change: an installed `SIGTTOU` handler stays installed, other threads keep
/// their own masks, and a concurrent handover cannot observe a half-changed
/// global state.
#[allow(unsafe_code)]
mod foreground_terminal {
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
use flash_platform::ProcessGroupId;
pub(super) fn owner(fd: BorrowedFd<'_>) -> io::Result<u64> {
// SAFETY: tcgetpgrp only reads the terminal state behind a valid
// descriptor and dereferences no caller memory.
let group = unsafe { libc::tcgetpgrp(fd.as_raw_fd()) };
if group == -1 {
return Err(io::Error::last_os_error());
}
Ok(u64::try_from(group).unwrap_or_default())
}
pub(super) fn hand_over(fd: BorrowedFd<'_>, group: ProcessGroupId) -> io::Result<()> {
let group = libc::pid_t::try_from(group.get()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"process group exceeds the POSIX process identifier range",
)
})?;
let blocked = block_terminal_stops()?;
// SAFETY: tcsetpgrp takes two scalars and dereferences no memory.
let result = unsafe { libc::tcsetpgrp(fd.as_raw_fd(), group) };
let outcome = if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
};
// The mask is restored even when the handover failed, so a failed
// transfer cannot leave the shell deaf to terminal stop signals.
restore_mask(&blocked)?;
outcome
}
/// Block the terminal stop signals for this thread, returning the old mask.
fn block_terminal_stops() -> io::Result<libc::sigset_t> {
// SAFETY: `zeroed` is a valid starting bit pattern for `sigset_t`, and
// sigemptyset overwrites it before any read.
let mut blocked: libc::sigset_t = unsafe { std::mem::zeroed() };
// SAFETY: `previous` is written by pthread_sigmask before it is read.
let mut previous: libc::sigset_t = unsafe { std::mem::zeroed() };
// SAFETY: every pointer below targets a live, correctly typed local.
unsafe {
if libc::sigemptyset(&raw mut blocked) == -1
|| libc::sigaddset(&raw mut blocked, libc::SIGTTOU) == -1
|| libc::sigaddset(&raw mut blocked, libc::SIGTTIN) == -1
|| libc::sigaddset(&raw mut blocked, libc::SIGTSTP) == -1
{
return Err(io::Error::last_os_error());
}
let result =
libc::pthread_sigmask(libc::SIG_BLOCK, &raw const blocked, &raw mut previous);
if result != 0 {
return Err(io::Error::from_raw_os_error(result));
}
}
Ok(previous)
}
fn restore_mask(previous: &libc::sigset_t) -> io::Result<()> {
// SAFETY: the pointer targets a live mask that pthread_sigmask reads.
let result =
unsafe { libc::pthread_sigmask(libc::SIG_SETMASK, previous, std::ptr::null_mut()) };
if result != 0 {
return Err(io::Error::from_raw_os_error(result));
}
Ok(())
}
}
/// The asynchronous-chain subshell's own hang-up disposition.
///
/// This is separate from the interactive shell's job-control arrangement:
/// only the re-executed child ignores hang-up, so it can survive long enough to
/// reap a grandchild that receives the same group-directed signal.
#[allow(unsafe_code)]
mod hangup_disposition {
use std::io;
use flash_platform::{Capability, PlatformError};
pub(super) fn ignore() -> Result<(), PlatformError> {
// SAFETY: an all-zero pattern is valid for `sigaction`, whose fields are
// integers and a function pointer; sigemptyset initializes the live
// mask field before sigaction reads the complete disposition.
let mut ignore: libc::sigaction = unsafe { std::mem::zeroed() };
ignore.sa_sigaction = libc::SIG_IGN;
// SAFETY: the pointer targets the live mask field of `ignore`.
if unsafe { libc::sigemptyset(&raw mut ignore.sa_mask) } == -1 {
return Err(unavailable(io::Error::last_os_error()));
}
// SAFETY: the input pointer targets the fully initialized disposition
// above, and the null output pointer asks for no previous disposition.
if unsafe { libc::sigaction(libc::SIGHUP, &raw const ignore, std::ptr::null_mut()) } == -1 {
return Err(unavailable(io::Error::last_os_error()));
}
Ok(())
}
fn unavailable(error: io::Error) -> PlatformError {
PlatformError::Unavailable {
capability: Capability::HangupDisposition,
reason: error.to_string(),
}
}
}
/// The interactive shell's own job-control signal dispositions.
///
/// The shell ignores the five signals a terminal can generate for it. Without
/// that, an interrupt aimed at a job the shell still owns the terminal for, or
/// at a shell whose platform cannot hand the terminal over, kills the shell.
/// Ignoring `SIGTTOU` and `SIGTTIN` does not replace the per-thread mask this
/// adapter takes around `tcsetpgrp`: the two mechanisms cover the same hazard
/// from different sides, and the mask still holds if a handler ever replaces
/// the ignore. One consequence is deliberate — a background read now fails with
/// `EIO` instead of stopping the shell.
///
/// The previous disposition is captured as a whole `sigaction`, not just a
/// handler pointer, so restoring reinstates whatever flags and mask were in
/// force before, and matches the reset the child hook installs on the other
/// side of `fork`.
#[allow(unsafe_code)]
mod job_control_signals {
use std::io;
use flash_platform::{Capability, JobControlSignalGuard, PlatformError};
/// The signals an interactive shell arranges for the life of the session.
const ARRANGED: [libc::c_int; 5] = [
libc::SIGINT,
libc::SIGQUIT,
libc::SIGTSTP,
libc::SIGTTOU,
libc::SIGTTIN,
];
/// The previous dispositions, restored explicitly or on drop.
pub(super) struct PosixJobControlSignalGuard {
previous: [libc::sigaction; 5],
restored: bool,
}
// `libc::sigaction` carries no `Debug` without the `extra_traits` feature,
// and its contents are opaque host state not worth formatting; the guard's
// observable field is whether it has already been restored.
impl std::fmt::Debug for PosixJobControlSignalGuard {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PosixJobControlSignalGuard")
.field("restored", &self.restored)
.finish_non_exhaustive()
}
}
/// Ignore the job-control signals until the returned guard is released.
pub(super) fn install() -> Result<PosixJobControlSignalGuard, PlatformError> {
// SAFETY: an all-zero pattern is valid for `sigaction`, whose fields are
// integers and a function pointer; the mask is initialized by
// sigemptyset before any read.
let mut ignore: libc::sigaction = unsafe { std::mem::zeroed() };
ignore.sa_sigaction = libc::SIG_IGN;
// SAFETY: the pointer targets the live mask field of `ignore`.
if unsafe { libc::sigemptyset(&raw mut ignore.sa_mask) } == -1 {
return Err(unavailable(io::Error::last_os_error()));
}
// SAFETY: an all-zero pattern is valid for every element of the array;
// each slot is overwritten by sigaction below before it is ever read.
let mut previous: [libc::sigaction; 5] = unsafe { std::mem::zeroed() };
for index in 0..ARRANGED.len() {
// SAFETY: the index is bounded by the array it iterates, and both
// pointers target live, correctly typed locals; sigaction installs
// `ignore` for the arranged signal and writes the prior disposition
// into that signal's slot.
let installed = unsafe {
libc::sigaction(ARRANGED[index], &raw const ignore, &raw mut previous[index])
};
if installed == -1 {
let failure = io::Error::last_os_error();
undo(&previous, index);
return Err(unavailable(failure));
}
}
Ok(PosixJobControlSignalGuard {
previous,
restored: false,
})
}
impl PosixJobControlSignalGuard {
fn restore_now(&mut self) -> Result<(), PlatformError> {
if self.restored {
return Ok(());
}
// Marked before the loop rather than after it, so a failure partway
// is reported once instead of being reattempted by the drop against
// a host that has already refused it.
self.restored = true;
for (previous, signal) in self.previous.iter().zip(ARRANGED) {
// SAFETY: `previous` is a disposition this process produced in
// `install`; sigaction reinstates it for `signal` and writes no
// other memory.
if unsafe { libc::sigaction(signal, &raw const *previous, std::ptr::null_mut()) }
== -1
{
return Err(unavailable(io::Error::last_os_error()));
}
}
Ok(())
}
}
impl JobControlSignalGuard for PosixJobControlSignalGuard {
fn restore(&mut self) -> Result<(), PlatformError> {
self.restore_now()
}
}
impl Drop for PosixJobControlSignalGuard {
fn drop(&mut self) {
// A process that exits with its interrupts still ignored cannot be
// stopped from the keyboard, so the drop is the backstop for every
// path that skipped the explicit restore.
let _ = self.restore_now();
}
}
/// Reinstate the first `installed` dispositions after a failed install.
///
/// An install that gave up partway would otherwise leave the signals it had
/// already changed ignored, with no guard in existence to change them back —
/// the one unrecoverable state this guard is built to prevent. Failures here
/// are discarded because the caller is already returning the error that
/// matters and has nothing better to attempt.
fn undo(previous: &[libc::sigaction; 5], installed: usize) {
for index in 0..installed {
// SAFETY: the disposition being reinstated is one this process
// captured moments earlier in `install`, the pointer targets a live
// element of the caller's array, and a null out-pointer asks for no
// previous value to be written back.
unsafe {
libc::sigaction(
ARRANGED[index],
&raw const previous[index],
std::ptr::null_mut(),
);
}
}
}
fn unavailable(error: io::Error) -> PlatformError {
PlatformError::Unavailable {
capability: Capability::Signals,
reason: error.to_string(),
}
}
}
/// Group-directed signal delivery.
///
/// POSIX spells a group target as a negated identifier, which is why the
/// adapter and not the runtime performs the negation: a platform-independent
/// caller must never encode a host signalling convention.
#[allow(unsafe_code)]
mod process_signals {
use std::io;
use flash_platform::{JobSignal, ProcessGroupId, SignalError};
pub(super) fn deliver(group: ProcessGroupId, signal: JobSignal) -> Result<(), SignalError> {
let number = match signal {
JobSignal::Stop => libc::SIGSTOP,
JobSignal::Continue => libc::SIGCONT,
JobSignal::Hangup => libc::SIGHUP,
JobSignal::Interrupt => libc::SIGINT,
JobSignal::Terminate => libc::SIGTERM,
JobSignal::Kill => libc::SIGKILL,
};
let target = libc::pid_t::try_from(group.get()).map_err(|_| SignalError::Operation {
kind: io::ErrorKind::InvalidInput,
message: format!("process group {group} exceeds the POSIX process identifier range"),
})?;
// Group 1 negates to the target POSIX reserves for "every process the
// caller may signal". Delivering a stop there would freeze the whole
// session, so the one identifier whose negation changes meaning is
// refused rather than sent.
if target == 1 {
return Err(SignalError::Operation {
kind: io::ErrorKind::InvalidInput,
message: "process group 1 is not addressable as a group".to_owned(),
});
}
let target = target.checked_neg().ok_or_else(|| SignalError::Operation {
kind: io::ErrorKind::InvalidInput,
message: format!("process group {group} has no negated group target"),
})?;
// SAFETY: kill takes two scalars and dereferences nothing.
if unsafe { libc::kill(target, number) } == -1 {
let error = io::Error::last_os_error();
return Err(SignalError::Operation {
kind: error.kind(),
message: error.to_string(),
});
}
Ok(())
}
}
/// Blocking child observation.
///
/// `std::process::Child::wait` is `waitpid` without `WUNTRACED`, so a stopped
/// child reports nothing and the caller blocks until it dies. The adapter
/// therefore reaps for itself. `Child` is kept only to own the pid and its
/// standard streams; std installs no `Drop` that reaps, so nothing the adapter
/// has already reaped is ever waited a second time.
///
/// One thing std's own wait does that this does not is close the child's input
/// handle before blocking, which is what stops a parent still holding that
/// handle from waiting on a child that is waiting on the parent. Nothing here
/// holds one: every spawned stream is handed over as an owned descriptor rather
/// than kept as a pipe, so the handles are already absent. A change that starts
/// retaining a child's input handle has to close it here before waiting.
#[allow(unsafe_code)]
mod child_wait {
use std::ffi::c_int;
use std::io;
#[cfg(target_os = "macos")]
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use flash_platform::{ProcessStatus, ProcessTransition, WaitError};
pub(super) fn observe(pid: c_int, flags: c_int) -> Result<ProcessTransition, WaitError> {
let mut status: c_int = 0;
loop {
// SAFETY: waitpid writes only through the status pointer, which
// points at a live local for the whole call.
let result = unsafe { libc::waitpid(pid, &raw mut status, flags) };
if result != -1 {
break;
}
let error = io::Error::last_os_error();
// A signal delivered to the shell while it waits is not a wait
// failure; the observation is simply not finished yet.
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(WaitError::new(error.kind(), error.to_string()));
}
decode(status)
}
pub(super) fn try_observe(
pid: c_int,
flags: c_int,
) -> Result<Option<ProcessTransition>, WaitError> {
let mut status: c_int = 0;
loop {
// SAFETY: waitpid writes only through the status pointer, which
// points at a live local for the whole call.
let result = unsafe { libc::waitpid(pid, &raw mut status, flags | libc::WNOHANG) };
if result == pid {
return decode(status).map(Some);
}
if result == 0 {
return Ok(None);
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(WaitError::new(error.kind(), error.to_string()));
}
}
#[cfg(target_os = "macos")]
pub(super) fn observe_transition(pid: c_int) -> Result<ProcessTransition, WaitError> {
if let Some(transition) = observe_nonblocking(pid)? {
return Ok(transition);
}
// Darwin's blocking waitpid can remain asleep when a terminal-stopped
// child is resumed by another process. EVFILT_PROC supplies the missing
// wakeup without a signal handler, timer, or polling loop; waitpid still
// remains the sole consumer and classifier of child status.
// SAFETY: kqueue takes no arguments and returns a new descriptor or -1.
let descriptor = unsafe { libc::kqueue() };
if descriptor == -1 {
let error = io::Error::last_os_error();
return Err(WaitError::new(error.kind(), error.to_string()));
}
// SAFETY: the descriptor was just created successfully and ownership
// transfers exactly once into this guard.
let queue = unsafe { OwnedFd::from_raw_fd(descriptor) };
// SAFETY: an all-zero kevent is a valid base that is fully initialized
// below before the kernel reads it.
let mut change: libc::kevent = unsafe { std::mem::zeroed() };
change.ident = pid as libc::uintptr_t;
change.filter = libc::EVFILT_PROC;
change.flags = (libc::EV_ADD | libc::EV_ENABLE | libc::EV_CLEAR) as libc::c_ushort;
change.fflags = libc::NOTE_SIGNAL | libc::NOTE_EXIT;
// SAFETY: change points to one initialized event, the output list is
// absent with count zero, and the queue descriptor remains owned.
let registered = unsafe {
libc::kevent(
queue.as_raw_fd(),
&raw const change,
1,
std::ptr::null_mut(),
0,
std::ptr::null(),
)
};
if registered == -1 {
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
// A short-lived child can exit between the nonblocking wait and
// registration. It still owns a waitable terminal status even
// though kqueue can no longer attach to the live process.
return observe(pid, libc::WUNTRACED | libc::WCONTINUED);
}
return Err(WaitError::new(error.kind(), error.to_string()));
}
loop {
if let Some(transition) = observe_nonblocking(pid)? {
return Ok(transition);
}
// SAFETY: kevent initializes every returned event field; zeroing
// the destination first also leaves a valid value on failure.
let mut event: libc::kevent = unsafe { std::mem::zeroed() };
// SAFETY: event points to space for exactly one result, no change
// list is supplied, and a null timeout requests an event-driven
// blocking wait.
let received = unsafe {
libc::kevent(
queue.as_raw_fd(),
std::ptr::null(),
0,
&raw mut event,
1,
std::ptr::null(),
)
};
if received == 1 {
if event.flags & libc::EV_ERROR as libc::c_ushort != 0 {
let code = i32::try_from(event.data).unwrap_or(libc::EINVAL);
let error = io::Error::from_raw_os_error(code);
return Err(WaitError::new(error.kind(), error.to_string()));
}
continue;
}
if received == 0 {
return Err(WaitError::new(
io::ErrorKind::Other,
"kqueue returned no process event without a timeout",
));
}
let error = io::Error::last_os_error();
if received == -1 && error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(WaitError::new(error.kind(), error.to_string()));
}
}
#[cfg(target_os = "macos")]
fn observe_nonblocking(pid: c_int) -> Result<Option<ProcessTransition>, WaitError> {
let mut status: c_int = 0;
loop {
// SAFETY: waitpid writes only through the status pointer, which
// points at a live local for the whole call.
let result = unsafe {
libc::waitpid(
pid,
&raw mut status,
libc::WNOHANG | libc::WUNTRACED | libc::WCONTINUED,
)
};
if result == pid {
return decode(status).map(Some);
}
if result == 0 {
return Ok(None);
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(WaitError::new(error.kind(), error.to_string()));
}
}
/// Classify one raw wait status.
///
/// Exit is tested rather than inferred from the other two failing. Only
/// Continued is checked explicitly before the terminal classifications so
/// no host representation can reach an exit-code macro and produce a
/// plausible-looking wrong number.
fn decode(status: c_int) -> Result<ProcessTransition, WaitError> {
if libc::WIFCONTINUED(status) {
return Ok(ProcessTransition::Continued);
}
if libc::WIFSTOPPED(status) {
return Ok(ProcessTransition::Stopped {
signal: libc::WSTOPSIG(status),
});
}
if libc::WIFSIGNALED(status) {
return Ok(ProcessTransition::Completed(ProcessStatus::Signaled(
libc::WTERMSIG(status),
)));
}
if libc::WIFEXITED(status) {
return Ok(ProcessTransition::Completed(ProcessStatus::Exited(
libc::WEXITSTATUS(status),
)));
}
Err(WaitError::new(
io::ErrorKind::Other,
format!("the host reported an unrecognized wait status {status:#x}"),
))
}
}
/// Terminal attribute and size primitives.
///
/// `dup2`/`close` in `child_descriptors` take only `c_int` and are declared
/// by hand. `tcgetattr`/`tcsetattr` take `*mut termios`, whose layout differs
/// across macOS, Linux and Redox, so the layouts come from `libc` rather than
/// being restated here where a wrong field would corrupt memory silently.
#[allow(unsafe_code)]
mod terminal_input {
use std::io;
use std::time::Duration;
#[cfg(target_os = "redox")]
pub(super) fn read(buffer: &mut [u8], timeout: Duration) -> io::Result<Option<usize>> {
// Redox PTYs can retain a queued byte without delivering a matching
// readiness notification. Avoid that lossy seam entirely: raw mode
// makes the descriptor nonblocking, and this bounded retry preserves
// the caller's timeout without spinning.
// SAFETY: F_GETFL only inspects the status flags of standard input.
let flags = unsafe { libc::fcntl(libc::STDIN_FILENO, libc::F_GETFL) };
if flags == -1 {
return Err(io::Error::last_os_error());
}
if flags & libc::O_NONBLOCK == 0 {
return Err(io::Error::other(
"Redox terminal input descriptor is not nonblocking",
));
}
if let Some(read) = read_nonblocking(buffer)? {
return Ok(Some(read));
}
std::thread::sleep(timeout);
read_nonblocking(buffer)
}
#[cfg(not(target_os = "redox"))]
pub(super) fn read(buffer: &mut [u8], timeout: Duration) -> io::Result<Option<usize>> {
let milliseconds = timeout.as_millis().min(i32::MAX as u128) as i32;
let mut descriptor = libc::pollfd {
fd: libc::STDIN_FILENO,
events: libc::POLLIN,
revents: 0,
};
// SAFETY: `descriptor` points to one initialized pollfd for the whole
// call and the descriptor count exactly matches that allocation.
let result = unsafe { libc::poll(&raw mut descriptor, 1, milliseconds) };
if result < 0 {
return Err(io::Error::last_os_error());
}
if result == 0 || descriptor.revents & (libc::POLLIN | libc::POLLHUP) == 0 {
return Ok(None);
}
read_nonblocking(buffer)
}
fn read_nonblocking(buffer: &mut [u8]) -> io::Result<Option<usize>> {
// SAFETY: `buffer` is valid for writes of its length and standard
// input remains owned by the process for the duration of the call.
let read =
unsafe { libc::read(libc::STDIN_FILENO, buffer.as_mut_ptr().cast(), buffer.len()) };
if read < 0 {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(None);
}
return Err(error);
}
Ok(Some(read as usize))
}
}
#[allow(unsafe_code)]
mod terminal_mode {
use std::io;
use std::os::fd::{AsRawFd, BorrowedFd};
pub(super) fn is_terminal(fd: BorrowedFd<'_>) -> bool {
// SAFETY: isatty only inspects the descriptor's kind and never
// dereferences memory; a borrowed fd is valid for the call.
unsafe { libc::isatty(fd.as_raw_fd()) == 1 }
}
pub(super) fn window_size(fd: BorrowedFd<'_>) -> io::Result<(u16, u16)> {
let mut size = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
// SAFETY: TIOCGWINSZ writes exactly one `winsize` through the pointer,
// which points at a live local of that type.
let result = unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCGWINSZ, &raw mut size) };
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok((size.ws_col, size.ws_row))
}
pub(super) fn current_attributes(fd: BorrowedFd<'_>) -> io::Result<libc::termios> {
// SAFETY: `zeroed` is a valid bit pattern for `termios` because every
// field is an integer or an array of integers, and tcgetattr
// overwrites the whole struct before it is read.
let mut attributes: libc::termios = unsafe { std::mem::zeroed() };
// SAFETY: the pointer targets a live, correctly typed local.
let result = unsafe { libc::tcgetattr(fd.as_raw_fd(), &raw mut attributes) };
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok(attributes)
}
pub(super) fn apply(fd: BorrowedFd<'_>, attributes: &libc::termios) -> io::Result<()> {
// SAFETY: the pointer targets a live, correctly typed value that
// tcsetattr only reads.
let result = unsafe { libc::tcsetattr(fd.as_raw_fd(), libc::TCSANOW, attributes) };
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) fn status_flags(fd: BorrowedFd<'_>) -> io::Result<libc::c_int> {
// SAFETY: F_GETFL only inspects the status flags of a live descriptor.
let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) };
if flags == -1 {
return Err(io::Error::last_os_error());
}
Ok(flags)
}
pub(super) fn apply_status_flags(fd: BorrowedFd<'_>, flags: libc::c_int) -> io::Result<()> {
// SAFETY: F_SETFL updates only the status flags of a live descriptor.
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, flags) };
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(super) fn raw_from(saved: &libc::termios) -> libc::termios {
let mut raw = *saved;
// SAFETY: cfmakeraw only writes the flag fields of a live struct.
unsafe { libc::cfmakeraw(&raw mut raw) };
// One byte at a time, no inter-byte timer: the decoder is incremental
// and must not wait for a full escape sequence to arrive.
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
raw
}
}
/// A raw-mode acquisition on the process's standard input.
#[derive(Debug)]
struct PosixTerminalModeGuard {
saved: libc::termios,
saved_status: libc::c_int,
restored: bool,
}
struct PosixTerminalModeToken {
attributes: libc::termios,
}
impl fmt::Debug for PosixTerminalModeToken {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PosixTerminalModeToken(..)")
}
}
impl TerminalModeToken for PosixTerminalModeToken {
fn as_any(&self) -> &dyn Any {
self
}
}
impl TerminalModeGuard for PosixTerminalModeGuard {
fn restore(&mut self) -> Result<(), PlatformError> {
if self.restored {
return Ok(());
}
let stdin = io::stdin();
let status_result = terminal_mode::apply_status_flags(stdin.as_fd(), self.saved_status);
let attributes_result = terminal_mode::apply(stdin.as_fd(), &self.saved);
if let Err(error) = status_result.and(attributes_result) {
return Err(PlatformError::Unavailable {
capability: Capability::TerminalInfo,
reason: format!("restoring terminal mode failed: {error}"),
});
}
self.restored = true;
Ok(())
}
}
impl Drop for PosixTerminalModeGuard {
fn drop(&mut self) {
// A failure here cannot be reported and must not panic during unwind.
let _ = self.restore();
}
}
/// Terminal ownership held by a foreground job on the process's standard input.
#[derive(Debug)]
struct PosixForegroundTerminalGuard {
previous: Option<ProcessGroupId>,
restored: bool,
}
impl ForegroundTerminalGuard for PosixForegroundTerminalGuard {
fn restore(&mut self) -> Result<(), PlatformError> {
if self.restored {
return Ok(());
}
// Nothing owned the terminal when the handover began, so there is no
// owner to give it back to and the guard is already settled.
let Some(previous) = self.previous else {
self.restored = true;
return Ok(());
};
foreground_terminal::hand_over(io::stdin().as_fd(), previous)
.map_err(foreground_unavailable)?;
self.restored = true;
Ok(())
}
fn previous_owner(&self) -> Option<ProcessGroupId> {
self.previous
}
}
impl Drop for PosixForegroundTerminalGuard {
fn drop(&mut self) {
// A failure here cannot be reported and must not panic during unwind.
let _ = self.restore();
}
}
fn foreground_unavailable(error: io::Error) -> PlatformError {
PlatformError::Unavailable {
capability: Capability::ForegroundTerminal,
reason: format!("terminal ownership operation failed: {error}"),
}
}