Add execvp and TIOCSWINSZ to PtySpawner::spawn

Change spawn() to spawn(cmd, args): on the parent side set the PTY
window size via TIOCSWINSZ mirroring stdin or defaulting to 80x24; on
the child side call execvp with the given command after login_tty.
Update the test to exercise /bin/true through the full exec path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-06-07 16:41:49 -04:00
parent 1be8048845
commit ffbe0f298e

View file

@ -1,6 +1,7 @@
use nix::pty::{openpty, OpenptyResult};
use nix::unistd::{fork, ForkResult, Pid};
use std::os::unix::io::{IntoRawFd, OwnedFd};
use nix::unistd::{execvp, fork, ForkResult, Pid};
use std::ffi::{CStr, CString};
use std::os::unix::io::{AsRawFd, IntoRawFd, OwnedFd};
use crate::error::{Error, Result};
@ -9,14 +10,44 @@ pub struct PtySpawner {
pub child_pid: Pid,
}
/// Read the window size from `fd`, falling back to 80×24 if it is not a tty.
fn get_winsize(fd: i32) -> libc::winsize {
let mut ws = libc::winsize {
ws_row: 24,
ws_col: 80,
ws_xpixel: 0,
ws_ypixel: 0,
};
// SAFETY: TIOCGWINSZ is a read ioctl; `ws` lives on the stack for its duration.
unsafe {
libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws);
}
if ws.ws_row == 0 {
ws.ws_row = 24;
}
if ws.ws_col == 0 {
ws.ws_col = 80;
}
ws
}
impl PtySpawner {
/// Open a PTY pair, fork, and call `login_tty` on the child so it becomes
/// the controlling terminal session leader. The child exits immediately
/// after login_tty. No exec is performed — that is deferred to a later phase.
pub fn spawn() -> Result<Self> {
/// Open a PTY pair, fork, set the PTY window size, call `login_tty` in the
/// child to make the slave the controlling terminal, then `execvp` `cmd`.
///
/// `args` contains only the arguments to the program — not argv\[0\].
/// argv\[0\] is set to `cmd` internally.
pub fn spawn(cmd: &CStr, args: &[CString]) -> Result<Self> {
let OpenptyResult { master, slave } = openpty(None, None)
.map_err(|e| Error::Internal(anyhow::anyhow!("openpty failed: {e}")))?;
// Mirror the controlling terminal's window size onto the PTY, or default 80×24.
let ws = get_winsize(libc::STDIN_FILENO);
// SAFETY: master is a valid PTY master fd; TIOCSWINSZ is a write ioctl.
unsafe {
libc::ioctl(master.as_raw_fd(), libc::TIOCSWINSZ, &ws);
}
// SAFETY: fork is async-signal-safe; no threads exist at this point in
// the single-threaded call path.
let fork_result = unsafe { fork() }
@ -32,11 +63,19 @@ impl PtySpawner {
}
ForkResult::Child => {
drop(master);
// login_tty(3): setsid, make slave the ctty, dup to stdio, close slave.
let slave_fd = slave.into_raw_fd();
// SAFETY: in child immediately after fork, single-threaded.
unsafe { libc::login_tty(slave_fd) };
std::process::exit(0);
// login_tty(3): setsid, make slave the ctty, dup2 to stdio, close slave.
// SAFETY: child is single-threaded immediately after fork.
if unsafe { libc::login_tty(slave_fd) } != 0 {
unsafe { libc::_exit(127) };
}
// Build full argv: [cmd, args...].
let mut argv: Vec<&CStr> = Vec::with_capacity(args.len() + 1);
argv.push(cmd);
argv.extend(args.iter().map(CString::as_c_str));
// execvp replaces the process image; it only returns on error.
let _ = execvp(cmd, &argv);
unsafe { libc::_exit(127) };
}
}
}
@ -48,8 +87,9 @@ mod tests {
use nix::sys::wait::{waitpid, WaitStatus};
#[test]
fn fork_and_login_tty_does_not_panic() {
let spawner = PtySpawner::spawn().expect("PtySpawner::spawn should succeed");
fn spawn_bin_true_exits_zero() {
let cmd = CString::new("/bin/true").unwrap();
let spawner = PtySpawner::spawn(&cmd, &[]).expect("PtySpawner::spawn should succeed");
let status = waitpid(spawner.child_pid, None).expect("waitpid should succeed");
match status {