Implement basic "PTY exec" in Rust

This commit is contained in:
Marcin Kulik
2023-10-21 14:40:39 +02:00
parent 0350caefde
commit d7d5ce4aaa
4 changed files with 374 additions and 2 deletions

173
Cargo.lock generated
View File

@@ -2,6 +2,179 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "asciinema"
version = "3.0.0-alpha.1"
dependencies = [
"anyhow",
"mio",
"nix",
"termion",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[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.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "log"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "mio"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
]
[[package]]
name = "nix"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"libc",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_termios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall",
]
[[package]]
name = "termion"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "659c1f379f3408c7e5e84c7d0da6d93404e3800b6b9d063ba24436419302ec90"
dependencies = [
"libc",
"numtoa",
"redox_syscall",
"redox_termios",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

View File

@@ -6,3 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.75"
nix = { version = "0.27", features = [ "fs", "term", "process" ] }
mio = { version ="0.8", features = ["os-poll", "os-ext"] }
termion = "2.0.1"

View File

@@ -1,3 +1,6 @@
fn main() {
println!("Hello, world!");
mod pty;
use anyhow::Result;
fn main() -> Result<()> {
pty::exec(&["/bin/bash"])
}

192
src/pty.rs Normal file
View File

@@ -0,0 +1,192 @@
use mio::unix::SourceFd;
use nix::{fcntl, libc, pty, unistd, unistd::ForkResult};
use std::fs;
use std::io::{self, Read, Write};
use std::ops::Deref;
use std::os::fd::RawFd;
use std::os::unix::io::{AsRawFd, FromRawFd};
use termion::raw::IntoRawMode;
pub fn exec<S: AsRef<str>>(args: &[S]) -> anyhow::Result<()> {
let tty = open_tty()?;
let winsize = get_tty_size(tty.as_raw_fd());
let result = unsafe { pty::forkpty(Some(&winsize), None) }?;
match result.fork_result {
ForkResult::Parent { .. } => {
handle_parent(result.master.as_raw_fd(), tty)?;
}
ForkResult::Child => {
handle_child(args)?;
// TODO wait for child pid
}
}
Ok(())
}
fn open_tty() -> io::Result<fs::File> {
fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")
}
fn get_tty_size(tty_fd: i32) -> pty::Winsize {
let mut winsize = pty::Winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe { libc::ioctl(tty_fd, libc::TIOCGWINSZ, &mut winsize) };
winsize
}
const MASTER: mio::Token = mio::Token(0);
const TTY: mio::Token = mio::Token(1);
const BUF_SIZE: usize = 128 * 1024;
fn handle_parent(master_fd: RawFd, tty: fs::File) -> anyhow::Result<()> {
let mut master_file = unsafe { fs::File::from_raw_fd(master_fd) };
let mut poll = mio::Poll::new()?;
let mut events = mio::Events::with_capacity(128);
let mut master_source = SourceFd(&master_fd);
let mut tty = tty.into_raw_mode()?;
let tty_fd = tty.as_raw_fd();
let mut tty_source = SourceFd(&tty_fd);
let mut buf = [0u8; BUF_SIZE];
let mut input: Vec<u8> = Vec::with_capacity(BUF_SIZE);
let mut output: Vec<u8> = Vec::with_capacity(BUF_SIZE);
set_non_blocking(&master_fd)?;
set_non_blocking(&tty_fd)?;
poll.registry()
.register(&mut master_source, MASTER, mio::Interest::READABLE)?;
poll.registry()
.register(&mut tty_source, TTY, mio::Interest::READABLE)?;
loop {
poll.poll(&mut events, None).unwrap();
for event in events.iter() {
match event.token() {
MASTER => {
if event.is_readable() {
let read = read_all(&mut master_file, &mut buf, &mut output)?;
if read > 0 {
poll.registry().reregister(
&mut tty_source,
TTY,
mio::Interest::READABLE | mio::Interest::WRITABLE,
)?;
}
}
if event.is_writable() {
master_file.write_all(&input).unwrap();
input.clear();
poll.registry().reregister(
&mut master_source,
MASTER,
mio::Interest::READABLE,
)?;
}
if event.is_read_closed() {
return Ok(());
// TODO don't return but deregister master_source and flush remaining output to tty
}
}
TTY => {
if event.is_writable() {
tty.write_all(&output)?;
output.clear();
poll.registry().reregister(
&mut tty_source,
TTY,
mio::Interest::READABLE,
)?;
}
if event.is_readable() {
let read = read_all(&mut tty.deref(), &mut buf, &mut input)?;
if read > 0 {
poll.registry().reregister(
&mut master_source,
MASTER,
mio::Interest::READABLE | mio::Interest::WRITABLE,
)?;
}
}
if event.is_read_closed() {
poll.registry().deregister(&mut tty_source).unwrap();
return Ok(());
// TODO don't return but deregister tty_source and flush remaining input to master
}
}
_ => (),
}
}
}
}
fn handle_child<S: AsRef<str>>(args: &[S]) -> anyhow::Result<()> {
use std::ffi::{CString, NulError};
let args = args
.iter()
.map(|s| CString::new(s.as_ref()))
.collect::<Result<Vec<CString>, NulError>>()?;
unistd::execvp(&args[0], &args)?;
unsafe { libc::_exit(1) }
}
fn set_non_blocking(fd: &i32) -> Result<(), io::Error> {
use fcntl::{fcntl, FcntlArg::*, OFlag};
let flags = fcntl(*fd, F_GETFL)?;
let mut oflags = OFlag::from_bits_truncate(flags);
oflags |= OFlag::O_NONBLOCK;
fcntl(*fd, F_SETFL(oflags))?;
Ok(())
}
fn read_all<R: Read>(source: &mut R, buf: &mut [u8], out: &mut Vec<u8>) -> io::Result<usize> {
let mut read = 0;
loop {
match source.read(buf) {
Ok(0) => (),
Ok(n) => {
out.extend_from_slice(&buf[0..n]);
read += n;
}
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock {
break;
} else {
return Err(e);
}
}
}
}
Ok(read)
}