use std::fs::File;
use std::os::unix::io::{FromRawFd, IntoRawFd};
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use crate::parser::{Ast, ShellCommand};
use crate::state::{Job, ShellState};
use super::expansion::{expand_variables_in_args, expand_wildcards};
use super::redirection::apply_redirections;
pub fn execute_async(ast: Ast, shell_state: &mut ShellState) -> i32 {
match ast {
Ast::Pipeline(commands) => {
if commands.is_empty() {
return 0;
}
if commands.len() == 1 {
let cmd = &commands[0];
if let Some(ref compound) = cmd.compound {
let description = match compound.as_ref() {
Ast::Subshell { .. } => "subshell",
Ast::CommandGroup { .. } => "command group",
Ast::If { .. } => "if statement",
Ast::For { .. } => "for loop",
Ast::While { .. } => "while loop",
Ast::Until { .. } => "until loop",
Ast::Case { .. } => "case statement",
_ => "compound command",
};
return execute_compound_async(*compound.clone(), description, shell_state);
}
if cmd.args.is_empty() {
return 0;
}
let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
let expanded_args = match expand_wildcards(&var_expanded_args, shell_state) {
Ok(args) => args,
Err(_) => return 1,
};
if expanded_args.is_empty() {
return 0;
}
if crate::builtins::is_builtin(&expanded_args[0]) {
return execute_builtin_async(cmd, shell_state, Some(expanded_args));
}
}
execute_external_async(&commands, shell_state)
}
Ast::Subshell { .. } => execute_compound_async(ast, "subshell", shell_state),
Ast::CommandGroup { .. } => execute_compound_async(ast, "command group", shell_state),
Ast::If { .. } => execute_compound_async(ast, "if statement", shell_state),
Ast::For { .. } => execute_compound_async(ast, "for loop", shell_state),
Ast::While { .. } => execute_compound_async(ast, "while loop", shell_state),
Ast::Until { .. } => execute_compound_async(ast, "until loop", shell_state),
Ast::Case { .. } => execute_compound_async(ast, "case statement", shell_state),
_ => {
super::execute(ast, shell_state)
}
}
}
fn execute_compound_async(ast: Ast, description: &str, shell_state: &mut ShellState) -> i32 {
let command_str = format!("{} &", description);
unsafe {
let pid = libc::fork();
if pid < 0 {
if shell_state.colors_enabled {
eprintln!(
"{}Failed to fork for background {}\x1b[0m",
shell_state.color_scheme.error, description
);
} else {
eprintln!("Failed to fork for background {}", description);
}
1
} else if pid == 0 {
let child_pid = libc::getpid();
libc::setpgid(child_pid, child_pid);
let dev_null = libc::open(b"/dev/null\0".as_ptr() as *const i8, libc::O_RDONLY);
if dev_null >= 0 {
libc::dup2(dev_null, 0);
libc::close(dev_null);
}
let exit_code = super::execute(ast, shell_state);
libc::_exit(exit_code);
} else {
let pid_u32 = pid as u32;
let job_id = shell_state.job_table.borrow_mut().allocate_job_id();
let job = Job::new(
job_id,
Some(pid_u32), command_str.clone(),
vec![pid_u32],
false, );
println!("[{}] {}", job_id, pid_u32);
shell_state.job_table.borrow_mut().add_job(job);
shell_state.last_background_pid = Some(pid_u32);
0
}
}
}
pub fn execute_builtin_async(
cmd: &ShellCommand,
shell_state: &mut ShellState,
pre_expanded_args: Option<Vec<String>>,
) -> i32 {
let expanded_args = if let Some(args) = pre_expanded_args {
args
} else {
let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
match expand_wildcards(&var_expanded_args, shell_state) {
Ok(args) => args,
Err(_) => return 1,
}
};
if expanded_args.is_empty() {
return 0;
}
let command_str = format_command_string(&expanded_args);
unsafe {
let pid = libc::fork();
if pid < 0 {
if shell_state.colors_enabled {
eprintln!(
"{}Failed to fork for background builtin\x1b[0m",
shell_state.color_scheme.error
);
} else {
eprintln!("Failed to fork for background builtin");
}
1
} else if pid == 0 {
let child_pid = libc::getpid();
libc::setpgid(child_pid, child_pid);
let dev_null = libc::open(b"/dev/null\0".as_ptr() as *const i8, libc::O_RDONLY);
if dev_null >= 0 {
libc::dup2(dev_null, 0);
libc::close(dev_null);
}
let temp_cmd = ShellCommand {
args: expanded_args,
redirections: cmd.redirections.clone(),
compound: None,
};
let exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
libc::_exit(exit_code);
} else {
let pid_u32 = pid as u32;
let job_id = shell_state.job_table.borrow_mut().allocate_job_id();
let job = Job::new(
job_id,
Some(pid_u32), command_str.clone(),
vec![pid_u32],
true, );
println!("[{}] {}", job_id, pid_u32);
shell_state.job_table.borrow_mut().add_job(job);
shell_state.last_background_pid = Some(pid_u32);
0
}
}
}
fn execute_builtin_in_background_pipeline(
expanded_args: &[String],
redirections: &[crate::parser::Redirection],
stdin: Option<File>,
is_first: bool,
is_last: bool,
pgid: Option<u32>,
shell_state: &mut ShellState,
) -> Result<(u32, Option<File>), String> {
use std::os::unix::io::AsRawFd;
let (read_fd, write_fd) = if !is_last {
let (reader, writer) =
std::io::pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
(Some(reader), Some(writer))
} else {
(None, None)
};
unsafe {
let pid = libc::fork();
if pid < 0 {
return Err("Failed to fork for builtin command".to_string());
}
if pid == 0 {
let child_pid = libc::getpid();
if let Some(group_id) = pgid {
libc::setpgid(child_pid, group_id as i32);
} else {
libc::setpgid(child_pid, child_pid);
}
if let Some(stdin_file) = stdin {
libc::dup2(stdin_file.as_raw_fd(), 0);
} else if is_first {
let dev_null = libc::open(b"/dev/null\0".as_ptr() as *const i8, libc::O_RDONLY);
if dev_null >= 0 {
libc::dup2(dev_null, 0);
libc::close(dev_null);
}
}
let pipe_write_fd = if let Some(writer) = write_fd {
let write_raw_fd = writer.into_raw_fd();
libc::dup2(write_raw_fd, 1);
Some(write_raw_fd)
} else {
None
};
if let Err(e) = super::redirection::apply_redirections(redirections, shell_state, None)
{
eprintln!("Redirection error: {}", e);
libc::_exit(1);
}
if let Some(pipe_fd) = pipe_write_fd {
let fd_table = shell_state.fd_table.borrow();
let current_stdout_fd = fd_table.get_raw_fd(1);
drop(fd_table);
let stdout_redirected = current_stdout_fd != Some(pipe_fd);
if stdout_redirected {
libc::close(pipe_fd);
}
}
let temp_cmd = ShellCommand {
args: expanded_args.to_vec(),
redirections: redirections.to_vec(),
compound: None,
};
let exit_code = crate::builtins::execute_builtin(&temp_cmd, shell_state, None);
libc::_exit(exit_code);
}
let pid_u32 = pid as u32;
drop(write_fd);
let next_stdout = if let Some(reader) = read_fd {
Some(File::from_raw_fd(reader.into_raw_fd()))
} else {
None
};
Ok((pid_u32, next_stdout))
}
}
fn execute_compound_in_background_pipeline(
compound_ast: &Ast,
redirections: &[crate::parser::Redirection],
stdin: Option<File>,
is_first: bool,
is_last: bool,
pgid: Option<u32>,
shell_state: &mut ShellState,
) -> Result<(u32, Option<File>), String> {
use std::os::unix::io::AsRawFd;
let (read_fd, write_fd) = if !is_last {
let (reader, writer) =
std::io::pipe().map_err(|e| format!("Failed to create pipe: {}", e))?;
(Some(reader), Some(writer))
} else {
(None, None)
};
unsafe {
let pid = libc::fork();
if pid < 0 {
return Err("Failed to fork for compound command".to_string());
}
if pid == 0 {
let child_pid = libc::getpid();
if let Some(group_id) = pgid {
libc::setpgid(child_pid, group_id as i32);
} else {
libc::setpgid(child_pid, child_pid);
}
if let Some(stdin_file) = stdin {
libc::dup2(stdin_file.as_raw_fd(), 0);
} else if is_first {
let dev_null = libc::open(b"/dev/null\0".as_ptr() as *const i8, libc::O_RDONLY);
if dev_null >= 0 {
libc::dup2(dev_null, 0);
libc::close(dev_null);
}
}
let pipe_write_fd = if let Some(writer) = write_fd {
let write_raw_fd = writer.into_raw_fd();
libc::dup2(write_raw_fd, 1);
Some(write_raw_fd)
} else {
None
};
if let Err(e) = super::redirection::apply_redirections(redirections, shell_state, None)
{
eprintln!("Redirection error: {}", e);
libc::_exit(1);
}
if let Some(pipe_fd) = pipe_write_fd {
let fd_table = shell_state.fd_table.borrow();
let current_stdout_fd = fd_table.get_raw_fd(1);
drop(fd_table);
let stdout_redirected = current_stdout_fd != Some(pipe_fd);
if stdout_redirected {
libc::close(pipe_fd);
}
}
let exit_code = super::execute(compound_ast.clone(), shell_state);
libc::_exit(exit_code);
}
let pid_u32 = pid as u32;
drop(write_fd);
let next_stdout = if let Some(reader) = read_fd {
Some(File::from_raw_fd(reader.into_raw_fd()))
} else {
None
};
Ok((pid_u32, next_stdout))
}
}
fn execute_external_async(commands: &[ShellCommand], shell_state: &mut ShellState) -> i32 {
let mut pids = Vec::new();
let mut previous_stdout: Option<File> = None;
let mut pgid: Option<u32> = None;
let command_str = format_pipeline_string(commands);
for (i, cmd) in commands.iter().enumerate() {
let is_last = i == commands.len() - 1;
if let Some(ref compound) = cmd.compound {
if shell_state.colors_enabled {
eprintln!(
"{}Warning: Compound command in background pipeline - executing via fork\x1b[0m",
shell_state.color_scheme.error
);
} else {
eprintln!("Warning: Compound command in background pipeline - executing via fork");
}
match execute_compound_in_background_pipeline(
compound.as_ref(),
&cmd.redirections,
previous_stdout.take(),
i == 0,
is_last,
pgid,
shell_state,
) {
Ok((pid, stdout)) => {
pids.push(pid);
if pgid.is_none() {
pgid = Some(pid);
}
if !is_last {
previous_stdout = stdout;
}
}
Err(e) => {
if shell_state.colors_enabled {
eprintln!(
"{}Error executing compound command in pipeline: {}\x1b[0m",
shell_state.color_scheme.error, e
);
} else {
eprintln!("Error executing compound command in pipeline: {}", e);
}
unsafe {
if let Some(group_id) = pgid {
libc::killpg(group_id as i32, libc::SIGKILL);
} else {
for &pid in &pids {
libc::kill(pid as i32, libc::SIGKILL);
}
}
}
drop(previous_stdout);
pids.clear();
return 1;
}
}
continue;
}
if cmd.args.is_empty() {
continue;
}
let var_expanded_args = expand_variables_in_args(&cmd.args, shell_state);
let expanded_args = match expand_wildcards(&var_expanded_args, shell_state) {
Ok(args) => args,
Err(_) => return 1,
};
if expanded_args.is_empty() {
continue;
}
if crate::builtins::is_builtin(&expanded_args[0]) {
match execute_builtin_in_background_pipeline(
&expanded_args,
&cmd.redirections,
previous_stdout.take(),
i == 0,
is_last,
pgid,
shell_state,
) {
Ok((pid, stdout)) => {
pids.push(pid);
if pgid.is_none() {
pgid = Some(pid);
}
if !is_last {
previous_stdout = stdout;
}
}
Err(e) => {
if shell_state.colors_enabled {
eprintln!(
"{}Error executing builtin command in pipeline: {}\x1b[0m",
shell_state.color_scheme.error, e
);
} else {
eprintln!("Error executing builtin command in pipeline: {}", e);
}
unsafe {
if let Some(group_id) = pgid {
libc::killpg(group_id as i32, libc::SIGKILL);
} else {
for &pid in &pids {
libc::kill(pid as i32, libc::SIGKILL);
}
}
}
drop(previous_stdout);
pids.clear();
return 1;
}
}
continue;
}
let mut command = Command::new(&expanded_args[0]);
command.args(&expanded_args[1..]);
let child_env = shell_state.get_env_for_child();
command.env_clear();
for (key, value) in child_env {
command.env(key, value);
}
if let Some(prev) = previous_stdout.take() {
command.stdin(Stdio::from(prev));
} else if i == 0 {
command.stdin(Stdio::null());
} else {
command.stdin(Stdio::null());
}
if !is_last {
command.stdout(Stdio::piped());
}
if let Err(e) = apply_redirections(&cmd.redirections, shell_state, Some(&mut command)) {
if shell_state.colors_enabled {
eprintln!(
"{}Redirection error: {}\x1b[0m",
shell_state.color_scheme.error, e
);
} else {
eprintln!("Redirection error: {}", e);
}
unsafe {
if let Some(group_id) = pgid {
libc::killpg(group_id as i32, libc::SIGKILL);
} else {
for &pid in &pids {
libc::kill(pid as i32, libc::SIGKILL);
}
}
}
drop(previous_stdout);
pids.clear();
return 1;
}
let current_pgid = pgid;
unsafe {
command.pre_exec(move || {
let pid = libc::getpid();
if let Some(group_id) = current_pgid {
libc::setpgid(pid, group_id as i32);
} else {
libc::setpgid(pid, pid);
}
Ok(())
});
}
match command.spawn() {
Ok(mut child) => {
let pid = child.id();
pids.push(pid);
if pgid.is_none() {
pgid = Some(pid);
}
if !is_last {
previous_stdout = child
.stdout
.take()
.map(|s| unsafe { File::from_raw_fd(s.into_raw_fd()) });
}
}
Err(e) => {
if shell_state.colors_enabled {
eprintln!(
"{}Error spawning command '{}': {}\x1b[0m",
shell_state.color_scheme.error, expanded_args[0], e
);
} else {
eprintln!("Error spawning command '{}': {}", expanded_args[0], e);
}
unsafe {
if let Some(group_id) = pgid {
libc::killpg(group_id as i32, libc::SIGKILL);
} else {
for &pid in &pids {
libc::kill(pid as i32, libc::SIGKILL);
}
}
}
drop(previous_stdout);
pids.clear();
return 1;
}
}
}
if pids.is_empty() {
return 0;
}
let job_id = shell_state.job_table.borrow_mut().allocate_job_id();
let job = Job::new(
job_id,
pgid,
command_str,
pids.clone(),
false, );
println!("[{}] {}", job_id, pids[0]);
shell_state.job_table.borrow_mut().add_job(job);
if let Some(&last_pid) = pids.last() {
shell_state.last_background_pid = Some(last_pid);
}
0
}
fn format_command_string(args: &[String]) -> String {
args.join(" ")
}
fn format_pipeline_string(commands: &[ShellCommand]) -> String {
let mut parts = Vec::new();
for cmd in commands {
if let Some(ref compound) = cmd.compound {
match compound.as_ref() {
Ast::Subshell { .. } => parts.push("(...)".to_string()),
Ast::CommandGroup { .. } => parts.push("{...}".to_string()),
_ => parts.push("compound".to_string()),
}
} else if !cmd.args.is_empty() {
parts.push(cmd.args.join(" "));
}
}
parts.join(" | ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_command_string() {
let args = vec!["echo".to_string(), "hello".to_string(), "world".to_string()];
assert_eq!(format_command_string(&args), "echo hello world");
}
#[test]
fn test_format_pipeline_string() {
let commands = vec![
ShellCommand {
args: vec!["ls".to_string(), "-la".to_string()],
redirections: vec![],
compound: None,
},
ShellCommand {
args: vec!["grep".to_string(), "txt".to_string()],
redirections: vec![],
compound: None,
},
];
assert_eq!(
format_pipeline_string(&commands),
"ls -la | grep txt"
);
}
#[test]
fn test_format_pipeline_with_subshell() {
let commands = vec![
ShellCommand {
args: vec![],
redirections: vec![],
compound: Some(Box::new(Ast::Subshell {
body: Box::new(Ast::Pipeline(vec![])),
})),
},
ShellCommand {
args: vec!["grep".to_string(), "txt".to_string()],
redirections: vec![],
compound: None,
},
];
assert_eq!(
format_pipeline_string(&commands),
"(...) | grep txt"
);
}
}