1use std::os::fd::{AsFd, AsRawFd, OwnedFd};
7use std::os::unix::process::CommandExt;
8use std::path::Path;
9use std::process::{Child, Command, Stdio};
10
11use nix::fcntl;
12use nix::libc;
13use nix::pty::openpty;
14
15pub struct PtyMaster {
17 pub master: OwnedFd,
19 pub child: Child,
21}
22
23impl PtyMaster {
24 pub fn spawn(shell: &str, pwd: Option<&Path>) -> std::io::Result<Self> {
29 let pty = openpty(None, None).map_err(std::io::Error::other)?;
30
31 let slave_out = pty.slave.try_clone()?;
32 let slave_err = pty.slave.try_clone()?;
33
34 let mut cmd = Command::new(shell);
35 cmd.stdin(Stdio::from(pty.slave))
36 .stdout(Stdio::from(slave_out))
37 .stderr(Stdio::from(slave_err))
38 .env("TERM", "xterm-256color")
39 .env("COLORTERM", "truecolor");
40
41 if let Some(dir) = pwd {
42 cmd.current_dir(dir);
43 }
44
45 let child = unsafe {
48 cmd.pre_exec(|| {
49 if libc::setsid() == -1 {
50 return Err(std::io::Error::last_os_error());
51 }
52 if libc::ioctl(libc::STDIN_FILENO, libc::TIOCSCTTY as _, 0) == -1 {
53 return Err(std::io::Error::last_os_error());
54 }
55 Ok(())
56 })
57 .spawn()?
58 };
59
60 let flags =
62 fcntl::fcntl(&pty.master, fcntl::FcntlArg::F_GETFL).map_err(std::io::Error::other)?;
63 let mut flags = fcntl::OFlag::from_bits_truncate(flags);
64 flags.insert(fcntl::OFlag::O_NONBLOCK);
65 fcntl::fcntl(&pty.master, fcntl::FcntlArg::F_SETFL(flags))
66 .map_err(std::io::Error::other)?;
67
68 Ok(PtyMaster {
69 master: pty.master,
70 child,
71 })
72 }
73}
74
75pub fn set_window_size(fd: impl AsFd, rows: u16, cols: u16) -> std::io::Result<()> {
80 let ws = libc::winsize {
81 ws_row: rows,
82 ws_col: cols,
83 ws_xpixel: 0,
84 ws_ypixel: 0,
85 };
86 let ret = unsafe { libc::ioctl(fd.as_fd().as_raw_fd(), libc::TIOCSWINSZ as _, &ws) };
89 if ret == -1 {
90 Err(std::io::Error::last_os_error())
91 } else {
92 Ok(())
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn test_spawn_and_child_alive() {
102 let mut pty = PtyMaster::spawn("/bin/sh", None).expect("spawn /bin/sh");
103 assert!(
105 pty.child.try_wait().unwrap().is_none(),
106 "child should be alive"
107 );
108 let _ = pty.child.kill();
110 let _ = pty.child.wait();
111 }
112
113 #[test]
114 fn test_set_window_size() {
115 let mut pty = PtyMaster::spawn("/bin/sh", None).expect("spawn /bin/sh");
116 set_window_size(&pty.master, 40, 120).expect("set_window_size should succeed");
117 let _ = pty.child.kill();
118 let _ = pty.child.wait();
119 }
120
121 #[test]
122 fn test_spawn_with_pwd() {
123 let dir = std::env::temp_dir();
124 let mut pty = PtyMaster::spawn("/bin/sh", Some(dir.as_path())).expect("spawn with pwd");
125 assert!(
126 pty.child.try_wait().unwrap().is_none(),
127 "child should be alive"
128 );
129 let _ = pty.child.kill();
130 let _ = pty.child.wait();
131 }
132}