Skip to main content

codex_utils_pty/
pipe.rs

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
121/// On Windows, process-tree containment is best-effort because Tokio returns
122/// only after the root process starts, so job assignment cannot be atomic.
123async 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        // Accept the small race: a descendant created between spawn and
185        // assignment is not guaranteed to join the job and can escape termination.
186        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        /*pty_handles*/ None,
306        /*resizer*/ None,
307    );
308
309    Ok(SpawnedProcess {
310        session: handle,
311        stdout_rx,
312        stderr_rx,
313        exit_rx,
314    })
315}
316
317/// Spawn a process using regular pipes and preserve selected inherited file
318/// descriptors across exec on Unix.
319pub 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
339/// Spawn a process using regular pipes, close stdin immediately, and preserve
340/// selected inherited file descriptors across exec on Unix.
341pub 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;