use super::*;
#[derive(Clone)]
pub(super) enum InputFd {
Lines(Vec<String>),
Pipe(Arc<pipe::ReadEnd>),
}
impl InputFd {
pub(super) fn pipe(&self) -> Option<&Arc<pipe::Pipe>> {
match self {
Self::Pipe(end) => Some(&end.0),
Self::Lines(_) => None,
}
}
}
#[derive(Clone)]
pub(super) struct CoprocWriter(Arc<pipe::WriteEnd>);
impl std::fmt::Debug for CoprocWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("CoprocWriter")
}
}
impl CoprocWriter {
pub(super) fn pipe(&self) -> &Arc<pipe::Pipe> {
&self.0.0
}
pub(super) fn write(&self, data: &[u8]) {
let _ = self.pipe().write(data);
}
}
pub(super) async fn coproc_backpressure(w: &CoprocWriter) {
Box::pin(w.pipe().writable()).await;
}
pub(super) async fn read_coproc_input(
pipe: Arc<pipe::Pipe>,
demand: StdinDemand,
) -> crate::StreamData {
let mut buf = Vec::new();
while !demand.satisfied(&buf) {
let chunk = if matches!(demand, StdinDemand::All | StdinDemand::Bytes(_)) {
pipe.read_some().await
} else {
pipe.read_line_chunk().await
};
if chunk.is_empty() {
break;
}
buf.extend_from_slice(&chunk);
}
buf.into()
}
fn lone_simple_command(body: &Command) -> Option<&SimpleCommand> {
match body {
Command::Simple(simple) => Some(simple),
Command::Compound(CompoundCommand::BraceGroup(cmds), redirects) if redirects.is_empty() => {
match cmds.as_slice() {
[Command::Simple(simple)] => Some(simple),
_ => None,
}
}
_ => None,
}
}
pub(super) struct CoprocEntry {
name: String,
job: usize,
pid: u32,
read_fd: i32,
write_fd: i32,
output: Arc<pipe::Pipe>,
input: Arc<pipe::Pipe>,
}
impl Interpreter {
pub(super) fn execute_coproc<'a>(
&'a mut self,
coproc: &'a CoprocCommand,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ExecResult>> + Send + 'a>> {
Box::pin(async move {
let mut stderr = String::new();
if let Some(warning) = self.coproc_still_exists_warning() {
stderr.push_str(&warning);
}
let (read_fd, write_fd) = self.allocate_coproc_fds()?;
let input = pipe::Pipe::new();
let output = pipe::Pipe::new();
let Some((mut child, slot)) = self.fork_for_coproc(&mut stderr) else {
self.last_exit_code = 1;
return Ok(ExecResult::err(stderr, 1));
};
child
.exec_fd_table
.retain(|_, t| !matches!(t, FdTarget::Coproc(_)));
if self.concurrent_jobs {
child.pipe_in = Some(Arc::clone(&input));
child.pipe_out = Some(Arc::clone(&output));
let sink = Arc::clone(&output);
child.output_callback = Some(Box::new(move |out, _err| {
sink.write(out.as_bytes());
}));
} else {
input.close_write();
child.pipeline_stdin = Some(crate::StreamData::new());
}
let body = coproc.body.clone();
let streaming = self.concurrent_jobs;
let job_diag = self.diag_prefix();
let read_end = pipe::ReadEnd(Arc::clone(&input));
let write_end = pipe::WriteEnd(Arc::clone(&output));
let mut fut: jobs::JobFuture = Box::pin(async move {
let _slot = slot;
let _read_end = read_end;
if streaming
&& let Some(simple) = lone_simple_command(&body)
&& Self::streams_stdout(simple)
{
child.stream_stdout_command = Some(simple as *const SimpleCommand as usize);
}
let jobs = Arc::clone(&child.jobs);
let result = jobs::with_jobs(&jobs, child.execute_command(&body)).await;
jobs.finish_all().await;
let (out, err) = jobs.lock().take_finished_output();
let mut r = match result {
Ok(r) => r,
Err(e) => return ExecResult::err(format!("{job_diag}{e}\n"), 1),
};
r.stdout.append(&out);
r.stderr.append(&err);
let sent = child.output_stream_stdout_bytes.min(r.stdout.len());
write_end.0.write(&r.stdout.as_bytes()[sent..]);
r.stdout = crate::StreamData::new();
if write_end.0.is_broken() {
r.exit_code = 141;
} else if let ControlFlow::Exit(code) | ControlFlow::Return(code) = r.control_flow {
r.exit_code = code;
}
r.control_flow = ControlFlow::None;
r
});
let text = format!("coproc {} {}", coproc.name, describe_command(&coproc.body));
let finished = if self.concurrent_jobs {
match std::future::poll_fn(|cx| std::task::Poll::Ready(fut.as_mut().poll(cx))).await
{
std::task::Poll::Ready(r) => Some(r),
std::task::Poll::Pending => None,
}
} else {
Some(fut.as_mut().await)
};
let (job, pid) = match finished {
Some(result) => {
stderr.push_str(&result.stderr.text_lossy());
self.jobs.lock().spawn_finished(text, result)
}
None => self.jobs.lock().spawn_running(text, fut),
};
self.coproc_buffers.insert(
read_fd,
InputFd::Pipe(Arc::new(pipe::ReadEnd(Arc::clone(&output)))),
);
self.exec_fd_table.insert(
write_fd,
FdTarget::Coproc(CoprocWriter(Arc::new(pipe::WriteEnd(Arc::clone(&input))))),
);
let name = coproc.name.clone();
let mut arr = HashMap::new();
arr.insert(0, read_fd.to_string());
arr.insert(1, write_fd.to_string());
self.arrays_mut().insert(name.clone(), arr);
self.vars_mut()
.insert(format!("{name}_PID"), pid.to_string());
self.last_bg_pid = Some(pid.to_string());
self.coprocs.push(CoprocEntry {
name,
job,
pid,
read_fd,
write_fd,
output,
input,
});
self.last_exit_code = 0;
Ok(ExecResult {
stderr: stderr.into(),
..Default::default()
})
})
}
fn coproc_still_exists_warning(&self) -> Option<String> {
let jobs = self.jobs.lock();
let running: Vec<usize> = jobs.running_ids();
let live = self
.coprocs
.iter()
.rev()
.find(|c| running.contains(&c.job))?;
Some(self.diag(format!(
"warning: execute_coproc: coproc [{}:{}] still exists\n",
live.pid, live.name
)))
}
fn allocate_coproc_fds(&self) -> Result<(i32, i32)> {
let mut free = Vec::with_capacity(4);
let mut fd = redirection::COPROC_FIRST_FD;
while free.len() < 4 && fd >= 3 {
if !self.fd_is_open(fd) {
free.push(fd);
}
fd -= 1;
}
let max = self.limits.max_file_descriptors;
if free.len() < 4 || self.persistent_fd_count() + 2 > max {
return Err(crate::limits::LimitExceeded::MaxFileDescriptors(max).into());
}
Ok((free[0], free[3]))
}
fn fork_for_coproc(&mut self, stderr: &mut String) -> Option<(Interpreter, jobs::JobSlot)> {
let slot = self.jobs.try_claim(self.limits.max_background_jobs);
let mut child = self.fork_for_job();
let depth_ok = child.counters.push_subshell(&child.limits).is_ok();
match slot.filter(|_| depth_ok) {
Some(slot) => Some((child, slot)),
None => {
stderr.push_str(&format!(
"{}: fork: retry: Resource temporarily unavailable (max {} background jobs, {} nested)\n",
self.diag_name(),
self.limits.max_background_jobs,
self.limits.max_subshell_depth
));
None
}
}
}
#[inline(never)]
pub(super) fn reap_coprocs(&mut self) {
if self.coprocs.is_empty() {
return;
}
let ids = self.jobs.lock().ids();
let (gone, live): (Vec<_>, Vec<_>) = std::mem::take(&mut self.coprocs)
.into_iter()
.partition(|c| !ids.contains(&c.job));
self.coprocs = live;
for c in gone {
self.drop_coproc(c);
}
}
pub(super) fn close_coprocs(&mut self) {
for c in std::mem::take(&mut self.coprocs) {
self.drop_coproc(c);
}
}
fn drop_coproc(&mut self, c: CoprocEntry) {
if self
.coproc_buffers
.get(&c.read_fd)
.and_then(InputFd::pipe)
.is_some_and(|p| Arc::ptr_eq(p, &c.output))
{
self.coproc_buffers.remove(&c.read_fd);
}
if matches!(self.exec_fd_table.get(&c.write_fd),
Some(FdTarget::Coproc(w)) if Arc::ptr_eq(w.pipe(), &c.input))
{
self.exec_fd_table.remove(&c.write_fd);
}
self.arrays_mut().remove(&c.name);
self.vars_mut().remove(&format!("{}_PID", c.name));
}
pub(super) fn read_u_source(&mut self, args: &[String]) -> Option<ReadSource> {
let mut iter = args.iter();
let fd = loop {
let arg = iter.next()?;
if arg == "-u" {
break iter.next()?.parse::<i32>().ok()?;
}
if let Some(rest) = arg.strip_prefix("-u")
&& let Ok(fd) = rest.parse::<i32>()
{
break fd;
}
};
match self.coproc_buffers.get_mut(&fd)? {
InputFd::Lines(buf) => Some(ReadSource::Ready(
buf.pop().map(|l| format!("{l}\n")).unwrap_or_default(),
)),
InputFd::Pipe(end) => Some(ReadSource::Pipe(Arc::clone(&end.0))),
}
}
}
pub(super) enum ReadSource {
Ready(String),
Pipe(Arc<pipe::Pipe>),
}