Rust · 8668 bytes Raw Blame History
1 //! Process substitution implementation using named pipes (FIFOs)
2 //!
3 //! Process substitution allows commands to be used where filenames are expected:
4 //! - `<(command)` creates a FIFO that provides the command's stdout
5 //! - `>(command)` creates a FIFO that feeds into the command's stdin
6 //!
7 //! Example: `diff <(ls dir1) <(ls dir2)`
8
9 use nix::sys::stat::Mode;
10 use nix::unistd::{fork, ForkResult, Pid};
11 use rush_expand::Context;
12 use std::collections::HashMap;
13 use std::path::PathBuf;
14 use std::sync::Mutex;
15 use thiserror::Error;
16
17 #[derive(Error, Debug)]
18 pub enum ProcessSubstError {
19 #[error("Failed to create FIFO: {0}")]
20 FifoCreationError(String),
21
22 #[error("Fork failed: {0}")]
23 ForkError(String),
24
25 #[error("I/O error: {0}")]
26 IoError(#[from] std::io::Error),
27
28 #[error("Command execution error: {0}")]
29 CommandError(String),
30 }
31
32 /// Global registry of active process substitutions
33 /// Tracks FIFOs and PIDs for cleanup
34 static PROCESS_SUBST_REGISTRY: Mutex<Option<ProcessSubstRegistry>> = Mutex::new(None);
35
36 struct ProcessSubstRegistry {
37 /// Map of FIFO paths to process IDs
38 active_fifos: HashMap<PathBuf, Pid>,
39 /// Counter for unique FIFO names
40 counter: u64,
41 }
42
43 impl ProcessSubstRegistry {
44 fn new() -> Self {
45 Self {
46 active_fifos: HashMap::new(),
47 counter: 0,
48 }
49 }
50
51 fn generate_fifo_path(&mut self) -> PathBuf {
52 let pid = std::process::id();
53 let count = self.counter;
54 self.counter += 1;
55 PathBuf::from(format!("/tmp/rush-psub-{}-{}", pid, count))
56 }
57
58 fn register(&mut self, path: PathBuf, pid: Pid) {
59 self.active_fifos.insert(path, pid);
60 }
61
62 fn cleanup_all(&mut self) {
63 // Remove all FIFOs
64 for (path, _) in self.active_fifos.drain() {
65 let _ = std::fs::remove_file(&path);
66 }
67 }
68 }
69
70 /// Initialize the process substitution registry
71 pub fn init_registry() {
72 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
73 if registry.is_none() {
74 *registry = Some(ProcessSubstRegistry::new());
75 }
76 }
77
78 /// Cleanup all process substitutions (call on shell exit)
79 pub fn cleanup_all() {
80 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
81 if let Some(ref mut reg) = *registry {
82 reg.cleanup_all();
83 }
84 }
85
86 /// Execute process substitution for input: <(command)
87 /// Creates a FIFO and forks a process that writes command stdout to it
88 /// Returns the FIFO path that can be used as a filename
89 #[cfg(unix)]
90 pub fn execute_process_subst_input(
91 command: &str,
92 context: &mut Context,
93 ) -> Result<String, ProcessSubstError> {
94 init_registry();
95
96 // Generate unique FIFO path
97 let fifo_path = {
98 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
99 let reg = registry.as_mut().unwrap();
100 reg.generate_fifo_path()
101 };
102
103 // Create the FIFO
104 nix::unistd::mkfifo(&fifo_path, Mode::S_IRUSR | Mode::S_IWUSR)
105 .map_err(|e| ProcessSubstError::FifoCreationError(format!("{}", e)))?;
106
107 // Fork a process to execute the command
108 match unsafe { fork() } {
109 Ok(ForkResult::Child) => {
110 // Child process: execute command with stdout redirected to FIFO
111 use std::fs::OpenOptions;
112 use std::os::unix::io::AsRawFd;
113
114 // Open FIFO for writing
115 let fifo_file = OpenOptions::new()
116 .write(true)
117 .open(&fifo_path)
118 .expect("Failed to open FIFO");
119
120 // Redirect stdout to FIFO
121 let fifo_fd = fifo_file.as_raw_fd();
122 nix::unistd::dup2(fifo_fd, 1).expect("Failed to dup2 stdout");
123
124 // Execute the command via the command substitution executor
125 let executor_opt = context.command_executor.0.clone();
126 if let Some(executor) = executor_opt {
127 match executor(command, context) {
128 Ok(_) => std::process::exit(0),
129 Err(_) => std::process::exit(1),
130 }
131 } else {
132 // Fallback: use sh -c
133 let status = std::process::Command::new("sh")
134 .arg("-c")
135 .arg(command)
136 .status()
137 .expect("Failed to execute command");
138 std::process::exit(status.code().unwrap_or(1));
139 }
140 }
141 Ok(ForkResult::Parent { child }) => {
142 // Parent process: register the FIFO and return path
143 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
144 let reg = registry.as_mut().unwrap();
145 reg.register(fifo_path.clone(), child);
146
147 Ok(fifo_path.to_string_lossy().to_string())
148 }
149 Err(e) => {
150 // Fork failed - clean up FIFO
151 let _ = std::fs::remove_file(&fifo_path);
152 Err(ProcessSubstError::ForkError(format!("{}", e)))
153 }
154 }
155 }
156
157 /// Execute process substitution for output: >(command)
158 /// Creates a FIFO and forks a process that reads from it as stdin
159 /// Returns the FIFO path that can be used as a filename
160 #[cfg(unix)]
161 pub fn execute_process_subst_output(
162 command: &str,
163 context: &mut Context,
164 ) -> Result<String, ProcessSubstError> {
165 init_registry();
166
167 // Generate unique FIFO path
168 let fifo_path = {
169 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
170 let reg = registry.as_mut().unwrap();
171 reg.generate_fifo_path()
172 };
173
174 // Create the FIFO
175 nix::unistd::mkfifo(&fifo_path, Mode::S_IRUSR | Mode::S_IWUSR)
176 .map_err(|e| ProcessSubstError::FifoCreationError(format!("{}", e)))?;
177
178 // Fork a process to execute the command
179 match unsafe { fork() } {
180 Ok(ForkResult::Child) => {
181 // Child process: execute command with stdin redirected from FIFO
182 use std::fs::OpenOptions;
183 use std::os::unix::io::AsRawFd;
184
185 // Open FIFO for reading
186 let fifo_file = OpenOptions::new()
187 .read(true)
188 .open(&fifo_path)
189 .expect("Failed to open FIFO");
190
191 // Redirect stdin from FIFO
192 let fifo_fd = fifo_file.as_raw_fd();
193 nix::unistd::dup2(fifo_fd, 0).expect("Failed to dup2 stdin");
194
195 // Execute the command via the command substitution executor
196 let executor_opt = context.command_executor.0.clone();
197 if let Some(executor) = executor_opt {
198 match executor(command, context) {
199 Ok(_) => std::process::exit(0),
200 Err(_) => std::process::exit(1),
201 }
202 } else {
203 // Fallback: use sh -c
204 let status = std::process::Command::new("sh")
205 .arg("-c")
206 .arg(command)
207 .status()
208 .expect("Failed to execute command");
209 std::process::exit(status.code().unwrap_or(1));
210 }
211 }
212 Ok(ForkResult::Parent { child }) => {
213 // Parent process: register the FIFO and return path
214 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
215 let reg = registry.as_mut().unwrap();
216 reg.register(fifo_path.clone(), child);
217
218 Ok(fifo_path.to_string_lossy().to_string())
219 }
220 Err(e) => {
221 // Fork failed - clean up FIFO
222 let _ = std::fs::remove_file(&fifo_path);
223 Err(ProcessSubstError::ForkError(format!("{}", e)))
224 }
225 }
226 }
227
228 /// Wait for all process substitutions to complete
229 #[cfg(unix)]
230 pub fn wait_for_all() {
231 use nix::sys::wait::{waitpid, WaitPidFlag};
232
233 let mut registry = PROCESS_SUBST_REGISTRY.lock().unwrap();
234 if let Some(ref mut reg) = *registry {
235 // Wait for all child processes (non-blocking)
236 for (path, pid) in reg.active_fifos.drain() {
237 let _ = waitpid(pid, Some(WaitPidFlag::WNOHANG));
238 let _ = std::fs::remove_file(&path);
239 }
240 }
241 }
242
243 #[cfg(not(unix))]
244 pub fn execute_process_subst_input(
245 _command: &str,
246 _context: &mut Context,
247 ) -> Result<String, ProcessSubstError> {
248 Err(ProcessSubstError::CommandError(
249 "Process substitution not supported on this platform".to_string(),
250 ))
251 }
252
253 #[cfg(not(unix))]
254 pub fn execute_process_subst_output(
255 _command: &str,
256 _context: &mut Context,
257 ) -> Result<String, ProcessSubstError> {
258 Err(ProcessSubstError::CommandError(
259 "Process substitution not supported on this platform".to_string(),
260 ))
261 }
262
263 #[cfg(not(unix))]
264 pub fn wait_for_all() {
265 // No-op on non-Unix
266 }
267