twine_core/terminal/
pty.rs1use std::io::{Read, Write};
2use std::path::PathBuf;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::{Arc, Mutex};
5use std::thread::JoinHandle;
6
7use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system};
8use tracing::debug;
9
10use super::launcher::verify_working_directory;
11use super::process::{SharedChild, shared_child, terminate_unobserved_child};
12use super::{TerminalError, TerminalId, TerminalSize, TerminalStream};
13
14pub(super) const READ_CHUNK_BYTES: usize = 16 * 1024;
15const READER_POLL_MILLISECONDS: libc::c_int = 50;
16
17#[cfg(unix)]
18pub(super) type TerminalReaderDescriptor = libc::c_int;
19
20#[cfg(not(unix))]
21pub(super) type TerminalReaderDescriptor = ();
22
23impl From<TerminalSize> for PtySize {
24 fn from(size: TerminalSize) -> Self {
25 Self {
26 rows: size.rows,
27 cols: size.columns,
28 pixel_width: size.pixel_width,
29 pixel_height: size.pixel_height,
30 }
31 }
32}
33
34pub(super) struct StartingTerminal {
35 pub(super) master: Box<dyn MasterPty + Send>,
36 pub(super) reader: Box<dyn Read + Send>,
37 pub(super) writer: Arc<Mutex<Box<dyn Write + Send>>>,
38 pub(super) child: SharedChild,
39 pub(super) reader_descriptor: TerminalReaderDescriptor,
40}
41
42pub(super) fn spawn_terminal_process(
43 command: CommandBuilder,
44 size: TerminalSize,
45 working_directory_handshake: Option<PathBuf>,
46) -> Result<StartingTerminal, TerminalError> {
47 let pair = native_pty_system()
48 .openpty(size.validate()?.into())
49 .map_err(|error| TerminalError::Pty {
50 operation: "open PTY",
51 message: error.to_string(),
52 })?;
53 let reader_descriptor = terminal_reader_descriptor(pair.master.as_ref())?;
54 let child = pair
55 .slave
56 .spawn_command(command)
57 .map_err(|error| TerminalError::Pty {
58 operation: "spawn shell",
59 message: error.to_string(),
60 })?;
61 drop(pair.slave);
62 let child = shared_child(child);
63 let mut reader = pair.master.try_clone_reader().map_err(|error| {
64 terminate_unobserved_child(&child);
65 TerminalError::Pty {
66 operation: "clone PTY reader",
67 message: error.to_string(),
68 }
69 })?;
70 if let Some(working_directory) = working_directory_handshake
71 && let Err(error) = verify_working_directory(&mut reader, &working_directory)
72 {
73 terminate_unobserved_child(&child);
74 return Err(error);
75 }
76 let writer = pair.master.take_writer().map_err(|error| {
77 terminate_unobserved_child(&child);
78 TerminalError::Pty {
79 operation: "take PTY writer",
80 message: error.to_string(),
81 }
82 })?;
83 Ok(StartingTerminal {
84 master: pair.master,
85 reader,
86 writer: Arc::new(Mutex::new(writer)),
87 child,
88 reader_descriptor,
89 })
90}
91
92#[cfg(unix)]
93fn terminal_reader_descriptor(
94 master: &(dyn MasterPty + Send),
95) -> Result<TerminalReaderDescriptor, TerminalError> {
96 master.as_raw_fd().ok_or_else(|| TerminalError::Pty {
97 operation: "inspect PTY reader",
98 message: "native PTY did not expose a file descriptor".to_owned(),
99 })
100}
101
102#[cfg(not(unix))]
103fn terminal_reader_descriptor(
104 _master: &(dyn MasterPty + Send),
105) -> Result<TerminalReaderDescriptor, TerminalError> {
106 Ok(())
107}
108
109pub(super) fn spawn_terminal_reader(
110 output: Arc<TerminalStream>,
111 terminal_id: TerminalId,
112 mut reader: Box<dyn Read + Send>,
113 reader_descriptor: TerminalReaderDescriptor,
114 cancelled: Arc<AtomicBool>,
115) -> Result<JoinHandle<()>, TerminalError> {
116 crate::blocking_worker::spawn(
117 format!("terminal-{}-reader", terminal_id.value()),
118 move || {
119 let mut buffer = vec![0; READ_CHUNK_BYTES];
120 loop {
121 match wait_for_terminal_output(reader_descriptor, &cancelled) {
122 Ok(true) => {}
123 Ok(false) => break,
124 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
125 Err(error) => {
126 debug!(terminal_id = terminal_id.value(), %error, "terminal reader poll stopped");
127 break;
128 }
129 }
130 match reader.read(&mut buffer) {
131 Ok(0) => break,
132 Ok(count) => {
133 if output
134 .publish_blocking(terminal_id, buffer[..count].to_vec())
135 .is_err()
136 {
137 break;
138 }
139 }
140 Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
141 Err(error) => {
142 debug!(terminal_id = terminal_id.value(), %error, "terminal reader stopped");
143 break;
144 }
145 }
146 }
147 let _ = output.finish(terminal_id);
148 },
149 )
150 .map_err(|error| TerminalError::Thread {
151 operation: "start PTY reader",
152 message: error.to_string(),
153 })
154}
155
156#[cfg(unix)]
157fn wait_for_terminal_output(
158 descriptor: TerminalReaderDescriptor,
159 cancelled: &AtomicBool,
160) -> std::io::Result<bool> {
161 loop {
162 if cancelled.load(Ordering::Acquire) {
163 return Ok(false);
164 }
165 let mut poll_descriptor = libc::pollfd {
166 fd: descriptor,
167 events: libc::POLLIN,
168 revents: 0,
169 };
170 let result = unsafe { libc::poll(&raw mut poll_descriptor, 1, READER_POLL_MILLISECONDS) };
173 if result > 0 {
174 return Ok(!cancelled.load(Ordering::Acquire));
175 }
176 if result < 0 {
177 return Err(std::io::Error::last_os_error());
178 }
179 }
180}
181
182#[cfg(not(unix))]
183fn wait_for_terminal_output(
184 _descriptor: TerminalReaderDescriptor,
185 cancelled: &AtomicBool,
186) -> std::io::Result<bool> {
187 Ok(!cancelled.load(Ordering::Acquire))
188}