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