pub mod search;
use crate::Env;
use crate::function::Function;
use crate::job::add_job_if_suspended;
use crate::semantics::{ExitStatus, Field, Result};
use crate::source::Location;
use crate::source::pretty::{Report, ReportType, Snippet};
use crate::subshell::{JobControl, Subshell};
use crate::system::resource::SetRlimit;
use crate::system::{
Close, Dup, Errno, Exec, Exit, Fork, GetPid, Open, SendSignal, SetPgid, ShellPath, Sigaction,
Sigmask, Signals, TcSetPgrp, Wait,
};
use itertools::Itertools as _;
use std::convert::Infallible;
use std::ffi::CString;
use std::ops::ControlFlow::Continue;
use std::pin::Pin;
use std::rc::Rc;
use thiserror::Error;
type PinFuture<'a, T = ()> = Pin<Box<dyn Future<Output = T> + 'a>>;
type FutureResult<'a, T = ()> = PinFuture<'a, Result<T>>;
type EnvPrepHook<S> = fn(&mut Env<S>) -> PinFuture<'_, ()>;
pub struct RunFunction<S>(
#[allow(clippy::type_complexity)]
pub for<'a> fn(
&'a mut Env<S>,
Rc<Function<S>>,
Vec<Field>,
Option<EnvPrepHook<S>>,
) -> FutureResult<'a>,
);
impl<S> Clone for RunFunction<S> {
fn clone(&self) -> Self {
*self
}
}
impl<S> Copy for RunFunction<S> {}
impl<S> std::fmt::Debug for RunFunction<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("RunFunction").field(&self.0).finish()
}
}
#[derive(Clone, Debug, Error)]
#[error("cannot execute external utility {path:?}: {errno}")]
pub struct ReplaceCurrentProcessError {
pub path: CString,
pub errno: Errno,
}
pub async fn replace_current_process<S: Exec + ShellPath + Signals + Sigmask + Sigaction>(
env: &mut Env<S>,
path: CString,
args: Vec<Field>,
) -> std::result::Result<Infallible, ReplaceCurrentProcessError> {
env.traps
.disable_internal_dispositions(&env.system)
.await
.ok();
let args = to_c_strings(args);
let envs = env.variables.env_c_strings();
let Err(errno) = env.system.execve(path.as_c_str(), &args, &envs).await;
env.exit_status = match errno {
Errno::ENOEXEC => {
fall_back_on_sh(&env.system, path.clone(), args, envs).await;
ExitStatus::NOEXEC
}
Errno::ENOENT | Errno::ENOTDIR => ExitStatus::NOT_FOUND,
_ => ExitStatus::NOEXEC,
};
Err(ReplaceCurrentProcessError { path, errno })
}
fn to_c_strings(s: Vec<Field>) -> Vec<CString> {
s.into_iter()
.filter_map(|f| {
let bytes = f.value.into_bytes();
CString::new(bytes).ok()
})
.collect()
}
async fn fall_back_on_sh<S: ShellPath + Exec>(
system: &S,
mut script_path: CString,
mut args: Vec<CString>,
envs: Vec<CString>,
) {
if script_path.as_bytes().starts_with("-".as_bytes()) {
let mut bytes = script_path.into_bytes();
bytes.splice(0..0, "./".bytes());
script_path = CString::new(bytes).unwrap();
}
args.insert(1, script_path);
c"sh".clone_into(&mut args[0]);
let sh_path = system.shell_path();
system.execve(&sh_path, &args, &envs).await.ok();
}
#[derive(Clone, Debug, Error)]
#[error("cannot start subshell for utility {utility:?}: {errno}")]
pub struct StartSubshellError {
pub utility: Field,
pub errno: Errno,
}
impl<'a> From<&'a StartSubshellError> for Report<'a> {
fn from(error: &'a StartSubshellError) -> Self {
let mut report = Report::new();
report.r#type = ReportType::Error;
report.title = format!(
"cannot start subshell for utility {:?}",
error.utility.value
)
.into();
report.snippets = Snippet::with_primary_span(
&error.utility.origin,
format!("{:?}: {}", error.utility.value, error.errno).into(),
);
report
}
}
pub async fn run_external_utility_in_subshell<S>(
env: &mut Env<S>,
path: CString,
args: Vec<Field>,
handle_start_subshell_error: fn(&mut Env<S>, StartSubshellError) -> PinFuture<'_>,
handle_replace_current_process_error: fn(
&mut Env<S>,
ReplaceCurrentProcessError,
Location,
) -> PinFuture<'_>,
) -> Result<ExitStatus>
where
S: Close
+ Dup
+ Exec
+ Exit
+ Fork
+ GetPid
+ Open
+ SendSignal
+ SetPgid
+ SetRlimit
+ ShellPath
+ Sigaction
+ Sigmask
+ Signals
+ TcSetPgrp
+ Wait
+ 'static,
{
let utility = args[0].clone();
let job_name = if env.controls_jobs() {
to_job_name(&args)
} else {
String::new()
};
let subshell = Subshell::new(move |env, _job_control| {
Box::pin(async move {
let location = args[0].origin.clone();
let Err(e) = replace_current_process(env, path, args).await;
handle_replace_current_process_error(env, e, location).await;
})
})
.job_control(JobControl::Foreground);
match subshell.start_and_wait(env).await {
Ok((pid, result)) => add_job_if_suspended(env, pid, result, || job_name),
Err(errno) => {
handle_start_subshell_error(env, StartSubshellError { utility, errno }).await;
Continue(ExitStatus::NOEXEC)
}
}
}
fn to_job_name(fields: &[Field]) -> String {
fields
.iter()
.format_with(" ", |field, f| f(&format_args!("{}", field.value)))
.to_string()
}