use std::collections::HashMap;
use std::ffi::CString;
use std::io::{self, Read, Write};
use std::os::unix::io::RawFd;
use crate::ported::zsh_h::{OPT_ISSET, OPT_ARG};
use std::os::unix::io::IntoRawFd;
use std::process::Command;
pub const READ_MAX: usize = 1024 * 1024;
#[derive(Debug)]
pub struct ptycmd {
pub name: String,
pub args: Vec<String>,
pub master_fd: RawFd,
pub pid: i32,
pub echo: bool,
pub nonblock: bool,
pub finished: bool,
pub buffer: Vec<u8>,
}
impl ptycmd {
pub fn new(
name: &str,
args: Vec<String>,
master_fd: RawFd,
pid: i32,
echo: bool,
nonblock: bool,
) -> Self {
Self {
name: name.to_string(),
args,
master_fd,
pid,
echo,
nonblock,
finished: false,
buffer: Vec::new(),
}
}
}
#[cfg(unix)]
pub fn get_pty() -> io::Result<(RawFd, RawFd)> { let master_fd = unsafe {
let fd = libc::posix_openpt(libc::O_RDWR | libc::O_NOCTTY);
if fd < 0 {
return Err(io::Error::last_os_error());
}
fd
};
unsafe {
if libc::grantpt(master_fd) < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
if libc::unlockpt(master_fd) < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
let slave_name = libc::ptsname(master_fd);
if slave_name.is_null() {
libc::close(master_fd);
return Err(io::Error::other("ptsname failed"));
}
let slave_fd = libc::open(slave_name, libc::O_RDWR | libc::O_NOCTTY);
if slave_fd < 0 {
libc::close(master_fd);
return Err(io::Error::last_os_error());
}
Ok((master_fd, slave_fd))
}
}
#[cfg(unix)]
pub fn ptynonblock(fd: RawFd) -> io::Result<()> { unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 {
return Err(io::Error::last_os_error());
}
if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn ptyread(fd: RawFd, pattern: Option<&str>, timeout_ms: Option<i32>) -> io::Result<String> { let mut buffer = vec![0u8; 4096];
let mut result = Vec::new();
#[cfg(unix)]
{
if let Some(timeout) = timeout_ms {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pfd, 1, timeout) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
if ret == 0 {
return Ok(String::new());
}
}
loop {
let n =
unsafe { libc::read(fd, buffer.as_mut_ptr() as *mut libc::c_void, buffer.len()) };
if n < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
break;
}
return Err(err);
}
if n == 0 {
break;
}
result.extend_from_slice(&buffer[..n as usize]);
if result.len() >= READ_MAX {
break;
}
if let Some(pat) = pattern {
if let Ok(s) = String::from_utf8(result.clone()) {
if s.contains(pat) {
break;
}
}
}
}
}
String::from_utf8(result).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn ptywritestr(fd: RawFd, data: &str) -> io::Result<usize> { #[cfg(unix)]
{
let bytes = data.as_bytes();
let n = unsafe { libc::write(fd, bytes.as_ptr() as *const libc::c_void, bytes.len()) };
if n < 0 {
return Err(io::Error::last_os_error());
}
Ok(n as usize)
}
#[cfg(not(unix))]
{
Err(io::Error::new(io::ErrorKind::Unsupported, "not supported"))
}
}
#[allow(non_snake_case)]
pub fn bin_zpty(_nam: &str, args: &[String], ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
let argv: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
let args = &argv[..];
let mut cmds_guard = ptycmds().lock()
.unwrap_or_else(|e| { e.into_inner() });
let cmds: &mut HashMap<String, ptycmd> = &mut *cmds_guard;
let (status, output): (i32, String) = (|| {
let mut output = String::new();
if OPT_ISSET(ops, b'd') {
if args.is_empty() {
let names: Vec<String> = cmds.keys().cloned().collect();
for name in names {
if let Some(cmd) = cmds.remove(&name) {
unsafe { libc::kill(cmd.pid, libc::SIGTERM); }
unsafe { libc::close(cmd.master_fd); }
}
}
return (0, output);
}
for name in args {
if let Some(cmd) = cmds.remove(*name) {
unsafe { libc::kill(cmd.pid, libc::SIGTERM); }
unsafe { libc::close(cmd.master_fd); }
} else {
output.push_str(&format!("zpty: no such pty command: {}\n", name));
return (1, output);
}
}
return (0, output);
}
if OPT_ISSET(ops, b'L') {
for (name, cmd) in cmds.iter() {
let status = if cmd.finished {
"(finished)"
} else {
"(running)"
};
output.push_str(&format!("{}: {} {}\n", name, cmd.args.join(" "), status));
}
return (0, output);
}
if OPT_ISSET(ops, b'w') {
if args.len() < 2 {
return (1, "zpty: -w requires a pty name and data\n".to_string());
}
let name = args[0];
let data: String = args[1..].join(" ");
if let Some(cmd) = cmds.get(name) {
match ptywritestr(cmd.master_fd, &data) {
Ok(_) => (0, output),
Err(e) => (1, format!("zpty: write failed: {}\n", e)),
}
} else {
(1, format!("zpty: no such pty command: {}\n", name))
}
} else if OPT_ISSET(ops, b'r') {
if args.is_empty() {
return (1, "zpty: -r requires a pty name\n".to_string());
}
let name = args[0];
let pattern = OPT_ARG(ops, b'm');
let timeout: Option<i32> = OPT_ARG(ops, b'T').and_then(|s| s.parse().ok());
if let Some(cmd) = cmds.get(name) {
match ptyread(cmd.master_fd, pattern, timeout) {
Ok(data) => {
output.push_str(&data);
(0, output)
}
Err(e) => (1, format!("zpty: read failed: {}\n", e)),
}
} else {
(1, format!("zpty: no such pty command: {}\n", name))
}
} else if OPT_ISSET(ops, b't') {
if args.is_empty() {
return (1, "zpty: -t requires a pty name\n".to_string());
}
let name = args[0];
if let Some(cmd) = cmds.get(name) {
#[cfg(unix)]
{
let mut pfd = libc::pollfd {
fd: cmd.master_fd,
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pfd, 1, 0) };
if ret < 0 {
(1, format!("zpty: test failed: {}\n", io::Error::last_os_error()))
} else if ret > 0 {
(0, output)
} else {
(1, output)
}
}
#[cfg(not(unix))]
(0, output)
} else {
(1, format!("zpty: no such pty command: {}\n", name))
}
} else {
if args.len() < 2 {
return (1, "zpty: requires a name and command\n".to_string());
}
let name = args[0];
if cmds.get(name).is_some() {
return (1, format!("zpty: pty command {} already exists\n", name));
}
let cmd_args: Vec<String> = args[1..].iter().map(|s| s.to_string()).collect();
#[cfg(unix)]
{
match get_pty() {
Ok((master, slave)) => match unsafe { libc::fork() } {
-1 => {
unsafe { libc::close(master); }
unsafe { libc::close(slave); }
(
1,
format!("zpty: fork failed: {}\n", io::Error::last_os_error()),
)
}
0 => {
unsafe { libc::close(master); }
unsafe {
libc::setsid();
libc::dup2(slave, 0);
libc::dup2(slave, 1);
libc::dup2(slave, 2);
if slave > 2 {
libc::close(slave);
}
}
if !OPT_ISSET(ops, b'e') {
unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(0, &mut termios) >= 0 {
termios.c_lflag &= !libc::ECHO;
let _ = libc::tcsetattr(0, libc::TCSADRAIN, &termios);
}
}
}
let cmd = CString::new(cmd_args[0].clone()).unwrap();
let c_args: Vec<CString> = cmd_args
.iter()
.map(|s| CString::new(s.as_str()).unwrap())
.collect();
let c_args_ptrs: Vec<*const libc::c_char> = c_args
.iter()
.map(|s| s.as_ptr())
.chain(std::iter::once(std::ptr::null()))
.collect();
unsafe {
libc::execvp(cmd.as_ptr(), c_args_ptrs.as_ptr());
libc::_exit(1);
}
}
pid => {
unsafe { libc::close(slave); }
if !OPT_ISSET(ops, b'b') {
let _ = ptynonblock(master);
}
let pty_cmd =
ptycmd::new(name, cmd_args, master, pid,
OPT_ISSET(ops, b'e'),
!OPT_ISSET(ops, b'b'));
cmds.insert(pty_cmd.name.clone(), pty_cmd);
(0, output)
}
},
Err(e) => (1, format!("zpty: can't open pty: {}\n", e)),
}
}
#[cfg(not(unix))]
{
(1, "zpty: not supported on this platform\n".to_string())
}
}
})();
drop(cmds_guard);
if !output.is_empty() {
if status == 0 { print!("{}", output); } else { eprint!("{}", output); }
}
status
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_cmds_manager() {
let mut cmds = HashMap::<String, ptycmd>::new();
assert!(cmds.is_empty());
let cmd = ptycmd::new("test", vec!["echo".to_string()], 5, 1234, true, false);
cmds.insert(cmd.name.clone(), cmd);
assert_eq!(cmds.len(), 1);
assert!(cmds.get("test").is_some());
assert!(cmds.get("nonexistent").is_none());
let names: Vec<String> = cmds.keys().cloned().collect();
assert!(names.contains(&"test".to_string()));
cmds.remove("test");
assert!(cmds.is_empty());
}
#[test]
fn test_pty_cmd_fields() {
let cmd = ptycmd::new(
"mypty",
vec!["bash".to_string(), "-c".to_string()],
10,
5678,
false,
true,
);
assert_eq!(cmd.name, "mypty");
assert_eq!(cmd.args, vec!["bash", "-c"]);
assert_eq!(cmd.master_fd, 10);
assert_eq!(cmd.pid, 5678);
assert!(!cmd.echo);
assert!(cmd.nonblock);
assert!(!cmd.finished);
}
fn ops_with_flag(c: u8) -> crate::ported::zsh_h::options {
use crate::ported::zsh_h::{options, MAX_OPS};
let mut o = options { ind: [0u8; MAX_OPS], args: Vec::new(),
argscount: 0, argsalloc: 0 };
o.ind[c as usize] = 1;
o
}
#[test]
fn test_builtin_zpty_list_empty() {
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'L'), 0);
assert_eq!(status, 0);
}
#[test]
fn test_builtin_zpty_delete_all() {
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'd'), 0);
assert_eq!(status, 0);
}
#[test]
fn test_builtin_zpty_write_no_args() {
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b'w'), 0);
assert_eq!(status, 1);
}
#[test]
fn test_builtin_zpty_test_no_args() {
*ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let status = bin_zpty("zpty", &[], &ops_with_flag(b't'), 0);
assert_eq!(status, 1);
}
}
use std::sync::Mutex;
use std::sync::OnceLock;
use crate::ported::zsh_h::module;
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 { 0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 { *features = featuresarray(m, module_features());
0
}
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 { handlefeatures(m, module_features(), enables)
}
pub static PTYCMDS: std::sync::OnceLock<Mutex<HashMap<String, ptycmd>>> = std::sync::OnceLock::new();
fn ptycmds() -> &'static Mutex<HashMap<String, ptycmd>> {
PTYCMDS.get_or_init(|| Mutex::new(HashMap::<String, ptycmd>::new()))
}
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 { *ptycmds().lock().unwrap() = HashMap::<String, ptycmd>::new();
let _ = ptyhook(&mut ptycmds().lock().unwrap()); 0
}
pub fn cleanup_(m: *const module) -> i32 { deleteallptycmds(&mut ptycmds().lock().unwrap());
setfeatureenables(m, module_features(), None)
}
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 { 0
}
pub fn getptycmd<'a>(cmds: &'a HashMap<String, ptycmd>, name: &str) -> Option<&'a ptycmd> { cmds.get(name) }
pub fn deleteptycmd(cmds: &mut HashMap<String, ptycmd>, name: &str) { if let Some(cmd) = cmds.remove(name) { unsafe { libc::close(cmd.master_fd); }
unsafe { libc::kill(-cmd.pid, libc::SIGHUP); }
}
}
pub fn deleteallptycmds(cmds: &mut HashMap<String, ptycmd>) { let names: Vec<String> = cmds.keys().cloned().collect();
for n in names { deleteptycmd(cmds, &n); }
}
pub fn checkptycmd(cmd: &mut ptycmd) { if cmd.finished { return;
}
let mut c: u8 = 0;
let r = unsafe { libc::read(cmd.master_fd, &mut c as *mut u8 as *mut _, 1) };
if r <= 0 { if unsafe { libc::kill(cmd.pid, 0) } < 0 {
cmd.finished = true; unsafe { libc::close(cmd.master_fd); } }
return;
}
cmd.buffer.push(c);
}
pub fn ptygettyinfo(fd: i32, ti: &mut libc::termios) -> i32 { if fd == -1 { return 1; }
let r = unsafe { libc::tcgetattr(fd, ti as *mut libc::termios) };
if r == -1 { return 1; }
0 }
pub fn ptysettyinfo(fd: i32, ti: &libc::termios) { if fd == -1 { return;
}
unsafe { libc::tcsetattr(fd, libc::TCSADRAIN, ti as *const libc::termios); }
}
pub fn ptywrite(cmd: &ptycmd, args: &[&str], nonl: i32) -> i32 { if !args.is_empty() { for (i, a) in args.iter().enumerate() { let unmeta = crate::ported::utils::unmeta(a);
let bytes = unmeta.as_bytes();
let r = unsafe { libc::write(cmd.master_fd, bytes.as_ptr() as *const _, bytes.len()) };
if r < 0 { return 1; } if i + 1 < args.len() { let sp = b' ';
let r = unsafe { libc::write(cmd.master_fd, &sp as *const u8 as *const _, 1) };
if r < 0 { return 1; }
}
}
if nonl == 0 { let nl = b'\n'; let r = unsafe { libc::write(cmd.master_fd, &nl as *const u8 as *const _, 1) };
if r < 0 { return 1; } }
} else { let mut buf = [0u8; 4096];
loop {
let n = unsafe { libc::read(0, buf.as_mut_ptr() as *mut _, buf.len()) };
if n <= 0 { break; }
let r = unsafe { libc::write(cmd.master_fd, buf.as_ptr() as *const _, n as usize) };
if r < 0 { return 1; } }
}
0 }
pub fn ptyhook(cmds: &mut HashMap<String, ptycmd>) -> i32 { deleteallptycmds(cmds); 0 }
pub fn newptycmd(cmds: &mut HashMap<String, ptycmd>, _nam: &str, pname: &str, args: &[String], echo: bool, nblock: bool) -> i32 { if args.is_empty() { return 1; }
let cmd_path = &args[0];
let cmd_args = &args[1..];
let child = match Command::new(cmd_path)
.args(cmd_args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(c) => c,
Err(_) => return 1,
};
let pid = child.id() as i32;
let stdin = child.stdin.expect("piped").into_raw_fd();
let new = ptycmd::new(pname, args.to_vec(), stdin, pid, echo, nblock);
cmds.insert(new.name.clone(), new);
0
}
use crate::ported::zsh_h::features as features_t;
static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
fn module_features() -> &'static Mutex<features_t> {
MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 0,
n_abstract: 0,
}))
}
fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
vec!["b:zpty".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features_t>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 1]);
}
0
}
fn setfeatureenables(
_m: *const module,
_f: &Mutex<features_t>,
_e: Option<&[i32]>,
) -> i32 {
0
}