Skip to main content

running_process/pty/
backend.rs

1//! PTY backend abstraction (#150).
2//!
3//! `native_pty_process.rs` was riddled with `#[cfg(windows)]` /
4//! `#[cfg(unix)]` branches around the underlying portable-pty calls.
5//! After the #150 rewrite we have two distinct backends:
6//!
7//! * Windows — [`conpty::ConPtyBackend`] (raw ConPTY via windows-sys
8//!   with `PSEUDOCONSOLE_PASSTHROUGH_MODE` enabled)
9//! * Unix — [`unix::PortablePtyBackend`] (a thin wrapper around
10//!   portable-pty's native_pty_system, unchanged behavior)
11//!
12//! The [`Backend`] type alias resolves to one or the other per-target,
13//! and `native_pty_process.rs` makes a single `Backend::openpty(...)`
14//! call instead of branching.
15
16use std::ffi::OsString;
17use std::io::{self, Read, Write};
18use std::path::Path;
19
20/// Caller-facing PTY dimensions. Pixel fields are ignored on Windows
21/// (ConPTY only consumes rows/cols). Mirrors portable-pty's shape so
22/// caller code passes them through unchanged.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct PtySize {
25    pub rows: u16,
26    pub cols: u16,
27    pub pixel_width: u16,
28    pub pixel_height: u16,
29}
30
31pub trait PtyMaster: Send + 'static {
32    fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>>;
33    fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>>;
34    fn resize(&self, size: PtySize) -> io::Result<()>;
35    /// Return the current PTY dimensions. On Windows the value is
36    /// the last size passed to `resize` (or the initial openpty
37    /// size); ConPTY exposes no live query API. Restored in 4.0.1
38    /// for downstream parity with portable-pty's `MasterPty::get_size`.
39    fn get_size(&self) -> io::Result<PtySize>;
40    /// On Unix returns the foreground process group leader of the
41    /// PTY (used by tools like `tcsetpgrp` checks). Always returns
42    /// `None` on Windows where the concept doesn't exist.
43    #[cfg(unix)]
44    fn process_group_leader(&self) -> Option<i32>;
45}
46
47pub trait PtyChild: Send + 'static {
48    fn pid(&self) -> u32;
49    /// Poll without blocking. `Ok(None)` means still running.
50    /// `Ok(Some(code))` means exited with that exit code.
51    /// `&mut self` because portable-pty's underlying Child::try_wait
52    /// takes &mut, and we keep the surface uniform across backends.
53    fn try_wait(&mut self) -> io::Result<Option<u32>>;
54    /// Block until the child exits, then return the exit code.
55    fn wait(&mut self) -> io::Result<u32>;
56    fn kill(&mut self) -> io::Result<()>;
57    /// Returns the Windows process HANDLE, if applicable. `None`
58    /// means the backend can't expose one (which is fatal for Job
59    /// Object containment — `assign_child_to_windows_kill_on_close_job`
60    /// requires a real handle). Matches portable_pty's signature.
61    #[cfg(windows)]
62    fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle>;
63}
64
65pub trait PtySlave: Send + 'static {
66    type Child: PtyChild;
67    fn spawn(
68        self,
69        argv: &[OsString],
70        cwd: Option<&Path>,
71        env: Option<&[(OsString, OsString)]>,
72    ) -> io::Result<Self::Child>;
73}
74
75pub trait PtyBackend {
76    type Master: PtyMaster;
77    type Slave: PtySlave;
78    fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)>;
79}
80
81#[cfg(windows)]
82mod conpty {
83    use super::*;
84    use crate::pty::conpty_passthrough;
85
86    pub(crate) struct ConPtyBackend;
87
88    impl PtyBackend for ConPtyBackend {
89        type Master = conpty_passthrough::ConPtyMaster;
90        type Slave = conpty_passthrough::ConPtySlave;
91
92        fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)> {
93            let pair = conpty_passthrough::openpty(conpty_passthrough::PtySize {
94                rows: size.rows,
95                cols: size.cols,
96                pixel_width: size.pixel_width,
97                pixel_height: size.pixel_height,
98            })?;
99            Ok((pair.master, pair.slave))
100        }
101    }
102
103    impl PtyMaster for conpty_passthrough::ConPtyMaster {
104        fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
105            conpty_passthrough::ConPtyMaster::try_clone_reader(self)
106        }
107        fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
108            conpty_passthrough::ConPtyMaster::take_writer(self)
109        }
110        fn resize(&self, size: PtySize) -> io::Result<()> {
111            conpty_passthrough::ConPtyMaster::resize(
112                self,
113                conpty_passthrough::PtySize {
114                    rows: size.rows,
115                    cols: size.cols,
116                    pixel_width: size.pixel_width,
117                    pixel_height: size.pixel_height,
118                },
119            )
120        }
121        fn get_size(&self) -> io::Result<PtySize> {
122            let s = conpty_passthrough::ConPtyMaster::get_size(self);
123            Ok(PtySize {
124                rows: s.rows,
125                cols: s.cols,
126                pixel_width: s.pixel_width,
127                pixel_height: s.pixel_height,
128            })
129        }
130    }
131
132    impl PtySlave for conpty_passthrough::ConPtySlave {
133        type Child = conpty_passthrough::child::ConPtyChild;
134        fn spawn(
135            self,
136            argv: &[OsString],
137            cwd: Option<&Path>,
138            env: Option<&[(OsString, OsString)]>,
139        ) -> io::Result<Self::Child> {
140            conpty_passthrough::ConPtySlave::spawn(self, argv, cwd, env)
141        }
142    }
143
144    impl PtyChild for conpty_passthrough::child::ConPtyChild {
145        fn pid(&self) -> u32 {
146            conpty_passthrough::child::ConPtyChild::pid(self)
147        }
148        fn try_wait(&mut self) -> io::Result<Option<u32>> {
149            conpty_passthrough::child::ConPtyChild::try_wait(self)
150        }
151        fn wait(&mut self) -> io::Result<u32> {
152            conpty_passthrough::child::ConPtyChild::wait(self)
153        }
154        fn kill(&mut self) -> io::Result<()> {
155            conpty_passthrough::child::ConPtyChild::kill(self)
156        }
157        fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
158            Some(conpty_passthrough::child::ConPtyChild::as_raw_handle(self))
159        }
160    }
161}
162
163#[cfg(unix)]
164mod unix {
165    use super::*;
166    use portable_pty::{
167        Child as PortableChild, CommandBuilder, MasterPty, PtySize as PortPtySize, SlavePty,
168        native_pty_system,
169    };
170
171    pub(crate) struct PortablePtyBackend;
172
173    pub(crate) struct PortablePtyMaster(Box<dyn MasterPty + Send>);
174    pub(crate) struct PortablePtySlave(Box<dyn SlavePty + Send>);
175    pub(crate) struct PortablePtyChild(Box<dyn PortableChild + Send + Sync>);
176
177    impl PtyBackend for PortablePtyBackend {
178        type Master = PortablePtyMaster;
179        type Slave = PortablePtySlave;
180
181        fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)> {
182            let sys = native_pty_system();
183            let pair = sys
184                .openpty(PortPtySize {
185                    rows: size.rows,
186                    cols: size.cols,
187                    pixel_width: size.pixel_width,
188                    pixel_height: size.pixel_height,
189                })
190                .map_err(io::Error::other)?;
191            Ok((PortablePtyMaster(pair.master), PortablePtySlave(pair.slave)))
192        }
193    }
194
195    impl PtyMaster for PortablePtyMaster {
196        fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
197            self.0.try_clone_reader().map_err(io::Error::other)
198        }
199        fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
200            self.0.take_writer().map_err(io::Error::other)
201        }
202        fn resize(&self, size: PtySize) -> io::Result<()> {
203            self.0
204                .resize(PortPtySize {
205                    rows: size.rows,
206                    cols: size.cols,
207                    pixel_width: size.pixel_width,
208                    pixel_height: size.pixel_height,
209                })
210                .map_err(io::Error::other)
211        }
212        fn get_size(&self) -> io::Result<PtySize> {
213            let s = self.0.get_size().map_err(io::Error::other)?;
214            Ok(PtySize {
215                rows: s.rows,
216                cols: s.cols,
217                pixel_width: s.pixel_width,
218                pixel_height: s.pixel_height,
219            })
220        }
221        fn process_group_leader(&self) -> Option<i32> {
222            self.0.process_group_leader()
223        }
224    }
225
226    impl PtySlave for PortablePtySlave {
227        type Child = PortablePtyChild;
228        fn spawn(
229            self,
230            argv: &[OsString],
231            cwd: Option<&Path>,
232            env: Option<&[(OsString, OsString)]>,
233        ) -> io::Result<Self::Child> {
234            if argv.is_empty() {
235                return Err(io::Error::other("portable-pty spawn requires non-empty argv"));
236            }
237            let mut cmd = CommandBuilder::new(&argv[0]);
238            for arg in &argv[1..] {
239                cmd.arg(arg);
240            }
241            if let Some(cwd) = cwd {
242                cmd.cwd(cwd);
243            }
244            if let Some(env) = env {
245                cmd.env_clear();
246                for (k, v) in env {
247                    cmd.env(k, v);
248                }
249            }
250            let child = self.0.spawn_command(cmd).map_err(io::Error::other)?;
251            Ok(PortablePtyChild(child))
252        }
253    }
254
255    impl PtyChild for PortablePtyChild {
256        fn pid(&self) -> u32 {
257            self.0.process_id().unwrap_or(0)
258        }
259        fn try_wait(&mut self) -> io::Result<Option<u32>> {
260            match self.0.try_wait()? {
261                Some(status) => Ok(Some(portable_pty_exit_code(status))),
262                None => Ok(None),
263            }
264        }
265        fn wait(&mut self) -> io::Result<u32> {
266            let status = self.0.wait()?;
267            Ok(portable_pty_exit_code(status))
268        }
269        fn kill(&mut self) -> io::Result<()> {
270            self.0.kill()
271        }
272    }
273
274    /// Convert portable-pty's ExitStatus to a u32 exit code.
275    /// Signal exits map to `128 + signal_index` per the standard
276    /// shell convention.
277    fn portable_pty_exit_code(status: portable_pty::ExitStatus) -> u32 {
278        // ExitStatus is opaque; format and parse the debug form which
279        // is "exited(code)" or "signal(name)". Cleaner would be to
280        // pattern-match on its accessor — but portable-pty's
281        // ExitStatus only exposes `exit_code() -> u32` directly.
282        status.exit_code()
283    }
284}
285
286// #150 W8: route Windows through ConPtyBackend (our new
287// PSEUDOCONSOLE_PASSTHROUGH_MODE implementation).
288#[cfg(windows)]
289pub(crate) type Backend = conpty::ConPtyBackend;
290#[cfg(unix)]
291pub(crate) type Backend = unix::PortablePtyBackend;
292
293// On Windows we still want the portable-pty wrapper available as
294// the temporary backend. Mirror the Unix module under a different
295// name so the cfg-pickup above works.
296#[cfg(windows)]
297mod unix_compat {
298    use super::*;
299    use portable_pty::{
300        Child as PortableChild, CommandBuilder, MasterPty, PtySize as PortPtySize, SlavePty,
301        native_pty_system,
302    };
303
304    pub(crate) struct PortablePtyBackend;
305    pub(crate) struct PortablePtyMaster(Box<dyn MasterPty + Send>);
306    pub(crate) struct PortablePtySlave(Box<dyn SlavePty + Send>);
307    pub(crate) struct PortablePtyChild(Box<dyn PortableChild + Send + Sync>);
308
309    impl PtyBackend for PortablePtyBackend {
310        type Master = PortablePtyMaster;
311        type Slave = PortablePtySlave;
312        fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)> {
313            let sys = native_pty_system();
314            let pair = sys
315                .openpty(PortPtySize {
316                    rows: size.rows,
317                    cols: size.cols,
318                    pixel_width: size.pixel_width,
319                    pixel_height: size.pixel_height,
320                })
321                .map_err(io::Error::other)?;
322            Ok((PortablePtyMaster(pair.master), PortablePtySlave(pair.slave)))
323        }
324    }
325
326    impl PtyMaster for PortablePtyMaster {
327        fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
328            self.0.try_clone_reader().map_err(io::Error::other)
329        }
330        fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
331            self.0.take_writer().map_err(io::Error::other)
332        }
333        fn resize(&self, size: PtySize) -> io::Result<()> {
334            self.0
335                .resize(PortPtySize {
336                    rows: size.rows,
337                    cols: size.cols,
338                    pixel_width: size.pixel_width,
339                    pixel_height: size.pixel_height,
340                })
341                .map_err(io::Error::other)
342        }
343        fn get_size(&self) -> io::Result<PtySize> {
344            let s = self.0.get_size().map_err(io::Error::other)?;
345            Ok(PtySize {
346                rows: s.rows,
347                cols: s.cols,
348                pixel_width: s.pixel_width,
349                pixel_height: s.pixel_height,
350            })
351        }
352    }
353
354    impl PtySlave for PortablePtySlave {
355        type Child = PortablePtyChild;
356        fn spawn(
357            self,
358            argv: &[OsString],
359            cwd: Option<&Path>,
360            env: Option<&[(OsString, OsString)]>,
361        ) -> io::Result<Self::Child> {
362            if argv.is_empty() {
363                return Err(io::Error::other("portable-pty spawn requires non-empty argv"));
364            }
365            let mut cmd = CommandBuilder::new(&argv[0]);
366            for arg in &argv[1..] {
367                cmd.arg(arg);
368            }
369            if let Some(cwd) = cwd {
370                cmd.cwd(cwd);
371            }
372            if let Some(env) = env {
373                cmd.env_clear();
374                for (k, v) in env {
375                    cmd.env(k, v);
376                }
377            }
378            let child = self.0.spawn_command(cmd).map_err(io::Error::other)?;
379            Ok(PortablePtyChild(child))
380        }
381    }
382
383    impl PtyChild for PortablePtyChild {
384        fn pid(&self) -> u32 {
385            self.0.process_id().unwrap_or(0)
386        }
387        fn try_wait(&mut self) -> io::Result<Option<u32>> {
388            match self.0.try_wait()? {
389                Some(status) => Ok(Some(status.exit_code())),
390                None => Ok(None),
391            }
392        }
393        fn wait(&mut self) -> io::Result<u32> {
394            let status = self.0.wait()?;
395            Ok(status.exit_code())
396        }
397        fn kill(&mut self) -> io::Result<()> {
398            self.0.kill()
399        }
400        fn as_raw_handle(&self) -> Option<std::os::windows::io::RawHandle> {
401            self.0.as_raw_handle()
402        }
403    }
404}