use crate::modules::input::Token;
use crate::{
RuntimeError,
constants::{FILE_CHUNK, INLINE_PAYLOAD, PROCESS_POLL},
executor,
futures::{
process::exit_status::{ExitStatus, ProcessOutput},
task::sealed,
task::{Nothing, Task},
},
modules::event_desc::EventDesc,
modules::{
fd::Fd,
int_check::IntCheck,
kevent::{KEvent, eventlist},
kqueue::{self, Waited},
wake_target::WakeTarget,
},
};
use std::{
ffi::{CStr, CString, OsStr},
mem,
os::unix::ffi::OsStrExt,
path::Path,
ptr,
sync::{Arc, OnceLock},
};
const _: () = assert!(mem::size_of::<Result<ExitStatus, RuntimeError>>() <= INLINE_PAYLOAD);
const _: () = assert!(mem::size_of::<Result<ProcessOutput, RuntimeError>>() <= INLINE_PAYLOAD);
const NO_CHILD: libc::pid_t = -1;
const IGNORED: libc::c_int = -1;
const F_SETNOSIGPIPE: libc::c_int = 73;
const DEV_NULL: &CStr = c"/dev/null";
const SPAWN_FLAGS: libc::c_short = (libc::POSIX_SPAWN_CLOEXEC_DEFAULT
| libc::POSIX_SPAWN_SETSIGDEF
| libc::POSIX_SPAWN_SETSIGMASK
| libc::POSIX_SPAWN_SETPGROUP) as libc::c_short;
#[derive(Debug, Clone)]
pub(crate) struct Program {
file: Option<CString>,
args: Option<Arc<[CString]>>,
}
impl Program {
pub(crate) fn new<S, A, I>(program: S, args: A) -> Self
where
S: AsRef<OsStr>,
A: IntoIterator<Item = I>,
I: AsRef<OsStr>,
{
Self {
file: as_c_arg(program),
args: args.into_iter().map(as_c_arg).collect(),
}
}
fn argv(&self, dir: Option<&CStr>) -> Result<(CString, Vec<*mut libc::c_char>), RuntimeError> {
let file = self.file.as_ref().ok_or(RuntimeError::BadPath)?;
let args = self.args.as_ref().ok_or(RuntimeError::BadArgument)?;
let mut argv = Vec::with_capacity(args.len() + 2);
argv.push(file.as_ptr().cast_mut());
argv.extend(args.iter().map(|arg| arg.as_ptr().cast_mut()));
argv.push(ptr::null_mut());
let spawn_as = match dir {
Some(dir) if relative(file) => join(dir, file)?,
_ => file.clone(),
};
Ok((spawn_as, argv))
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct Setup {
pub(crate) input: Option<Arc<[u8]>>,
pub(crate) dir: Dir,
pub(crate) env: Env,
}
#[derive(Debug, Clone, Default)]
pub(crate) enum Dir {
#[default]
Inherited,
At(CString),
Bad,
}
#[derive(Debug, Clone, Default)]
pub(crate) enum Env {
#[default]
Inherited,
Over(Arc<[CString]>),
Only(Arc<[CString]>),
Bad,
}
#[derive(Debug, Clone)]
#[must_use = "a task does nothing until it is run or spawned"]
pub struct StatusTask {
program: Program,
setup: Setup,
}
#[derive(Debug, Clone)]
#[must_use = "a task does nothing until it is run or spawned"]
pub struct OutputTask {
program: Program,
setup: Setup,
}
impl StatusTask {
pub(crate) fn new<S, A, I>(program: S, args: A) -> Self
where
S: AsRef<OsStr>,
A: IntoIterator<Item = I>,
I: AsRef<OsStr>,
{
Self {
program: Program::new(program, args),
setup: Setup::default(),
}
}
pub fn input(mut self, data: impl Into<Arc<[u8]>>) -> Self {
self.setup.input = Some(data.into());
self
}
pub fn in_dir(mut self, path: impl AsRef<Path>) -> Self {
self.setup.dir = as_dir(path);
self
}
pub fn env<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.setup.env = as_env(vars, Env::Over);
self
}
pub fn env_only<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.setup.env = as_env(vars, Env::Only);
self
}
}
impl OutputTask {
pub(crate) fn new<S, A, I>(program: S, args: A) -> Self
where
S: AsRef<OsStr>,
A: IntoIterator<Item = I>,
I: AsRef<OsStr>,
{
Self {
program: Program::new(program, args),
setup: Setup::default(),
}
}
pub fn input(mut self, data: impl Into<Arc<[u8]>>) -> Self {
self.setup.input = Some(data.into());
self
}
pub fn in_dir(mut self, path: impl AsRef<Path>) -> Self {
self.setup.dir = as_dir(path);
self
}
pub fn env<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.setup.env = as_env(vars, Env::Over);
self
}
pub fn env_only<I, K, V>(mut self, vars: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.setup.env = as_env(vars, Env::Only);
self
}
}
impl sealed::Sealed for StatusTask {}
impl sealed::Sealed for OutputTask {}
impl Task for StatusTask {
type Output = Result<ExitStatus, RuntimeError>;
type Input = Nothing;
fn execute(&self, _token: Token, _reactor_id: i32, _task_id: usize) -> Self::Output {
if executor::cancelled() {
return Err(RuntimeError::Cancelled);
}
let data = self.setup.input.as_deref().unwrap_or(&[]);
if data.is_empty() {
let stdio = Stdio {
input: None,
capture: None,
};
let pid = spawn_child(&self.program, &self.setup, stdio)?;
let mut child = Child::new(pid);
return wait_exit(&mut child, kqueue::id().ok());
}
let (in_read, in_write) = input_pipe()?;
let stdio = Stdio {
input: Some(in_read.raw()),
capture: None,
};
let pid = spawn_child(&self.program, &self.setup, stdio)?;
let mut child = Child::new(pid);
drop(in_read);
let mut in_write = Some(in_write);
let queue = kqueue::id().ok();
let fed = match queue {
Some(queue) => exchange(queue, &mut in_write, data, None, None),
None => poll_exchange(&mut in_write, data, None, None),
};
fed?;
drop(in_write);
wait_exit(&mut child, queue)
}
fn blocking(&self, _token: Token) -> bool {
true
}
}
impl Task for OutputTask {
type Output = Result<ProcessOutput, RuntimeError>;
type Input = Nothing;
fn execute(&self, _token: Token, _reactor_id: i32, _task_id: usize) -> Self::Output {
if executor::cancelled() {
return Err(RuntimeError::Cancelled);
}
let data = self.setup.input.as_deref().unwrap_or(&[]);
let feeding = match data.is_empty() {
true => None,
false => Some(input_pipe()?),
};
let (out_read, out_write) = pipe()?;
let (err_read, err_write) = pipe()?;
let stdio = Stdio {
input: feeding.as_ref().map(|(read, _)| read.raw()),
capture: Some((out_write.raw(), err_write.raw())),
};
let pid = spawn_child(&self.program, &self.setup, stdio)?;
let mut child = Child::new(pid);
let mut in_write = match feeding {
Some((in_read, in_write)) => {
drop(in_read);
Some(in_write)
}
None => None,
};
drop(out_write);
drop(err_write);
let queue = kqueue::id().ok();
let (stdout, stderr) = match queue {
Some(queue) => exchange(queue, &mut in_write, data, Some(&out_read), Some(&err_read)),
None => poll_exchange(&mut in_write, data, Some(&out_read), Some(&err_read)),
}?;
drop(in_write);
drop(out_read);
drop(err_read);
let status = wait_exit(&mut child, queue)?;
Ok(ProcessOutput::new(stdout, stderr, status))
}
fn blocking(&self, _token: Token) -> bool {
true
}
}
pub(crate) struct Child {
pid: libc::pid_t,
queue: Option<i32>,
}
impl Child {
pub(crate) fn new(pid: libc::pid_t) -> Self {
Self { pid, queue: None }
}
fn watching(&mut self, queue: i32) {
self.queue = Some(queue);
}
pub(crate) fn reaped(&mut self) {
self.pid = NO_CHILD;
}
}
impl Drop for Child {
fn drop(&mut self) {
if self.pid == NO_CHILD {
return;
}
if let Some(queue) = self.queue {
unwatch_proc(queue, self.pid);
}
kill_and_reap(self.pid);
}
}
struct SpawnAttr(libc::posix_spawnattr_t);
impl SpawnAttr {
fn new() -> Result<Self, RuntimeError> {
let mut raw: libc::posix_spawnattr_t = ptr::null_mut();
spawn_check(unsafe { libc::posix_spawnattr_init(&mut raw) })?;
Ok(Self(raw))
}
}
impl Drop for SpawnAttr {
fn drop(&mut self) {
unsafe { libc::posix_spawnattr_destroy(&mut self.0) };
}
}
struct FileActions(libc::posix_spawn_file_actions_t);
impl FileActions {
fn new() -> Result<Self, RuntimeError> {
let mut raw: libc::posix_spawn_file_actions_t = ptr::null_mut();
spawn_check(unsafe { libc::posix_spawn_file_actions_init(&mut raw) })?;
Ok(Self(raw))
}
}
impl Drop for FileActions {
fn drop(&mut self) {
unsafe { libc::posix_spawn_file_actions_destroy(&mut self.0) };
}
}
fn spawn_check(code: libc::c_int) -> Result<(), RuntimeError> {
if code == 0 {
return Ok(());
}
Err(RuntimeError::CheckError(Some(code)))
}
fn as_c_arg(arg: impl AsRef<OsStr>) -> Option<CString> {
CString::new(arg.as_ref().as_bytes()).ok()
}
type AddChdir =
unsafe extern "C" fn(*mut libc::posix_spawn_file_actions_t, *const libc::c_char) -> libc::c_int;
const RTLD_DEFAULT: *mut libc::c_void = -2isize as *mut libc::c_void;
const ADD_CHDIR_NP: &CStr = c"posix_spawn_file_actions_addchdir_np";
const ADD_CHDIR: &CStr = c"posix_spawn_file_actions_addchdir";
fn add_chdir() -> Option<AddChdir> {
static FOUND: OnceLock<Option<AddChdir>> = OnceLock::new();
*FOUND.get_or_init(|| {
for name in [ADD_CHDIR_NP, ADD_CHDIR] {
let symbol = unsafe { libc::dlsym(RTLD_DEFAULT, name.as_ptr()) };
if symbol.is_null() {
continue;
}
return Some(unsafe { mem::transmute::<*mut libc::c_void, AddChdir>(symbol) });
}
None
})
}
fn relative(file: &CStr) -> bool {
let bytes = file.to_bytes();
!bytes.starts_with(b"/") && bytes.contains(&b'/')
}
fn join(dir: &CStr, file: &CStr) -> Result<CString, RuntimeError> {
let dir = dir.to_bytes();
let file = file.to_bytes();
let mut path = Vec::with_capacity(dir.len() + file.len() + 1);
path.extend_from_slice(dir);
if !dir.ends_with(b"/") {
path.push(b'/');
}
path.extend_from_slice(file);
CString::new(path).map_err(|_| RuntimeError::BadDirectory)
}
pub(crate) fn as_dir(path: impl AsRef<Path>) -> Dir {
let path = path.as_ref();
if !path.is_absolute() {
return Dir::Bad;
}
match CString::new(path.as_os_str().as_bytes()) {
Ok(dir) => Dir::At(dir),
Err(_) => Dir::Bad,
}
}
fn as_c_var(name: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> Option<CString> {
let name = name.as_ref().as_bytes();
let value = value.as_ref().as_bytes();
if name.is_empty() || name.contains(&b'=') {
return None;
}
let mut entry = Vec::with_capacity(name.len() + value.len() + 1);
entry.extend_from_slice(name);
entry.push(b'=');
entry.extend_from_slice(value);
CString::new(entry).ok()
}
pub(crate) fn as_env<I, K, V>(vars: I, into: fn(Arc<[CString]>) -> Env) -> Env
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let converted = vars
.into_iter()
.map(|(name, value)| as_c_var(name, value))
.collect::<Option<Arc<[CString]>>>();
match converted {
Some(vars) => into(vars),
None => Env::Bad,
}
}
impl Env {
fn envp(&self) -> Result<Option<Vec<*mut libc::c_char>>, RuntimeError> {
match self {
Self::Inherited => Ok(None),
Self::Bad => Err(RuntimeError::BadVariable),
Self::Only(vars) => {
let mut envp = Vec::with_capacity(vars.len() + 1);
envp.extend(vars.iter().map(|var| var.as_ptr().cast_mut()));
envp.push(ptr::null_mut());
Ok(Some(envp))
}
Self::Over(vars) => Ok(Some(merge(&inherited(), vars))),
}
}
}
fn merge(base: &[*mut libc::c_char], over: &[CString]) -> Vec<*mut libc::c_char> {
let mut envp = Vec::with_capacity(base.len() + over.len() + 1);
for entry in base {
let name = key(unsafe { CStr::from_ptr(*entry) }.to_bytes());
if over.iter().any(|var| key(var.to_bytes()) == name) {
continue;
}
envp.push(*entry);
}
envp.extend(over.iter().map(|var| var.as_ptr().cast_mut()));
envp.push(ptr::null_mut());
envp
}
fn inherited() -> Vec<*mut libc::c_char> {
let mut found = Vec::new();
let mut at = unsafe { *libc::_NSGetEnviron() };
if at.is_null() {
return found;
}
loop {
let entry = unsafe { *at };
if entry.is_null() {
return found;
}
found.push(entry);
at = unsafe { at.add(1) };
}
}
fn key(entry: &[u8]) -> &[u8] {
match entry.iter().position(|byte| *byte == b'=') {
Some(at) => &entry[..at],
None => entry,
}
}
pub(crate) fn pipe() -> Result<(Fd, Fd), RuntimeError> {
let mut ends: [libc::c_int; 2] = [-1, -1];
unsafe { libc::pipe(ends.as_mut_ptr()) }.check()?;
let read = Fd::new(ends[0]);
let write = Fd::new(ends[1]);
unsafe { libc::fcntl(read.raw(), libc::F_SETFD, libc::FD_CLOEXEC) }.check()?;
unsafe { libc::fcntl(write.raw(), libc::F_SETFD, libc::FD_CLOEXEC) }.check()?;
Ok((read, write))
}
pub(crate) fn input_pipe() -> Result<(Fd, Fd), RuntimeError> {
let (read, write) = pipe()?;
unsafe { libc::fcntl(write.raw(), libc::F_SETFL, libc::O_NONBLOCK) }.check()?;
let _ = unsafe { libc::fcntl(write.raw(), F_SETNOSIGPIPE, 1) };
Ok((read, write))
}
pub(crate) struct Stdio {
pub(crate) input: Option<libc::c_int>,
pub(crate) capture: Option<(libc::c_int, libc::c_int)>,
}
pub(crate) fn spawn_child(
program: &Program,
setup: &Setup,
stdio: Stdio,
) -> Result<libc::pid_t, RuntimeError> {
let dir = match &setup.dir {
Dir::Inherited => None,
Dir::At(dir) => Some(dir.as_c_str()),
Dir::Bad => return Err(RuntimeError::BadDirectory),
};
let (file, argv) = program.argv(dir)?;
let envp = setup.env.envp()?;
let mut actions = FileActions::new()?;
let mut attr = SpawnAttr::new()?;
match stdio.input {
Some(fd) => {
spawn_check(unsafe { libc::posix_spawn_file_actions_adddup2(&mut actions.0, fd, 0) })?;
}
None => {
spawn_check(unsafe {
libc::posix_spawn_file_actions_addopen(
&mut actions.0,
0,
DEV_NULL.as_ptr(),
libc::O_RDONLY,
0,
)
})?;
}
}
match stdio.capture {
Some((out, err)) => {
spawn_check(unsafe { libc::posix_spawn_file_actions_adddup2(&mut actions.0, out, 1) })?;
spawn_check(unsafe { libc::posix_spawn_file_actions_adddup2(&mut actions.0, err, 2) })?;
}
None => {
spawn_check(unsafe { libc::posix_spawn_file_actions_adddup2(&mut actions.0, 1, 1) })?;
spawn_check(unsafe { libc::posix_spawn_file_actions_adddup2(&mut actions.0, 2, 2) })?;
}
}
if let Some(dir) = dir {
let Some(chdir) = add_chdir() else {
return Err(RuntimeError::CheckError(Some(libc::ENOSYS)));
};
spawn_check(unsafe { chdir(&mut actions.0, dir.as_ptr()) })?;
}
let mut empty: libc::sigset_t = unsafe { mem::zeroed() };
let mut full: libc::sigset_t = unsafe { mem::zeroed() };
unsafe { libc::sigemptyset(&mut empty) }.check()?;
unsafe { libc::sigfillset(&mut full) }.check()?;
spawn_check(unsafe { libc::posix_spawnattr_setsigmask(&mut attr.0, &empty) })?;
spawn_check(unsafe { libc::posix_spawnattr_setsigdefault(&mut attr.0, &full) })?;
spawn_check(unsafe { libc::posix_spawnattr_setpgroup(&mut attr.0, 0) })?;
spawn_check(unsafe { libc::posix_spawnattr_setflags(&mut attr.0, SPAWN_FLAGS) })?;
let mut pid: libc::pid_t = 0;
let handed = match &envp {
Some(envp) => envp.as_ptr(),
None => (unsafe { *libc::_NSGetEnviron() }) as *const *mut libc::c_char,
};
let code = unsafe {
libc::posix_spawnp(
&mut pid,
file.as_ptr(),
&actions.0,
&attr.0,
argv.as_ptr(),
handed,
)
};
spawn_check(code)?;
Ok(pid)
}
struct Feed<'a> {
end: &'a mut Option<Fd>,
data: &'a [u8],
sent: usize,
queue: i32,
}
impl Feed<'_> {
fn open(&self) -> bool {
self.end.is_some()
}
fn is(&self, ident: usize, filter: i16) -> bool {
match self.end.as_ref() {
Some(end) => filter == libc::EVFILT_WRITE && ident == end.raw() as usize,
None => false,
}
}
fn watch(&self) -> Result<(), RuntimeError> {
let Some(end) = self.end.as_ref() else {
return Ok(());
};
unsafe {
KEvent::register(
self.queue,
end.raw() as usize,
0,
WakeTarget::None.encode(),
EventDesc::new_write(),
)
}
.check()?;
Ok(())
}
fn push(&mut self) -> Result<(), RuntimeError> {
let Some(fd) = self.end.as_ref().map(|end| end.raw()) else {
return Ok(());
};
if !write_chunk(fd, self.data, &mut self.sent)? {
self.finish();
}
Ok(())
}
fn finish(&mut self) {
let Some(end) = self.end.as_ref() else {
return;
};
unwatch_write(self.queue, end.raw());
self.end.take();
}
}
impl Drop for Feed<'_> {
fn drop(&mut self) {
self.finish();
}
}
struct Reads<'a> {
queue: i32,
ends: &'a [libc::c_int; 2],
open: [bool; 2],
}
impl Drop for Reads<'_> {
fn drop(&mut self) {
for (slot, end) in self.ends.iter().enumerate() {
if self.open[slot] {
unwatch_read(self.queue, *end);
self.open[slot] = false;
}
}
}
}
fn exchange(
queue: i32,
input: &mut Option<Fd>,
data: &[u8],
out: Option<&Fd>,
err: Option<&Fd>,
) -> Result<(Vec<u8>, Vec<u8>), RuntimeError> {
let ends = [
out.map_or(IGNORED, |end| end.raw()),
err.map_or(IGNORED, |end| end.raw()),
];
let mut found = [Vec::new(), Vec::new()];
let mut reads = Reads {
queue,
ends: &ends,
open: [false, false],
};
let mut feed = Feed {
end: input,
data,
sent: 0,
queue,
};
let mut outcome = Ok(());
for (slot, end) in ends.iter().enumerate() {
if *end == IGNORED {
continue;
}
let watched = unsafe {
KEvent::register(
queue,
*end as usize,
0,
WakeTarget::None.encode(),
EventDesc::new_read(),
)
}
.check();
match watched {
Ok(_) => reads.open[slot] = true,
Err(error) => {
outcome = Err(error);
break;
}
}
}
if outcome.is_ok() {
outcome = feed.watch();
}
if outcome.is_ok() {
outcome = match executor::waiting_on(queue) {
true => {
let pumped = pump(queue, &ends, &mut reads.open, &mut found, &mut feed);
match executor::stopped_waiting() {
true => pumped,
false => Err(RuntimeError::Cancelled),
}
}
false => Err(RuntimeError::Cancelled),
};
}
drop(reads);
feed.finish();
outcome?;
let [stdout, stderr] = found;
Ok((stdout, stderr))
}
fn pump(
queue: i32,
ends: &[libc::c_int; 2],
open: &mut [bool; 2],
found: &mut [Vec<u8>; 2],
feed: &mut Feed<'_>,
) -> Result<(), RuntimeError> {
let mut events = eventlist();
while open[0] || open[1] || feed.open() {
let count = match unsafe { KEvent::listen(queue, &mut events) }.check() {
Ok(count) => count as usize,
Err(RuntimeError::CheckError(Some(libc::EINTR))) => continue,
Err(error) => return Err(error),
};
for event in events.iter().take(count) {
if event.flags & libc::EV_ERROR != 0 {
continue;
}
if feed.is(event.ident, event.filter) {
feed.push()?;
continue;
}
if event.filter != libc::EVFILT_READ {
continue;
}
let Some(slot) = ends.iter().position(|end| *end as usize == event.ident) else {
continue;
};
if !open[slot] {
continue;
}
if !read_chunk(ends[slot], &mut found[slot])? {
unwatch_read(queue, ends[slot]);
open[slot] = false;
}
}
if executor::cancelled() {
return Err(RuntimeError::Cancelled);
}
}
Ok(())
}
fn poll_exchange(
input: &mut Option<Fd>,
data: &[u8],
out: Option<&Fd>,
err: Option<&Fd>,
) -> Result<(Vec<u8>, Vec<u8>), RuntimeError> {
let ends = [
out.map_or(IGNORED, |end| end.raw()),
err.map_or(IGNORED, |end| end.raw()),
];
let mut found = [Vec::new(), Vec::new()];
let mut open = [ends[0] != IGNORED, ends[1] != IGNORED];
let mut sent = 0;
while open[0] || open[1] || input.is_some() {
if executor::cancelled() {
return Err(RuntimeError::Cancelled);
}
let writing = input.as_ref().map_or(IGNORED, |end| end.raw());
let mut watched = [
libc::pollfd {
fd: if open[0] { ends[0] } else { IGNORED },
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: if open[1] { ends[1] } else { IGNORED },
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: writing,
events: libc::POLLOUT,
revents: 0,
},
];
let ready = unsafe {
libc::poll(
watched.as_mut_ptr(),
watched.len() as libc::nfds_t,
PROCESS_POLL.as_millis() as libc::c_int,
)
}
.check();
match ready {
Ok(0) => continue,
Ok(_) => {}
Err(RuntimeError::CheckError(Some(libc::EINTR))) => continue,
Err(error) => return Err(error),
}
for slot in 0..ends.len() {
if !open[slot] || watched[slot].revents == 0 {
continue;
}
if !read_chunk(ends[slot], &mut found[slot])? {
open[slot] = false;
}
}
if watched[2].revents != 0 && !write_chunk(writing, data, &mut sent)? {
input.take();
}
}
let [stdout, stderr] = found;
Ok((stdout, stderr))
}
fn write_chunk(fd: libc::c_int, data: &[u8], sent: &mut usize) -> Result<bool, RuntimeError> {
let want = (data.len() - *sent).min(FILE_CHUNK);
if want == 0 {
return Ok(false);
}
let from = unsafe { data.as_ptr().add(*sent) }.cast::<libc::c_void>();
let written = unsafe { libc::write(fd, from, want) }.check();
let put = match written {
Ok(put) => put as usize,
Err(RuntimeError::CheckError(Some(libc::EINTR))) => return Ok(true),
Err(RuntimeError::CheckError(Some(libc::EAGAIN))) => return Ok(true),
Err(RuntimeError::CheckError(Some(libc::EPIPE))) => return Ok(false),
Err(error) => return Err(error),
};
*sent += put;
Ok(*sent < data.len())
}
fn read_chunk(fd: libc::c_int, into: &mut Vec<u8>) -> Result<bool, RuntimeError> {
into.reserve(FILE_CHUNK);
let read = unsafe {
libc::read(
fd,
into.spare_capacity_mut()
.as_mut_ptr()
.cast::<libc::c_void>(),
FILE_CHUNK,
)
}
.check();
let got = match read {
Ok(got) => got as usize,
Err(RuntimeError::CheckError(Some(libc::EINTR))) => return Ok(true),
Err(error) => return Err(error),
};
if got == 0 {
return Ok(false);
}
unsafe { into.set_len(into.len() + got) };
Ok(true)
}
pub(crate) fn wait_exit(child: &mut Child, queue: Option<i32>) -> Result<ExitStatus, RuntimeError> {
let pid = child.pid;
let Some(queue) = queue else {
return poll_exit(child);
};
let watched = unsafe {
KEvent::register(
queue,
pid as usize,
0,
WakeTarget::None.encode(),
EventDesc::new_proc_exit(),
)
}
.check();
if watched.is_err() {
return poll_exit(child);
}
child.watching(queue);
match try_reap(pid) {
Ok(Some(status)) => {
child.reaped();
unwatch_proc(queue, pid);
return Ok(status);
}
Ok(None) => {}
Err(error) => {
child.reaped();
unwatch_proc(queue, pid);
return Err(error);
}
}
if !executor::waiting_on(queue) {
unwatch_proc(queue, pid);
return Err(RuntimeError::Cancelled);
}
let mut outcome = None;
loop {
let waited = kqueue::wait_for_upto(queue, pid as usize, libc::EVFILT_PROC, PROCESS_POLL);
match try_reap(pid) {
Ok(Some(status)) => {
outcome = Some(Ok(status));
break;
}
Err(error) => {
outcome = Some(Err(error));
break;
}
Ok(None) => {}
}
if waited == Waited::Cancelled || executor::cancelled() {
outcome = Some(Err(RuntimeError::Cancelled));
break;
}
if waited == Waited::Failed {
break;
}
}
let carry_on = executor::stopped_waiting();
unwatch_proc(queue, pid);
let Some(outcome) = outcome else {
return poll_exit(child);
};
if !carry_on {
return Err(RuntimeError::Cancelled);
}
child.reaped();
outcome
}
fn poll_exit(child: &mut Child) -> Result<ExitStatus, RuntimeError> {
loop {
match try_reap(child.pid) {
Ok(Some(status)) => {
child.reaped();
return Ok(status);
}
Ok(None) => {}
Err(error) => {
child.reaped();
return Err(error);
}
}
if executor::cancelled() {
return Err(RuntimeError::Cancelled);
}
unsafe { libc::poll(ptr::null_mut(), 0, PROCESS_POLL.as_millis() as libc::c_int) };
}
}
fn try_reap(pid: libc::pid_t) -> Result<Option<ExitStatus>, RuntimeError> {
let mut status: libc::c_int = 0;
loop {
let waited = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }.check();
return match waited {
Ok(0) => Ok(None),
Ok(_) => Ok(Some(ExitStatus::from_raw(status))),
Err(RuntimeError::CheckError(Some(libc::EINTR))) => continue,
Err(error) => Err(error),
};
}
}
fn reap(pid: libc::pid_t) -> Result<ExitStatus, RuntimeError> {
let mut status: libc::c_int = 0;
loop {
let waited = unsafe { libc::waitpid(pid, &mut status, 0) }.check();
return match waited {
Ok(_) => Ok(ExitStatus::from_raw(status)),
Err(RuntimeError::CheckError(Some(libc::EINTR))) => continue,
Err(error) => Err(error),
};
}
}
pub(crate) fn kill_and_reap(pid: libc::pid_t) {
unsafe { libc::kill(-pid, libc::SIGKILL) };
unsafe { libc::kill(pid, libc::SIGKILL) };
let _ = reap(pid);
}
fn unwatch_read(queue: i32, fd: libc::c_int) {
let _ = unsafe {
KEvent::register(
queue,
fd as usize,
0,
ptr::null_mut(),
EventDesc::new_read_delete(),
)
};
}
fn unwatch_write(queue: i32, fd: libc::c_int) {
let _ = unsafe {
KEvent::register(
queue,
fd as usize,
0,
ptr::null_mut(),
EventDesc::new_write_delete(),
)
};
}
fn unwatch_proc(queue: i32, pid: libc::pid_t) {
let _ = unsafe {
KEvent::register(
queue,
pid as usize,
0,
ptr::null_mut(),
EventDesc::new_proc_delete(),
)
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modules::input::token;
use std::time::{Duration, Instant};
#[test]
fn every_process_task_says_it_blocks() {
assert!(StatusTask::new("a", [""; 0]).blocking(token()), "run");
assert!(OutputTask::new("a", [""; 0]).blocking(token()), "output");
assert!(
StatusTask::new("a", [""; 0])
.input(b"x".as_slice())
.in_dir("/usr")
.env([("A", "b")])
.blocking(token()),
"a configured run"
);
assert!(
OutputTask::new("a", [""; 0])
.input(b"x".as_slice())
.in_dir("/usr")
.env_only([("A", "b")])
.blocking(token()),
"a configured output"
);
}
#[test]
fn an_argument_with_a_zero_byte_does_not_convert() {
assert!(as_c_arg("a\0b").is_none(), "a zero byte must not convert");
assert!(
as_c_arg("ab").is_some(),
"an ordinary argument must convert"
);
}
#[test]
fn a_bad_program_and_a_bad_argument_are_told_apart() {
let bad_program = Program::new("a\0b", ["fine"]);
let bad_argument = Program::new("fine", ["a\0b"]);
assert_eq!(
bad_program.argv(None).unwrap_err(),
RuntimeError::BadPath,
"a zero byte in the program is a bad path"
);
assert_eq!(
bad_argument.argv(None).unwrap_err(),
RuntimeError::BadArgument,
"a zero byte in an argument is a bad argument"
);
}
#[test]
fn the_argument_vector_is_the_shape_exec_reads() {
let program = Program::new("/bin/echo", ["one", "two"]);
let (file, argv) = program
.argv(None)
.expect("an ordinary program must convert");
assert_eq!(
file.to_bytes(),
b"/bin/echo",
"the program is passed as itself"
);
assert_eq!(argv.len(), 4, "name, two arguments, and a null");
assert!(argv[3].is_null(), "the vector must end in a null");
let name = unsafe { std::ffi::CStr::from_ptr(argv[0]) };
assert_eq!(
name.to_bytes(),
b"/bin/echo",
"the program's own name comes first"
);
}
#[test]
fn dropping_a_child_does_not_wait_for_it() {
let program = Program::new("/bin/sleep", ["30"]);
let stdio = Stdio {
input: None,
capture: None,
};
let pid = spawn_child(&program, &Setup::default(), stdio).expect("sleep must spawn");
let child = Child::new(pid);
let started = Instant::now();
drop(child);
assert!(
started.elapsed() < Duration::from_secs(5),
"the guard must kill rather than wait, took {:?}",
started.elapsed()
);
}
#[test]
fn an_entry_is_split_at_its_first_equals() {
assert_eq!(key(b"A=B"), b"A", "the ordinary case");
assert_eq!(key(b"A=B=C"), b"A", "a value may hold more of them");
assert_eq!(
key(b"NOEQUALS"),
b"NOEQUALS",
"a malformed entry is its own name"
);
assert_eq!(key(b"=X"), b"", "an empty name is still where the split is");
}
#[test]
fn a_variable_that_cannot_be_passed_on_does_not_convert() {
assert!(as_c_var("A\0B", "x").is_none(), "a zero byte in the name");
assert!(as_c_var("A", "x\0y").is_none(), "a zero byte in the value");
assert!(as_c_var("A=B", "x").is_none(), "an equals sign in the name");
assert!(as_c_var("", "x").is_none(), "an empty name names nothing");
let ordinary = as_c_var("A", "x").expect("an ordinary variable must convert");
assert_eq!(ordinary.to_bytes(), b"A=x", "joined as the kernel takes it");
let valued = as_c_var("A", "x=y").expect("an equals sign in a value is fine");
assert_eq!(valued.to_bytes(), b"A=x=y", "and is left where it was");
}
#[test]
fn an_overlay_writes_over_rather_than_alongside() {
let base = [
CString::new("HOME=/old").unwrap(),
CString::new("PATH=/bin").unwrap(),
];
let pointers = base
.iter()
.map(|var| var.as_ptr().cast_mut())
.collect::<Vec<_>>();
let over = [CString::new("HOME=/new").unwrap()];
let merged = merge(&pointers, &over);
assert!(
merged.last().expect("never empty").is_null(),
"must end in a null"
);
let entries = merged[..merged.len() - 1]
.iter()
.map(|entry| unsafe { CStr::from_ptr(*entry) }.to_bytes().to_vec())
.collect::<Vec<_>>();
assert_eq!(
entries.len(),
2,
"the overlay replaces rather than adds, got {entries:?}"
);
assert!(
entries.iter().any(|entry| entry == b"PATH=/bin"),
"an untouched variable stays"
);
assert!(
entries.iter().any(|entry| entry == b"HOME=/new"),
"the overlay's value is the one that survives"
);
assert!(
!entries.iter().any(|entry| entry == b"HOME=/old"),
"and the old one is gone rather than alongside"
);
}
#[test]
fn a_relative_program_is_told_from_the_others() {
assert!(relative(c"./foo"), "a leading dot is relative");
assert!(relative(c"a/b"), "so is anything else with a slash in it");
assert!(!relative(c"/bin/ls"), "a leading slash is absolute");
assert!(!relative(c"ls"), "no slash at all is a PATH lookup");
}
#[test]
fn a_directory_that_cannot_be_used_does_not_convert() {
assert!(
matches!(as_dir("/usr"), Dir::At(_)),
"an absolute path converts"
);
assert!(
matches!(as_dir("build"), Dir::Bad),
"a relative one does not"
);
assert!(matches!(as_dir("./build"), Dir::Bad), "nor a leading dot");
assert!(matches!(as_dir("/a\0b"), Dir::Bad), "nor a zero byte");
}
#[test]
fn a_relative_program_is_joined_onto_its_directory() {
let program = Program::new("./sh", ["-c", "true"]);
let (file, argv) = program.argv(Some(c"/bin")).expect("must convert");
assert_eq!(
file.to_bytes(),
b"/bin/./sh",
"the spawn is given an absolute path"
);
let plain = Program::new("a/b", [""; 0]);
let (file, _) = plain.argv(Some(c"/usr")).expect("must convert");
assert_eq!(
file.to_bytes(),
b"/usr/a/b",
"a relative path without a leading dot joins plainly"
);
let name = unsafe { CStr::from_ptr(argv[0]) };
assert_eq!(
name.to_bytes(),
b"./sh",
"argv[0] is left as the caller wrote it"
);
let absolute = Program::new("/bin/sh", [""; 0]);
let (file, _) = absolute.argv(Some(c"/usr")).expect("must convert");
assert_eq!(
file.to_bytes(),
b"/bin/sh",
"an absolute program is left alone"
);
let looked_up = Program::new("sh", [""; 0]);
let (file, _) = looked_up.argv(Some(c"/usr")).expect("must convert");
assert_eq!(file.to_bytes(), b"sh", "a PATH lookup is left alone");
}
#[test]
fn a_pipe_can_be_asked_not_to_raise_sigpipe() {
const F_GETNOSIGPIPE: libc::c_int = 74;
let (_read, write) = pipe().expect("a pipe must be made");
let set = unsafe { libc::fcntl(write.raw(), F_SETNOSIGPIPE, 1) };
assert_eq!(set, 0, "the kernel refused the request outright");
let read_back = unsafe { libc::fcntl(write.raw(), F_GETNOSIGPIPE) };
assert_eq!(read_back, 1, "it was accepted but did not stick");
}
}