#[cfg(feature = "pty")]
mod pty {
use crate::platform::terminal::{
PtyBackend, PtyChild, PtyInterruptTarget, PtyMaster, PtySize, PtySlave,
};
use portable_pty::{
native_pty_system, Child as PortableChild, CommandBuilder, MasterPty,
PtySize as PortablePtySize, SlavePty,
};
use std::ffi::OsString;
use std::io::{self, Read, Write};
use std::path::Path;
pub struct PortablePtyBackend;
pub struct PortablePtyMaster(Box<dyn MasterPty + Send>);
pub struct PortablePtySlave(Box<dyn SlavePty + Send>);
pub struct PortablePtyChild(Box<dyn PortableChild + Send + Sync>);
impl PtyBackend for PortablePtyBackend {
type Master = PortablePtyMaster;
type Slave = PortablePtySlave;
fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)> {
let pair = native_pty_system()
.openpty(PortablePtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
.map_err(io::Error::other)?;
Ok((PortablePtyMaster(pair.master), PortablePtySlave(pair.slave)))
}
}
impl PtyMaster for PortablePtyMaster {
fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
self.0.try_clone_reader().map_err(io::Error::other)
}
fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
self.0.take_writer().map_err(io::Error::other)
}
fn resize(&self, size: PtySize) -> io::Result<()> {
self.0
.resize(PortablePtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
.map_err(io::Error::other)
}
fn get_size(&self) -> io::Result<PtySize> {
let size = self.0.get_size().map_err(io::Error::other)?;
Ok(PtySize {
rows: size.rows,
cols: size.cols,
pixel_width: size.pixel_width,
pixel_height: size.pixel_height,
})
}
fn process_group_leader(&self) -> Option<i32> {
self.0.process_group_leader()
}
fn as_raw_fd(&self) -> Option<i32> {
self.0.as_raw_fd()
}
fn interrupt_target(&self) -> io::Result<PtyInterruptTarget> {
if let Some(pid) = self.0.process_group_leader() {
return Ok(PtyInterruptTarget::new(move |_writer| {
super::super::unix_signal_process_group(
pid,
crate::platform::process::UnixSignalKind::Interrupt,
)?;
Ok(false)
}));
}
use std::os::fd::{AsRawFd as _, FromRawFd as _};
let fd = self
.0
.as_raw_fd()
.ok_or_else(|| io::Error::other("PTY master does not expose a Unix descriptor"))?;
let duplicated = unsafe { libc::dup(fd) };
if duplicated < 0 {
return Err(io::Error::last_os_error());
}
let owned = unsafe { std::os::fd::OwnedFd::from_raw_fd(duplicated) };
Ok(PtyInterruptTarget::new(move |writer| {
let _writer = match writer.try_lock() {
Ok(writer) => writer,
Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
Err(std::sync::TryLockError::Poisoned(_)) => {
return Err(io::Error::other("pty writer mutex poisoned"));
}
};
super::write_nonblocking_byte(owned.as_raw_fd(), 0x03)?;
Ok(true)
}))
}
fn kill_process_group(&self) -> io::Result<()> {
match self.0.process_group_leader() {
Some(pid) => super::super::unix_signal_process_group(
pid,
crate::platform::process::UnixSignalKind::Kill,
),
None => Ok(()),
}
}
fn preferred_pid(&self, child: &dyn PtyChild) -> Option<u32> {
self.0
.process_group_leader()
.and_then(|pid| u32::try_from(pid).ok())
.or_else(|| Some(child.pid()))
}
}
impl PtySlave for PortablePtySlave {
type Child = PortablePtyChild;
fn spawn(
self,
argv: &[OsString],
cwd: Option<&Path>,
env: Option<&[(OsString, OsString)]>,
) -> io::Result<Self::Child> {
if argv.is_empty() {
return Err(io::Error::other("portable-pty spawn requires non-empty argv"));
}
let mut command = CommandBuilder::new(&argv[0]);
for arg in &argv[1..] {
command.arg(arg);
}
if let Some(cwd) = cwd {
command.cwd(cwd);
}
if let Some(env) = env {
command.env_clear();
for (key, value) in env {
command.env(key, value);
}
}
let child = self.0.spawn_command(command).map_err(io::Error::other)?;
Ok(PortablePtyChild(child))
}
}
impl PtyChild for PortablePtyChild {
fn pid(&self) -> u32 {
self.0.process_id().unwrap_or(0)
}
fn try_wait(&mut self) -> io::Result<Option<u32>> {
self.0
.try_wait()
.map(|status| status.map(|status| status.exit_code()))
}
fn wait(&mut self) -> io::Result<u32> {
self.0.wait().map(|status| status.exit_code())
}
fn kill(&mut self) -> io::Result<()> {
self.0.kill()
}
}
pub type Backend = PortablePtyBackend;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConPtyBackendKind {
Unavailable,
}
pub fn current_backend_kind() -> ConPtyBackendKind {
ConPtyBackendKind::Unavailable
}
}
#[cfg(feature = "pty")]
pub use pty::*;
#[cfg(feature = "pty")]
use crate::platform::process::UnixSignalKind;
#[cfg(feature = "pty")]
use crate::platform::terminal::PtyInputChunk;
#[cfg(feature = "pty")]
pub struct PtySpawnContext;
#[cfg(feature = "pty")]
pub struct PtyProcessGuard;
#[cfg(feature = "pty")]
impl PtyProcessGuard {
pub fn assign_pid(&self, _pid: u32) -> std::io::Result<()> { Ok(()) }
}
#[cfg(feature = "pty")]
impl Drop for PtyProcessGuard {
fn drop(&mut self) {}
}
#[cfg(feature = "pty")]
#[derive(Debug, Clone)]
pub struct ChildProcessInfo {
pub pid: u32,
pub name: String,
}
#[cfg(feature = "pty")]
#[derive(Debug, Clone)]
pub struct OrphanConhostInfo {
pub pid: u32,
pub parent_pid: u32,
pub parent_name: String,
}
#[cfg(feature = "pty")]
pub fn before_pty_spawn() -> PtySpawnContext {
PtySpawnContext
}
#[cfg(feature = "pty")]
pub fn prepare_unmanaged_pty_child(
_context: PtySpawnContext,
_nice: Option<i32>,
) -> std::io::Result<PtyProcessGuard> {
Ok(PtyProcessGuard)
}
#[cfg(feature = "pty")]
pub fn input_payload(data: &[u8]) -> Vec<u8> {
data.to_vec()
}
#[cfg(feature = "pty")]
pub fn query_responses(_data: &[u8]) -> Vec<Vec<u8>> {
Vec::new()
}
#[cfg(feature = "pty")]
pub fn shell_argv(command: &str) -> Vec<String> {
vec!["/bin/sh".into(), "-c".into(), command.into()]
}
#[cfg(feature = "pty")]
pub fn wait_before_pty_close_supported() -> bool { true }
#[cfg(feature = "pty")]
pub fn is_ignorable_process_control_error(error: &std::io::Error) -> bool {
matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
) || error.raw_os_error() == Some(libc::ESRCH)
}
#[cfg(feature = "pty")]
fn set_fd_flags(fd: i32, flags: libc::c_int) -> std::io::Result<()> {
loop {
if unsafe { libc::fcntl(fd, libc::F_SETFL, flags) } != -1 {
return Ok(());
}
let error = std::io::Error::last_os_error();
if error.kind() != std::io::ErrorKind::Interrupted {
return Err(error);
}
}
}
#[cfg(feature = "pty")]
fn write_nonblocking_byte(fd: i32, byte: u8) -> std::io::Result<()> {
let original_flags = loop {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags != -1 {
break flags;
}
let error = std::io::Error::last_os_error();
if error.kind() != std::io::ErrorKind::Interrupted {
return Err(error);
}
};
set_fd_flags(fd, original_flags | libc::O_NONBLOCK)?;
let written = unsafe { libc::write(fd, (&byte as *const u8).cast(), 1) };
let result = if written == 1 {
Ok(())
} else {
let error = std::io::Error::last_os_error();
if written == -1
&& matches!(
error.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
)
{
Ok(())
} else if written == -1 {
Err(error)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::WriteZero,
"PTY interrupt fallback wrote zero bytes",
))
}
};
let restore = set_fd_flags(fd, original_flags);
restore.and(result)
}
#[cfg(feature = "pty")]
pub fn terminate_pty_child(pid: u32) -> std::io::Result<bool> {
super::unix_signal_process(pid, UnixSignalKind::Terminate)?;
Ok(false)
}
#[cfg(feature = "pty")]
fn descendant_pids(system: &sysinfo::System, pid: sysinfo::Pid) -> Vec<sysinfo::Pid> {
let mut children = std::collections::HashMap::<sysinfo::Pid, Vec<sysinfo::Pid>>::new();
for (child_pid, process) in system.processes() {
if let Some(parent) = process.parent() {
children.entry(parent).or_default().push(*child_pid);
}
}
let mut descendants = Vec::new();
let mut stack = vec![pid];
while let Some(current) = stack.pop() {
if let Some(direct) = children.get(¤t) {
for &child in direct {
descendants.push(child);
stack.push(child);
}
}
}
descendants
}
#[cfg(feature = "pty")]
pub fn signal_pty_tree(pid: u32, force: bool) -> std::io::Result<bool> {
let system = sysinfo::System::new_all();
let root = sysinfo::Pid::from_u32(pid);
if system.process(root).is_none() {
return Ok(false);
}
let mut targets = descendant_pids(&system, root);
targets.reverse();
targets.push(root);
let signal = if force {
UnixSignalKind::Kill
} else {
UnixSignalKind::Terminate
};
for target in targets {
if let Err(error) = super::unix_signal_process(target.as_u32(), signal) {
if !is_ignorable_process_control_error(&error) {
return Err(error);
}
}
}
Ok(false)
}
#[cfg(feature = "pty")]
pub fn resize_pty(
master: &dyn crate::platform::terminal::PtyMaster,
size: crate::platform::terminal::PtySize,
) -> std::io::Result<()> {
master.resize(size)
}
#[cfg(feature = "pty")]
pub fn find_child_processes(_parent_pid: u32) -> Vec<ChildProcessInfo> {
Vec::new()
}
#[cfg(feature = "pty")]
pub fn find_orphan_conhosts() -> Vec<OrphanConhostInfo> {
Vec::new()
}
#[cfg(feature = "pty")]
pub struct TerminalInputSession {
stdin_fd: i32,
original_mode: libc::termios,
}
#[cfg(feature = "pty")]
impl TerminalInputSession {
pub fn new() -> std::io::Result<Option<Self>> {
let stdin_fd = libc::STDIN_FILENO;
if unsafe { libc::isatty(stdin_fd) } != 1 {
return Ok(None);
}
let mut original_mode = std::mem::MaybeUninit::<libc::termios>::uninit();
if unsafe { libc::tcgetattr(stdin_fd, original_mode.as_mut_ptr()) } != 0 {
return Err(std::io::Error::last_os_error());
}
let original_mode = unsafe { original_mode.assume_init() };
let mut raw_mode = original_mode;
unsafe { libc::cfmakeraw(&mut raw_mode) };
if unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw_mode) } != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(Some(Self {
stdin_fd,
original_mode,
}))
}
pub fn read_chunk(&self, timeout: std::time::Duration) -> std::io::Result<Option<PtyInputChunk>> {
let timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
let mut pollfd = libc::pollfd {
fd: self.stdin_fd,
events: libc::POLLIN,
revents: 0,
};
let ready = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
if ready < 0 {
let error = std::io::Error::last_os_error();
return if error.kind() == std::io::ErrorKind::Interrupted {
Ok(None)
} else {
Err(error)
};
}
if ready == 0 || pollfd.revents & libc::POLLIN == 0 {
return Ok(None);
}
let mut buffer = vec![0_u8; 65536];
let count = unsafe { libc::read(self.stdin_fd, buffer.as_mut_ptr().cast(), buffer.len()) };
if count <= 0 {
return Ok(None);
}
buffer.truncate(count as usize);
Ok(Some(PtyInputChunk {
submit: buffer.iter().any(|byte| matches!(*byte, b'\r' | b'\n')),
data: buffer,
}))
}
}
#[cfg(feature = "pty")]
impl Drop for TerminalInputSession {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(self.stdin_fd, libc::TCSANOW, &self.original_mode);
}
}
}
pub fn active_graphics_probe(
timeout: std::time::Duration,
) -> crate::platform::terminal::TerminalGraphicsProbe {
use std::fs::OpenOptions;
use std::io::{Read as _, Write as _};
use std::os::fd::AsRawFd as _;
use std::time::Instant;
let Ok(mut tty) = OpenOptions::new().read(true).write(true).open("/dev/tty") else {
return crate::platform::terminal::TerminalGraphicsProbe::default();
};
let fd = tty.as_raw_fd();
let mut old_termios = std::mem::MaybeUninit::<libc::termios>::uninit();
let have_termios = unsafe { libc::tcgetattr(fd, old_termios.as_mut_ptr()) == 0 };
let old_termios = have_termios.then(|| unsafe { old_termios.assume_init() });
if let Some(mut raw) = old_termios {
raw.c_lflag &= !(libc::ICANON | libc::ECHO);
raw.c_cc[libc::VMIN] = 0;
raw.c_cc[libc::VTIME] = 0;
let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) };
}
let old_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if old_flags >= 0 {
let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags | libc::O_NONBLOCK) };
}
let _ = tty.write_all(
b"\x1b[c\x1b[?2;1;0S\x1b_Gi=running-process-probe,a=q;\x1b\\\x1b]1337;Capabilities\x07",
);
let _ = tty.flush();
let deadline = Instant::now() + timeout;
let mut bytes = Vec::new();
while Instant::now() < deadline {
let mut chunk = [0_u8; 512];
match tty.read(&mut chunk) {
Ok(0) => std::thread::sleep(std::time::Duration::from_millis(5)),
Ok(count) => bytes.extend_from_slice(&chunk[..count]),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
std::thread::sleep(std::time::Duration::from_millis(5));
}
Err(_) => break,
}
}
if old_flags >= 0 {
let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags) };
}
if let Some(old) = old_termios {
let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
}
let reply = String::from_utf8_lossy(&bytes).into_owned();
crate::platform::terminal::TerminalGraphicsProbe {
sixel_xtsmgraphics: reply.contains('S').then(|| reply.clone()),
sixel_da1: reply.contains("[?").then(|| reply.clone()),
kitty_graphics: reply.contains("_G").then(|| reply.clone()),
iterm2_capabilities: reply.contains("Capabilities=").then_some(reply),
}
}
#[cfg(all(test, feature = "pty"))]
mod tests {
use super::*;
use crate::platform::terminal::{PtyInterruptTarget, PtyMaster, PtySize};
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct NoGroupMaster(OwnedFd);
impl PtyMaster for NoGroupMaster {
fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "unused by test"))
}
fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
Err(io::Error::new(io::ErrorKind::Unsupported, "unused by test"))
}
fn resize(&self, _size: PtySize) -> io::Result<()> {
Ok(())
}
fn get_size(&self) -> io::Result<PtySize> {
Ok(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})
}
fn interrupt_target(&self) -> io::Result<PtyInterruptTarget> {
let duplicated = unsafe { libc::dup(self.0.as_raw_fd()) };
if duplicated < 0 {
return Err(io::Error::last_os_error());
}
let owned = unsafe { OwnedFd::from_raw_fd(duplicated) };
Ok(PtyInterruptTarget::new(move |writer| {
let _writer = match writer.try_lock() {
Ok(writer) => writer,
Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
Err(std::sync::TryLockError::Poisoned(_)) => {
return Err(io::Error::other("pty writer mutex poisoned"));
}
};
write_nonblocking_byte(owned.as_raw_fd(), 0x03)?;
Ok(true)
}))
}
}
fn open_full_pty_input_queue() -> (OwnedFd, OwnedFd) {
let mut master = -1;
let mut slave = -1;
assert_eq!(
unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
},
0,
"openpty failed: {}",
io::Error::last_os_error()
);
let master = unsafe { OwnedFd::from_raw_fd(master) };
let slave = unsafe { OwnedFd::from_raw_fd(slave) };
let mut termios = std::mem::MaybeUninit::<libc::termios>::uninit();
assert_eq!(
unsafe { libc::tcgetattr(slave.as_raw_fd(), termios.as_mut_ptr()) },
0
);
let mut termios = unsafe { termios.assume_init() };
unsafe { libc::cfmakeraw(&mut termios) };
assert_eq!(
unsafe { libc::tcsetattr(slave.as_raw_fd(), libc::TCSANOW, &termios) },
0
);
let flags = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
assert_ne!(flags, -1);
assert_ne!(
unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) },
-1
);
let chunk = [b'x'; 1024];
loop {
let written =
unsafe { libc::write(master.as_raw_fd(), chunk.as_ptr().cast(), chunk.len()) };
if written >= 0 {
continue;
}
assert_eq!(io::Error::last_os_error().kind(), io::ErrorKind::WouldBlock);
break;
}
assert_ne!(
unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags) },
-1
);
(master, slave)
}
#[test]
fn interrupt_fallback_does_not_block_on_full_pty_input_queue() {
let (master, _slave) = open_full_pty_input_queue();
let started = Instant::now();
write_nonblocking_byte(master.as_raw_fd(), 0x03)
.expect("a full input queue is an expected best-effort drop");
assert!(started.elapsed() < Duration::from_secs(1));
}
#[test]
fn nonblocking_interrupt_write_restores_descriptor_flags() {
let (master, _slave) = open_full_pty_input_queue();
let before = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
assert_ne!(before, -1);
write_nonblocking_byte(master.as_raw_fd(), 0x03)
.expect("a full input queue is an expected best-effort drop");
let after = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
assert_eq!(after, before, "fallback changed the PTY descriptor flags");
}
#[test]
fn nonblocking_interrupt_write_reports_fcntl_failure() {
let error = write_nonblocking_byte(-1, 0x03).expect_err("invalid fd must fail");
assert_eq!(error.raw_os_error(), Some(libc::EBADF));
}
#[test]
fn interrupt_fallback_does_not_wait_for_busy_writer_mutex() {
let (master, _slave) = open_full_pty_input_queue();
let writer_fd = unsafe { libc::dup(master.as_raw_fd()) };
assert_ne!(writer_fd, -1);
let writer = Arc::new(Mutex::new(
Box::new(unsafe { File::from_raw_fd(writer_fd) }) as Box<dyn Write + Send>,
));
let writer_guard = writer.lock().expect("writer mutex");
let started = Instant::now();
let target = NoGroupMaster(master)
.interrupt_target()
.expect("prepare interrupt target");
let wrote_fallback = target
.send(&writer)
.expect("busy writer fallback should remain best-effort");
assert!(!wrote_fallback);
assert!(started.elapsed() < Duration::from_secs(1));
drop(writer_guard);
}
}