1use std::collections::HashMap;
2use std::io;
3use std::io::ErrorKind;
4use std::path::Path;
5use std::process::Stdio;
6use std::sync::Arc;
7use std::sync::Mutex as StdMutex;
8use std::sync::atomic::AtomicBool;
9
10use anyhow::Result;
11use tokio::io::AsyncRead;
12use tokio::io::AsyncReadExt;
13use tokio::io::AsyncWriteExt;
14use tokio::io::BufReader;
15use tokio::process::Command;
16use tokio::sync::mpsc;
17use tokio::sync::oneshot;
18use tokio::task::JoinHandle;
19
20use crate::process::ChildTerminator;
21use crate::process::ProcessHandle;
22use crate::process::ProcessSignal;
23use crate::process::SpawnedProcess;
24use crate::process::exit_code_from_status;
25
26#[cfg(target_os = "linux")]
27use libc;
28
29#[cfg(windows)]
30enum WindowsChildTerminator {
31 Job(Arc<crate::win::JobObject>),
32 Process(u32),
33}
34
35struct PipeChildTerminator {
36 #[cfg(windows)]
37 windows: WindowsChildTerminator,
38 #[cfg(unix)]
39 process_group_id: u32,
40}
41
42impl ChildTerminator for PipeChildTerminator {
43 fn signal(&mut self, signal: ProcessSignal) -> io::Result<()> {
44 match signal {
45 ProcessSignal::Interrupt => {
46 #[cfg(unix)]
47 {
48 crate::process_group::interrupt_process_group(self.process_group_id)
49 }
50
51 #[cfg(not(unix))]
52 {
53 Err(crate::process::unsupported_signal(signal))
54 }
55 }
56 }
57 }
58
59 fn kill(&mut self) -> io::Result<()> {
60 #[cfg(unix)]
61 {
62 crate::process_group::kill_process_group(self.process_group_id)
63 }
64
65 #[cfg(windows)]
66 {
67 match &self.windows {
68 WindowsChildTerminator::Job(job) => job.terminate(),
69 WindowsChildTerminator::Process(pid) => kill_process(*pid),
70 }
71 }
72
73 #[cfg(not(any(unix, windows)))]
74 {
75 Ok(())
76 }
77 }
78}
79
80#[cfg(windows)]
81fn kill_process(pid: u32) -> io::Result<()> {
82 unsafe {
83 let handle = winapi::um::processthreadsapi::OpenProcess(
84 winapi::um::winnt::PROCESS_TERMINATE,
85 0,
86 pid,
87 );
88 if handle.is_null() {
89 return Err(io::Error::last_os_error());
90 }
91 let success = winapi::um::processthreadsapi::TerminateProcess(handle, 1);
92 let err = io::Error::last_os_error();
93 winapi::um::handleapi::CloseHandle(handle);
94 if success == 0 { Err(err) } else { Ok(()) }
95 }
96}
97
98async fn read_output_stream<R>(mut reader: R, output_tx: mpsc::Sender<Vec<u8>>)
99where
100 R: AsyncRead + Unpin,
101{
102 let mut buf = vec![0u8; 8_192];
103 loop {
104 match reader.read(&mut buf).await {
105 Ok(0) => break,
106 Ok(n) => {
107 let _ = output_tx.send(buf[..n].to_vec()).await;
108 }
109 Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
110 Err(_) => break,
111 }
112 }
113}
114
115#[derive(Clone, Copy)]
116enum PipeStdinMode {
117 Piped,
118 Null,
119}
120
121async fn spawn_process_with_stdin_mode(
124 program: &str,
125 args: &[String],
126 cwd: &Path,
127 env: &HashMap<String, String>,
128 arg0: &Option<String>,
129 stdin_mode: PipeStdinMode,
130 inherited_fds: &[i32],
131) -> Result<SpawnedProcess> {
132 if program.is_empty() {
133 anyhow::bail!("missing program for pipe spawn");
134 }
135
136 #[cfg(not(unix))]
137 let _ = inherited_fds;
138
139 let mut command = Command::new(program);
140 #[cfg(unix)]
141 if let Some(arg0) = arg0 {
142 command.arg0(arg0);
143 }
144 #[cfg(target_os = "linux")]
145 let parent_pid = unsafe { libc::getpid() };
146 #[cfg(unix)]
147 let inherited_fds = inherited_fds.to_vec();
148 #[cfg(unix)]
149 unsafe {
150 command.pre_exec(move || {
151 crate::process_group::detach_from_tty()?;
152 #[cfg(target_os = "linux")]
153 crate::process_group::set_parent_death_signal(parent_pid)?;
154 crate::pty::close_inherited_fds_except(&inherited_fds);
155 Ok(())
156 });
157 }
158 #[cfg(not(unix))]
159 let _ = arg0;
160 command.current_dir(cwd);
161 command.env_clear();
162 for (key, value) in env {
163 command.env(key, value);
164 }
165 for arg in args {
166 command.arg(arg);
167 }
168 match stdin_mode {
169 PipeStdinMode::Piped => {
170 command.stdin(Stdio::piped());
171 }
172 PipeStdinMode::Null => {
173 command.stdin(Stdio::null());
174 }
175 }
176 command.stdout(Stdio::piped());
177 command.stderr(Stdio::piped());
178
179 #[cfg(windows)]
180 let job = crate::win::JobObject::create().map(Arc::new);
181 let mut child = command.spawn()?;
182 #[cfg(windows)]
183 let windows_terminator = {
184 let pid = child
187 .id()
188 .ok_or_else(|| io::Error::other("missing child pid"))?;
189 let assigned_job = job.and_then(|job| {
190 let process_handle = child
191 .raw_handle()
192 .ok_or_else(|| io::Error::other("missing child process handle"))?;
193 job.assign_process(process_handle)?;
194 Ok(job)
195 });
196 match assigned_job {
197 Ok(job) => WindowsChildTerminator::Job(job),
198 Err(err) => {
199 log::warn!(
200 "Windows pipe process tree containment unavailable for pid {pid}: {err}"
201 );
202 WindowsChildTerminator::Process(pid)
203 }
204 }
205 };
206 #[cfg(unix)]
207 let process_group_id = child
208 .id()
209 .ok_or_else(|| io::Error::other("missing child pid"))?;
210
211 let stdin = child.stdin.take();
212 let stdout = child.stdout.take();
213 let stderr = child.stderr.take();
214
215 let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(128);
216 let (stdout_tx, stdout_rx) = mpsc::channel::<Vec<u8>>(128);
217 let (stderr_tx, stderr_rx) = mpsc::channel::<Vec<u8>>(128);
218 let writer_handle = if let Some(stdin) = stdin {
219 tokio::spawn(async move {
220 let mut writer = stdin;
221 while let Some(bytes) = writer_rx.recv().await {
222 let _ = writer.write_all(&bytes).await;
223 let _ = writer.flush().await;
224 }
225 })
226 } else {
227 drop(writer_rx);
228 tokio::spawn(async {})
229 };
230
231 let stdout_handle = stdout.map(|stdout| {
232 let stdout_tx = stdout_tx.clone();
233 tokio::spawn(async move {
234 read_output_stream(BufReader::new(stdout), stdout_tx).await;
235 })
236 });
237 let stderr_handle = stderr.map(|stderr| {
238 let stderr_tx = stderr_tx.clone();
239 tokio::spawn(async move {
240 read_output_stream(BufReader::new(stderr), stderr_tx).await;
241 })
242 });
243 let mut reader_abort_handles = Vec::new();
244 if let Some(handle) = stdout_handle.as_ref() {
245 reader_abort_handles.push(handle.abort_handle());
246 }
247 if let Some(handle) = stderr_handle.as_ref() {
248 reader_abort_handles.push(handle.abort_handle());
249 }
250 let reader_handle = tokio::spawn(async move {
251 if let Some(handle) = stdout_handle {
252 let _ = handle.await;
253 }
254 if let Some(handle) = stderr_handle {
255 let _ = handle.await;
256 }
257 });
258
259 let (exit_tx, exit_rx) = oneshot::channel::<i32>();
260 let exit_status = Arc::new(AtomicBool::new(false));
261 let wait_exit_status = Arc::clone(&exit_status);
262 let exit_code = Arc::new(StdMutex::new(None));
263 let wait_exit_code = Arc::clone(&exit_code);
264 #[cfg(windows)]
265 let wait_job = match &windows_terminator {
266 WindowsChildTerminator::Job(job) => Some(Arc::clone(job)),
267 WindowsChildTerminator::Process(_) => None,
268 };
269 let wait_handle: JoinHandle<()> = tokio::spawn(async move {
270 let code = match child.wait().await {
271 Ok(status) => {
272 #[cfg(windows)]
273 if let Some(job) = wait_job
274 && let Err(err) = job.preserve_descendants()
275 {
276 log::warn!(
277 "Windows pipe failed to preserve descendants after root exit: {err}"
278 );
279 }
280 exit_code_from_status(status)
281 }
282 Err(_) => -1,
283 };
284 wait_exit_status.store(true, std::sync::atomic::Ordering::SeqCst);
285 if let Ok(mut guard) = wait_exit_code.lock() {
286 *guard = Some(code);
287 }
288 let _ = exit_tx.send(code);
289 });
290
291 let handle = ProcessHandle::new(
292 writer_tx,
293 Box::new(PipeChildTerminator {
294 #[cfg(windows)]
295 windows: windows_terminator,
296 #[cfg(unix)]
297 process_group_id,
298 }),
299 reader_handle,
300 reader_abort_handles,
301 writer_handle,
302 wait_handle,
303 exit_status,
304 exit_code,
305 None,
306 None,
307 );
308
309 Ok(SpawnedProcess {
310 session: handle,
311 stdout_rx,
312 stderr_rx,
313 exit_rx,
314 })
315}
316
317pub async fn spawn_process(
320 program: &str,
321 args: &[String],
322 cwd: &Path,
323 env: &HashMap<String, String>,
324 arg0: &Option<String>,
325 inherited_fds: &[i32],
326) -> Result<SpawnedProcess> {
327 spawn_process_with_stdin_mode(
328 program,
329 args,
330 cwd,
331 env,
332 arg0,
333 PipeStdinMode::Piped,
334 inherited_fds,
335 )
336 .await
337}
338
339pub async fn spawn_process_no_stdin(
342 program: &str,
343 args: &[String],
344 cwd: &Path,
345 env: &HashMap<String, String>,
346 arg0: &Option<String>,
347 inherited_fds: &[i32],
348) -> Result<SpawnedProcess> {
349 spawn_process_with_stdin_mode(
350 program,
351 args,
352 cwd,
353 env,
354 arg0,
355 PipeStdinMode::Null,
356 inherited_fds,
357 )
358 .await
359}
360
361#[cfg(all(test, windows))]
362#[path = "pipe_tests.rs"]
363mod tests;