Skip to main content

running_process_platform_internal/platform_linux/
terminal.rs

1//! Linux PTY implementation.
2
3#[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(&current) {
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}