1#[cfg(feature = "pty")]
4mod pty {
5use crate::platform::terminal::{
6 PtyBackend, PtyChild, PtyChildControlToken, PtyMaster, PtyMasterControlToken, PtySize, PtySlave,
7};
8use portable_pty::{
9 native_pty_system, Child as PortableChild, CommandBuilder, MasterPty,
10 PtySize as PortablePtySize, SlavePty,
11};
12use std::ffi::OsString;
13use std::io::{self, Read, Write};
14use std::path::Path;
15
16pub struct PortablePtyBackend;
17pub struct PortablePtyMaster(Box<dyn MasterPty + Send>);
18pub struct PortablePtySlave(Box<dyn SlavePty + Send>);
19pub struct PortablePtyChild(Box<dyn PortableChild + Send + Sync>);
20
21impl PtyBackend for PortablePtyBackend {
22 type Master = PortablePtyMaster;
23 type Slave = PortablePtySlave;
24
25 fn openpty(size: PtySize) -> io::Result<(Self::Master, Self::Slave)> {
26 let pair = native_pty_system()
27 .openpty(PortablePtySize {
28 rows: size.rows,
29 cols: size.cols,
30 pixel_width: size.pixel_width,
31 pixel_height: size.pixel_height,
32 })
33 .map_err(io::Error::other)?;
34 Ok((PortablePtyMaster(pair.master), PortablePtySlave(pair.slave)))
35 }
36}
37
38impl PtyMaster for PortablePtyMaster {
39 fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
40 self.0.try_clone_reader().map_err(io::Error::other)
41 }
42
43 fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
44 self.0.take_writer().map_err(io::Error::other)
45 }
46
47 fn resize(&self, size: PtySize) -> io::Result<()> {
48 self.0
49 .resize(PortablePtySize {
50 rows: size.rows,
51 cols: size.cols,
52 pixel_width: size.pixel_width,
53 pixel_height: size.pixel_height,
54 })
55 .map_err(io::Error::other)
56 }
57
58 fn get_size(&self) -> io::Result<PtySize> {
59 let size = self.0.get_size().map_err(io::Error::other)?;
60 Ok(PtySize {
61 rows: size.rows,
62 cols: size.cols,
63 pixel_width: size.pixel_width,
64 pixel_height: size.pixel_height,
65 })
66 }
67
68 fn control_token(&self) -> PtyMasterControlToken {
69 PtyMasterControlToken {
70 process_group_leader: self.0.process_group_leader(),
71 raw_fd: self.0.as_raw_fd(),
72 }
73 }
74}
75
76impl PtySlave for PortablePtySlave {
77 type Child = PortablePtyChild;
78
79 fn spawn(
80 self,
81 argv: &[OsString],
82 cwd: Option<&Path>,
83 env: Option<&[(OsString, OsString)]>,
84 ) -> io::Result<Self::Child> {
85 if argv.is_empty() {
86 return Err(io::Error::other("portable-pty spawn requires non-empty argv"));
87 }
88 let mut command = CommandBuilder::new(&argv[0]);
89 for arg in &argv[1..] {
90 command.arg(arg);
91 }
92 if let Some(cwd) = cwd {
93 command.cwd(cwd);
94 }
95 if let Some(env) = env {
96 command.env_clear();
97 for (key, value) in env {
98 command.env(key, value);
99 }
100 }
101 let child = self.0.spawn_command(command).map_err(io::Error::other)?;
102 Ok(PortablePtyChild(child))
103 }
104}
105
106impl PtyChild for PortablePtyChild {
107 fn pid(&self) -> u32 {
108 self.0.process_id().unwrap_or(0)
109 }
110
111 fn try_wait(&mut self) -> io::Result<Option<u32>> {
112 self.0
113 .try_wait()
114 .map(|status| status.map(|status| status.exit_code()))
115 }
116
117 fn wait(&mut self) -> io::Result<u32> {
118 self.0.wait().map(|status| status.exit_code())
119 }
120
121 fn kill(&mut self) -> io::Result<()> {
122 self.0.kill()
123 }
124
125 fn control_token(&self) -> PtyChildControlToken {
126 PtyChildControlToken::default()
127 }
128}
129
130pub type Backend = PortablePtyBackend;
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum ConPtyBackendKind {
134 Unavailable,
135}
136
137pub fn current_backend_kind() -> ConPtyBackendKind {
138 ConPtyBackendKind::Unavailable
139}
140}
141
142#[cfg(feature = "pty")]
143pub use pty::*;
144
145#[cfg(feature = "pty")]
146use crate::platform::process::UnixSignalKind;
147#[cfg(feature = "pty")]
148use crate::platform::terminal::{PtyInputChunk, SharedPtyWriter};
149
150#[cfg(feature = "pty")]
151pub struct PtySpawnContext;
152
153#[cfg(feature = "pty")]
154pub struct PtyProcessGuard;
155
156#[cfg(feature = "pty")]
157impl PtyProcessGuard {
158 pub fn assign_pid(&self, _pid: u32) -> std::io::Result<()> { Ok(()) }
159}
160
161#[cfg(feature = "pty")]
162impl Drop for PtyProcessGuard {
163 fn drop(&mut self) {}
164}
165
166#[cfg(feature = "pty")]
167#[derive(Debug, Clone)]
168pub struct ChildProcessInfo {
169 pub pid: u32,
170 pub name: String,
171}
172
173#[cfg(feature = "pty")]
174#[derive(Debug, Clone)]
175pub struct OrphanConhostInfo {
176 pub pid: u32,
177 pub parent_pid: u32,
178 pub parent_name: String,
179}
180
181#[cfg(feature = "pty")]
182pub fn before_pty_spawn() -> PtySpawnContext {
183 PtySpawnContext
184}
185
186#[cfg(feature = "pty")]
187pub fn prepare_pty_child(
188 _context: PtySpawnContext,
189 _child: crate::platform::terminal::PtyChildControlToken,
190 _nice: Option<i32>,
191) -> std::io::Result<PtyProcessGuard> {
192 Ok(PtyProcessGuard)
193}
194
195#[cfg(feature = "pty")]
196pub fn input_payload(data: &[u8]) -> Vec<u8> {
197 data.to_vec()
198}
199
200#[cfg(feature = "pty")]
201pub fn query_responses(_data: &[u8]) -> Vec<Vec<u8>> {
202 Vec::new()
203}
204
205#[cfg(feature = "pty")]
206pub fn shell_argv(command: &str) -> Vec<String> {
207 vec!["/bin/sh".into(), "-c".into(), command.into()]
208}
209
210#[cfg(feature = "pty")]
211pub fn wait_before_pty_close_supported() -> bool { true }
212
213#[cfg(feature = "pty")]
214pub fn is_ignorable_process_control_error(error: &std::io::Error) -> bool {
215 matches!(
216 error.kind(),
217 std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
218 ) || error.raw_os_error() == Some(libc::ESRCH)
219}
220
221#[cfg(feature = "pty")]
222fn set_fd_flags(fd: i32, flags: libc::c_int) -> std::io::Result<()> {
223 loop {
224 if unsafe { libc::fcntl(fd, libc::F_SETFL, flags) } != -1 {
225 return Ok(());
226 }
227 let error = std::io::Error::last_os_error();
228 if error.kind() != std::io::ErrorKind::Interrupted {
229 return Err(error);
230 }
231 }
232}
233
234#[cfg(feature = "pty")]
235fn write_nonblocking_byte(fd: i32, byte: u8) -> std::io::Result<()> {
236 let original_flags = loop {
237 let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
238 if flags != -1 {
239 break flags;
240 }
241 let error = std::io::Error::last_os_error();
242 if error.kind() != std::io::ErrorKind::Interrupted {
243 return Err(error);
244 }
245 };
246 set_fd_flags(fd, original_flags | libc::O_NONBLOCK)?;
247 let written = unsafe { libc::write(fd, (&byte as *const u8).cast(), 1) };
248 let result = if written == 1 {
249 Ok(())
250 } else {
251 let error = std::io::Error::last_os_error();
252 if written == -1
253 && matches!(
254 error.kind(),
255 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted
256 )
257 {
258 Ok(())
259 } else if written == -1 {
260 Err(error)
261 } else {
262 Err(std::io::Error::new(
263 std::io::ErrorKind::WriteZero,
264 "PTY interrupt fallback wrote zero bytes",
265 ))
266 }
267 };
268 let restore = set_fd_flags(fd, original_flags);
269 restore.and(result)
270}
271
272#[cfg(feature = "pty")]
273pub fn send_pty_interrupt(
274 target: crate::platform::terminal::PtyMasterControlToken,
275 writer: &SharedPtyWriter,
276) -> std::io::Result<bool> {
277 if let Some(pid) = target.process_group_leader {
278 super::unix_signal_process_group(pid, UnixSignalKind::Interrupt)?;
279 return Ok(false);
280 }
281 let _writer = match writer.try_lock() {
282 Ok(writer) => writer,
283 Err(std::sync::TryLockError::WouldBlock) => return Ok(false),
284 Err(std::sync::TryLockError::Poisoned(_)) => {
285 return Err(std::io::Error::other("pty writer mutex poisoned"));
286 }
287 };
288 let fd = target
289 .raw_fd
290 .ok_or_else(|| std::io::Error::other("PTY master does not expose a Unix descriptor"))?;
291 write_nonblocking_byte(fd, 0x03)?;
292 Ok(true)
293}
294
295#[cfg(feature = "pty")]
296pub fn terminate_pty_child(pid: u32) -> std::io::Result<bool> {
297 super::unix_signal_process(pid, UnixSignalKind::Terminate)?;
298 Ok(false)
299}
300
301#[cfg(feature = "pty")]
302fn descendant_pids(system: &sysinfo::System, pid: sysinfo::Pid) -> Vec<sysinfo::Pid> {
303 let mut children = std::collections::HashMap::<sysinfo::Pid, Vec<sysinfo::Pid>>::new();
304 for (child_pid, process) in system.processes() {
305 if let Some(parent) = process.parent() {
306 children.entry(parent).or_default().push(*child_pid);
307 }
308 }
309 let mut descendants = Vec::new();
310 let mut stack = vec![pid];
311 while let Some(current) = stack.pop() {
312 if let Some(direct) = children.get(¤t) {
313 for &child in direct {
314 descendants.push(child);
315 stack.push(child);
316 }
317 }
318 }
319 descendants
320}
321
322#[cfg(feature = "pty")]
323pub fn signal_pty_tree(pid: u32, force: bool) -> std::io::Result<bool> {
324 let system = sysinfo::System::new_all();
325 let root = sysinfo::Pid::from_u32(pid);
326 if system.process(root).is_none() {
327 return Ok(false);
328 }
329 let mut targets = descendant_pids(&system, root);
330 targets.reverse();
331 targets.push(root);
332 let signal = if force {
333 UnixSignalKind::Kill
334 } else {
335 UnixSignalKind::Terminate
336 };
337 for target in targets {
338 if let Err(error) = super::unix_signal_process(target.as_u32(), signal) {
339 if !is_ignorable_process_control_error(&error) {
340 return Err(error);
341 }
342 }
343 }
344 Ok(false)
345}
346
347#[cfg(feature = "pty")]
348pub fn resize_pty(
349 master: &dyn crate::platform::terminal::PtyMaster,
350 size: crate::platform::terminal::PtySize,
351) -> std::io::Result<()> {
352 master.resize(size)
353}
354
355#[cfg(feature = "pty")]
356pub fn preferred_pty_pid(
357 master: &dyn crate::platform::terminal::PtyMaster,
358 child: &dyn crate::platform::terminal::PtyChild,
359) -> Option<u32> {
360 master
361 .control_token()
362 .process_group_leader
363 .and_then(|pid| u32::try_from(pid).ok())
364 .or_else(|| Some(child.pid()))
365}
366
367#[cfg(feature = "pty")]
368pub fn kill_pty_process_group(
369 target: crate::platform::terminal::PtyMasterControlToken,
370) -> std::io::Result<()> {
371 match target.process_group_leader {
372 Some(pid) => super::unix_signal_process_group(pid, UnixSignalKind::Kill),
373 None => Ok(()),
374 }
375}
376
377#[cfg(feature = "pty")]
378pub fn find_child_processes(_parent_pid: u32) -> Vec<ChildProcessInfo> {
379 Vec::new()
380}
381
382#[cfg(feature = "pty")]
383pub fn find_orphan_conhosts() -> Vec<OrphanConhostInfo> {
384 Vec::new()
385}
386
387#[cfg(feature = "pty")]
388pub struct TerminalInputSession {
389 stdin_fd: i32,
390 original_mode: libc::termios,
391}
392
393#[cfg(feature = "pty")]
394impl TerminalInputSession {
395 pub fn new() -> std::io::Result<Option<Self>> {
396 let stdin_fd = libc::STDIN_FILENO;
397 if unsafe { libc::isatty(stdin_fd) } != 1 {
398 return Ok(None);
399 }
400 let mut original_mode = std::mem::MaybeUninit::<libc::termios>::uninit();
401 if unsafe { libc::tcgetattr(stdin_fd, original_mode.as_mut_ptr()) } != 0 {
402 return Err(std::io::Error::last_os_error());
403 }
404 let original_mode = unsafe { original_mode.assume_init() };
405 let mut raw_mode = original_mode;
406 unsafe { libc::cfmakeraw(&mut raw_mode) };
407 if unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw_mode) } != 0 {
408 return Err(std::io::Error::last_os_error());
409 }
410 Ok(Some(Self {
411 stdin_fd,
412 original_mode,
413 }))
414 }
415
416 pub fn read_chunk(&self, timeout: std::time::Duration) -> std::io::Result<Option<PtyInputChunk>> {
417 let timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
418 let mut pollfd = libc::pollfd {
419 fd: self.stdin_fd,
420 events: libc::POLLIN,
421 revents: 0,
422 };
423 let ready = unsafe { libc::poll(&mut pollfd, 1, timeout_ms) };
424 if ready < 0 {
425 let error = std::io::Error::last_os_error();
426 return if error.kind() == std::io::ErrorKind::Interrupted {
427 Ok(None)
428 } else {
429 Err(error)
430 };
431 }
432 if ready == 0 || pollfd.revents & libc::POLLIN == 0 {
433 return Ok(None);
434 }
435 let mut buffer = vec![0_u8; 65536];
436 let count = unsafe { libc::read(self.stdin_fd, buffer.as_mut_ptr().cast(), buffer.len()) };
437 if count <= 0 {
438 return Ok(None);
439 }
440 buffer.truncate(count as usize);
441 Ok(Some(PtyInputChunk {
442 submit: buffer.iter().any(|byte| matches!(*byte, b'\r' | b'\n')),
443 data: buffer,
444 }))
445 }
446}
447
448#[cfg(feature = "pty")]
449impl Drop for TerminalInputSession {
450 fn drop(&mut self) {
451 unsafe {
452 libc::tcsetattr(self.stdin_fd, libc::TCSANOW, &self.original_mode);
453 }
454 }
455}
456
457pub fn active_graphics_probe(
458 timeout: std::time::Duration,
459) -> crate::platform::terminal::TerminalGraphicsProbe {
460 use std::fs::OpenOptions;
461 use std::io::{Read as _, Write as _};
462 use std::os::fd::AsRawFd as _;
463 use std::time::Instant;
464
465 let Ok(mut tty) = OpenOptions::new().read(true).write(true).open("/dev/tty") else {
466 return crate::platform::terminal::TerminalGraphicsProbe::default();
467 };
468 let fd = tty.as_raw_fd();
469 let mut old_termios = std::mem::MaybeUninit::<libc::termios>::uninit();
470 let have_termios = unsafe { libc::tcgetattr(fd, old_termios.as_mut_ptr()) == 0 };
471 let old_termios = have_termios.then(|| unsafe { old_termios.assume_init() });
472 if let Some(mut raw) = old_termios {
473 raw.c_lflag &= !(libc::ICANON | libc::ECHO);
474 raw.c_cc[libc::VMIN] = 0;
475 raw.c_cc[libc::VTIME] = 0;
476 let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) };
477 }
478 let old_flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
479 if old_flags >= 0 {
480 let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags | libc::O_NONBLOCK) };
481 }
482
483 let _ = tty.write_all(
484 b"\x1b[c\x1b[?2;1;0S\x1b_Gi=running-process-probe,a=q;\x1b\\\x1b]1337;Capabilities\x07",
485 );
486 let _ = tty.flush();
487
488 let deadline = Instant::now() + timeout;
489 let mut bytes = Vec::new();
490 while Instant::now() < deadline {
491 let mut chunk = [0_u8; 512];
492 match tty.read(&mut chunk) {
493 Ok(0) => std::thread::sleep(std::time::Duration::from_millis(5)),
494 Ok(count) => bytes.extend_from_slice(&chunk[..count]),
495 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
496 std::thread::sleep(std::time::Duration::from_millis(5));
497 }
498 Err(_) => break,
499 }
500 }
501
502 if old_flags >= 0 {
503 let _ = unsafe { libc::fcntl(fd, libc::F_SETFL, old_flags) };
504 }
505 if let Some(old) = old_termios {
506 let _ = unsafe { libc::tcsetattr(fd, libc::TCSANOW, &old) };
507 }
508
509 let reply = String::from_utf8_lossy(&bytes).into_owned();
510 crate::platform::terminal::TerminalGraphicsProbe {
511 sixel_xtsmgraphics: reply.contains('S').then(|| reply.clone()),
512 sixel_da1: reply.contains("[?").then(|| reply.clone()),
513 kitty_graphics: reply.contains("_G").then(|| reply.clone()),
514 iterm2_capabilities: reply.contains("Capabilities=").then_some(reply),
515 }
516}
517
518#[cfg(all(test, feature = "pty"))]
519mod tests {
520 use super::*;
521 use crate::platform::terminal::{PtyMaster, PtyMasterControlToken, PtySize};
522 use std::fs::File;
523 use std::io::{self, Read, Write};
524 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
525 use std::sync::{Arc, Mutex};
526 use std::time::{Duration, Instant};
527
528 struct NoGroupMaster(OwnedFd);
529
530 impl PtyMaster for NoGroupMaster {
531 fn try_clone_reader(&mut self) -> io::Result<Box<dyn Read + Send>> {
532 Err(io::Error::new(io::ErrorKind::Unsupported, "unused by test"))
533 }
534
535 fn take_writer(&mut self) -> io::Result<Box<dyn Write + Send>> {
536 Err(io::Error::new(io::ErrorKind::Unsupported, "unused by test"))
537 }
538
539 fn resize(&self, _size: PtySize) -> io::Result<()> {
540 Ok(())
541 }
542
543 fn get_size(&self) -> io::Result<PtySize> {
544 Ok(PtySize {
545 rows: 24,
546 cols: 80,
547 pixel_width: 0,
548 pixel_height: 0,
549 })
550 }
551
552 fn control_token(&self) -> PtyMasterControlToken {
553 PtyMasterControlToken {
554 process_group_leader: None,
555 raw_fd: Some(self.0.as_raw_fd()),
556 }
557 }
558 }
559
560 fn open_full_pty_input_queue() -> (OwnedFd, OwnedFd) {
561 let mut master = -1;
562 let mut slave = -1;
563 assert_eq!(
564 unsafe {
565 libc::openpty(
566 &mut master,
567 &mut slave,
568 std::ptr::null_mut(),
569 std::ptr::null_mut(),
570 std::ptr::null_mut(),
571 )
572 },
573 0,
574 "openpty failed: {}",
575 io::Error::last_os_error()
576 );
577 let master = unsafe { OwnedFd::from_raw_fd(master) };
578 let slave = unsafe { OwnedFd::from_raw_fd(slave) };
579
580 let mut termios = std::mem::MaybeUninit::<libc::termios>::uninit();
581 assert_eq!(
582 unsafe { libc::tcgetattr(slave.as_raw_fd(), termios.as_mut_ptr()) },
583 0
584 );
585 let mut termios = unsafe { termios.assume_init() };
586 unsafe { libc::cfmakeraw(&mut termios) };
587 assert_eq!(
588 unsafe { libc::tcsetattr(slave.as_raw_fd(), libc::TCSANOW, &termios) },
589 0
590 );
591
592 let flags = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
593 assert_ne!(flags, -1);
594 assert_ne!(
595 unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) },
596 -1
597 );
598 let chunk = [b'x'; 1024];
599 loop {
600 let written =
601 unsafe { libc::write(master.as_raw_fd(), chunk.as_ptr().cast(), chunk.len()) };
602 if written >= 0 {
603 continue;
604 }
605 assert_eq!(io::Error::last_os_error().kind(), io::ErrorKind::WouldBlock);
606 break;
607 }
608 assert_ne!(
609 unsafe { libc::fcntl(master.as_raw_fd(), libc::F_SETFL, flags) },
610 -1
611 );
612 (master, slave)
613 }
614
615 #[test]
616 fn interrupt_fallback_does_not_block_on_full_pty_input_queue() {
617 let (master, _slave) = open_full_pty_input_queue();
618 let started = Instant::now();
619 write_nonblocking_byte(master.as_raw_fd(), 0x03)
620 .expect("a full input queue is an expected best-effort drop");
621 assert!(started.elapsed() < Duration::from_secs(1));
622 }
623
624 #[test]
625 fn nonblocking_interrupt_write_restores_descriptor_flags() {
626 let (master, _slave) = open_full_pty_input_queue();
627 let before = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
628 assert_ne!(before, -1);
629 write_nonblocking_byte(master.as_raw_fd(), 0x03)
630 .expect("a full input queue is an expected best-effort drop");
631 let after = unsafe { libc::fcntl(master.as_raw_fd(), libc::F_GETFL) };
632 assert_eq!(after, before, "fallback changed the PTY descriptor flags");
633 }
634
635 #[test]
636 fn nonblocking_interrupt_write_reports_fcntl_failure() {
637 let error = write_nonblocking_byte(-1, 0x03).expect_err("invalid fd must fail");
638 assert_eq!(error.raw_os_error(), Some(libc::EBADF));
639 }
640
641 #[test]
642 fn interrupt_fallback_does_not_wait_for_busy_writer_mutex() {
643 let (master, _slave) = open_full_pty_input_queue();
644 let writer_fd = unsafe { libc::dup(master.as_raw_fd()) };
645 assert_ne!(writer_fd, -1);
646 let writer = Arc::new(Mutex::new(
647 Box::new(unsafe { File::from_raw_fd(writer_fd) }) as Box<dyn Write + Send>,
648 ));
649 let writer_guard = writer.lock().expect("writer mutex");
650 let started = Instant::now();
651 let wrote_fallback = send_pty_interrupt(NoGroupMaster(master).control_token(), &writer)
652 .expect("busy writer fallback should remain best-effort");
653 assert!(!wrote_fallback);
654 assert!(started.elapsed() < Duration::from_secs(1));
655 drop(writer_guard);
656 }
657}