use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions};
use std::io;
use std::io::ErrorKind;
use std::sync::{Arc, OnceLock};
use crate::exec::Redirection;
#[cfg(windows)]
use crate::process::ExtProcessState;
use crate::process::Process;
pub(crate) use os::OsOptions;
pub use os::make_pipe;
#[derive(Clone, Debug)]
pub(crate) enum Arg {
Regular(OsString),
#[cfg(windows)]
Raw(OsString),
}
impl Arg {
pub fn display_escaped(&self) -> String {
match self {
Arg::Regular(s) => display_escape(&s.to_string_lossy()).into_owned(),
#[cfg(windows)]
Arg::Raw(s) => s.to_string_lossy().into_owned(),
}
}
}
pub(crate) fn display_escape(s: &str) -> Cow<'_, str> {
fn nice_char(c: char) -> bool {
match c {
'-' | '_' | '.' | ',' | '/' => true,
c if c.is_ascii_alphanumeric() => true,
_ => false,
}
}
if !s.chars().all(nice_char) {
Cow::Owned(format!("'{}'", s.replace("'", r#"'\''"#)))
} else {
Cow::Borrowed(s)
}
}
pub(crate) struct SpawnResult {
pub process: Process,
pub stdin: Option<File>,
pub stdout: Option<File>,
pub stderr: Option<File>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn(
argv: Vec<Arg>,
stdin: Arc<Redirection>,
stdout: Arc<Redirection>,
stderr: Arc<Redirection>,
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<SpawnResult> {
if argv.is_empty() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"argv must not be empty",
));
}
let (parent_ends, child_ends) = setup_streams(stdin, stdout, stderr)?;
let process = os::os_start(argv, child_ends, detached, executable, env, cwd, os_options)?;
Ok(SpawnResult {
process,
stdin: parent_ends.0,
stdout: parent_ends.1,
stderr: parent_ends.2,
})
}
fn child_file(r: &Redirection) -> &File {
match r {
Redirection::File(f) => f,
_ => unreachable!(),
}
}
fn prepare_child_stream(
redir: Arc<Redirection>,
is_input: bool,
) -> io::Result<(Option<File>, Option<Arc<Redirection>>)> {
if matches!(&*redir, Redirection::File(_)) {
return match Arc::try_unwrap(redir) {
Ok(Redirection::File(f)) => Ok((None, Some(os::prepare_file(f)?))),
Err(arc) => Ok((None, Some(os::prepare_file_shared(arc)?))),
_ => unreachable!(),
};
}
match &*redir {
Redirection::Pipe => {
let (parent, child) = prepare_pipe(is_input)?;
Ok((Some(parent), Some(child)))
}
Redirection::Null => Ok((None, Some(prepare_null_file(is_input)?))),
Redirection::None => Ok((None, None)),
_ => unreachable!(),
}
}
fn prepare_pipe(parent_writes: bool) -> io::Result<(File, Arc<Redirection>)> {
let (read, write) = os::make_pipe()?;
let (parent_end, child_end) = if parent_writes {
(write, read)
} else {
(read, write)
};
Ok((parent_end, os::prepare_file(child_end)?))
}
fn prepare_null_file(for_read: bool) -> io::Result<Arc<Redirection>> {
let file = if for_read {
OpenOptions::new().read(true).open(os::NULL_DEVICE)?
} else {
OpenOptions::new().write(true).open(os::NULL_DEVICE)?
};
os::prepare_file(file)
}
fn reuse_stream(
dest: &mut Option<Arc<Redirection>>,
src: &mut Option<Arc<Redirection>>,
src_id: StandardStream,
) -> io::Result<()> {
if src.is_none() {
*src = Some(get_redirection_to_standard_stream(src_id)?);
}
*dest = src.clone();
Ok(())
}
fn setup_streams(
stdin: Arc<Redirection>,
stdout: Arc<Redirection>,
stderr: Arc<Redirection>,
) -> io::Result<(
(Option<File>, Option<File>, Option<File>),
(
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
)> {
#[derive(PartialEq, Eq, Copy, Clone)]
enum MergeKind {
ErrToOut, OutToErr, None,
}
if matches!(&*stdin, Redirection::Merge) {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Redirection::Merge not valid for stdin",
));
}
let merge = match (
matches!(&*stdout, Redirection::Merge),
matches!(&*stderr, Redirection::Merge),
) {
(false, false) => MergeKind::None,
(false, true) => MergeKind::ErrToOut,
(true, false) => MergeKind::OutToErr,
(true, true) => {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Redirection::Merge not valid for both stdout and stderr",
));
}
};
let (parent_stdin, child_stdin) = prepare_child_stream(stdin, true)?;
let (parent_stdout, mut child_stdout) = if merge == MergeKind::OutToErr {
(None, None)
} else {
prepare_child_stream(stdout, false)?
};
let (parent_stderr, mut child_stderr) = if merge == MergeKind::ErrToOut {
(None, None)
} else {
prepare_child_stream(stderr, false)?
};
match merge {
MergeKind::ErrToOut => {
reuse_stream(&mut child_stderr, &mut child_stdout, StandardStream::Output)?
}
MergeKind::OutToErr => {
reuse_stream(&mut child_stdout, &mut child_stderr, StandardStream::Error)?
}
MergeKind::None => (),
}
Ok((
(parent_stdin, parent_stdout, parent_stderr),
(child_stdin, child_stdout, child_stderr),
))
}
#[derive(Debug, Copy, Clone)]
#[allow(dead_code)]
pub(crate) enum StandardStream {
Input = 0,
Output = 1,
Error = 2,
}
fn get_redirection_to_standard_stream(which: StandardStream) -> io::Result<Arc<Redirection>> {
static STREAMS: [OnceLock<Arc<Redirection>>; 3] =
[OnceLock::new(), OnceLock::new(), OnceLock::new()];
let lock = &STREAMS[which as usize];
if let Some(stream) = lock.get() {
return Ok(Arc::clone(stream));
}
let stream = os::make_redirection_to_standard_stream(which)?;
Ok(Arc::clone(lock.get_or_init(|| stream)))
}
#[cfg(unix)]
pub(crate) mod os {
use super::*;
#[derive(Clone, Default)]
pub struct OsOptions {
pub setuid: Option<u32>,
pub setgid: Option<u32>,
pub setpgid: Option<u32>,
}
impl OsOptions {
pub fn setpgid_is_set(&self) -> bool {
self.setpgid.is_some()
}
pub fn set_pgid_value(&mut self, pgid: u32) {
self.setpgid = Some(pgid);
}
}
pub const NULL_DEVICE: &str = "/dev/null";
use crate::posix;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd};
pub use crate::posix::make_redirection_to_standard_stream;
fn read_exact_or_eof<const N: usize>(source: &mut File) -> io::Result<Option<[u8; N]>> {
let mut buf = [0u8; N];
let mut total_read = 0;
while total_read < N {
let n = source.read(&mut buf[total_read..])?;
if n == 0 {
break;
}
total_read += n;
}
match total_read {
0 => Ok(None),
n if n == N => Ok(Some(buf)),
_ => Err(io::ErrorKind::UnexpectedEof.into()),
}
}
pub(crate) fn os_start(
argv: Vec<Arg>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<Process> {
let argv: Vec<OsString> = argv.into_iter().map(|Arg::Regular(s)| s).collect();
let mut exec_fail_pipe = posix::pipe()?;
let child_env = env.map(format_env);
let cmd_to_exec = executable.unwrap_or(&argv[0]);
let just_exec = posix::prep_exec(cmd_to_exec, &argv, child_env.as_deref())?;
let do_chdir = cwd.map(posix::prep_chdir).transpose()?;
let pid;
unsafe {
match posix::fork()? {
Some(child_pid) => {
pid = child_pid;
}
None => {
drop(exec_fail_pipe.0);
let result = do_exec(just_exec, child_ends, do_chdir, &os_options);
let error_code = match result {
Ok(()) => unreachable!(),
Err(e) => e.raw_os_error().unwrap_or(-1),
} as u32;
exec_fail_pipe.1.write_all(&error_code.to_le_bytes()).ok();
posix::_exit(127);
}
}
}
drop(child_ends);
drop(exec_fail_pipe.1);
match read_exact_or_eof::<4>(&mut exec_fail_pipe.0)? {
None => Ok(Process::new(pid, (), detached)),
Some(error_buf) => {
let error_code = u32::from_le_bytes(error_buf);
Err(io::Error::from_raw_os_error(error_code as i32))
}
}
}
fn format_env(env: &[(OsString, OsString)]) -> Vec<OsString> {
let mut seen = HashSet::<&OsStr>::new();
let mut formatted: Vec<_> = env
.iter()
.rev()
.filter(|&(k, _)| seen.insert(k))
.map(|(k, v)| {
let mut fmt = k.clone();
fmt.push("=");
fmt.push(v);
fmt
})
.collect();
formatted.reverse();
formatted
}
fn install_child_fd(end: Option<Arc<Redirection>>, target_fd: i32) -> io::Result<()> {
let mut end = std::mem::ManuallyDrop::new(end);
if let Some(r) = &*end {
let fd = child_file(r).as_raw_fd();
if fd != target_fd {
posix::dup2(fd, target_fd)?;
} else {
set_inheritable(child_file(r), true)?;
}
}
if let Some(end) = end.take() {
prevent_dealloc(end);
}
Ok(())
}
fn prevent_dealloc(r: Arc<Redirection>) {
if Arc::strong_count(&r) == 1
&& let Redirection::File(f) = &*r
{
let _ = unsafe { File::from_raw_fd(f.as_raw_fd()) };
};
std::mem::forget(r);
}
fn do_exec(
just_exec: impl FnOnce() -> io::Result<()>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
chdir: Option<impl FnOnce() -> io::Result<()>>,
os_options: &OsOptions,
) -> io::Result<()> {
let (stdin, stdout, stderr) = child_ends;
let mut stdin = std::mem::ManuallyDrop::new(stdin);
let mut stdout = std::mem::ManuallyDrop::new(stdout);
let mut stderr = std::mem::ManuallyDrop::new(stderr);
let mut just_exec = std::mem::ManuallyDrop::new(just_exec);
if let Some(chdir) = chdir {
chdir()?;
}
install_child_fd(stdin.take(), 0)?;
install_child_fd(stdout.take(), 1)?;
install_child_fd(stderr.take(), 2)?;
posix::reset_sigpipe()?;
if let Some(gid) = os_options.setgid {
posix::setgid(gid)?;
}
if let Some(uid) = os_options.setuid {
posix::setuid(uid)?;
}
if let Some(pgid) = os_options.setpgid {
posix::setpgid(0, pgid)?;
}
let just_exec = unsafe { std::mem::ManuallyDrop::take(&mut just_exec) };
just_exec()?;
unreachable!();
}
pub fn set_inheritable(f: &File, inheritable: bool) -> io::Result<()> {
let fd = f.as_raw_fd();
let old = posix::fcntl(fd, posix::F_GETFD, None)?;
let new = if inheritable {
old & !posix::FD_CLOEXEC
} else {
old | posix::FD_CLOEXEC
};
if new != old {
posix::fcntl(fd, posix::F_SETFD, Some(new))?;
}
Ok(())
}
pub fn prepare_file(file: File) -> io::Result<Arc<Redirection>> {
Ok(Arc::new(Redirection::File(file)))
}
pub fn prepare_file_shared(arc: Arc<Redirection>) -> io::Result<Arc<Redirection>> {
Ok(arc)
}
pub fn make_pipe() -> io::Result<(File, File)> {
posix::pipe()
}
}
#[cfg(windows)]
pub(crate) mod os {
use super::*;
#[derive(Clone, Default)]
pub struct OsOptions {
pub creation_flags: u32,
}
pub const NULL_DEVICE: &str = "nul";
use std::collections::HashSet;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::io::{AsRawHandle, RawHandle};
use crate::win32;
pub use crate::win32::make_redirection_to_standard_stream;
pub(crate) fn os_start(
argv: Vec<Arg>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<Process> {
fn raw(opt: Option<&Arc<Redirection>>) -> Option<RawHandle> {
opt.map(|r| child_file(r).as_raw_handle())
}
let (mut child_stdin, mut child_stdout, mut child_stderr) = child_ends;
ensure_child_stream(&mut child_stdin, StandardStream::Input)?;
ensure_child_stream(&mut child_stdout, StandardStream::Output)?;
ensure_child_stream(&mut child_stderr, StandardStream::Error)?;
let cmdline = assemble_cmdline(argv)?;
let env_block = env.map(format_env_block);
let executable_located = executable.map(|e| locate_in_path(e.to_owned()));
let (handle, pid) = win32::CreateProcess(
executable_located.as_ref().map(OsString::as_ref),
&cmdline,
env_block.as_deref(),
cwd,
true,
os_options.creation_flags,
raw(child_stdin.as_ref()),
raw(child_stdout.as_ref()),
raw(child_stderr.as_ref()),
win32::STARTF_USESTDHANDLES,
)?;
Ok(Process::new(pid as u32, ExtProcessState(handle), detached))
}
fn format_env_block(env: &[(OsString, OsString)]) -> Vec<u16> {
fn to_uppercase(s: &OsStr) -> OsString {
OsString::from_wide(
&s.encode_wide()
.map(|c| {
if c < 128 {
(c as u8).to_ascii_uppercase() as u16
} else {
c
}
})
.collect::<Vec<_>>(),
)
}
let mut pruned: Vec<_> = {
let mut seen = HashSet::<OsString>::new();
env.iter()
.rev()
.filter(|&(k, _)| seen.insert(to_uppercase(k)))
.collect()
};
pruned.reverse();
let mut block = vec![];
for (k, v) in pruned {
block.extend(k.encode_wide());
block.push('=' as u16);
block.extend(v.encode_wide());
block.push(0);
}
block.push(0);
block
}
fn ensure_child_stream(
stream: &mut Option<Arc<Redirection>>,
which: StandardStream,
) -> io::Result<()> {
if stream.is_none() {
*stream = Some(get_redirection_to_standard_stream(which)?);
}
Ok(())
}
pub fn set_inheritable(f: &File, inheritable: bool) -> io::Result<()> {
win32::SetHandleInformation(
f,
win32::HANDLE_FLAG_INHERIT,
if inheritable { 1 } else { 0 },
)?;
Ok(())
}
pub fn prepare_file(file: File) -> io::Result<Arc<Redirection>> {
set_inheritable(&file, true)?;
Ok(Arc::new(Redirection::File(file)))
}
pub fn prepare_file_shared(arc: Arc<Redirection>) -> io::Result<Arc<Redirection>> {
set_inheritable(child_file(&arc), true)?;
Ok(arc)
}
pub fn make_pipe() -> io::Result<(File, File)> {
win32::make_pipe()
}
fn locate_in_path(executable: OsString) -> OsString {
let Some(path_var) = env::var_os("PATH") else {
return executable;
};
for dir in env::split_paths(&path_var) {
let candidate = dir
.join(&executable)
.with_extension(std::env::consts::EXE_EXTENSION);
if candidate.exists() {
return candidate.into_os_string();
}
}
executable
}
fn assemble_cmdline(argv: Vec<Arg>) -> io::Result<OsString> {
let mut cmdline = vec![];
for (i, arg) in argv.iter().enumerate() {
if i > 0 {
cmdline.push(' ' as u16);
}
let s = match arg {
Arg::Regular(s) | Arg::Raw(s) => s,
};
if s.encode_wide().any(|c| c == 0) {
return Err(io::Error::from_raw_os_error(win32::ERROR_BAD_PATHNAME as _));
}
match arg {
Arg::Regular(s) => append_quoted(s, &mut cmdline),
Arg::Raw(s) => cmdline.extend(s.encode_wide()),
}
}
Ok(OsString::from_wide(&cmdline))
}
fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) {
if !arg.is_empty()
&& !arg.encode_wide().any(|c| {
c == ' ' as u16
|| c == '\t' as u16
|| c == '\n' as u16
|| c == '\x0b' as u16
|| c == '\"' as u16
})
{
cmdline.extend(arg.encode_wide());
return;
}
cmdline.push('"' as u16);
let arg: Vec<_> = arg.encode_wide().collect();
let mut i = 0;
while i < arg.len() {
let mut num_backslashes = 0;
while i < arg.len() && arg[i] == '\\' as u16 {
i += 1;
num_backslashes += 1;
}
if i == arg.len() {
for _ in 0..num_backslashes * 2 {
cmdline.push('\\' as u16);
}
break;
} else if arg[i] == b'"' as u16 {
for _ in 0..num_backslashes * 2 + 1 {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
} else {
for _ in 0..num_backslashes {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
}
i += 1;
}
cmdline.push('"' as u16);
}
}