#![deny(clippy::all)]
#![deny(clippy::correctness)]
#![deny(clippy::style)]
#![deny(clippy::complexity)]
#![deny(clippy::perf)]
#![deny(clippy::pedantic)]
#![deny(rustdoc::missing_doc_code_examples)]
#![deny(
absolute_paths_not_starting_with_crate,
anonymous_parameters,
bad_style,
const_err,
dead_code,
keyword_idents,
improper_ctypes,
macro_use_extern_crate,
meta_variable_misuse, // May have false positives
missing_abi,
missing_debug_implementations, // can affect compile time/code size
missing_docs,
no_mangle_generic_items,
non_shorthand_field_patterns,
noop_method_call,
overflowing_literals,
path_statements,
patterns_in_fns_without_body,
pointer_structural_match,
private_in_public,
semicolon_in_expressions_from_macros,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
unaligned_references,
unconditional_recursion,
unreachable_pub,
unused,
unused_allocation,
unused_comparisons,
unused_extern_crates,
unused_import_braces,
unused_lifetimes,
unused_parens,
unused_qualifications,
variant_size_differences,
while_true
)]
use nix::errno::Errno;
use nix::pty::{forkpty, ForkptyResult};
use nix::sys::signal::Signal;
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::ForkResult;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to fork: {0}")]
Fork(#[source] Errno),
#[error("failed to wait on child: {0}")]
Wait(#[source] Errno),
#[error("child process killed by signal {0}")]
KilledBySignal(Signal),
#[error("child process exited with code {0}")]
NonZeroExitCode(i32),
}
pub unsafe fn run_in_pty<F>(func: F) -> Result<(), Error>
where
F: FnOnce() -> Result<(), i32>,
{
let ForkptyResult {
master: _,
fork_result,
} = forkpty(None, None).map_err(Error::Fork)?;
match fork_result {
ForkResult::Child => {
let exit_code = match func() {
Ok(()) => 0,
Err(code) => code,
};
std::process::exit(exit_code);
}
ForkResult::Parent { child } => {
loop {
let result = waitpid(child, None).map_err(Error::Wait)?;
#[allow(clippy::match_same_arms)]
match result {
WaitStatus::Exited(_, status_code) => {
if status_code == 0 {
break Ok(());
}
break Err(Error::NonZeroExitCode(status_code));
}
WaitStatus::Signaled(_, signal, _generated_core_dump) => {
break Err(Error::KilledBySignal(signal));
}
WaitStatus::Stopped(..) | WaitStatus::Continued(..) => {}
#[cfg(any(target_os = "linux", target_os = "android"))]
WaitStatus::PtraceEvent(..) | WaitStatus::PtraceSyscall(..) => {}
WaitStatus::StillAlive => {} }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[allow(clippy::unnecessary_wraps)]
fn successful_func() -> Result<(), i32> {
Ok(())
}
#[allow(clippy::needless_pass_by_value)]
fn assert_exit_code(exit_code: i32, result: Result<(), Error>) {
if exit_code == 0 {
assert!(
matches!(result, Ok(())),
"expected success, got {:?}",
result
);
} else {
assert!(
matches!(result, Err(Error::NonZeroExitCode(code)) if code == exit_code),
"expected child to exit with code {}, instead got {:?}",
exit_code,
result
);
}
}
#[test]
fn test_basic_successful() {
unsafe {
run_in_pty(|| Ok(())).expect("running successful closure should not error");
}
unsafe {
run_in_pty(successful_func).expect("running successful function should not error");
}
}
#[test]
fn test_basic_erroring_closure() {
for code in 1..i8::MAX {
let result = unsafe { run_in_pty(move || Err(code.into())) };
assert_exit_code(code.into(), result);
}
}
#[test]
fn test_delayed_closure() {
unsafe {
assert_exit_code(
0,
run_in_pty(|| {
std::thread::sleep(Duration::from_secs(3));
Ok(())
}),
);
assert_exit_code(
9,
run_in_pty(|| {
std::thread::sleep(Duration::from_secs(3));
Err(9)
}),
);
}
}
#[test]
fn test_closure_capturing_moved_values() {
unsafe {
let success = Ok(());
let error_code = 23;
let error = Err(error_code);
assert_exit_code(0, run_in_pty(move || success));
assert_exit_code(error_code, run_in_pty(move || error));
}
}
#[test]
fn test_closure_allocating_memory() {
for _ in 0..10 {
unsafe {
assert_exit_code(
0,
run_in_pty(|| {
const CAPACITY: usize = 1024 * 1024 * 512;
let mut v = Vec::with_capacity(CAPACITY);
v.resize(CAPACITY, 100_u8);
assert_eq!(v.len(), CAPACITY);
Ok(())
}),
);
}
}
}
#[test]
fn test_file_access() {
use std::io::Write;
unsafe {
assert_exit_code(
0,
run_in_pty(|| {
let mut random_file =
tempfile::tempfile().expect("creating temp file should not fail");
random_file
.write_all(&vec![0; 0x4000_0000])
.expect("writing to temp file should not fail");
Ok(())
}),
);
}
}
#[test]
fn test_kill_with_signal() {
use nix::sys::signal;
use nix::unistd::Pid;
unsafe {
let result = run_in_pty(|| {
signal::kill(Pid::this(), Signal::SIGKILL).expect("sending signal should succeed");
std::thread::sleep(Duration::from_secs(3));
Ok(())
});
assert!(
matches!(result, Err(Error::KilledBySignal(Signal::SIGKILL))),
"expected child process to kill itself with SIGKILL"
);
}
}
}