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