| 1 | use crate::command::find_in_path; |
| 2 | use crate::redirect::apply_redirects; |
| 3 | use crate::{ExecutionError, PipelineError}; |
| 4 | use rush_expand::Context; |
| 5 | use rush_interactive::ErrorHints; |
| 6 | use rush_parser::ast::{Pipeline, SimpleCommand}; |
| 7 | use std::process::{Command, Stdio}; |
| 8 | |
| 9 | #[cfg(unix)] |
| 10 | use nix::unistd::{setpgid, Pid}; |
| 11 | |
| 12 | /// Execute a simple command in the background |
| 13 | /// |
| 14 | /// Returns (pid, pgid, command_string) for adding to JobList |
| 15 | #[cfg(unix)] |
| 16 | pub fn execute_simple_background( |
| 17 | simple_cmd: &SimpleCommand, |
| 18 | context: &mut Context, |
| 19 | ) -> Result<(Pid, Pid, String), ExecutionError> { |
| 20 | // Expand words |
| 21 | let expanded = rush_expand::expand_words(&simple_cmd.words, context) |
| 22 | .map_err(|e| ExecutionError::IoError(std::io::Error::new( |
| 23 | std::io::ErrorKind::Other, |
| 24 | e.to_string(), |
| 25 | )))?; |
| 26 | |
| 27 | if expanded.is_empty() { |
| 28 | return Err(ExecutionError::EmptyCommand); |
| 29 | } |
| 30 | |
| 31 | let command_name = &expanded[0]; |
| 32 | let args = &expanded[1..]; |
| 33 | |
| 34 | // Find the command in PATH |
| 35 | let program_path = find_in_path(command_name) |
| 36 | .ok_or_else(|| ExecutionError::CommandNotFound(ErrorHints::command_not_found(command_name)))?; |
| 37 | |
| 38 | // Build the command string for display |
| 39 | let command_string = format!("{} {}", command_name, args.join(" ")); |
| 40 | |
| 41 | // Build the command |
| 42 | let mut cmd = Command::new(program_path); |
| 43 | cmd.args(args); |
| 44 | |
| 45 | // Apply redirections |
| 46 | apply_redirects(&mut cmd, &simple_cmd.redirects, context) |
| 47 | .map_err(|e| ExecutionError::IoError(std::io::Error::new( |
| 48 | std::io::ErrorKind::Other, |
| 49 | e.to_string(), |
| 50 | )))?; |
| 51 | |
| 52 | // Spawn the command |
| 53 | let child = cmd.spawn()?; |
| 54 | let pid = Pid::from_raw(child.id() as i32); |
| 55 | |
| 56 | // Put the child in its own process group |
| 57 | // This is important for job control |
| 58 | let pgid = pid; // Use the PID as the PGID (process becomes group leader) |
| 59 | if let Err(e) = setpgid(pid, pgid) { |
| 60 | eprintln!("Warning: failed to set process group: {}", e); |
| 61 | } |
| 62 | |
| 63 | Ok((pid, pgid, command_string)) |
| 64 | } |
| 65 | |
| 66 | /// Execute a pipeline in the background |
| 67 | /// |
| 68 | /// Returns (pids, pgid, command_string) for adding to JobList |
| 69 | #[cfg(unix)] |
| 70 | pub fn execute_pipeline_background( |
| 71 | pipeline: &Pipeline, |
| 72 | context: &mut Context, |
| 73 | ) -> Result<(Vec<Pid>, Pid, String), PipelineError> { |
| 74 | if pipeline.commands.is_empty() { |
| 75 | return Err(PipelineError::EmptyPipeline); |
| 76 | } |
| 77 | |
| 78 | // Build command string for display |
| 79 | let mut command_parts = Vec::new(); |
| 80 | for element in &pipeline.commands { |
| 81 | let simple_cmd = match element { |
| 82 | rush_parser::ast::PipelineElement::Simple(cmd) => cmd, |
| 83 | rush_parser::ast::PipelineElement::Subshell(_) => { |
| 84 | return Err(PipelineError::ExecutionError(ExecutionError::CommandNotFound( |
| 85 | "Subshells in pipelines not yet fully supported".to_string() |
| 86 | ))); |
| 87 | } |
| 88 | rush_parser::ast::PipelineElement::ExtendedTest(_) => { |
| 89 | return Err(PipelineError::ExecutionError(ExecutionError::CommandNotFound( |
| 90 | "Extended tests in pipelines not yet supported".to_string() |
| 91 | ))); |
| 92 | } |
| 93 | }; |
| 94 | let expanded = rush_expand::expand_words(&simple_cmd.words, context) |
| 95 | .map_err(|e| PipelineError::ExpansionError(e.to_string()))?; |
| 96 | if !expanded.is_empty() { |
| 97 | command_parts.push(expanded.join(" ")); |
| 98 | } |
| 99 | } |
| 100 | let command_string = command_parts.join(" | "); |
| 101 | |
| 102 | // Build and spawn all commands in the pipeline |
| 103 | let mut pids = Vec::new(); |
| 104 | let mut prev_stdout = None; |
| 105 | let mut pgid: Option<Pid> = None; |
| 106 | |
| 107 | for (i, element) in pipeline.commands.iter().enumerate() { |
| 108 | let is_first = i == 0; |
| 109 | let is_last = i == pipeline.commands.len() - 1; |
| 110 | |
| 111 | // Get the simple command from the element |
| 112 | let simple_cmd = match element { |
| 113 | rush_parser::ast::PipelineElement::Simple(cmd) => cmd, |
| 114 | rush_parser::ast::PipelineElement::Subshell(_) => { |
| 115 | return Err(PipelineError::ExecutionError(ExecutionError::CommandNotFound( |
| 116 | "Subshells in pipelines not yet fully supported".to_string() |
| 117 | ))); |
| 118 | } |
| 119 | rush_parser::ast::PipelineElement::ExtendedTest(_) => { |
| 120 | return Err(PipelineError::ExecutionError(ExecutionError::CommandNotFound( |
| 121 | "Extended tests in pipelines not yet supported".to_string() |
| 122 | ))); |
| 123 | } |
| 124 | }; |
| 125 | |
| 126 | // Expand words |
| 127 | let expanded = rush_expand::expand_words(&simple_cmd.words, context) |
| 128 | .map_err(|e| PipelineError::ExpansionError(e.to_string()))?; |
| 129 | |
| 130 | if expanded.is_empty() { |
| 131 | continue; |
| 132 | } |
| 133 | |
| 134 | let command_name = &expanded[0]; |
| 135 | let args = &expanded[1..]; |
| 136 | |
| 137 | // Find the command in PATH |
| 138 | let program_path = find_in_path(command_name) |
| 139 | .ok_or_else(|| ExecutionError::CommandNotFound(ErrorHints::command_not_found(command_name)))?; |
| 140 | |
| 141 | // Build the command |
| 142 | let mut cmd = Command::new(program_path); |
| 143 | cmd.args(args); |
| 144 | |
| 145 | // Set up stdin |
| 146 | if is_first { |
| 147 | cmd.stdin(Stdio::inherit()); |
| 148 | } else if let Some(prev_out) = prev_stdout.take() { |
| 149 | cmd.stdin(prev_out); |
| 150 | } |
| 151 | |
| 152 | // Set up stdout |
| 153 | if is_last { |
| 154 | cmd.stdout(Stdio::inherit()); |
| 155 | } else { |
| 156 | cmd.stdout(Stdio::piped()); |
| 157 | } |
| 158 | |
| 159 | cmd.stderr(Stdio::inherit()); |
| 160 | |
| 161 | // Apply redirections |
| 162 | apply_redirects(&mut cmd, &simple_cmd.redirects, context)?; |
| 163 | |
| 164 | // Spawn the command |
| 165 | let mut child = cmd.spawn()?; |
| 166 | let pid = Pid::from_raw(child.id() as i32); |
| 167 | |
| 168 | // Set up process group |
| 169 | // First process in pipeline becomes the group leader |
| 170 | if pgid.is_none() { |
| 171 | pgid = Some(pid); |
| 172 | if let Err(e) = setpgid(pid, pid) { |
| 173 | eprintln!("Warning: failed to set process group leader: {}", e); |
| 174 | } |
| 175 | } else { |
| 176 | // Other processes join the group |
| 177 | if let Err(e) = setpgid(pid, pgid.unwrap()) { |
| 178 | eprintln!("Warning: failed to join process group: {}", e); |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | pids.push(pid); |
| 183 | |
| 184 | // Save stdout for next command |
| 185 | if !is_last { |
| 186 | prev_stdout = child.stdout.take().map(Stdio::from); |
| 187 | } |
| 188 | |
| 189 | // Don't wait - we're running in background |
| 190 | std::mem::forget(child); // Prevent child from being killed when dropped |
| 191 | } |
| 192 | |
| 193 | Ok((pids, pgid.unwrap(), command_string)) |
| 194 | } |
| 195 | |
| 196 | #[cfg(not(unix))] |
| 197 | pub fn execute_simple_background( |
| 198 | _simple_cmd: &SimpleCommand, |
| 199 | _context: &mut Context, |
| 200 | ) -> Result<(u32, u32, String), ExecutionError> { |
| 201 | Err(ExecutionError::IoError(std::io::Error::new( |
| 202 | std::io::ErrorKind::Unsupported, |
| 203 | "Background execution not supported on this platform", |
| 204 | ))) |
| 205 | } |
| 206 | |
| 207 | #[cfg(not(unix))] |
| 208 | pub fn execute_pipeline_background( |
| 209 | _pipeline: &Pipeline, |
| 210 | _context: &mut Context, |
| 211 | ) -> Result<(Vec<u32>, u32, String), PipelineError> { |
| 212 | Err(PipelineError::IoError(std::io::Error::new( |
| 213 | std::io::ErrorKind::Unsupported, |
| 214 | "Background execution not supported on this platform", |
| 215 | ))) |
| 216 | } |
| 217 |