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_or(0, |d| d.subsec_micros())
139 } else {
140 0
141 };
142
143 let this = Self {
144 options,
145 stamp,
146 config,
147 scanner,
148 writer,
149 connection,
150 };
151
152 this.log("terminal::connect")?;
153 Ok(this)
154 }
155
156 /// Get the options used when opening this connection.
157 #[inline]
158 pub fn options(&self) -> &Options {
159 &self.options
160 }
161
162 /// Get both terminal input and output.
163 ///
164 /// The returned input and output objects ensure mutually exclusive access
165 /// to the terminal's input and output, respectively. Dropping them releases
166 /// access again.
167 #[inline]
168 pub fn io(&self) -> (Input<'_>, Output<'_>) {
169 (self.input(), self.output())
170 }
171
172 /// Get the terminal input.
173 ///
174 /// The returned input object ensures mutually exclusive access to the
175 /// terminal's input. Dropping the input object releases access again.
176 ///
177 /// # Panics
178 ///
179 /// If the underlying mutex has been poisoned.
180 #[inline]
181 pub fn input(&self) -> Input<'_> {
182 Input {
183 scanner: self.scanner.lock().expect("can't lock poisoned mutex"),
184 }
185 }
186
187 /// Get the terminal output.
188 ///
189 /// The returned output object ensures mutually exclusive access to the
190 /// terminal's output. Dropping the output object releases access again.
191 ///
192 /// # Panics
193 ///
194 /// If the underlying mutex has been poisoned.
195 #[inline]
196 pub fn output(&self) -> Output<'_> {
197 Output {
198 writer: self.writer.lock().expect("can't lock poisoned mutex"),
199 }
200 }
201
202 fn log(&self, message: impl AsRef<str>) -> Result<()> {
203 if !matches!(self.options.volume(), Volume::Silent) {
204 // Don't wait for output.
205 let mut writer = self
206 .writer
207 .try_lock()
208 .map_err(|_| Error::from(ErrorKind::WouldBlock))?;
209
210 write!(
211 writer,
212 "{} pid={} group={} stamp={}\r\n",
213 message.as_ref(),
214 std::process::id(),
215 self.connection.group().unwrap_or(0),
216 self.stamp
217 )?;
218 writer.flush()
219 } else {
220 Ok(())
221 }
222 }
223}
224
225impl Drop for Connection {
226 fn drop(&mut self) {
227 let _ = self.log("terminal::disconnect");
228
229 // Execute deferred commands and flush output.
230 let _ = self.writer.lock().map(|mut writer| {
231 for cmd in writer.take().into_iter().rev() {
232 let _ = write!(writer, "{}", cmd);
233 }
234 let _ = writer.flush();
235 });
236
237 // Restore terminal configuration
238 if let Some(ref cfg) = self.config {
239 let _ = cfg.write(&self.connection);
240 }
241 }
242}
243
244// -----------------------------------------------------------------------------------------------
245
246/// A terminal [`Connection`]'s input.
247///
248/// In addition to [`Read`] and [`BufRead`], terminal input also implements
249/// [`Scan`] for ingesting text and ANSI escape sequences. The implementation of
250/// all three traits uses the same, shared buffer. At the same time, it does not
251/// share any state (nor implementation) with standard I/O in Rust's standard
252/// library.
253///
254/// Reads from the terminal connection time out after a duration configurable in
255/// 0.1s increments. In that case, [`Read::read`] returns a count of 0,
256/// [`BufRead::fill_buf`] an empty slice, and [`Scan::read_token`] an error with
257/// kind [`ErrorKind::TimedOut`](std::io::ErrorKind::TimedOut). On Unix, the
258/// timeout is implemented with the terminal's [`MIN` and `TIME`
259/// parameters](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap11.html#tag_11_01_07_03)
260/// On Windows, the timeout is implemented with
261/// [`WaitForSingleObject`](https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject).
262///
263///
264/// # Scanning Tokens vs Reading Bytes
265///
266/// Despite requiring a fairly elaborate state machine, the implementation of
267/// [`read_token()`](crate::Scan::read_token) has been carefully engineered to
268/// return to the start state whenever possible. However, that is not possible
269/// when reading from the terminal connection results in an error or when a
270/// [`Token::Control`](crate::Token::Control) appears in the middle of a
271/// [`Token::Sequence`](crate::Token::Sequence). In these cases,
272/// [`in_flight()`](crate::Scan::in_flight) returns `true`.
273///
274/// It is possible to interleave reading bytes through [`Read`] and [`BufRead`]
275/// as well as tokens through [`Scan`], as long as byte-reads consume data at
276/// token granularity as well. For that reason,
277/// [`fill_buf()`](BufRead::fill_buf) and [`consume()`](BufRead::consume) are
278/// much preferred over [`read()`](Read::read) because the former two methods
279/// provide exact control over consumed bytes, whereas the latter method does
280/// not. For the same reason, byte-reads fail with
281/// [`ErrorKind::InFlight`](crate::err::ErrorKind::InFlight), if the state
282/// machine currently is in-flight.
283///
284///
285/// # Error Recovery
286///
287/// Unless the terminal connection keeps erroring, a viable error recovery
288/// strategy is to keep reading tokens. The state machine usually returns to the
289/// start state after the first error. Doing so requires reading at most 3 bytes
290/// for UTF-8 characters and, in theory, an unlimited number of bytes for
291/// pathological ANSI escape sequences.
292///
293///
294/// # Pathological Input
295///
296/// To protect against such pathological inputs, the implementation gracefully
297/// handles out-of-memory conditions, i.e., when a sequence is longer than the
298/// internal buffer size. It does *not* dynamically grow the buffer size, but
299/// instead keeps processing bytes until the sequence is complete and then
300/// returns [`ErrorKind::OutOfMemory`](crate::err::ErrorKind::OutOfMemory).
301/// However, if a sequence is much longer than the buffer size, continuing to
302/// scan it makes little sense. Hence, upon reaching a configurable limit, the
303/// state machine forcibly resets and discards any unread bytes before returning
304/// [`ErrorKind::PathologicalSequence`](crate::err::ErrorKind::PathologicalSequence).
305/// In that case, it probably is advisable to terminate the terminal connection,
306/// since a denial-of-service attack appears to be under way.
307#[derive(Debug)]
308pub struct Input<'a> {
309 pub scanner: MutexGuard<'a, Scanner<Box<dyn Read + Send>>>,
310}
311
312impl Input<'_> {
313 /// Determine whether the input has bytes buffered.
314 #[must_use = "the only reason to invoke method is to access the returned value"]
315 pub fn is_readable(&self) -> bool {
316 self.scanner.is_readable()
317 }
318}
319
320impl Scan for Input<'_> {
321 #[inline]
322 fn in_flight(&self) -> bool {
323 self.scanner.in_flight()
324 }
325
326 #[inline]
327 fn read_token(&mut self) -> Result<crate::Token<'_>> {
328 self.scanner.read_token().map_err(core::convert::Into::into)
329 }
330}
331
332impl Read for Input<'_> {
333 #[inline]
334 fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
335 let mut source = self.scanner.fill_buf()?;
336 let count = source.read(buf)?;
337 self.scanner.consume(count)?;
338 Ok(count)
339 }
340}
341
342impl BufRead for Input<'_> {
343 #[inline]
344 fn fill_buf(&mut self) -> Result<&[u8]> {
345 self.scanner.fill_buf().map_err(core::convert::Into::into)
346 }
347
348 #[inline]
349 fn consume(&mut self, amt: usize) {
350 // Don't panic...
351 let _ = self.scanner.consume(amt);
352 }
353}
354
355// -----------------------------------------------------------------------------------------------
356
357/// A terminal [`Connection`]'s output.
358///
359/// Since terminal output is buffered, actually executing commands requires
360/// flushing the output. As a convenience, [`Output::print`] and
361/// [`Output::println`] write strings and [`Output::exec`] writes individual
362/// commands, while also flushing the output on every invocation.
363#[derive(Debug)]
364pub struct Output<'a> {
365 writer: MutexGuard<'a, DeferredWriter>,
366}
367
368impl Output<'_> {
369 /// Write and flush the text.
370 #[inline]
371 #[must_use = "method returns result that may indicate an error"]
372 pub fn print<T: AsRef<str>>(&mut self, text: T) -> Result<()> {
373 self.writer.write_all(text.as_ref().as_bytes())?;
374 self.writer.flush()
375 }
376
377 /// Write and flush the text followed by carriage return and line feed.
378 #[inline]
379 #[must_use = "method returns result that may indicate an error"]
380 pub fn println<T: AsRef<str>>(&mut self, text: T) -> Result<()> {
381 self.writer.write_all(text.as_ref().as_bytes())?;
382 self.writer.write_all(b"\r\n")?;
383 self.writer.flush()
384 }
385
386 /// Execute the command.
387 ///
388 /// This method writes the display for the given command and then flushes
389 /// the terminal output.
390 #[inline]
391 #[must_use = "method returns result that may indicate an error"]
392 pub fn exec<C: Command>(&mut self, cmd: C) -> Result<()> {
393 write!(self.writer, "{}", cmd)?;
394 self.writer.flush()
395 }
396
397 /// Execute one command and defer the other.
398 ///
399 /// This method tries to write the first command to the terminal's output.
400 /// If that succeeds, it enqueues the second command for execution when the
401 /// connection is being closed and then flushes the output.
402 ///
403 /// The second command must be `'static`, so that it is alive for the
404 /// lifetime of the connection. It must be `Send`, so that connection
405 /// objects can be moved across threads. Since most commands are zero-sized
406 /// types, they trivially fulfill both requirements.
407 #[must_use = "method returns result that may indicate an error"]
408 pub fn exec_defer<C1, C2>(&mut self, cmd1: C1, cmd2: C2) -> Result<()>
409 where
410 C1: Command,
411 C2: Command + Send + 'static,
412 {
413 write!(self.writer, "{}", cmd1)?;
414 self.writer.defer(cmd2);
415 self.writer.flush()
416 }
417}
418
419impl Write for Output<'_> {
420 #[inline]
421 fn write(&mut self, buf: &[u8]) -> Result<usize> {
422 self.writer.write(buf)
423 }
424
425 #[inline]
426 fn flush(&mut self) -> Result<()> {
427 self.writer.flush()
428 }
429}