ptyprocess-0.4.1/.cargo_vcs_info.json0000644000000001360000000000100132510ustar { "git": { "sha1": "70bd0ab4d97c0bdc4ae66e085e14fb3c8d414383" }, "path_in_vcs": "" }ptyprocess-0.4.1/.github/workflows/ci.yml000064400000000000000000000036751046102023000165670ustar 00000000000000name: Build on: push: branches: - main pull_request: {} env: CARGO_TERM_COLOR: always jobs: check: name: Check strategy: fail-fast: false matrix: rust: [nightly, stable] platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust || 'stable' }} override: true - uses: actions-rs/cargo@v1 with: command: check test: name: Test Suite strategy: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - uses: actions-rs/cargo@v1 with: command: test args: --verbose fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: rustup component add clippy - uses: actions-rs/cargo@v1 with: command: clippy args: -- -D warnings audit: name: Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: cargo install cargo-audit - uses: actions-rs/cargo@v1 with: command: audit ptyprocess-0.4.1/.github/workflows/coverage.yml000064400000000000000000000012111046102023000177470ustar 00000000000000name: Code Coverage on: push: branches: - main pull_request: branches: - main env: CARGO_TERM_COLOR: always jobs: coverage: name: CodeCov runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: version: "0.15.0" args: "" - name: Upload to codecov.io uses: codecov/codecov-action@v1.0.2 with: token: ${{secrets.CODECOV_TOKEN}} ptyprocess-0.4.1/.gitignore000064400000000000000000000005001046102023000140240ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk ptyprocess-0.4.1/Cargo.lock0000644000000034740000000000100112340ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "libc" version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "nix" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ "bitflags", "cfg-if", "libc", "memoffset", "pin-utils", "static_assertions", ] [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ptyprocess" version = "0.4.1" dependencies = [ "nix", ] [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" ptyprocess-0.4.1/Cargo.toml0000644000000017370000000000100112570ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "ptyprocess" version = "0.4.1" authors = ["Maxim Zhiburt "] description = "A library to work with PTY/TTY on Unix systems" homepage = "https://github.com/zhiburt/ptyprocess" documentation = "https://docs.rs/ptyprocess" readme = "README.md" keywords = [ "pty", "tty", "terminal", ] categories = [ "development-tools", "command-line-interface", ] license = "MIT" repository = "https://github.com/zhiburt/ptyprocess" [dependencies.nix] version = "0.26" ptyprocess-0.4.1/Cargo.toml.orig000064400000000000000000000007521046102023000147340ustar 00000000000000[package] name = "ptyprocess" version = "0.4.1" authors = ["Maxim Zhiburt "] edition = "2018" description = "A library to work with PTY/TTY on Unix systems" repository = "https://github.com/zhiburt/ptyprocess" documentation = "https://docs.rs/ptyprocess" homepage = "https://github.com/zhiburt/ptyprocess" license = "MIT" readme = "README.md" keywords = ["pty", "tty", "terminal"] categories = ["development-tools", "command-line-interface"] [dependencies] nix = "0.26" ptyprocess-0.4.1/LICENSE000064400000000000000000000020561046102023000130510ustar 00000000000000MIT License Copyright (c) 2021 Maxim Zhiburt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ptyprocess-0.4.1/README.md000064400000000000000000000030621046102023000133210ustar 00000000000000# ptyprocess [![Build](https://github.com/zhiburt/ptyprocess/actions/workflows/ci.yml/badge.svg)](https://github.com/zhiburt/ptyprocess/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/zhiburt/ptyprocess/branch/main/graph/badge.svg?token=QBQLAT904B)](https://codecov.io/gh/zhiburt/ptyprocess) [![Crate](https://img.shields.io/crates/v/ptyprocess)](https://crates.io/crates/ptyprocess) [![docs.rs](https://img.shields.io/docsrs/ptyprocess?color=blue)](https://docs.rs/ptyprocess/0.1.0/ptyprocess/) [![license](https://img.shields.io/github/license/zhiburt/ptyprocess)](./LICENSE.txt) A library provides an interface for a unix [PTY/TTY](https://en.wikipedia.org/wiki/Pseudoterminal). It aims to work on all major Unix variants. The library was developed as a backend for a https://github.com/zhiburt/expectrl. If you're interested in a high level operations may you'd better take a look at `zhiburt/expectrl`. ## Usage ```rust use ptyprocess::PtyProcess; use std::io::{BufRead, BufReader, Result, Write}; use std::process::Command; fn main() -> Result<()> { // spawn a cat process let mut process = PtyProcess::spawn(Command::new("cat"))?; // create a communication stream let mut stream = process.get_raw_handle()?; // send a message to process writeln!(stream, "Hello cat")?; // read a line from the stream let mut reader = BufReader::new(stream); let mut buf = String::new(); reader.read_line(&mut buf)?; println!("line was entered {buf:?}"); // stop the process assert!(process.exit(true)?); Ok(()) } ```ptyprocess-0.4.1/examples/basic.rs000064400000000000000000000012011046102023000153000ustar 00000000000000use ptyprocess::PtyProcess; use std::io::{BufRead, BufReader, Result, Write}; use std::process::Command; fn main() -> Result<()> { // spawn a cat process let mut process = PtyProcess::spawn(Command::new("cat"))?; // create a communication stream let mut stream = process.get_raw_handle()?; // send a message to process writeln!(stream, "Hello cat")?; // read a line from the stream let mut reader = BufReader::new(stream); let mut buf = String::new(); reader.read_line(&mut buf)?; println!("line was entered {buf:?}"); // stop the process assert!(process.exit(true)?); Ok(()) } ptyprocess-0.4.1/examples/cat.rs000064400000000000000000000015631046102023000150010ustar 00000000000000use ptyprocess::PtyProcess; use std::{ fs::File, io::{self, Read, Write}, process::Command, }; fn main() { let process = PtyProcess::spawn(Command::new("cat")).expect("Error while spawning process"); let mut stream = process .get_pty_stream() .expect("Failed to get a pty handle"); let mut this_file = File::open(".gitignore").expect("Can't open a file"); io::copy(&mut this_file, &mut stream).expect("Can't copy a file"); // EOT stream .write_all(&[4]) .expect("Error while exiting a process"); // We can't read_to_end as the process isn't DEAD but at time time it is it's already a EOF let mut buf = [0; 128]; loop { let n = stream.read(&mut buf).expect("Erorr on read"); print!("{}", String::from_utf8_lossy(&buf[..n])); if n == 0 { break; } } } ptyprocess-0.4.1/examples/find.rs000064400000000000000000000014631046102023000151510ustar 00000000000000/// To run an example run the following command /// `cargo run --example find`. /// /// The example is based on https://github.com/zhiburt/ptyprocess/issues/2 use ptyprocess::PtyProcess; use std::io::{BufRead, BufReader}; use std::process::Command; fn main() { let mut cmd = Command::new("find"); cmd.args(vec!["/home/", "-name", "foo"]); cmd.stderr(std::process::Stdio::null()); let process = PtyProcess::spawn(cmd).unwrap(); let mut reader = BufReader::new(process.get_raw_handle().unwrap()); let mut buf = String::new(); loop { let n = reader.read_line(&mut buf).expect("readline error"); if n == 0 { break; } // by -1 we drop \n. let text = &buf[0..buf.len() - 1]; println!("buffer: {text}"); buf.clear(); } } ptyprocess-0.4.1/src/lib.rs000064400000000000000000000557451046102023000137640ustar 00000000000000//! A library provides an interface for a unix [PTY/TTY](https://en.wikipedia.org/wiki/Pseudoterminal). //! //! It aims to work on all major Unix variants. //! //! The library was developed as a backend for a https://github.com/zhiburt/expectrl. //! If you're interested in a high level operations may you'd better take a look at `zhiburt/expectrl`. //! //! ## Usage //! //! ```rust //! use ptyprocess::PtyProcess; //! use std::process::Command; //! use std::io::{BufRead, Write, BufReader}; //! //! // spawn a cat process //! let mut process = PtyProcess::spawn(Command::new("cat")).expect("failed to spawn a process"); //! //! // create a communication stream //! let mut stream = process.get_raw_handle().expect("failed to create a stream"); //! //! // send a message to process //! writeln!(stream, "Hello cat").expect("failed to write to a stream"); //! //! // read a line from the stream //! let mut reader = BufReader::new(stream); //! let mut buf = String::new(); //! reader.read_line(&mut buf).expect("failed to read a process output"); //! //! println!("line={}", buf); //! //! // stop the process //! assert!(process.exit(true).expect("failed to stop the process")) //! ``` pub mod stream; pub use nix::errno; pub use nix::sys::signal::Signal; pub use nix::sys::wait::WaitStatus; pub use nix::Error; use nix::fcntl::{fcntl, open, FcntlArg, FdFlag, OFlag}; use nix::libc::{self, winsize, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; use nix::pty::PtyMaster; use nix::pty::{grantpt, posix_openpt, unlockpt}; use nix::sys::stat::Mode; use nix::sys::wait::{self, waitpid}; use nix::sys::{signal, termios}; use nix::unistd::{ self, close, dup, dup2, fork, isatty, pipe, setsid, sysconf, write, ForkResult, Pid, SysconfVar, }; use nix::{ioctl_write_ptr_bad, Result}; use signal::Signal::SIGKILL; use std::fs::File; use std::os::unix::prelude::{AsRawFd, CommandExt, FromRawFd, RawFd}; use std::process::{self, Command}; use std::thread; use std::time::{self, Duration}; use stream::Stream; use termios::SpecialCharacterIndices; const DEFAULT_TERM_COLS: u16 = 80; const DEFAULT_TERM_ROWS: u16 = 24; const DEFAULT_VEOF_CHAR: u8 = 0x4; // ^D const DEFAULT_INTR_CHAR: u8 = 0x3; // ^C const DEFAULT_TERMINATE_DELAY: Duration = Duration::from_millis(100); /// PtyProcess controls a spawned process and communication with this. /// /// It implements [std::io::Read] and [std::io::Write] to communicate with /// a child. /// /// ```no_run,ignore /// use ptyprocess::PtyProcess; /// use std::io::Write; /// use std::process::Command; /// /// let mut process = PtyProcess::spawn(Command::new("cat")).unwrap(); /// process.write_all(b"Hello World").unwrap(); /// process.flush().unwrap(); /// ``` #[derive(Debug)] pub struct PtyProcess { master: Master, child_pid: Pid, eof_char: u8, intr_char: u8, terminate_delay: Duration, } impl PtyProcess { /// Spawns a child process and create a [PtyProcess]. /// /// ```no_run /// # use std::process::Command; /// # use ptyprocess::PtyProcess; /// let proc = PtyProcess::spawn(Command::new("bash")); /// ``` pub fn spawn(mut command: Command) -> Result { let master = Master::open()?; master.grant_slave_access()?; master.unlock_slave()?; // handle errors in child executions by pipe let (exec_err_pipe_r, exec_err_pipe_w) = pipe()?; let fork = unsafe { fork()? }; match fork { ForkResult::Child => { let err = || -> Result<()> { make_controlling_tty(&master)?; let slave_fd = master.get_slave_fd()?; redirect_std_streams(slave_fd)?; set_echo(STDIN_FILENO, false)?; set_term_size(STDIN_FILENO, DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?; // Do not allow child to inherit open file descriptors from parent close_all_descriptors(&[ 0, 1, 2, slave_fd, exec_err_pipe_w, exec_err_pipe_r, master.as_raw_fd(), ])?; close(slave_fd)?; close(exec_err_pipe_r)?; drop(master); // close pipe on sucessfull exec fcntl(exec_err_pipe_w, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))?; let _ = command.exec(); Err(Error::last()) }() .unwrap_err(); let code = err as i32; // Intentionally ignoring errors to exit the process properly let _ = write(exec_err_pipe_w, &code.to_be_bytes()); let _ = close(exec_err_pipe_w); process::exit(code); } ForkResult::Parent { child } => { close(exec_err_pipe_w)?; let mut pipe_buf = [0u8; 4]; unistd::read(exec_err_pipe_r, &mut pipe_buf)?; close(exec_err_pipe_r)?; let code = i32::from_be_bytes(pipe_buf); if code != 0 { return Err(errno::from_i32(code)); } // Some systems may work in this way? (not sure) // that we need to set a terminal size in a parent. set_term_size(master.as_raw_fd(), DEFAULT_TERM_COLS, DEFAULT_TERM_ROWS)?; let eof_char = get_eof_char(); let intr_char = get_intr_char(); Ok(Self { master, child_pid: child, eof_char, intr_char, terminate_delay: DEFAULT_TERMINATE_DELAY, }) } } } /// Returns a pid of a child process pub fn pid(&self) -> Pid { self.child_pid } /// Returns a file representation of a PTY, which can be used /// to communicate with a spawned process. /// /// The file behaivor is platform dependent. /// /// # Safety /// /// Be carefull changing a descriptors inner state (e.g `fcntl`) /// because it affects all structures which use it. /// /// Be carefull using this method in async mode. /// Because descriptor is set to a non-blocking mode will affect all dublicated descriptors /// which may be unexpected. /// /// # Example /// /// ```no_run /// use ptyprocess::PtyProcess; /// use std::{process::Command, io::{BufReader, LineWriter}}; /// /// let mut process = PtyProcess::spawn(Command::new("cat")).unwrap(); /// let pty = process.get_raw_handle().unwrap(); /// let mut writer = LineWriter::new(&pty); /// let mut reader = BufReader::new(&pty); /// ``` pub fn get_raw_handle(&self) -> Result { self.master.get_file_handle() } /// Returns a stream representation of a PTY. /// Which can be used to communicate with a spawned process. /// /// It differs from [Self::get_raw_handle] because it is /// platform independent. pub fn get_pty_stream(&self) -> Result { self.get_raw_handle().map(Stream::new) } /// Get a end of file character if set or a default. pub fn get_eof_char(&self) -> u8 { self.eof_char } /// Get a interapt character if set or a default. pub fn get_intr_char(&self) -> u8 { self.intr_char } /// Get window size of a terminal. /// /// Default size is 80x24. pub fn get_window_size(&self) -> Result<(u16, u16)> { get_term_size(self.master.as_raw_fd()) } /// Sets a terminal size. pub fn set_window_size(&mut self, cols: u16, rows: u16) -> Result<()> { set_term_size(self.master.as_raw_fd(), cols, rows) } /// The function returns true if an echo setting is setup. pub fn get_echo(&self) -> Result { termios::tcgetattr(self.master.as_raw_fd()) .map(|flags| flags.local_flags.contains(termios::LocalFlags::ECHO)) } /// Sets a echo setting for a terminal pub fn set_echo(&mut self, on: bool, timeout: Option) -> Result { set_echo(self.master.as_raw_fd(), on)?; self.wait_echo(on, timeout) } /// Returns true if a underline `fd` connected with a TTY. pub fn isatty(&self) -> Result { isatty(self.master.as_raw_fd()) } /// Set the pty process's terminate approach delay. pub fn set_terminate_delay(&mut self, terminate_approach_delay: Duration) { self.terminate_delay = terminate_approach_delay; } /// Status returns a status a of child process. pub fn status(&self) -> Result { waitpid(self.child_pid, Some(wait::WaitPidFlag::WNOHANG)) } /// Kill sends a signal to a child process. /// /// The operation is non-blocking. pub fn kill(&mut self, signal: signal::Signal) -> Result<()> { signal::kill(self.child_pid, signal) } /// Signal is an alias to [PtyProcess::kill]. /// /// [PtyProcess::kill]: struct.PtyProcess.html#method.kill pub fn signal(&mut self, signal: signal::Signal) -> Result<()> { self.kill(signal) } /// Wait blocks until a child process exits. /// /// It returns a error if the child was DEAD or not exist /// at the time of a call. /// /// If you need to verify that a process is dead in non-blocking way you can use /// [is_alive] method. /// /// [is_alive]: struct.PtyProcess.html#method.is_alive pub fn wait(&self) -> Result { waitpid(self.child_pid, None) } /// Checks if a process is still exists. /// /// It's a non blocking operation. /// /// Keep in mind that after calling this method process might be marked as DEAD by kernel, /// because a check of its status. /// Therefore second call to [Self::status] or [Self::is_alive] might return a different status. pub fn is_alive(&self) -> Result { let status = self.status(); match status { Ok(status) if status == WaitStatus::StillAlive => Ok(true), Ok(_) | Err(Error::ECHILD) | Err(Error::ESRCH) => Ok(false), Err(err) => Err(err), } } /// Try to force a child to terminate. /// /// This returns true if the child was terminated. and returns false if the /// child could not be terminated. /// /// It makes 4 tries getting more thorough. /// /// 1. SIGHUP /// 2. SIGCONT /// 3. SIGINT /// 4. SIGTERM /// /// If "force" is `true` then moves onto SIGKILL. pub fn exit(&mut self, force: bool) -> Result { if !self.is_alive()? { return Ok(true); } for &signal in &[ signal::SIGHUP, signal::SIGCONT, signal::SIGINT, signal::SIGTERM, ] { if self.try_to_terminate(signal)? { return Ok(true); } } if !force { return Ok(false); } self.try_to_terminate(SIGKILL) } fn try_to_terminate(&mut self, signal: signal::Signal) -> Result { self.kill(signal)?; thread::sleep(self.terminate_delay); self.is_alive().map(|is_alive| !is_alive) } fn wait_echo(&self, on: bool, timeout: Option) -> Result { let now = time::Instant::now(); while timeout.is_none() || now.elapsed() < timeout.unwrap() { if on == self.get_echo()? { return Ok(true); } thread::sleep(Duration::from_millis(100)); } Ok(false) } } impl Drop for PtyProcess { fn drop(&mut self) { if let Ok(WaitStatus::StillAlive) = self.status() { self.exit(true).unwrap(); } } } fn set_term_size(fd: i32, cols: u16, rows: u16) -> Result<()> { ioctl_write_ptr_bad!(_set_window_size, libc::TIOCSWINSZ, winsize); let size = winsize { ws_row: rows, ws_col: cols, ws_xpixel: 0, ws_ypixel: 0, }; let _ = unsafe { _set_window_size(fd, &size) }?; Ok(()) } fn get_term_size(fd: i32) -> Result<(u16, u16)> { nix::ioctl_read_bad!(_get_window_size, libc::TIOCGWINSZ, winsize); let mut size = winsize { ws_col: 0, ws_row: 0, ws_xpixel: 0, ws_ypixel: 0, }; let _ = unsafe { _get_window_size(fd, &mut size) }?; Ok((size.ws_col, size.ws_row)) } #[derive(Debug)] struct Master { fd: PtyMaster, } impl Master { fn open() -> Result { let master_fd = posix_openpt(OFlag::O_RDWR)?; Ok(Self { fd: master_fd }) } fn grant_slave_access(&self) -> Result<()> { grantpt(&self.fd) } fn unlock_slave(&self) -> Result<()> { unlockpt(&self.fd) } fn get_slave_name(&self) -> Result { get_slave_name(&self.fd) } #[cfg(not(target_os = "freebsd"))] fn get_slave_fd(&self) -> Result { let slave_name = self.get_slave_name()?; let slave_fd = open( slave_name.as_str(), OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty(), )?; Ok(slave_fd) } #[cfg(target_os = "freebsd")] fn get_slave_fd(&self) -> Result { let slave_name = self.get_slave_name()?; let slave_fd = open( format!("/dev/{}", slave_name.as_str()).as_str(), OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty(), )?; Ok(slave_fd) } fn get_file_handle(&self) -> Result { let fd = dup(self.as_raw_fd())?; let file = unsafe { File::from_raw_fd(fd) }; Ok(file) } } impl AsRawFd for Master { fn as_raw_fd(&self) -> RawFd { self.fd.as_raw_fd() } } #[cfg(target_os = "linux")] fn get_slave_name(fd: &PtyMaster) -> Result { nix::pty::ptsname_r(fd) } #[cfg(target_os = "freebsd")] fn get_slave_name(fd: &PtyMaster) -> Result { use std::ffi::CStr; use std::os::raw::c_char; use std::os::unix::prelude::AsRawFd; let fd = fd.as_raw_fd(); if !isptmaster(fd)? { // never reached according current implementation of isptmaster return Err(nix::Error::EINVAL); } // todo: Need to determine the correct size via some contstant like SPECNAMELEN in let mut buf: [c_char; 128] = [0; 128]; let _ = fdevname_r(fd, &mut buf)?; // todo: determine how CStr::from_ptr handles not NUL terminated string. let string = unsafe { CStr::from_ptr(buf.as_ptr()) } .to_string_lossy() .into_owned(); return Ok(string); } // https://github.com/freebsd/freebsd-src/blob/main/lib/libc/stdlib/ptsname.c#L52 #[cfg(target_os = "freebsd")] fn isptmaster(fd: RawFd) -> Result { use nix::libc::ioctl; use nix::libc::TIOCPTMASTER; match unsafe { ioctl(fd, TIOCPTMASTER as u64, 0) } { 0 => Ok(true), _ => Err(Error::last()), } } /* automatically generated by rust-bindgen 0.59.1 */ // bindgen filio.h --allowlist-type fiodgname_arg -o bindings.rs // it may be worth to use a build.rs if we will need more FFI structures. #[cfg(target_os = "freebsd")] #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct fiodgname_arg { pub len: ::std::os::raw::c_int, pub buf: *mut ::std::os::raw::c_void, } // https://github.com/freebsd/freebsd-src/blob/6ae38ab45396edaea26b4725e0c7db8cffa5f208/lib/libc/gen/fdevname.c#L39 #[cfg(target_os = "freebsd")] fn fdevname_r(fd: RawFd, buf: &mut [std::os::raw::c_char]) -> Result<()> { use nix::libc::{ioctl, FIODGNAME}; nix::ioctl_read_bad!(_ioctl_fiodgname, FIODGNAME, fiodgname_arg); let mut fgn = fiodgname_arg { len: buf.len() as i32, buf: buf.as_mut_ptr() as *mut ::std::os::raw::c_void, }; let _ = unsafe { _ioctl_fiodgname(fd, &mut fgn) }?; Ok(()) } /// Getting a slave name on darvin platform /// https://blog.tarq.io/ptsname-on-osx-with-rust/ #[cfg(target_os = "macos")] fn get_slave_name(fd: &PtyMaster) -> Result { use nix::libc::ioctl; use nix::libc::TIOCPTYGNAME; use std::ffi::CStr; use std::os::raw::c_char; use std::os::unix::prelude::AsRawFd; // ptsname_r is a linux extension but ptsname isn't thread-safe // we could use a static mutex but instead we re-implemented ptsname_r with a syscall // ioctl(fd, TIOCPTYGNAME, buf) manually // the buffer size on OSX is 128, defined by sys/ttycom.h let mut buf: [c_char; 128] = [0; 128]; let fd = fd.as_raw_fd(); match unsafe { ioctl(fd, TIOCPTYGNAME as u64, &mut buf) } { 0 => { let string = unsafe { CStr::from_ptr(buf.as_ptr()) } .to_string_lossy() .into_owned(); return Ok(string); } _ => Err(Error::last()), } } fn redirect_std_streams(fd: RawFd) -> Result<()> { // If fildes2 is already a valid open file descriptor, it shall be closed first close(STDIN_FILENO)?; close(STDOUT_FILENO)?; close(STDERR_FILENO)?; // use slave fd as std[in/out/err] dup2(fd, STDIN_FILENO)?; dup2(fd, STDOUT_FILENO)?; dup2(fd, STDERR_FILENO)?; Ok(()) } fn set_echo(fd: RawFd, on: bool) -> Result<()> { // Set echo off // Even though there may be something left behind https://stackoverflow.com/a/59034084 let mut flags = termios::tcgetattr(fd)?; match on { true => flags.local_flags |= termios::LocalFlags::ECHO, false => flags.local_flags &= !termios::LocalFlags::ECHO, } termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?; Ok(()) } pub fn set_raw(fd: RawFd) -> Result<()> { let mut flags = termios::tcgetattr(fd)?; #[cfg(not(target_os = "macos"))] { termios::cfmakeraw(&mut flags); } #[cfg(target_os = "macos")] { // implementation is taken from https://github.com/python/cpython/blob/3.9/Lib/tty.py use nix::libc::{VMIN, VTIME}; use termios::ControlFlags; use termios::InputFlags; use termios::LocalFlags; use termios::OutputFlags; flags.input_flags &= !(InputFlags::BRKINT | InputFlags::ICRNL | InputFlags::INPCK | InputFlags::ISTRIP | InputFlags::IXON); flags.output_flags &= !OutputFlags::OPOST; flags.control_flags &= !(ControlFlags::CSIZE | ControlFlags::PARENB); flags.control_flags |= ControlFlags::CS8; flags.local_flags &= !(LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::IEXTEN | LocalFlags::ISIG); flags.control_chars[VMIN] = 1; flags.control_chars[VTIME] = 0; } termios::tcsetattr(fd, termios::SetArg::TCSANOW, &flags)?; Ok(()) } fn get_this_term_char(char: SpecialCharacterIndices) -> Option { for &fd in &[STDIN_FILENO, STDOUT_FILENO] { if let Ok(char) = get_term_char(fd, char) { return Some(char); } } None } fn get_intr_char() -> u8 { get_this_term_char(SpecialCharacterIndices::VINTR).unwrap_or(DEFAULT_INTR_CHAR) } fn get_eof_char() -> u8 { get_this_term_char(SpecialCharacterIndices::VEOF).unwrap_or(DEFAULT_VEOF_CHAR) } fn get_term_char(fd: RawFd, char: SpecialCharacterIndices) -> Result { let flags = termios::tcgetattr(fd)?; let b = flags.control_chars[char as usize]; Ok(b) } fn make_controlling_tty(ptm: &Master) -> Result<()> { #[cfg(not(any(target_os = "freebsd", target_os = "macos")))] { let pts_name = ptm.get_slave_name()?; // https://github.com/pexpect/ptyprocess/blob/c69450d50fbd7e8270785a0552484182f486092f/ptyprocess/_fork_pty.py // Disconnect from controlling tty, if any // // it may be a simmilar call to ioctl TIOCNOTTY // https://man7.org/linux/man-pages/man4/tty_ioctl.4.html let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty()); match fd { Ok(fd) => { close(fd)?; } Err(Error::ENXIO) => { // Sometimes we get ENXIO right here which 'probably' means // that we has been already disconnected from controlling tty. // Specifically it was discovered on ubuntu-latest Github CI platform. } Err(err) => return Err(err), } // setsid() will remove the controlling tty. Also the ioctl TIOCNOTTY does this. // https://www.win.tue.nl/~aeb/linux/lk/lk-10.html setsid()?; // Verify we are disconnected from controlling tty by attempting to open // it again. We expect that OSError of ENXIO should always be raised. let fd = open("/dev/tty", OFlag::O_RDWR | OFlag::O_NOCTTY, Mode::empty()); match fd { Err(Error::ENXIO) => {} // ok Ok(fd) => { close(fd)?; return Err(Error::ENOTSUP); } Err(_) => return Err(Error::ENOTSUP), } // Verify we can open child pty. let fd = open(pts_name.as_str(), OFlag::O_RDWR, Mode::empty())?; close(fd)?; // Verify we now have a controlling tty. let fd = open("/dev/tty", OFlag::O_WRONLY, Mode::empty())?; close(fd)?; } #[cfg(any(target_os = "freebsd", target_os = "macos"))] { let pts_fd = ptm.get_slave_fd()?; // https://docs.freebsd.org/44doc/smm/01.setup/paper-3.html setsid()?; use nix::libc::ioctl; use nix::libc::TIOCSCTTY; match unsafe { ioctl(pts_fd, TIOCSCTTY as u64, 0) } { 0 => {} _ => return Err(Error::last()), } } Ok(()) } // Except is used for cases like double free memory fn close_all_descriptors(except: &[RawFd]) -> Result<()> { // On linux could be used getrlimit(RLIMIT_NOFILE, rlim) interface let max_open_fds = sysconf(SysconfVar::OPEN_MAX)?.unwrap() as i32; (0..max_open_fds) .filter(|fd| !except.contains(fd)) .for_each(|fd| { // We don't handle errors intentionally, // because it will be hard to determine which descriptors closed already. let _ = close(fd); }); Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn create_pty() -> Result<()> { let master = Master::open()?; master.grant_slave_access()?; master.unlock_slave()?; let slavename = master.get_slave_name()?; let expected_path = if cfg!(target_os = "freebsd") { "pts/" } else if cfg!(target_os = "macos") { "/dev/ttys" } else { "/dev/pts/" }; if !slavename.starts_with(expected_path) { assert_eq!(expected_path, slavename); } Ok(()) } #[test] #[ignore = "The test should be run in a sigle thread mode --jobs 1 or --test-threads 1"] fn release_pty_master() -> Result<()> { let master = Master::open()?; let old_master_fd = master.fd.as_raw_fd(); drop(master); let master = Master::open()?; assert!(master.fd.as_raw_fd() == old_master_fd); Ok(()) } } ptyprocess-0.4.1/src/stream.rs000064400000000000000000000036221046102023000144740ustar 00000000000000/// Stream represent a IO stream. use std::{ fs::File, io::{self, Read, Write}, os::unix::{io::AsRawFd, prelude::RawFd}, }; /// Stream represent a duplex pipe. /// /// It must work in the same way on all platforms. #[derive(Debug)] pub struct Stream { inner: File, } impl Stream { /// The function returns a new Stream from a file. pub fn new(file: File) -> Self { Self { inner: file } } } impl Write for Stream { fn write(&mut self, buf: &[u8]) -> io::Result { self.inner.write(buf) } fn flush(&mut self) -> io::Result<()> { self.inner.flush() } fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result { self.inner.write_vectored(bufs) } } impl Read for Stream { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self.inner.read(buf) { Err(ref err) if has_reached_end_of_sdtout(err) => Ok(0), result => result, } } } impl AsRawFd for Stream { fn as_raw_fd(&self) -> RawFd { self.inner.as_raw_fd() } } /// PTY may doesn't have anything to read but the process is not DEAD, /// and this erorr may be returned. fn has_reached_end_of_sdtout(err: &std::io::Error) -> bool { // We don't match `err.kind()` because on stable we would expect `Other` but for those who uses nightly // we would need to expect `Uncategorized` behind `#![feature(io_error_uncategorized)]` unstable feature. // https://doc.rust-lang.org/beta/unstable-book/library-features/io-error-uncategorized.html // https://doc.rust-lang.org/nightly/std/io/struct.Error.html#method.kind // // But we can't use a cfg!() currently to determine if a unstable feature is turned on. // https://stackoverflow.com/questions/67454353/how-to-detect-if-an-unstable-feature-is-enabled // // So we match only errno code. err.raw_os_error() == Some(5) } ptyprocess-0.4.1/tests/cmd.rs000064400000000000000000000004051046102023000143130ustar 00000000000000use ptyprocess::PtyProcess; use std::{io, process::Command}; #[test] fn empty() { let err = PtyProcess::spawn(Command::new("")).unwrap_err(); assert_eq!( io::ErrorKind::NotFound, io::Error::from_raw_os_error(err as i32).kind() ); } ptyprocess-0.4.1/tests/io.rs000064400000000000000000000160241046102023000141630ustar 00000000000000use ptyprocess::{PtyProcess, Signal, WaitStatus}; use std::{ io::{BufRead, BufReader, LineWriter, Read, Write}, process::Command, thread, time::Duration, }; #[test] fn custom_reader_writer() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let pty = proc.get_raw_handle().unwrap(); let mut writer = LineWriter::new(&pty); let mut reader = BufReader::new(&pty); writer.write_all(b"hello cat\n").unwrap(); let mut buf = String::new(); reader.read_line(&mut buf).unwrap(); assert_eq!(buf, "hello cat\r\n"); drop(writer); drop(reader); assert!(proc.exit(true).unwrap()); } #[test] fn cat_intr() { let proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let mut w = proc.get_raw_handle().unwrap(); // this sleep solves an edge case of some cases when cat is somehow not "ready" // to take the ^C (occasional test hangs) // Ctrl-C is etx(End of text). Thus send \x03. thread::sleep(Duration::from_millis(300)); w.write_all(&[3]).unwrap(); // send ^C w.flush().unwrap(); assert_eq!( proc.wait().unwrap(), WaitStatus::Signaled(proc.pid(), Signal::SIGINT, false), ); } #[test] fn cat_eof() { let proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let mut w = proc.get_raw_handle().unwrap(); // this sleep solves an edge case of some cases when cat is somehow not "ready" // to take the ^D (occasional test hangs) thread::sleep(Duration::from_millis(300)); w.write_all(&[4]).unwrap(); // send ^D w.flush().unwrap(); assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); } #[test] fn read_more_then_process_gives() { let mut command = Command::new("echo"); command.arg("hello cat"); let proc = PtyProcess::spawn(command).unwrap(); let mut w = proc.get_pty_stream().unwrap(); let mut buf = Vec::new(); w.read_to_end(&mut buf).unwrap(); assert_eq!(buf, b"hello cat\r\n"); assert_eq!(0, w.read(&mut [0; 128]).unwrap()); assert_eq!(0, w.read(&mut [0; 128]).unwrap()); assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); } #[test] fn read_after_process_exit() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let mut w = proc.get_pty_stream().unwrap(); writeln!(w, "Hello").unwrap(); std::thread::sleep(Duration::from_millis(500)); let exited = proc.exit(true).unwrap(); assert!(exited); #[cfg(target_os = "linux")] { assert_eq!(7, w.read(&mut [0; 128]).unwrap()); } assert_eq!(0, w.read(&mut [0; 128]).unwrap()); assert_eq!(0, w.read(&mut [0; 128]).unwrap()); // on macos we can't write after proces is exited // on linux its ok if writeln!(w, "World").is_ok() { assert_eq!(0, w.read(&mut [0; 128]).unwrap()); assert_eq!(0, w.read(&mut [0; 128]).unwrap()); assert_eq!(0, w.read(&mut [0; 128]).unwrap()); } } #[test] fn ptyprocess_check_terminal_line_settings() { let mut command = Command::new("stty"); command.arg("-a"); let proc = PtyProcess::spawn(command).unwrap(); let mut w = proc.get_pty_stream().unwrap(); let mut buf = String::new(); w.read_to_string(&mut buf).unwrap(); assert!(buf.split_whitespace().any(|word| word == "-echo")); } #[test] fn read_line() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let w = proc.get_raw_handle().unwrap(); let mut r = BufReader::new(&w); writeln!(&w, "Hello World 1").unwrap(); writeln!(&w, "Hello World 2").unwrap(); let mut buf = String::new(); r.read_line(&mut buf).unwrap(); assert_eq!(buf, "Hello World 1\r\n"); let mut buf = String::new(); r.read_line(&mut buf).unwrap(); assert_eq!(buf, "Hello World 2\r\n"); assert!(proc.exit(true).unwrap()); } #[test] fn read_until() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); let w = proc.get_raw_handle().unwrap(); let mut r = BufReader::new(&w); writeln!(&w, "Hello World 1").unwrap(); // give cat a time to react on input thread::sleep(Duration::from_millis(100)); let mut buf = Vec::new(); r.read_until(b' ', &mut buf).unwrap(); assert_eq!(buf, b"Hello "); let mut buf = vec![0; 128]; let n = r.read(&mut buf).unwrap(); assert_eq!(&buf[..n], b"World 1\r\n"); assert!(proc.exit(true).unwrap()); } #[test] fn read_to_end() { let mut cmd = Command::new("echo"); cmd.arg("Hello World"); let proc = PtyProcess::spawn(cmd).unwrap(); let mut w = proc.get_pty_stream().unwrap(); // without a sleep we can't guarantee what we actually test std::thread::sleep(Duration::from_millis(1500)); let mut buf = Vec::new(); w.read_to_end(&mut buf).unwrap(); #[cfg(target_os = "linux")] { assert_eq!(buf, b"Hello World\r\n"); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { assert_eq!(buf, b""); } } #[test] fn read_to_end_on_handle() { let mut cmd = Command::new("echo"); cmd.arg("Hello World"); let proc = PtyProcess::spawn(cmd).unwrap(); let mut w = proc.get_raw_handle().unwrap(); // without a sleep we can't guarantee what we actually test std::thread::sleep(Duration::from_millis(1500)); #[cfg(target_os = "linux")] { let err = w.read_to_end(&mut Vec::new()).unwrap_err(); assert_eq!(Some(5), err.raw_os_error()); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { let n = w.read_to_end(&mut Vec::new()).unwrap(); assert_eq!(0, n); } } #[test] fn read_after_process_is_gone() { let mut cmd = Command::new("echo"); cmd.arg("Hello World"); let proc = PtyProcess::spawn(cmd).unwrap(); let mut w = proc.get_raw_handle().unwrap(); // after we check a status of child // it should be marked DEAD. assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); // Just in case; make a little delay thread::sleep(Duration::from_millis(500)); #[cfg(target_os = "linux")] { let mut buf = vec![0; 128]; let n = w.read(&mut buf).unwrap(); assert_eq!(&buf[..n], b"Hello World\r\n"); } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { assert_eq!(0, w.read(&mut [0; 128]).unwrap()); } } #[test] fn read_to_end_after_process_is_gone() { let mut cmd = Command::new("echo"); cmd.arg("Hello World"); let proc = PtyProcess::spawn(cmd).unwrap(); let mut w = proc.get_pty_stream().unwrap(); // after we check a status of child // it should be marked DEAD. assert_eq!(proc.wait().unwrap(), WaitStatus::Exited(proc.pid(), 0)); // Just in case; make a little delay thread::sleep(Duration::from_millis(500)); #[cfg(target_os = "linux")] { let mut buf = Vec::new(); w.read_to_end(&mut buf).unwrap(); assert_eq!(buf, b"Hello World\r\n") } #[cfg(any(target_os = "macos", target_os = "freebsd"))] { let mut buf = Vec::new(); w.read_to_end(&mut buf).unwrap(); assert_eq!(buf, b"") } } ptyprocess-0.4.1/tests/settings.rs000064400000000000000000000015441046102023000154150ustar 00000000000000use ptyprocess::PtyProcess; use std::{process::Command, time::Duration}; #[test] fn default_win_size() { let proc = PtyProcess::spawn(Command::new("cat")).unwrap(); assert_eq!(proc.get_window_size().unwrap(), (80, 24)); } #[test] fn set_win_size() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); proc.set_window_size(100, 200).unwrap(); assert_eq!(proc.get_window_size().unwrap(), (100, 200)); } #[test] fn default_echo() { let proc = PtyProcess::spawn(Command::new("cat")).unwrap(); assert!(!proc.get_echo().unwrap()); } #[test] fn set_echo() { let mut proc = PtyProcess::spawn(Command::new("cat")).unwrap(); assert!(proc.isatty().unwrap()); let is_set = proc .set_echo(true, Some(Duration::from_millis(500))) .unwrap(); assert!(is_set); assert!(proc.get_echo().unwrap()); }