term_transcript/
traits.rs

1//! Traits for interaction with the terminal.
2
3use std::{
4    ffi::OsStr,
5    io,
6    path::Path,
7    process::{Child, ChildStdin, Command, Stdio},
8};
9
10use crate::utils::is_recoverable_kill_error;
11
12/// Common denominator for types that can be used to configure commands for
13/// execution in the terminal.
14pub trait ConfigureCommand {
15    /// Sets the current directory.
16    fn current_dir(&mut self, dir: &Path);
17    /// Sets an environment variable.
18    fn env(&mut self, name: &str, value: &OsStr);
19}
20
21impl ConfigureCommand for Command {
22    fn current_dir(&mut self, dir: &Path) {
23        self.current_dir(dir);
24    }
25
26    fn env(&mut self, name: &str, value: &OsStr) {
27        self.env(name, value);
28    }
29}
30
31/// Encapsulates spawning and sending inputs / receiving outputs from the shell.
32///
33/// The crate provides two principal implementations of this trait:
34///
35/// - [`Command`] and [`StdShell`](crate::StdShell) communicate with the spawned process
36///   via OS pipes. Because stdin of the child process is not connected to a terminal / TTY,
37///   this can lead to the differences in output compared to launching the process in a terminal
38///   (no coloring, different formatting, etc.). On the other hand, this is the most widely
39///   supported option.
40/// - [`PtyCommand`](crate::PtyCommand) (available with the `portable-pty` crate feature)
41///   communicates with the child process via a pseudo-terminal (PTY). This makes the output
42///   closer to what it would like in the terminal, at the cost of lesser platform coverage
43///   (Unix + newer Windows distributions).
44///
45/// External implementations are possible as well! E.g., for REPL applications written in Rust
46/// or packaged as a [WASI] module, it could be possible to write an implementation that "spawns"
47/// the application in the same process.
48///
49/// [WASI]: https://wasi.dev/
50pub trait SpawnShell: ConfigureCommand {
51    /// Spawned shell process.
52    type ShellProcess: ShellProcess;
53    /// Reader of the shell output.
54    type Reader: io::Read + 'static + Send;
55    /// Writer to the shell input.
56    type Writer: io::Write + 'static + Send;
57
58    /// Spawns a shell process.
59    ///
60    /// # Errors
61    ///
62    /// Returns an error if the shell process cannot be spawned for whatever reason.
63    fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>>;
64}
65
66/// Representation of a shell process.
67pub trait ShellProcess {
68    /// Checks if the process is alive.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the process is not alive. Should include debug details if possible
73    /// (e.g., the exit status of the process).
74    fn check_is_alive(&mut self) -> io::Result<()>;
75
76    /// Terminates the shell process. This can include killing it if necessary.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the process cannot be killed.
81    fn terminate(self) -> io::Result<()>;
82
83    /// Returns `true` if the input commands are echoed back to the output.
84    ///
85    /// The default implementation returns `false`.
86    fn is_echoing(&self) -> bool {
87        false
88    }
89}
90
91/// Wrapper for spawned shell and related I/O returned by [`SpawnShell::spawn_shell()`].
92#[derive(Debug)]
93pub struct SpawnedShell<S: SpawnShell + ?Sized> {
94    /// Shell process.
95    pub shell: S::ShellProcess,
96    /// Reader of shell output.
97    pub reader: S::Reader,
98    /// Writer to shell input.
99    pub writer: S::Writer,
100}
101
102/// Uses pipes to communicate with the spawned process. This has a potential downside that
103/// the process output will differ from the case if the process was launched in the shell.
104/// See [`PtyCommand`] for an alternative that connects the spawned process to a pseudo-terminal
105/// (PTY).
106///
107/// [`PtyCommand`]: crate::PtyCommand
108impl SpawnShell for Command {
109    type ShellProcess = Child;
110    type Reader = os_pipe::PipeReader;
111    type Writer = ChildStdin;
112
113    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
114    fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>> {
115        let (pipe_reader, pipe_writer) = os_pipe::pipe()?;
116        #[cfg(feature = "tracing")]
117        tracing::debug!("created OS pipe");
118
119        let mut shell = self
120            .stdin(Stdio::piped())
121            .stdout(pipe_writer.try_clone()?)
122            .stderr(pipe_writer)
123            .spawn()?;
124        #[cfg(feature = "tracing")]
125        tracing::debug!("created child");
126
127        self.stdout(Stdio::null()).stderr(Stdio::null());
128
129        let stdin = shell.stdin.take().unwrap();
130        // ^-- `unwrap()` is safe due to configuration of the shell process.
131
132        Ok(SpawnedShell {
133            shell,
134            reader: pipe_reader,
135            writer: stdin,
136        })
137    }
138}
139
140impl ShellProcess for Child {
141    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
142    fn check_is_alive(&mut self) -> io::Result<()> {
143        if let Some(exit_status) = self.try_wait()? {
144            let message = format!("Shell process has prematurely exited: {exit_status}");
145            Err(io::Error::new(io::ErrorKind::BrokenPipe, message))
146        } else {
147            Ok(())
148        }
149    }
150
151    #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", err))]
152    fn terminate(mut self) -> io::Result<()> {
153        if self.try_wait()?.is_none() {
154            self.kill().or_else(|err| {
155                if is_recoverable_kill_error(&err) {
156                    // The shell has already exited. We don't consider this an error.
157                    Ok(())
158                } else {
159                    Err(err)
160                }
161            })?;
162        }
163        Ok(())
164    }
165}
166
167/// Wrapper that allows configuring echoing of the shell process.
168///
169/// A shell process is echoing if each line provided to the input is echoed to the output.
170#[derive(Debug, Clone)]
171pub struct Echoing<S> {
172    inner: S,
173    is_echoing: bool,
174}
175
176impl<S> Echoing<S> {
177    /// Wraps the provided `inner` type.
178    pub fn new(inner: S, is_echoing: bool) -> Self {
179        Self { inner, is_echoing }
180    }
181}
182
183impl<S: ConfigureCommand> ConfigureCommand for Echoing<S> {
184    fn current_dir(&mut self, dir: &Path) {
185        self.inner.current_dir(dir);
186    }
187
188    fn env(&mut self, name: &str, value: &OsStr) {
189        self.inner.env(name, value);
190    }
191}
192
193impl<S: SpawnShell> SpawnShell for Echoing<S> {
194    type ShellProcess = Echoing<S::ShellProcess>;
195    type Reader = S::Reader;
196    type Writer = S::Writer;
197
198    fn spawn_shell(&mut self) -> io::Result<SpawnedShell<Self>> {
199        let spawned = self.inner.spawn_shell()?;
200        Ok(SpawnedShell {
201            shell: Echoing {
202                inner: spawned.shell,
203                is_echoing: self.is_echoing,
204            },
205            reader: spawned.reader,
206            writer: spawned.writer,
207        })
208    }
209}
210
211impl<S: ShellProcess> ShellProcess for Echoing<S> {
212    fn check_is_alive(&mut self) -> io::Result<()> {
213        self.inner.check_is_alive()
214    }
215
216    fn terminate(self) -> io::Result<()> {
217        self.inner.terminate()
218    }
219
220    fn is_echoing(&self) -> bool {
221        self.is_echoing
222    }
223}