#[cfg(feature = "unix-runtime")]
mod backend;
mod fd;
#[cfg(any(feature = "unix-runtime", feature = "frontend"))]
mod signals;
#[cfg(feature = "test-support")]
mod test_runtimes;
#[cfg(feature = "test-support")]
mod test_stdio;
#[cfg(feature = "unix-runtime")]
mod unix_exec;
mod wait;
use std::ffi::CString;
use std::io;
#[cfg(any(feature = "unix-runtime", feature = "test-support"))]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::path::PathBuf;
#[cfg(feature = "unix-runtime")]
use self::backend::{FdAction, exec, pid_from_handle, spawn_command};
pub use self::fd::{FileDescriptor, OsPipe};
#[cfg(any(feature = "unix-runtime", feature = "frontend"))]
pub(crate) use self::signals::SignalDispositionGuard;
#[cfg(feature = "test-support")]
pub use self::test_runtimes::{DeterministicRuntime, InMemoryCommand, InMemoryRuntime};
#[cfg(feature = "test-support")]
pub use self::test_stdio::{StringStdioIn, StringStdioOut};
use self::wait::wait_child_status;
#[cfg(feature = "unix-runtime")]
use self::wait::wait_process;
pub use self::wait::{ProcessEvent, WaitMode};
pub fn parent_pid() -> u32 {
unsafe { libc::getppid() as u32 }
}
pub fn get_umask() -> u32 {
let mask = unsafe { libc::umask(0) };
unsafe { libc::umask(mask) };
mask as u32
}
pub fn set_umask(mask: u32) {
unsafe { libc::umask(mask as libc::mode_t) };
}
#[cfg(feature = "embed")]
#[derive(Clone, Copy)]
pub(crate) struct ResourceLimitSpec {
pub(crate) resource: libc::c_int,
pub(crate) name: &'static str,
}
#[cfg(feature = "embed")]
#[derive(Clone, Copy)]
pub(crate) struct CapturedResourceLimit {
resource: libc::c_int,
limit: libc::rlimit,
}
#[cfg(feature = "embed")]
pub(crate) const SHELL_RESOURCE_LIMITS: &[ResourceLimitSpec] = &[
ResourceLimitSpec {
resource: libc::RLIMIT_FSIZE as libc::c_int,
name: "file size",
},
ResourceLimitSpec {
resource: libc::RLIMIT_NOFILE as libc::c_int,
name: "open files",
},
];
#[cfg(feature = "embed")]
pub(crate) fn get_resource_limit(resource: libc::c_int) -> io::Result<libc::rlimit> {
let mut limit = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(resource as _, &mut limit) } != 0 {
Err(io::Error::last_os_error())
} else {
Ok(limit)
}
}
#[cfg(feature = "embed")]
pub(crate) fn set_resource_limit(resource: libc::c_int, limit: &libc::rlimit) -> io::Result<()> {
if unsafe { libc::setrlimit(resource as _, limit) } != 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(feature = "embed")]
pub(crate) fn capture_resource_limits() -> Vec<CapturedResourceLimit> {
SHELL_RESOURCE_LIMITS
.iter()
.filter_map(|spec| {
get_resource_limit(spec.resource)
.ok()
.map(|limit| CapturedResourceLimit {
resource: spec.resource,
limit,
})
})
.collect()
}
#[cfg(feature = "embed")]
pub(crate) fn restore_resource_limits(limits: &[CapturedResourceLimit]) {
for limit in limits {
let _ = set_resource_limit(limit.resource, &limit.limit);
}
}
pub struct ProcessTimes {
pub user_secs: f64,
pub sys_secs: f64,
pub child_user_secs: f64,
pub child_sys_secs: f64,
}
pub fn get_times() -> ProcessTimes {
let mut tms = libc::tms {
tms_utime: 0,
tms_stime: 0,
tms_cutime: 0,
tms_cstime: 0,
};
unsafe { libc::times(&mut tms) };
let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as f64;
ProcessTimes {
user_secs: tms.tms_utime as f64 / ticks,
sys_secs: tms.tms_stime as f64 / ticks,
child_user_secs: tms.tms_cutime as f64 / ticks,
child_sys_secs: tms.tms_cstime as f64 / ticks,
}
}
fn glob_literal_escape(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for ch in value.chars() {
if matches!(ch, '\\' | '*' | '?' | '[') {
escaped.push('\\');
}
escaped.push(ch);
}
escaped
}
fn glob_pattern_with_cwd(pattern: &str, cwd: &Path) -> (String, Option<String>) {
if Path::new(pattern).is_absolute() {
return (pattern.to_string(), None);
}
let cwd = cwd.to_string_lossy();
let mut glob_pattern = glob_literal_escape(&cwd);
if !glob_pattern.ends_with('/') {
glob_pattern.push('/');
}
glob_pattern.push_str(pattern);
let mut result_prefix = cwd.into_owned();
if !result_prefix.ends_with('/') {
result_prefix.push('/');
}
(glob_pattern, Some(result_prefix))
}
pub fn glob_expand(pattern: &str, cwd: &Path) -> Vec<String> {
let (pattern, result_prefix) = glob_pattern_with_cwd(pattern, cwd);
let c_pattern = match CString::new(pattern) {
Ok(c) => c,
Err(_) => return Vec::new(),
};
let mut glob_buf: libc::glob_t = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::glob(c_pattern.as_ptr(), 0, None, &mut glob_buf) };
let mut results = Vec::new();
if ret == 0 {
for i in 0..glob_buf.gl_pathc {
let path = unsafe { *glob_buf.gl_pathv.add(i) };
if !path.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(path) };
if let Ok(s) = s.to_str() {
let result = result_prefix
.as_deref()
.and_then(|prefix| s.strip_prefix(prefix))
.unwrap_or(s);
results.push(result.to_string());
}
}
}
}
unsafe { libc::globfree(&mut glob_buf) };
results
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ProcessHandle(u64);
impl ProcessHandle {
pub const fn new(raw: u64) -> Self {
Self(raw)
}
pub const fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpawnMode {
Foreground,
BackgroundJob,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RuntimeSignal {
Continue,
Stop,
Interrupt,
Terminate,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpawnedProcess {
pub handle: ProcessHandle,
pub display_pid: Option<u32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PassedFileDescriptor {
pub parent_fd: FileDescriptor,
pub child_fd: FileDescriptor,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ChildSignalPlan {
pub default_signals: Vec<i32>,
pub ignored_signals: Vec<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExternalCommand {
pub program: String,
pub argv: Vec<String>,
pub env: Vec<(String, String)>,
pub cwd: PathBuf,
pub create_process_group: bool,
pub join_process_group: Option<ProcessHandle>,
pub passed_fds: Vec<PassedFileDescriptor>,
pub signal_plan: ChildSignalPlan,
}
#[cfg(feature = "embed")]
pub(crate) fn spawn_error_exit_status(err: &io::Error) -> i32 {
if err.kind() == io::ErrorKind::NotFound || err.raw_os_error() == Some(libc::ENOENT) {
127
} else {
126
}
}
pub trait Runtime: Send {
type ForegroundGuard;
fn fork(&self) -> Result<Self, std::io::Error>
where
Self: Sized;
fn spawn_external_command(
&mut self,
command: &ExternalCommand,
stdio: SpawnStdio,
close_fds: &[FileDescriptor],
mode: SpawnMode,
) -> Result<SpawnedProcess, std::io::Error>;
fn wait_process(
&mut self,
process: ProcessHandle,
mode: WaitMode,
) -> Result<ProcessEvent, std::io::Error>;
fn wait_child(&mut self, process: ProcessHandle) -> i32 {
wait_child_status(|| self.wait_process(process, WaitMode::Block))
}
fn signal_process_group(
&mut self,
process: ProcessHandle,
signal: RuntimeSignal,
) -> Result<(), std::io::Error>;
fn claim_foreground(
&mut self,
process: ProcessHandle,
tty: FileDescriptor,
) -> Result<Self::ForegroundGuard, std::io::Error>;
fn release_foreground(&mut self, guard: Self::ForegroundGuard) -> Result<(), std::io::Error>;
fn has_command(&self, _program: &str) -> bool {
false
}
fn resolve_command_path(
&self,
program: &str,
_path_var: &str,
_cwd: &Path,
) -> Result<PathBuf, io::Error> {
if self.has_command(program) {
Ok(PathBuf::from(program))
} else {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{program}: command not found"),
))
}
}
fn exec_replace(
&self,
program: &str,
argv: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<(), std::io::Error>;
fn exec_replace_command(
&self,
command: &ExternalCommand,
stdio: SpawnStdio,
close_fds: &[FileDescriptor],
) -> Result<(), std::io::Error> {
let _ = (command, stdio, close_fds);
Err(io::Error::new(
io::ErrorKind::Unsupported,
"planned exec replacement is unavailable in this runtime",
))
}
}
#[cfg(any(feature = "unix-runtime", feature = "test-support"))]
fn resolve_path_against(cwd: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
}
#[cfg(any(feature = "unix-runtime", feature = "test-support"))]
fn resolve_executable_path(path: &Path) -> Result<PathBuf, io::Error> {
let meta = match std::fs::metadata(path) {
Ok(meta) => meta,
Err(err) => {
return Err(io::Error::new(io::ErrorKind::NotFound, err.to_string()));
}
};
if !meta.is_file() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{}: Permission denied", path.display()),
));
}
if meta.permissions().mode() & 0o111 == 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{}: Permission denied", path.display()),
));
}
Ok(path.to_path_buf())
}
#[cfg(any(feature = "unix-runtime", feature = "test-support"))]
fn resolve_command_path(program: &str, path_var: &str, cwd: &Path) -> Result<PathBuf, io::Error> {
if program.contains('/') {
return resolve_executable_path(&resolve_path_against(cwd, Path::new(program)));
}
let mut permission_denied = None;
for dir in path_var.split(':') {
let candidate = resolve_path_against(cwd, Path::new(dir)).join(program);
match resolve_executable_path(&candidate) {
Ok(path) => return Ok(path),
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) if err.kind() == io::ErrorKind::PermissionDenied => {
permission_denied.get_or_insert(err);
}
Err(err) => return Err(err),
}
}
if let Some(err) = permission_denied {
return Err(err);
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{program}: command not found"),
))
}
#[cfg(feature = "unix-runtime")]
#[derive(Clone, Debug)]
pub struct UnixRuntime {}
#[cfg(feature = "unix-runtime")]
#[derive(Clone, Copy, Debug)]
pub struct UnixForegroundGuard {
tty: FileDescriptor,
previous_pgid: libc::pid_t,
}
#[cfg(feature = "unix-runtime")]
impl UnixRuntime {
pub fn new() -> Self {
Self {}
}
}
#[cfg(feature = "unix-runtime")]
impl Default for UnixRuntime {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "unix-runtime")]
impl Runtime for UnixRuntime {
type ForegroundGuard = UnixForegroundGuard;
fn fork(&self) -> Result<Self, std::io::Error> {
Ok(Self::new())
}
fn spawn_external_command(
&mut self,
command: &ExternalCommand,
stdio: SpawnStdio,
close_fds: &[FileDescriptor],
mode: SpawnMode,
) -> Result<SpawnedProcess, std::io::Error> {
let fd_actions: Vec<FdAction> = close_fds.iter().copied().map(FdAction::Close).collect();
let create_process_group =
command.create_process_group || matches!(mode, SpawnMode::BackgroundJob);
let join_process_group = (!create_process_group)
.then_some(command.join_process_group)
.flatten();
let pid = spawn_command(
command,
create_process_group,
join_process_group,
stdio,
&fd_actions,
)?;
Ok(SpawnedProcess {
handle: ProcessHandle::new(pid as u64),
display_pid: Some(pid as u32),
})
}
fn wait_process(
&mut self,
process: ProcessHandle,
mode: WaitMode,
) -> Result<ProcessEvent, std::io::Error> {
wait_process(pid_from_handle(process)?, mode)
}
fn signal_process_group(
&mut self,
process: ProcessHandle,
signal: RuntimeSignal,
) -> Result<(), std::io::Error> {
let pid = pid_from_handle(process)?;
let signal = match signal {
RuntimeSignal::Continue => libc::SIGCONT,
RuntimeSignal::Stop => libc::SIGSTOP,
RuntimeSignal::Interrupt => libc::SIGINT,
RuntimeSignal::Terminate => libc::SIGTERM,
};
if unsafe { libc::kill(-pid, signal) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
fn claim_foreground(
&mut self,
process: ProcessHandle,
tty: FileDescriptor,
) -> Result<Self::ForegroundGuard, std::io::Error> {
let pid = pid_from_handle(process)?;
let previous_pgid = unsafe { libc::tcgetpgrp(tty.into_raw_fd()) };
if previous_pgid < 0 {
return Err(std::io::Error::last_os_error());
}
if unsafe { libc::tcsetpgrp(tty.into_raw_fd(), pid) } == 0 {
Ok(UnixForegroundGuard { tty, previous_pgid })
} else {
Err(std::io::Error::last_os_error())
}
}
fn release_foreground(&mut self, guard: Self::ForegroundGuard) -> Result<(), std::io::Error> {
if unsafe { libc::tcsetpgrp(guard.tty.into_raw_fd(), guard.previous_pgid) } == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
fn has_command(&self, _program: &str) -> bool {
false
}
fn resolve_command_path(
&self,
program: &str,
path_var: &str,
cwd: &Path,
) -> Result<PathBuf, io::Error> {
resolve_command_path(program, path_var, cwd)
}
fn exec_replace(
&self,
program: &str,
argv: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<(), std::io::Error> {
exec(program, argv, env, cwd)
}
fn exec_replace_command(
&self,
command: &ExternalCommand,
stdio: SpawnStdio,
close_fds: &[FileDescriptor],
) -> Result<(), std::io::Error> {
self::unix_exec::exec_replace_command(command, stdio, close_fds)
}
}
#[derive(Clone, Copy, Debug)]
pub struct SpawnStdio {
pub stdin_fd: FileDescriptor,
pub stdout_fd: FileDescriptor,
pub stderr_fd: FileDescriptor,
}
impl Default for SpawnStdio {
fn default() -> Self {
Self {
stdin_fd: FileDescriptor::STDIN,
stdout_fd: FileDescriptor::STDOUT,
stderr_fd: FileDescriptor::STDERR,
}
}
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
mod tests;