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