use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::Session;
const F_SETFL: i32 = 4;
const O_NONBLOCK: i32 = 0o4000;
const EIO: i32 = 5;
const EAGAIN: i32 = 11;
const EBADF: i32 = 9;
pub struct Proc {
pub pid: u32,
master: OwnedFd,
alive: Arc<AtomicBool>,
}
fn cstring(s: &str) -> Vec<u8> {
let mut v: Vec<u8> = s.bytes().collect();
v.push(0);
v
}
pub fn spawn(
command: &str,
args: &[String],
cwd: &str,
env: &[(String, String)],
) -> Result<Proc, String> {
let has_nul = command.contains('\0')
|| cwd.contains('\0')
|| args.iter().any(|a| a.contains('\0'))
|| env
.iter()
.any(|(k, v)| k.contains('\0') || v.contains('\0'));
if has_nul {
return Err("pty spawn input contains a NUL byte".to_string());
}
let cwd_c = cstring(cwd);
let env_strings: Vec<Vec<u8>> = env
.iter()
.map(|(k, v)| cstring(&format!("{k}={v}")))
.collect();
let mut envv: Vec<*const libc::c_char> = env_strings
.iter()
.map(|s| s.as_ptr() as *const libc::c_char)
.collect();
envv.push(std::ptr::null());
let file = cstring(command);
let arg_strings: Vec<Vec<u8>> = args.iter().map(|a| cstring(a)).collect();
let mut argv: Vec<*const libc::c_char> = Vec::with_capacity(args.len() + 2);
argv.push(file.as_ptr() as *const libc::c_char);
for a in &arg_strings {
argv.push(a.as_ptr() as *const libc::c_char);
}
argv.push(std::ptr::null());
unsafe {
let flags = libc::O_RDWR | libc::O_NOCTTY;
let master = libc::posix_openpt(flags);
if master < 0 {
return Err(format!(
"posix_openpt failed: {}",
std::io::Error::last_os_error()
));
}
if libc::grantpt(master) != 0 || libc::unlockpt(master) != 0 {
libc::close(master);
return Err(format!(
"grantpt/unlockpt failed: {}",
std::io::Error::last_os_error()
));
}
let mut name = [0 as libc::c_char; 128];
if libc::ptsname_r(master, name.as_mut_ptr(), name.len()) != 0 {
libc::close(master);
return Err("ptsname_r failed".into());
}
let fd = libc::fork();
if fd < 0 {
libc::close(master);
return Err(format!("fork failed: {}", std::io::Error::last_os_error()));
}
if fd == 0 {
let slave = libc::open(name.as_ptr(), flags);
if slave < 0 {
libc::_exit(127);
}
libc::setsid();
libc::ioctl(slave, libc::TIOCSCTTY as _, 0);
libc::dup2(slave, 0);
libc::dup2(slave, 1);
libc::dup2(slave, 2);
if slave > 2 {
libc::close(slave);
}
libc::close(master);
libc::chdir(cwd_c.as_ptr() as *const libc::c_char);
libc::execve(
file.as_ptr() as *const libc::c_char,
argv.as_ptr(),
envv.as_ptr(),
);
libc::_exit(127);
}
let f = libc::fcntl(master, F_SETFL, O_NONBLOCK);
if f < 0 {
}
Ok(Proc {
pid: fd as u32,
master: OwnedFd::from_raw_fd(master),
alive: Arc::new(AtomicBool::new(true)),
})
}
}
impl Proc {
pub fn write(&self, data: &str) {
let bytes = data.as_bytes();
let fd = self.master.as_raw_fd();
let mut off = 0usize;
while off < bytes.len() {
let n = unsafe {
libc::write(
fd,
bytes[off..].as_ptr() as *const libc::c_void,
bytes.len() - off,
)
};
if n <= 0 {
break;
}
off += n as usize;
}
}
pub fn resize(&self, rows: u16, cols: u16) {
let ws = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: 0,
ws_ypixel: 0,
};
unsafe {
libc::ioctl(
self.master.as_raw_fd(),
libc::TIOCSWINSZ as _,
&ws as *const libc::winsize,
);
}
}
pub fn kill(&self) {
if !self.alive.load(Ordering::SeqCst) {
return;
}
unsafe {
if libc::kill(-(self.pid as i32), libc::SIGTERM) != 0 {
libc::kill(self.pid as i32, libc::SIGTERM);
}
}
}
pub fn start(&self, session: Arc<Session>, on_exit: Arc<dyn Fn(Option<i32>) + Send + Sync>) {
let master = unsafe { libc::dup(self.master.as_raw_fd()) };
if master < 0 {
}
let alive = self.alive.clone();
let alive_reader = self.alive.clone();
let pid = self.pid as i32;
if master >= 0 {
std::thread::Builder::new()
.name("pty-reader".into())
.spawn(move || {
struct FdGuard(i32);
impl Drop for FdGuard {
fn drop(&mut self) {
unsafe { libc::close(self.0) };
}
}
let _guard = FdGuard(master);
let mut buf = [0u8; 65536];
loop {
let n = unsafe {
libc::read(master, buf.as_mut_ptr() as *mut libc::c_void, buf.len())
};
if n > 0 {
session.push_output(
String::from_utf8_lossy(&buf[..n as usize]).into_owned(),
);
continue;
}
let err = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
match err {
0 if n == 0 => break, EIO | EBADF => break,
EAGAIN => {
std::thread::sleep(std::time::Duration::from_millis(1));
if !alive_reader.load(Ordering::SeqCst) {
break;
}
}
_ => break,
}
}
})
.ok();
}
std::thread::Builder::new()
.name("pty-waiter".into())
.spawn(move || {
let mut status: i32 = 0;
let code = loop {
let r = unsafe { libc::waitpid(pid, &mut status, 0) };
if r == pid {
break exit_code(status);
}
if r < 0 {
let err = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if err == libc::ECHILD {
break None;
}
}
};
alive.store(false, Ordering::SeqCst);
std::thread::sleep(std::time::Duration::from_millis(20));
on_exit(code);
})
.ok();
}
}
fn exit_code(status: i32) -> Option<i32> {
if status & 0x7f == 0 {
Some((status >> 8) & 0xff)
} else {
None
}
}
#[cfg(test)]
mod nul_tests {
use super::spawn;
#[test]
fn nul_bytes_rejected_in_command_args_env_and_cwd() {
let ok = spawn("/bin/true", &[], "/", &[]);
assert!(ok.is_ok(), "clean spawn works");
let e = match spawn("/bin/true", &["a\0b".into()], "/", &[]) {
Err(e) => e,
Ok(_) => panic!("NUL in args must be rejected"),
};
assert!(e.contains("NUL"), "{e}");
let e = match spawn("/bin/true", &[], "/", &[("K".into(), "v\0X".into())]) {
Err(e) => e,
Ok(_) => panic!("NUL in env must be rejected"),
};
assert!(e.contains("NUL"), "{e}");
let e = match spawn("/bin/true", &[], "/a\0b", &[]) {
Err(e) => e,
Ok(_) => panic!("NUL in cwd must be rejected"),
};
assert!(e.contains("NUL"), "{e}");
}
}