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}