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, 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(&current) {
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}