prettytty/conn.rs
1use core::cell::RefCell;
2use std::io::{BufRead, BufWriter, Error, ErrorKind, Read, Result, Write};
3use std::sync::{Mutex, MutexGuard};
4
5use crate::opt::{Options, Volume};
6use crate::read::{DoggedReader, VerboseReader};
7use crate::scan::Scanner;
8use crate::sys::{RawConfig, RawConnection, RawOutput};
9use crate::{Command, Scan};
10
11// -----------------------------------------------------------------------------------------------
12
13/// A writer with associated deferred commands.
14#[derive(Debug)]
15struct DeferredWriter {
16 writer: BufWriter<RawOutput>,
17 deferred: RefCell<Vec<Box<dyn Command + Send>>>,
18}
19
20impl DeferredWriter {
21 /// Create a new list of deferred commands.
22 pub fn new(writer: RawOutput, options: &Options) -> Self {
23 Self {
24 writer: BufWriter::with_capacity(options.write_buffer_size(), writer),
25 deferred: RefCell::new(Vec::new()),
26 }
27 }
28
29 /// Defer the execution of the given command.
30 pub fn defer<C>(&self, cmd: C)
31 where
32 C: Command + Send + 'static,
33 {
34 self.deferred.borrow_mut().push(Box::new(cmd));
35 }
36
37 /// Take the list of commands and leave empty list behind.
38 pub fn take(&self) -> Vec<Box<dyn Command + Send>> {
39 self.deferred.take()
40 }
41}
42
43impl Write for DeferredWriter {
44 fn write(&mut self, buf: &[u8]) -> Result<usize> {
45 self.writer.write(buf)
46 }
47
48 fn flush(&mut self) -> Result<()> {
49 self.writer.flush()
50 }
51}
52
53// -----------------------------------------------------------------------------------------------
54
55/// A terminal connection providing [`Input`] and [`Output`].
56///
57/// This object owns the connection to the terminal. It provides independent,
58/// mutually exclusive, and thread-safe access to [`Input`] as well as
59/// [`Output`]. On Unix, the I/O types share the same underlying file
60/// descriptor, whereas on Windows each I/O type uses a distinct handle.
61///
62/// To facilitate reading from the terminal, this type reconfigures the
63/// terminal, at a minimum by disabling the terminal's line editing mode. Since
64/// its drop handler restores the original configuration, **an application
65/// should go out of its way to always execute this type's drop handler** before
66/// exit. Use [`drop`](std::mem::drop) to manually close a connection before it
67/// goes out of scope.
68///
69/// An application may need to make further changes, such as using the alternate
70/// screen or hiding the cursor, that also need to be undone before exit. In
71/// that case, the application can use [`Output::exec_defer`]. The method takes
72/// two [`Command`]s, executes the first command right away, and defers the
73/// second command to just before the terminal connection is closed.
74#[derive(Debug)]
75pub struct Connection {
76 options: Options,
77 stamp: u32,
78 config: Option<RawConfig>,
79 scanner: Mutex<Scanner<Box<dyn Read + Send>>>,
80 writer: Mutex<DeferredWriter>,
81 connection: RawConnection,
82}
83
84fn _assert_connection_is_send_sync() {
85 fn is_send_sync<T: Send + Sync>() {}
86 is_send_sync::<Connection>();
87}
88
89impl Connection {
90 /// Open a terminal connection with the default options.
91 pub fn open() -> Result<Self> {
92 Self::with_options(Options::default())
93 }
94
95 /// Open a terminal connection with the given options.
96 ///
97 /// If this method cannot establish a connection to the controlling
98 /// terminal, it fails with a [`ErrorKind::ConnectionRefused`] error.
99 #[allow(clippy::print_stdout)]
100 pub fn with_options(options: Options) -> Result<Self> {
101 let connection = RawConnection::open(&options)
102 .map_err(|e| Error::new(ErrorKind::ConnectionRefused, e))?;
103
104 let config = RawConfig::read(&connection)?;
105 let verbose = !matches!(options.volume(), Volume::Silent);
106 if verbose {
107 println!("terminal::config {:?}", &config);
108 }
109 let config = config.apply(&options).map_or_else(
110 || Ok::<Option<RawConfig>, Error>(None),
111 |reconfig| {
112 if verbose {
113 println!("terminal::reconfig {:?}", &reconfig);
114 }
115 reconfig.write(&connection)?;
116 if verbose {
117 // We need explicit carriage-return and line-feed characters
118 // because the reconfiguration just took effect.
119 print!("terminal::reconfigured\r\n")
120 }
121 Ok(Some(config))
122 },
123 )?;
124
125 let reader: Box<dyn Read + Send> = if matches!(options.volume(), Volume::Detailed) {
126 Box::new(VerboseReader::new(connection.input(), options.timeout()))
127 } else {
128 Box::new(DoggedReader::new(connection.input()))
129 };
130 let scanner = Mutex::new(Scanner::with_options(&options, reader));
131 let writer = Mutex::new(DeferredWriter::new(connection.output(), &options));
132 let stamp = if verbose {
133 // macOS duration has microsecond resolution only, so that's our
134 // least common denominator. If duration_since() fails, we use an
135 // obviously wrong value as stamp.
136 std::time::SystemTime::now()
137 .duration_since(std::time::SystemTime::UNIX_EPOCH)
138 .map(|d| d.subsec_micros())
139 .unwrap_or(0)
140 } else {
141 0
142 };
143
144 let this = Self {
145 options,
146 stamp,
147 config,
148 scanner,
149 writer,
150 connection,
151 };
152
153 this.log("terminal::connect")?;
154 Ok(this)
155 }
156
157 /// Get the options used when opening this connection.
158 #[inline]
159 pub fn options(&self) -> &Options {
160 &self.options
161 }
162
163 /// Get both terminal input and output.
164 ///
165 /// The returned input and output objects ensure mutually exclusive access
166 /// to the terminal's input and output, respectively. Dropping them releases
167 /// access again.
168 #[inline]
169 pub fn io(&self) -> (Input, Output) {
170 (self.input(), self.output())
171 }
172
173 /// Get the terminal input.
174 ///
175 /// The returned input object ensures mutually exclusive access to the
176 /// terminal's input. Dropping the input object releases access again.
177 ///
178 /// # Panics
179 ///
180 /// If the underlying mutex has been poisoned.
181 #[inline]
182 pub fn input(&self) -> Input {
183 Input {
184 scanner: self.scanner.lock().expect("can't lock poisoned mutex"),
185 }
186 }
187
188 /// Get the terminal output.
189 ///
190 /// The returned output object ensures mutually exclusive access to the
191 /// terminal's output. Dropping the output object releases access again.
192 ///
193 /// # Panics
194 ///
195 /// If the underlying mutex has been poisoned.
196 #[inline]
197 pub fn output(&self) -> Output {
198 Output {
199 writer: self.writer.lock().expect("can't lock poisoned mutex"),
200 }
201 }
202
203 fn log(&self, message: impl AsRef<str>) -> Result<()> {
204 if !matches!(self.options.volume(), Volume::Silent) {
205 // Don't wait for output.
206 let mut writer = self
207 .writer
208 .try_lock()
209 .map_err(|_| Error::from(ErrorKind::WouldBlock))?;
210
211 write!(
212 writer,
213 "{} pid={} group={} stamp={}\r\n",
214 message.as_ref(),
215 std::process::id(),
216 self.connection.group().unwrap_or(0),
217 self.stamp
218 )?;
219 writer.flush()
220 } else {
221 Ok(())
222 }
223 }
224}
225
226impl Drop for Connection {
227 fn drop(&mut self) {
228 let _ = self.log("terminal::disconnect");
229
230 // Execute deferred commands and flush output.
231 let _ = self.writer.lock().map(|mut writer| {
232 for cmd in writer.take().into_iter().rev() {
233 let _ = write!(writer, "{}", cmd);
234 }
235 let _ = writer.flush();
236 });
237
238 // Restore terminal configuration
239 if let Some(ref cfg) = self.config {
240 let _ = cfg.write(&self.connection);
241 }
242 }
243}
244
245// -----------------------------------------------------------------------------------------------
246
247/// A terminal [`Connection`]'s input.
248///
249/// In addition to [`Read`] and [`BufRead`], terminal input also implements
250/// [`Scan`] for ingesting text and ANSI escape sequences. The implementation of
251/// all three traits uses the same, shared buffer. At the same time, it does not
252/// share any state (nor implementation) with standard I/O in Rust's standard
253/// library.
254///
255/// Reads from the terminal connection time out after a duration configurable in
256/// 0.1s increments. In that case, [`Read::read`] returns a count of 0,
257/// [`BufRead::fill_buf`] an empty slice, and [`Scan::read_token`] an error with
258/// kind [`ErrorKind::TimedOut`](std::io::ErrorKind::TimedOut). On Unix, the
259/// timeout is implemented with the terminal's [`MIN` and `TIME`
260/// parameters](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11_01_07_03)
261/// On Windows, the timeout is implemented with
262/// [`WaitForSingleObject`](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject).
263///
264///
265/// # Scanning Tokens vs Reading Bytes
266///
267/// Despite requiring a fairly elaborate state machine, the implementation of
268/// [`read_token()`](crate::Scan::read_token) has been carefully engineered to
269/// return to the start state whenever possible. However, that is not possible
270/// when reading from the terminal connection results in an error or when a
271/// [`Token::Control`](crate::Token::Control) appears in the middle of a
272/// [`Token::Sequence`](crate::Token::Sequence). In these cases,
273/// [`in_flight()`](crate::Scan::in_flight) returns `true`.
274///
275/// It is possible to interleave reading bytes through [`Read`] and [`BufRead`]
276/// as well as tokens through [`Scan`], as long as byte-reads consume data at
277/// token granularity as well. For that reason,
278/// [`fill_buf()`](BufRead::fill_buf) and [`consume()`](BufRead::consume) are
279/// much preferred over [`read()`](Read::read) because the former two methods
280/// provide exact control over consumed bytes, whereas the latter method does
281/// not. For the same reason, byte-reads fail with
282/// [`ErrorKind::InFlight`](crate::err::ErrorKind::InFlight), if the state
283/// machine currently is in-flight.
284///
285///
286/// # Error Recovery
287///
288/// Unless the terminal connection keeps erroring, a viable error recovery
289/// strategy is to keep reading tokens. The state machine usually returns to the
290/// start state after the first error. Doing so requires reading at most 3 bytes
291/// for UTF-8 characters and, in theory, an unlimited number of bytes for
292/// pathological ANSI escape sequences.
293///
294///
295/// # Pathological Input
296///
297/// To protect against such pathological inputs, the implementation gracefully
298/// handles out-of-memory conditions, i.e., when a sequence is longer than the
299/// internal buffer size. It does *not* dynamically grow the buffer size, but
300/// instead keeps processing bytes until the sequence is complete and then
301/// returns [`ErrorKind::OutOfMemory`](crate::err::ErrorKind::OutOfMemory).
302/// However, if a sequence is much longer than the buffer size, continuing to
303/// scan it makes little sense. Hence, upon reaching a configurable limit, the
304/// state machine forcibly resets and discards any unread bytes before returning
305/// [`ErrorKind::PathologicalSequence`](crate::err::ErrorKind::PathologicalSequence).
306/// In that case, it probably is advisable to terminate the terminal connection,
307/// since a denial-of-service attack appears to be under way.
308#[derive(Debug)]
309pub struct Input<'a> {
310 pub scanner: MutexGuard<'a, Scanner<Box<dyn Read + Send>>>,
311}
312
313impl Input<'_> {
314 /// Determine whether the input has bytes buffered.
315 #[must_use = "the only reason to invoke method is to access the returned value"]
316 pub fn is_readable(&self) -> bool {
317 self.scanner.is_readable()
318 }
319}
320
321impl Scan for Input<'_> {
322 #[inline]
323 fn in_flight(&self) -> bool {
324 self.scanner.in_flight()
325 }
326
327 #[inline]
328 fn read_token(&mut self) -> Result<crate::Token> {
329 self.scanner.read_token().map_err(core::convert::Into::into)
330 }
331}
332
333impl Read for Input<'_> {
334 #[inline]
335 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
336 let mut source = self.scanner.fill_buf()?;
337 let count = source.read(buf)?;
338 self.scanner.consume(count)?;
339 Ok(count)
340 }
341}
342
343impl BufRead for Input<'_> {
344 #[inline]
345 fn fill_buf(&mut self) -> Result<&[u8]> {
346 self.scanner.fill_buf().map_err(core::convert::Into::into)
347 }
348
349 #[inline]
350 fn consume(&mut self, amt: usize) {
351 // Don't panic...
352 let _ = self.scanner.consume(amt);
353 }
354}
355
356// -----------------------------------------------------------------------------------------------
357
358/// A terminal [`Connection`]'s output.
359///
360/// Since terminal output is buffered, actually executing commands requires
361/// flushing the output. As a convenience, [`Output::print`] and
362/// [`Output::println`] write strings and [`Output::exec`] writes individual
363/// commands, while also flushing the output on every invocation.
364#[derive(Debug)]
365pub struct Output<'a> {
366 writer: MutexGuard<'a, DeferredWriter>,
367}
368
369impl Output<'_> {
370 /// Write and flush the text.
371 #[inline]
372 #[must_use = "method returns result that may indicate an error"]
373 pub fn print<T: AsRef<str>>(&mut self, text: T) -> Result<()> {
374 self.writer.write_all(text.as_ref().as_bytes())?;
375 self.writer.flush()
376 }
377
378 /// Write and flush the text followed by carriage return and line feed.
379 #[inline]
380 #[must_use = "method returns result that may indicate an error"]
381 pub fn println<T: AsRef<str>>(&mut self, text: T) -> Result<()> {
382 self.writer.write_all(text.as_ref().as_bytes())?;
383 self.writer.write_all(b"\r\n")?;
384 self.writer.flush()
385 }
386
387 /// Execute the command.
388 ///
389 /// This method writes the display for the given command and then flushes
390 /// the terminal output.
391 #[inline]
392 #[must_use = "method returns result that may indicate an error"]
393 pub fn exec<C: Command>(&mut self, cmd: C) -> Result<()> {
394 write!(self.writer, "{}", cmd)?;
395 self.writer.flush()
396 }
397
398 /// Execute one command and defer the other.
399 ///
400 /// This method tries to write the first command to the terminal's output.
401 /// If that succeeds, it enqueues the second command for execution when the
402 /// connection is being closed and then flushes the output.
403 ///
404 /// The second command must be `'static`, so that it is alive for the
405 /// lifetime of the connection. It must be `Send`, so that connection
406 /// objects can be moved across threads. Since most commands are zero-sized
407 /// types, they trivially fulfill both requirements.
408 #[must_use = "method returns result that may indicate an error"]
409 pub fn exec_defer<C1, C2>(&mut self, cmd1: C1, cmd2: C2) -> Result<()>
410 where
411 C1: Command,
412 C2: Command + Send + 'static,
413 {
414 write!(self.writer, "{}", cmd1)?;
415 self.writer.defer(cmd2);
416 self.writer.flush()
417 }
418}
419
420impl Write for Output<'_> {
421 #[inline]
422 fn write(&mut self, buf: &[u8]) -> Result<usize> {
423 self.writer.write(buf)
424 }
425
426 #[inline]
427 fn flush(&mut self) -> Result<()> {
428 self.writer.flush()
429 }
430}