tty_spawn/
lib.rs

1//! `tty-spawn` is the underlying library on which
2//! [`teetty`](https://github.com/mitsuhiko/teetty) is built.  It lets you spawn
3//! processes in a fake TTY and duplex stdin/stdout so you can communicate with an
4//! otherwise user attended process.
5use std::ffi::{CString, OsStr, OsString};
6use std::fs::File;
7use std::io::Write;
8use std::os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd};
9use std::os::unix::prelude::{AsRawFd, OpenOptionsExt, OsStrExt};
10use std::path::Path;
11use std::sync::atomic::{AtomicBool, Ordering};
12use std::sync::Arc;
13use std::{env, io};
14
15use nix::errno::Errno;
16use nix::libc::{login_tty, O_NONBLOCK, TIOCGWINSZ, TIOCSWINSZ, VEOF};
17use nix::pty::{openpty, Winsize};
18use nix::sys::select::{select, FdSet};
19use nix::sys::signal::{killpg, Signal};
20use nix::sys::stat::Mode;
21use nix::sys::termios::{
22    cfmakeraw, tcgetattr, tcsetattr, LocalFlags, OutputFlags, SetArg, Termios,
23};
24use nix::sys::time::TimeVal;
25use nix::sys::wait::{waitpid, WaitStatus};
26use nix::unistd::{dup2, execvp, fork, isatty, mkfifo, read, tcgetpgrp, write, ForkResult, Pid};
27use signal_hook::consts::SIGWINCH;
28
29/// Lets you spawn processes with a TTY connected.
30pub struct TtySpawn {
31    options: Option<SpawnOptions>,
32}
33
34impl TtySpawn {
35    /// Creates a new [`TtySpawn`] for a given command.
36    pub fn new<S: AsRef<OsStr>>(cmd: S) -> TtySpawn {
37        TtySpawn {
38            options: Some(SpawnOptions {
39                command: vec![cmd.as_ref().to_os_string()],
40                stdin_file: None,
41                stdout_file: None,
42                script_mode: false,
43                no_flush: false,
44                no_echo: false,
45                no_pager: false,
46                no_raw: false,
47            }),
48        }
49    }
50
51    /// Alternative way to construct a [`TtySpawn`].
52    ///
53    /// Takes an iterator of command and arguments.  If the iterator is empty this
54    /// panicks.
55    ///
56    /// # Panicks
57    ///
58    /// If the iterator is empty, this panics.
59    pub fn new_cmdline<S: AsRef<OsStr>, I: Iterator<Item = S>>(mut cmdline: I) -> Self {
60        let mut rv = TtySpawn::new(cmdline.next().expect("empty cmdline"));
61        rv.args(cmdline);
62        rv
63    }
64
65    /// Adds a new argument to the command.
66    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
67        self.options_mut().command.push(arg.as_ref().to_os_string());
68        self
69    }
70
71    /// Adds multiple arguments from an iterator.
72    pub fn args<S: AsRef<OsStr>, I: Iterator<Item = S>>(&mut self, args: I) -> &mut Self {
73        for arg in args {
74            self.arg(arg);
75        }
76        self
77    }
78
79    /// Sets an input file for stdin.
80    ///
81    /// It's recommended that this is a named pipe and as a general recommendation
82    /// this file should be opened with `O_NONBLOCK`.
83    ///
84    /// # Platform Specifics
85    ///
86    /// While we will never write into the file it's strongly recommended to
87    /// ensure that the file is opened writable too.  The reason for this is that
88    /// on Linux, if the last writer (temporarily) disconnects from a FIFO polling
89    /// primitives such as the one used by `tty-spawn` will keep reporting that the
90    /// file is ready while there not actually being any more data coming in.  The
91    /// solution to this problem is to ensure that there is at least always one
92    /// writer open which can be ensured by also opening this file for writing.
93    pub fn stdin_file(&mut self, f: File) -> &mut Self {
94        self.options_mut().stdin_file = Some(f);
95        self
96    }
97
98    /// Sets a path as input file for stdin.
99    pub fn stdin_path<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self, io::Error> {
100        let path = path.as_ref();
101        mkfifo_atomic(path)?;
102        // for the justification for write(true) see the explanation on
103        // [`stdin_file`](Self::stdin_file).
104        Ok(self.stdin_file(
105            File::options()
106                .read(true)
107                .write(true)
108                .custom_flags(O_NONBLOCK)
109                .open(path)?,
110        ))
111    }
112
113    /// Sets an output file for stdout.
114    pub fn stdout_file(&mut self, f: File) -> &mut Self {
115        self.options_mut().stdout_file = Some(f);
116        self
117    }
118
119    /// Sets a path as output file for stdout.
120    ///
121    /// If the `truncate` flag is set to `true` the file will be truncated
122    /// first, otherwise it will be appended to.
123    pub fn stdout_path<P: AsRef<Path>>(
124        &mut self,
125        path: P,
126        truncate: bool,
127    ) -> Result<&mut Self, io::Error> {
128        Ok(self.stdout_file(if !truncate {
129            File::options().append(true).create(true).open(path)?
130        } else {
131            File::options()
132                .create(true)
133                .truncate(true)
134                .write(true)
135                .open(path)?
136        }))
137    }
138
139    /// Enables script mode.
140    ///
141    /// In script mode stdout/stderr are retained as separate streams, the terminal is
142    /// not opened in raw mode.  Additionally some output processing is disabled so
143    /// usually you will find LF retained and not converted to CLRF.  This will also
144    /// attempt to disable pagers and turn off ECHO intelligently in some cases.
145    pub fn script_mode(&mut self, yes: bool) -> &mut Self {
146        self.options_mut().script_mode = yes;
147        self
148    }
149
150    /// Can be used to turn flushing off.
151    ///
152    /// By default output is flushed constantly.
153    pub fn flush(&mut self, yes: bool) -> &mut Self {
154        self.options_mut().no_flush = !yes;
155        self
156    }
157
158    /// Can be used to turn echo off.
159    ///
160    /// By default echo is turned on.
161    pub fn echo(&mut self, yes: bool) -> &mut Self {
162        self.options_mut().no_echo = !yes;
163        self
164    }
165
166    /// Tries to use `cat` as pager.
167    ///
168    /// When this is enabled then processes are instructed to use `cat` as pager.
169    /// This is useful when raw terminals are disabled in which case most pagers
170    /// will break.
171    pub fn pager(&mut self, yes: bool) -> &mut Self {
172        self.options_mut().no_pager = !yes;
173        self
174    }
175
176    /// Can be used to turn raw terminal mode off.
177    ///
178    /// By default the terminal is in raw mode but in some cases you might want to
179    /// turn this off.  If raw mode is disabled then pagers will not work and so
180    /// will most input operations.
181    pub fn raw(&mut self, yes: bool) -> &mut Self {
182        self.options_mut().no_raw = !yes;
183        self
184    }
185
186    /// Spawns the application in the TTY.
187    pub fn spawn(&mut self) -> Result<i32, io::Error> {
188        Ok(spawn(
189            self.options.take().expect("builder only works once"),
190        )?)
191    }
192
193    fn options_mut(&mut self) -> &mut SpawnOptions {
194        self.options.as_mut().expect("builder only works once")
195    }
196}
197
198struct SpawnOptions {
199    command: Vec<OsString>,
200    stdin_file: Option<File>,
201    stdout_file: Option<File>,
202    script_mode: bool,
203    no_flush: bool,
204    no_echo: bool,
205    no_pager: bool,
206    no_raw: bool,
207}
208
209/// Spawns a process in a PTY in a manor similar to `script`
210/// but with separate stdout/stderr.
211///
212/// It leaves stdin/stdout/stderr connected but also writes events into the
213/// optional `out` log file.  Additionally it can retrieve instructions from
214/// the given control socket.
215fn spawn(mut opts: SpawnOptions) -> Result<i32, Errno> {
216    // if we can't retrieve the terminal atts we're not directly connected
217    // to a pty in which case we won't do any of the terminal related
218    // operations.
219    let term_attrs = tcgetattr(io::stdin()).ok();
220    let winsize = term_attrs
221        .as_ref()
222        .and_then(|_| get_winsize(io::stdin().as_fd()));
223
224    // Create the outer pty for stdout
225    let pty = openpty(&winsize, &term_attrs)?;
226
227    // In script mode we set up a secondary pty.  One could also use `pipe()`
228    // here but in that case the `isatty()` call on stderr would report that
229    // it's not connected to a tty which is what we want to prevent.
230    let (_restore_term, stderr_pty) = if opts.script_mode {
231        let term_attrs = tcgetattr(io::stderr()).ok();
232        let winsize = term_attrs
233            .as_ref()
234            .and_then(|_| get_winsize(io::stderr().as_fd()));
235        let stderr_pty = openpty(&winsize, &term_attrs)?;
236        (None, Some(stderr_pty))
237
238    // If we are not disabling raw, we change to raw mode.  This switches the
239    // terminal to raw mode and restores it on Drop.  Unfortunately due to all
240    // our shenanigans here we have no real guarantee that `Drop` is called so
241    // there will be cases where the term is left in raw state and requires a
242    // reset :(
243    } else if !opts.no_raw {
244        (
245            term_attrs.as_ref().map(|term_attrs| {
246                let mut raw_attrs = term_attrs.clone();
247                cfmakeraw(&mut raw_attrs);
248                raw_attrs.local_flags.remove(LocalFlags::ECHO);
249                tcsetattr(io::stdin(), SetArg::TCSAFLUSH, &raw_attrs).ok();
250                RestoreTerm(term_attrs.clone())
251            }),
252            None,
253        )
254
255    // at this point we're neither in scrop mode, nor is raw enabled. do nothing
256    } else {
257        (None, None)
258    };
259
260    // set some flags after pty has been created.  There are cases where we
261    // want to remove the ECHO flag so we don't see ^D and similar things in
262    // the output.  Likewise in script mode we want to remove OPOST which will
263    // otherwise convert LF to CRLF.
264    if let Ok(mut term_attrs) = tcgetattr(&pty.master) {
265        if opts.script_mode {
266            term_attrs.output_flags.remove(OutputFlags::OPOST);
267        }
268        if opts.no_echo || (opts.script_mode && !isatty(io::stdin().as_raw_fd()).unwrap_or(false)) {
269            term_attrs.local_flags.remove(LocalFlags::ECHO);
270        }
271        tcsetattr(&pty.master, SetArg::TCSAFLUSH, &term_attrs).ok();
272    }
273
274    // Fork and establish the communication loop in the parent.  This unfortunately
275    // has to merge stdout/stderr since the pseudo terminal only has one stream for
276    // both.
277    if let ForkResult::Parent { child } = unsafe { fork()? } {
278        drop(pty.slave);
279        let stderr_pty = if let Some(stderr_pty) = stderr_pty {
280            drop(stderr_pty.slave);
281            Some(stderr_pty.master)
282        } else {
283            None
284        };
285        return communication_loop(
286            pty.master,
287            child,
288            term_attrs.is_some(),
289            opts.stdout_file.as_mut(),
290            opts.stdin_file.as_mut(),
291            stderr_pty,
292            !opts.no_flush,
293        );
294    }
295
296    // set the pagers to `cat` if it's disabled.
297    if opts.no_pager || opts.script_mode {
298        unsafe {
299            env::set_var("PAGER", "cat");
300        }
301    }
302
303    // If we reach this point we're the child and we want to turn into the
304    // target executable after having set up the tty with `login_tty` which
305    // rebinds stdin/stdout/stderr to the pty.
306    let args = opts
307        .command
308        .iter()
309        .filter_map(|x| CString::new(x.as_bytes()).ok())
310        .collect::<Vec<_>>();
311
312    drop(pty.master);
313    unsafe {
314        login_tty(pty.slave.into_raw_fd());
315        if let Some(stderr_pty) = stderr_pty {
316            dup2(stderr_pty.slave.into_raw_fd(), io::stderr().as_raw_fd())?;
317        }
318    }
319
320    // Since this returns Infallible rather than ! due to limitations, we need
321    // this dummy match.
322    match execvp(&args[0], &args)? {}
323}
324
325fn communication_loop(
326    master: OwnedFd,
327    child: Pid,
328    is_tty: bool,
329    mut out_file: Option<&mut File>,
330    in_file: Option<&mut File>,
331    stderr: Option<OwnedFd>,
332    flush: bool,
333) -> Result<i32, Errno> {
334    let mut buf = [0; 4096];
335    let mut read_stdin = true;
336    let mut done = false;
337    let stdin = io::stdin();
338
339    let got_winch = Arc::new(AtomicBool::new(false));
340    if is_tty {
341        signal_hook::flag::register(SIGWINCH, Arc::clone(&got_winch)).ok();
342    }
343
344    while !done {
345        if got_winch.load(Ordering::Relaxed) {
346            forward_winsize(master.as_fd(), stderr.as_ref().map(|x| x.as_fd()))?;
347            got_winch.store(false, Ordering::Relaxed);
348        }
349
350        let mut read_fds = FdSet::new();
351        let mut timeout = TimeVal::new(1, 0);
352        read_fds.insert(master.as_fd());
353        if !read_stdin && is_tty {
354            read_stdin = true;
355        }
356        if read_stdin {
357            read_fds.insert(stdin.as_fd());
358        }
359        if let Some(ref f) = in_file {
360            read_fds.insert(f.as_fd());
361        }
362        if let Some(ref fd) = stderr {
363            read_fds.insert(fd.as_fd());
364        }
365        match select(None, Some(&mut read_fds), None, None, Some(&mut timeout)) {
366            Ok(0) | Err(Errno::EINTR | Errno::EAGAIN) => continue,
367            Ok(_) => {}
368            Err(err) => return Err(err),
369        }
370
371        if read_fds.contains(stdin.as_fd()) {
372            match read(stdin.as_raw_fd(), &mut buf) {
373                Ok(0) => {
374                    send_eof_sequence(master.as_fd());
375                    read_stdin = false;
376                }
377                Ok(n) => {
378                    write_all(master.as_fd(), &buf[..n])?;
379                }
380                Err(Errno::EINTR | Errno::EAGAIN) => {}
381                // on linux a closed tty raises EIO
382                Err(Errno::EIO) => {
383                    done = true;
384                }
385                Err(err) => return Err(err),
386            };
387        }
388        if let Some(ref f) = in_file {
389            if read_fds.contains(f.as_fd()) {
390                // use read() here so that we can handle EAGAIN/EINTR
391                // without this we might receive resource temporary unavailable
392                // see https://github.com/mitsuhiko/teetty/issues/3
393                match read(f.as_raw_fd(), &mut buf) {
394                    Ok(0) | Err(Errno::EAGAIN | Errno::EINTR) => {}
395                    Err(err) => return Err(err),
396                    Ok(n) => {
397                        write_all(master.as_fd(), &buf[..n])?;
398                    }
399                }
400            }
401        }
402        if let Some(ref fd) = stderr {
403            if read_fds.contains(fd.as_fd()) {
404                match read(fd.as_raw_fd(), &mut buf) {
405                    Ok(0) | Err(_) => {}
406                    Ok(n) => {
407                        forward_and_log(io::stderr().as_fd(), &mut out_file, &buf[..n], flush)?;
408                    }
409                }
410            }
411        }
412        if read_fds.contains(master.as_fd()) {
413            match read(master.as_raw_fd(), &mut buf) {
414                // on linux a closed tty raises EIO
415                Ok(0) | Err(Errno::EIO) => {
416                    done = true;
417                }
418                Ok(n) => forward_and_log(io::stdout().as_fd(), &mut out_file, &buf[..n], flush)?,
419                Err(Errno::EAGAIN | Errno::EINTR) => {}
420                Err(err) => return Err(err),
421            };
422        }
423    }
424
425    Ok(match waitpid(child, None)? {
426        WaitStatus::Exited(_, status) => status,
427        WaitStatus::Signaled(_, signal, _) => 128 + signal as i32,
428        _ => 1,
429    })
430}
431
432fn forward_and_log(
433    fd: BorrowedFd,
434    out_file: &mut Option<&mut File>,
435    buf: &[u8],
436    flush: bool,
437) -> Result<(), Errno> {
438    if let Some(logfile) = out_file {
439        logfile.write_all(buf).map_err(|x| match x.raw_os_error() {
440            Some(errno) => Errno::from_raw(errno),
441            None => Errno::EINVAL,
442        })?;
443        if flush {
444            logfile.flush().ok();
445        }
446    }
447    write_all(fd, buf)?;
448    Ok(())
449}
450
451/// Forwards the winsize and emits SIGWINCH
452fn forward_winsize(master: BorrowedFd, stderr_master: Option<BorrowedFd>) -> Result<(), Errno> {
453    if let Some(winsize) = get_winsize(io::stdin().as_fd()) {
454        set_winsize(master, winsize).ok();
455        if let Some(second_master) = stderr_master {
456            set_winsize(second_master, winsize).ok();
457        }
458        if let Ok(pgrp) = tcgetpgrp(master) {
459            killpg(pgrp, Signal::SIGWINCH).ok();
460        }
461    }
462    Ok(())
463}
464
465/// If possible, returns the terminal size of the given fd.
466fn get_winsize(fd: BorrowedFd) -> Option<Winsize> {
467    nix::ioctl_read_bad!(_get_window_size, TIOCGWINSZ, Winsize);
468    let mut size: Winsize = unsafe { std::mem::zeroed() };
469    unsafe { _get_window_size(fd.as_raw_fd(), &mut size).ok()? };
470    Some(size)
471}
472
473/// Sets the winsize
474fn set_winsize(fd: BorrowedFd, winsize: Winsize) -> Result<(), Errno> {
475    nix::ioctl_write_ptr_bad!(_set_window_size, TIOCSWINSZ, Winsize);
476    unsafe { _set_window_size(fd.as_raw_fd(), &winsize) }?;
477    Ok(())
478}
479
480/// Sends an EOF signal to the terminal if it's in canonical mode.
481fn send_eof_sequence(fd: BorrowedFd) {
482    if let Ok(attrs) = tcgetattr(fd) {
483        if attrs.local_flags.contains(LocalFlags::ICANON) {
484            write(fd, &[attrs.control_chars[VEOF]]).ok();
485        }
486    }
487}
488
489/// Calls write in a loop until it's done.
490fn write_all(fd: BorrowedFd, mut buf: &[u8]) -> Result<(), Errno> {
491    while !buf.is_empty() {
492        // we generally assume that EINTR/EAGAIN can't happen on write()
493        let n = write(fd, buf)?;
494        buf = &buf[n..];
495    }
496    Ok(())
497}
498
499/// Creates a FIFO at the path if the file does not exist yet.
500fn mkfifo_atomic(path: &Path) -> Result<(), Errno> {
501    match mkfifo(path, Mode::S_IRUSR | Mode::S_IWUSR) {
502        Ok(()) | Err(Errno::EEXIST) => Ok(()),
503        Err(err) => Err(err),
504    }
505}
506
507struct RestoreTerm(Termios);
508
509impl Drop for RestoreTerm {
510    fn drop(&mut self) {
511        tcsetattr(io::stdin(), SetArg::TCSAFLUSH, &self.0).ok();
512    }
513}