shell_tunnel/pty/
native.rs1use portable_pty::{native_pty_system, CommandBuilder, PtySize as NativePtySize};
4use std::io::{Read, Write};
5
6use super::{PtyHandle, PtySize};
7use crate::error::ShellTunnelError;
8use crate::Result;
9
10pub fn default_shell() -> &'static str {
12 #[cfg(unix)]
13 {
14 std::env::var("SHELL")
15 .ok()
16 .map(|s| Box::leak(s.into_boxed_str()) as &'static str)
17 .unwrap_or("/bin/sh")
18 }
19 #[cfg(windows)]
20 {
21 "powershell.exe"
22 }
23}
24
25pub struct NativePty {
27 pty_system: Box<dyn portable_pty::PtySystem + Send>,
28}
29
30impl NativePty {
31 pub fn new() -> Self {
33 Self {
34 pty_system: native_pty_system(),
35 }
36 }
37
38 pub fn spawn(
49 &self,
50 shell: &str,
51 size: PtySize,
52 ) -> Result<PtyHandle<Box<dyn Read + Send>, Box<dyn Write + Send>>> {
53 let native_size = NativePtySize {
54 rows: size.rows,
55 cols: size.cols,
56 pixel_width: 0,
57 pixel_height: 0,
58 };
59
60 let pair = self
61 .pty_system
62 .openpty(native_size)
63 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
64
65 let cmd = CommandBuilder::new(shell);
66
67 let child = pair
71 .slave
72 .spawn_command(cmd)
73 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
74
75 let pid = child.process_id().unwrap_or(0);
76
77 let reader = pair
78 .master
79 .try_clone_reader()
80 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
81
82 let writer = pair
83 .master
84 .take_writer()
85 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
86
87 Ok(PtyHandle::new(
88 reader,
89 writer,
90 pid,
91 Box::new((pair.master, child)),
92 ))
93 }
94
95 pub fn spawn_default(
97 &self,
98 size: PtySize,
99 ) -> Result<PtyHandle<Box<dyn Read + Send>, Box<dyn Write + Send>>> {
100 self.spawn(default_shell(), size)
101 }
102
103 pub fn spawn_shell(&mut self, working_dir: Option<&std::path::Path>) -> Result<SpawnedShell> {
108 let native_size = NativePtySize {
109 rows: 24,
110 cols: 80,
111 pixel_width: 0,
112 pixel_height: 0,
113 };
114
115 let pair = self
116 .pty_system
117 .openpty(native_size)
118 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
119
120 let mut cmd = CommandBuilder::new(default_shell());
121
122 if let Some(dir) = working_dir {
123 cmd.cwd(dir);
124 }
125
126 let child = pair
127 .slave
128 .spawn_command(cmd)
129 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
130
131 Ok(SpawnedShell {
132 master: pair.master,
133 child,
134 reader: None,
135 writer: None,
136 })
137 }
138
139 pub fn spawn_command(
144 &mut self,
145 command_line: &str,
146 working_dir: Option<&std::path::Path>,
147 ) -> Result<SpawnedShell> {
148 let native_size = NativePtySize {
149 rows: 24,
150 cols: 80,
151 pixel_width: 0,
152 pixel_height: 0,
153 };
154
155 let pair = self
156 .pty_system
157 .openpty(native_size)
158 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
159
160 #[cfg(unix)]
161 let mut cmd = {
162 let mut c = CommandBuilder::new("/bin/sh");
163 c.arg("-c");
164 c.arg(command_line);
165 c
166 };
167
168 #[cfg(windows)]
169 let mut cmd = {
170 let mut c = CommandBuilder::new("cmd.exe");
171 c.arg("/c");
172 c.arg(command_line);
173 c
174 };
175
176 if let Some(dir) = working_dir {
177 cmd.cwd(dir);
178 }
179
180 let child = pair
181 .slave
182 .spawn_command(cmd)
183 .map_err(|e| ShellTunnelError::Pty(e.to_string()))?;
184
185 Ok(SpawnedShell {
186 master: pair.master,
187 child,
188 reader: None,
189 writer: None,
190 })
191 }
192}
193
194pub struct SpawnedShell {
196 master: Box<dyn portable_pty::MasterPty + Send>,
197 child: Box<dyn portable_pty::Child + Send + Sync>,
198 reader: Option<Box<dyn Read + Send>>,
199 writer: Option<Box<dyn Write + Send>>,
200}
201
202impl SpawnedShell {
203 pub fn take_writer(&mut self) -> Result<Box<dyn Write + Send>> {
205 if let Some(writer) = self.writer.take() {
206 return Ok(writer);
207 }
208 self.master
209 .take_writer()
210 .map_err(|e| ShellTunnelError::Pty(e.to_string()))
211 }
212
213 pub fn take_reader(&mut self) -> Result<Box<dyn Read + Send>> {
215 if let Some(reader) = self.reader.take() {
216 return Ok(reader);
217 }
218 self.master
219 .try_clone_reader()
220 .map_err(|e| ShellTunnelError::Pty(e.to_string()))
221 }
222
223 pub fn try_wait(&mut self) -> std::io::Result<Option<portable_pty::ExitStatus>> {
225 self.child.try_wait()
226 }
227
228 pub fn wait(&mut self) -> std::io::Result<portable_pty::ExitStatus> {
230 self.child.wait()
231 }
232}
233
234impl Default for NativePty {
235 fn default() -> Self {
236 Self::new()
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_default_shell() {
246 let shell = default_shell();
247 assert!(!shell.is_empty());
248
249 #[cfg(unix)]
250 {
251 assert!(shell.starts_with('/') || !shell.contains('/'));
253 }
254
255 #[cfg(windows)]
256 {
257 assert!(shell.ends_with(".exe"));
258 }
259 }
260
261 #[test]
262 fn test_spawn_shell() {
263 let pty = NativePty::new();
264 let handle = pty.spawn_default(PtySize::default());
265
266 assert!(handle.is_ok(), "Failed to spawn shell: {:?}", handle.err());
267
268 let handle = handle.unwrap();
269 assert!(handle.pid > 0, "PID should be positive");
270 }
271
272 #[test]
276 #[ignore]
277 fn test_write_and_read() {
278 use std::time::Duration;
279
280 let pty = NativePty::new();
281 let mut handle = pty.spawn_default(PtySize::default()).unwrap();
282
283 #[cfg(unix)]
285 let cmd = "echo SHELL_TUNNEL_TEST_OUTPUT; exit\n";
286 #[cfg(windows)]
287 let cmd = "echo SHELL_TUNNEL_TEST_OUTPUT\r\nexit\r\n";
288
289 handle.writer.write_all(cmd.as_bytes()).unwrap();
290 handle.writer.flush().unwrap();
291
292 let mut output = Vec::new();
294 let mut buf = [0u8; 1024];
295 let deadline = std::time::Instant::now() + Duration::from_secs(5);
296
297 while std::time::Instant::now() < deadline {
298 match handle.reader.read(&mut buf) {
299 Ok(0) => break,
300 Ok(n) => {
301 output.extend_from_slice(&buf[..n]);
302 if String::from_utf8_lossy(&output).contains("SHELL_TUNNEL_TEST_OUTPUT") {
303 break;
304 }
305 }
306 Err(e) => {
307 #[cfg(unix)]
308 if e.raw_os_error() == Some(libc::EIO) {
309 break;
310 }
311 if e.kind() == std::io::ErrorKind::WouldBlock {
312 std::thread::sleep(Duration::from_millis(10));
313 continue;
314 }
315 break;
316 }
317 }
318 }
319
320 assert!(!output.is_empty(), "Should have received some output");
321 }
322
323 #[test]
324 fn test_custom_size() {
325 let pty = NativePty::new();
326 let size = PtySize::new(40, 120);
327 let handle = pty.spawn_default(size);
328
329 assert!(handle.is_ok());
330 }
331
332 #[test]
333 #[cfg(unix)]
334 fn test_spawn_specific_shell() {
335 let pty = NativePty::new();
336 let handle = pty.spawn("/bin/sh", PtySize::default());
337
338 assert!(handle.is_ok());
339 }
340
341 #[test]
342 #[cfg(windows)]
343 fn test_spawn_cmd() {
344 let pty = NativePty::new();
345 let handle = pty.spawn("cmd.exe", PtySize::default());
346
347 assert!(handle.is_ok());
348 }
349}