1use std::io;
7use std::pin::Pin;
8use std::task::{Context, Poll};
9
10use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
11
12use crate::backend::ChildExit;
13use crate::config::SessionConfig;
14use crate::error::{ExpectError, Result, SpawnError};
15use crate::types::ProcessExitStatus;
16
17pub struct PtyTransport {
19 reader: Box<dyn AsyncRead + Unpin + Send>,
21 writer: Box<dyn AsyncWrite + Unpin + Send>,
23 pid: Option<u32>,
25}
26
27impl PtyTransport {
28 pub fn new<R, W>(reader: R, writer: W) -> Self
30 where
31 R: AsyncRead + Unpin + Send + 'static,
32 W: AsyncWrite + Unpin + Send + 'static,
33 {
34 Self {
35 reader: Box::new(reader),
36 writer: Box::new(writer),
37 pid: None,
38 }
39 }
40
41 pub const fn set_pid(&mut self, pid: u32) {
43 self.pid = Some(pid);
44 }
45
46 #[must_use]
48 pub const fn pid(&self) -> Option<u32> {
49 self.pid
50 }
51}
52
53impl AsyncRead for PtyTransport {
54 fn poll_read(
55 mut self: Pin<&mut Self>,
56 cx: &mut Context<'_>,
57 buf: &mut ReadBuf<'_>,
58 ) -> Poll<io::Result<()>> {
59 Pin::new(&mut self.reader).poll_read(cx, buf)
60 }
61}
62
63impl AsyncWrite for PtyTransport {
64 fn poll_write(
65 mut self: Pin<&mut Self>,
66 cx: &mut Context<'_>,
67 buf: &[u8],
68 ) -> Poll<io::Result<usize>> {
69 Pin::new(&mut self.writer).poll_write(cx, buf)
70 }
71
72 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
73 Pin::new(&mut self.writer).poll_flush(cx)
74 }
75
76 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
77 Pin::new(&mut self.writer).poll_shutdown(cx)
78 }
79}
80
81#[derive(Debug, Clone)]
83#[non_exhaustive]
84pub struct PtyConfig {
85 pub dimensions: (u16, u16),
87 pub login_shell: bool,
89 pub env_mode: EnvMode,
91 pub env: std::collections::HashMap<String, String>,
94 pub working_directory: Option<std::path::PathBuf>,
97}
98
99impl Default for PtyConfig {
100 fn default() -> Self {
101 Self {
102 dimensions: (80, 24),
103 login_shell: false,
104 env_mode: EnvMode::Inherit,
105 env: std::collections::HashMap::new(),
106 working_directory: None,
107 }
108 }
109}
110
111impl From<&SessionConfig> for PtyConfig {
112 fn from(config: &SessionConfig) -> Self {
113 Self {
114 dimensions: config.dimensions,
115 login_shell: false,
116 env_mode: match (config.inherit_env, config.env.is_empty()) {
117 (false, _) => EnvMode::Clear,
118 (true, true) => EnvMode::Inherit,
119 (true, false) => EnvMode::Extend,
120 },
121 env: config.env.clone(),
122 working_directory: config.working_dir.clone(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum EnvMode {
130 Inherit,
132 Clear,
134 Extend,
136}
137
138#[cfg(unix)]
151#[allow(unsafe_code)]
152unsafe fn apply_env_in_child(
153 env_mode: EnvMode,
154 env_pairs: &[(std::ffi::CString, std::ffi::CString)],
155) {
156 unsafe {
160 match env_mode {
161 EnvMode::Inherit | EnvMode::Extend => {}
162 EnvMode::Clear => {
163 #[cfg(target_os = "linux")]
164 {
165 libc::clearenv();
166 }
167 #[cfg(not(target_os = "linux"))]
168 {
169 unsafe extern "C" {
179 static mut environ: *mut *mut libc::c_char;
180 }
181 let mut names: Vec<std::ffi::CString> = Vec::new();
182 if !environ.is_null() {
183 let mut p = environ;
184 while !(*p).is_null() {
185 let entry = *p;
186 let mut len = 0usize;
188 while *entry.add(len) != 0 && *entry.add(len) != b'=' as libc::c_char {
189 len += 1;
190 }
191 if len > 0 {
192 let bytes = std::slice::from_raw_parts(entry.cast::<u8>(), len);
193 if let Ok(c) = std::ffi::CString::new(bytes) {
194 names.push(c);
195 }
196 }
197 p = p.add(1);
198 }
199 }
200 for name in &names {
201 libc::unsetenv(name.as_ptr());
202 }
203 }
204 }
205 }
206 for (k, v) in env_pairs {
207 libc::setenv(k.as_ptr(), v.as_ptr(), 1);
208 }
209 }
210}
211
212#[cfg(unix)]
222fn build_env_cstrings(
223 env: &std::collections::HashMap<String, String>,
224) -> Result<Vec<(std::ffi::CString, std::ffi::CString)>> {
225 use std::ffi::CString;
226
227 let mut pairs: Vec<(CString, CString)> = Vec::with_capacity(env.len());
228 for (k, v) in env {
229 if k.contains('=') {
230 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
231 kind: "env key".to_string(),
232 value: k.clone(),
233 reason: "env key contains '='".to_string(),
234 }));
235 }
236 let key = CString::new(k.as_str()).map_err(|_| {
237 ExpectError::Spawn(SpawnError::InvalidArgument {
238 kind: "env key".to_string(),
239 value: k.clone(),
240 reason: "env key contains null byte".to_string(),
241 })
242 })?;
243 let val = CString::new(v.as_str()).map_err(|_| {
244 ExpectError::Spawn(SpawnError::InvalidArgument {
245 kind: "env value".to_string(),
246 value: v.clone(),
247 reason: "env value contains null byte".to_string(),
248 })
249 })?;
250 pairs.push((key, val));
251 }
252 Ok(pairs)
253}
254
255#[cfg(unix)]
262fn build_cwd_cstring(
263 working_directory: Option<&std::path::PathBuf>,
264) -> Result<Option<std::ffi::CString>> {
265 use std::ffi::CString;
266 use std::os::unix::ffi::OsStrExt;
267
268 let Some(path) = working_directory else {
269 return Ok(None);
270 };
271 if !path.is_dir() {
272 return Err(ExpectError::Spawn(SpawnError::InvalidWorkingDir {
273 path: path.display().to_string(),
274 }));
275 }
276 let cstring = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
277 ExpectError::Spawn(SpawnError::InvalidWorkingDir {
278 path: path.display().to_string(),
279 })
280 })?;
281 Ok(Some(cstring))
282}
283
284pub struct PtySpawner {
286 config: PtyConfig,
287}
288
289impl PtySpawner {
290 #[must_use]
292 pub fn new() -> Self {
293 Self {
294 config: PtyConfig::default(),
295 }
296 }
297
298 #[must_use]
300 pub const fn with_config(config: PtyConfig) -> Self {
301 Self { config }
302 }
303
304 pub const fn set_dimensions(&mut self, cols: u16, rows: u16) {
306 self.config.dimensions = (cols, rows);
307 }
308
309 #[cfg(unix)]
338 #[allow(unsafe_code)]
339 #[allow(clippy::unused_async)]
340 pub async fn spawn(&self, command: &str, args: &[String]) -> Result<PtyHandle> {
341 use std::ffi::CString;
342
343 let cmd_cstring = CString::new(command).map_err(|_| {
345 ExpectError::Spawn(SpawnError::InvalidArgument {
346 kind: "command".to_string(),
347 value: command.to_string(),
348 reason: "command contains null byte".to_string(),
349 })
350 })?;
351
352 let mut argv_cstrings: Vec<CString> = Vec::with_capacity(args.len() + 1);
353 argv_cstrings.push(cmd_cstring.clone());
354
355 for (idx, arg) in args.iter().enumerate() {
356 let arg_cstring = CString::new(arg.as_str()).map_err(|_| {
357 ExpectError::Spawn(SpawnError::InvalidArgument {
358 kind: format!("argument[{idx}]"),
359 value: arg.clone(),
360 reason: "argument contains null byte".to_string(),
361 })
362 })?;
363 argv_cstrings.push(arg_cstring);
364 }
365
366 let env_pairs = build_env_cstrings(&self.config.env)?;
368 let env_mode = self.config.env_mode;
369
370 let workdir_cstring = build_cwd_cstring(self.config.working_directory.as_ref())?;
373
374 let pty_result = unsafe {
379 let mut master: libc::c_int = 0;
380 let mut slave: libc::c_int = 0;
381
382 if libc::openpty(
384 &raw mut master,
385 &raw mut slave,
386 std::ptr::null_mut(),
387 std::ptr::null_mut(),
388 std::ptr::null_mut(),
389 ) != 0
390 {
391 return Err(ExpectError::Spawn(SpawnError::PtyAllocation {
392 reason: "Failed to open PTY".to_string(),
393 }));
394 }
395
396 (master, slave)
397 };
398
399 let (master_fd, slave_fd) = pty_result;
400
401 let pid = unsafe { libc::fork() };
406
407 match pid {
408 -1 => Err(ExpectError::Spawn(SpawnError::Io(
409 io::Error::last_os_error(),
410 ))),
411 0 => {
412 unsafe {
423 libc::close(master_fd);
424 libc::setsid();
425 libc::ioctl(slave_fd, libc::c_ulong::from(libc::TIOCSCTTY), 0);
427
428 libc::dup2(slave_fd, 0);
429 libc::dup2(slave_fd, 1);
430 libc::dup2(slave_fd, 2);
431
432 if slave_fd > 2 {
433 libc::close(slave_fd);
434 }
435
436 if let Some(ref cwd) = workdir_cstring
438 && libc::chdir(cwd.as_ptr()) != 0
439 {
440 libc::_exit(1);
441 }
442
443 apply_env_in_child(env_mode, &env_pairs);
445
446 let argv_ptrs: Vec<*const libc::c_char> = argv_cstrings
448 .iter()
449 .map(|s| s.as_ptr())
450 .chain(std::iter::once(std::ptr::null()))
451 .collect();
452
453 libc::execvp(cmd_cstring.as_ptr(), argv_ptrs.as_ptr());
454 libc::_exit(1);
455 }
456 }
457 child_pid => {
458 unsafe {
462 libc::close(slave_fd);
463 }
464
465 unsafe {
470 let flags = libc::fcntl(master_fd, libc::F_GETFL);
471 libc::fcntl(master_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
472 }
473
474 Ok(PtyHandle {
475 master_fd,
476 pid: child_pid as u32,
477 dimensions: self.config.dimensions,
478 })
479 }
480 }
481 }
482
483 #[cfg(windows)]
492 pub async fn spawn(&self, command: &str, args: &[String]) -> Result<WindowsPtyHandle> {
493 use rust_pty::{PtySystem, WindowsPtySystem};
494
495 let built_env: Option<std::collections::HashMap<std::ffi::OsString, std::ffi::OsString>> =
501 match self.config.env_mode {
502 EnvMode::Inherit if self.config.env.is_empty() => None,
503 EnvMode::Inherit | EnvMode::Extend => {
504 let mut m: std::collections::HashMap<_, _> = std::env::vars_os().collect();
505 for (k, v) in &self.config.env {
506 m.insert(std::ffi::OsString::from(k), std::ffi::OsString::from(v));
507 }
508 Some(m)
509 }
510 EnvMode::Clear => Some(
511 self.config
512 .env
513 .iter()
514 .map(|(k, v)| (std::ffi::OsString::from(k), std::ffi::OsString::from(v)))
515 .collect(),
516 ),
517 };
518
519 let pty_config = rust_pty::PtyConfig {
521 window_size: self.config.dimensions,
522 env: match self.config.env_mode {
523 EnvMode::Clear if self.config.env.is_empty() => {
524 Some(std::collections::HashMap::new())
525 }
526 _ => built_env,
527 },
528 working_directory: self.config.working_directory.clone(),
529 ..Default::default()
530 };
531
532 let (master, child) =
534 WindowsPtySystem::spawn(command, args.iter().map(|s| s.as_str()), &pty_config)
535 .await
536 .map_err(|e| {
537 ExpectError::Spawn(SpawnError::PtyAllocation {
538 reason: format!("Windows ConPTY spawn failed: {e}"),
539 })
540 })?;
541
542 Ok(WindowsPtyHandle {
543 master,
544 child,
545 dimensions: self.config.dimensions,
546 })
547 }
548}
549
550impl Default for PtySpawner {
551 fn default() -> Self {
552 Self::new()
553 }
554}
555
556#[cfg(unix)]
558#[derive(Debug)]
559pub struct PtyHandle {
560 master_fd: i32,
562 pid: u32,
564 dimensions: (u16, u16),
566}
567
568#[cfg(windows)]
570pub struct WindowsPtyHandle {
571 pub(crate) master: rust_pty::WindowsPtyMaster,
573 pub(crate) child: rust_pty::WindowsPtyChild,
575 dimensions: (u16, u16),
577}
578
579#[cfg(windows)]
580impl std::fmt::Debug for WindowsPtyHandle {
581 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
582 f.debug_struct("WindowsPtyHandle")
583 .field("dimensions", &self.dimensions)
584 .finish_non_exhaustive()
585 }
586}
587
588#[cfg(unix)]
589impl PtyHandle {
590 #[must_use]
592 pub const fn pid(&self) -> u32 {
593 self.pid
594 }
595
596 #[must_use]
598 pub const fn dimensions(&self) -> (u16, u16) {
599 self.dimensions
600 }
601
602 #[allow(unsafe_code)]
604 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
605 let winsize = libc::winsize {
606 ws_row: rows,
607 ws_col: cols,
608 ws_xpixel: 0,
609 ws_ypixel: 0,
610 };
611
612 let result =
617 unsafe { libc::ioctl(self.master_fd, libc::TIOCSWINSZ as libc::c_ulong, &winsize) };
618
619 if result != 0 {
620 Err(ExpectError::Io(io::Error::last_os_error()))
621 } else {
622 self.dimensions = (cols, rows);
623 Ok(())
624 }
625 }
626
627 #[allow(unsafe_code)]
629 pub fn wait(&self) -> Result<i32> {
630 let mut status: libc::c_int = 0;
631 let result = unsafe { libc::waitpid(self.pid as i32, &raw mut status, 0) };
635
636 if result == -1 {
637 Err(ExpectError::Io(io::Error::last_os_error()))
638 } else if libc::WIFEXITED(status) {
639 Ok(libc::WEXITSTATUS(status))
640 } else if libc::WIFSIGNALED(status) {
641 Ok(128 + libc::WTERMSIG(status))
642 } else {
643 Ok(-1)
644 }
645 }
646
647 #[allow(unsafe_code)]
649 pub fn signal(&self, signal: i32) -> Result<()> {
650 let result = unsafe { libc::kill(self.pid as i32, signal) };
654 if result != 0 {
655 Err(ExpectError::Io(io::Error::last_os_error()))
656 } else {
657 Ok(())
658 }
659 }
660
661 pub fn kill(&self) -> Result<()> {
663 self.signal(libc::SIGKILL)
664 }
665}
666
667#[cfg(windows)]
668impl WindowsPtyHandle {
669 #[must_use]
671 pub fn pid(&self) -> u32 {
672 self.child.pid()
673 }
674
675 #[must_use]
677 pub const fn dimensions(&self) -> (u16, u16) {
678 self.dimensions
679 }
680
681 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
683 use rust_pty::{PtyMaster, WindowSize};
684 let size = WindowSize::new(cols, rows);
685 self.master
686 .resize(size)
687 .map_err(|e| ExpectError::Io(io::Error::other(format!("resize failed: {e}"))))?;
688 self.dimensions = (cols, rows);
689 Ok(())
690 }
691
692 #[must_use]
694 pub fn is_running(&self) -> bool {
695 self.child.is_running()
696 }
697
698 pub fn kill(&mut self) -> Result<()> {
700 self.child
701 .kill()
702 .map_err(|e| ExpectError::Io(io::Error::other(format!("kill failed: {e}"))))
703 }
704}
705
706#[cfg(unix)]
707impl Drop for PtyHandle {
708 #[allow(unsafe_code)]
709 fn drop(&mut self) {
710 unsafe {
715 libc::close(self.master_fd);
716 }
717 }
718}
719
720#[cfg(unix)]
725pub struct AsyncPty {
726 inner: tokio::io::unix::AsyncFd<std::os::unix::io::RawFd>,
728 pid: u32,
730 dimensions: (u16, u16),
732 exit_status: Option<ProcessExitStatus>,
738}
739
740#[cfg(unix)]
741impl AsyncPty {
742 pub fn from_handle(handle: PtyHandle) -> io::Result<Self> {
750 let fd = handle.master_fd;
751 let pid = handle.pid;
752 let dimensions = handle.dimensions;
753
754 std::mem::forget(handle);
756
757 let inner = tokio::io::unix::AsyncFd::new(fd)?;
758 Ok(Self {
759 inner,
760 pid,
761 dimensions,
762 exit_status: None,
763 })
764 }
765
766 #[allow(unsafe_code)]
773 pub fn try_wait(&mut self) -> Option<ProcessExitStatus> {
774 if let Some(status) = self.exit_status {
775 return Some(status);
776 }
777
778 let mut raw: libc::c_int = 0;
779 loop {
780 let result = unsafe { libc::waitpid(self.pid as i32, &raw mut raw, libc::WNOHANG) };
783
784 if result == 0 {
785 return None;
787 }
788
789 if result == -1 {
790 let err = io::Error::last_os_error();
791 if err.kind() == io::ErrorKind::Interrupted {
792 continue; }
794 let status = ProcessExitStatus::Unknown;
797 self.exit_status = Some(status);
798 return Some(status);
799 }
800
801 let status = decode_wait_status(raw);
802 self.exit_status = Some(status);
803 return Some(status);
804 }
805 }
806
807 pub fn is_running(&mut self) -> bool {
812 self.try_wait().is_none()
813 }
814
815 #[must_use]
817 pub const fn pid(&self) -> u32 {
818 self.pid
819 }
820
821 #[must_use]
823 pub const fn dimensions(&self) -> (u16, u16) {
824 self.dimensions
825 }
826
827 #[allow(unsafe_code)]
829 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
830 let winsize = libc::winsize {
831 ws_row: rows,
832 ws_col: cols,
833 ws_xpixel: 0,
834 ws_ypixel: 0,
835 };
836
837 let result = unsafe {
840 libc::ioctl(
841 *self.inner.get_ref(),
842 libc::TIOCSWINSZ as libc::c_ulong,
843 &winsize,
844 )
845 };
846
847 if result != 0 {
848 Err(ExpectError::Io(io::Error::last_os_error()))
849 } else {
850 self.dimensions = (cols, rows);
851 Ok(())
852 }
853 }
854
855 #[allow(unsafe_code)]
857 pub fn signal(&self, signal: i32) -> Result<()> {
858 let result = unsafe { libc::kill(self.pid as i32, signal) };
860 if result != 0 {
861 Err(ExpectError::Io(io::Error::last_os_error()))
862 } else {
863 Ok(())
864 }
865 }
866
867 pub fn kill(&self) -> Result<()> {
869 self.signal(libc::SIGKILL)
870 }
871}
872
873#[cfg(unix)]
874impl AsyncRead for AsyncPty {
875 #[allow(unsafe_code)]
876 fn poll_read(
877 self: Pin<&mut Self>,
878 cx: &mut Context<'_>,
879 buf: &mut ReadBuf<'_>,
880 ) -> Poll<io::Result<()>> {
881 loop {
882 let mut guard = match self.inner.poll_read_ready(cx) {
883 Poll::Ready(Ok(guard)) => guard,
884 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
885 Poll::Pending => return Poll::Pending,
886 };
887
888 let fd = *self.inner.get_ref();
889 let unfilled = buf.initialize_unfilled();
890
891 let result = unsafe {
893 libc::read(
894 fd,
895 unfilled.as_mut_ptr().cast::<libc::c_void>(),
896 unfilled.len(),
897 )
898 };
899
900 if result >= 0 {
901 buf.advance(result as usize);
902 return Poll::Ready(Ok(()));
903 }
904
905 let err = io::Error::last_os_error();
906 if err.kind() == io::ErrorKind::WouldBlock {
907 guard.clear_ready();
908 continue;
909 }
910 return Poll::Ready(Err(err));
911 }
912 }
913}
914
915#[cfg(unix)]
916impl AsyncWrite for AsyncPty {
917 #[allow(unsafe_code)]
918 fn poll_write(
919 mut self: Pin<&mut Self>,
920 cx: &mut Context<'_>,
921 buf: &[u8],
922 ) -> Poll<io::Result<usize>> {
923 if self.as_mut().get_mut().try_wait().is_some() {
925 return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()));
926 }
927 loop {
928 let mut guard = match self.inner.poll_write_ready(cx) {
929 Poll::Ready(Ok(guard)) => guard,
930 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
931 Poll::Pending => return Poll::Pending,
932 };
933
934 let fd = *self.inner.get_ref();
935
936 let result = unsafe { libc::write(fd, buf.as_ptr().cast::<libc::c_void>(), buf.len()) };
938
939 if result >= 0 {
940 return Poll::Ready(Ok(result as usize));
941 }
942
943 let err = io::Error::last_os_error();
944 if err.kind() == io::ErrorKind::WouldBlock {
945 guard.clear_ready();
946 continue;
947 }
948 return Poll::Ready(Err(err));
949 }
950 }
951
952 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
953 Poll::Ready(Ok(()))
955 }
956
957 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
958 Poll::Ready(Ok(()))
960 }
961}
962
963#[cfg(unix)]
964impl Drop for AsyncPty {
965 #[allow(unsafe_code)]
966 fn drop(&mut self) {
967 unsafe {
969 libc::close(*self.inner.get_ref());
970 }
971 }
972}
973
974#[cfg(unix)]
975impl std::fmt::Debug for AsyncPty {
976 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
977 f.debug_struct("AsyncPty")
978 .field("fd", self.inner.get_ref())
979 .field("pid", &self.pid)
980 .field("dimensions", &self.dimensions)
981 .field("exit_status", &self.exit_status)
982 .finish()
983 }
984}
985
986#[cfg(unix)]
987impl ChildExit for AsyncPty {
988 fn try_exit_status(&mut self) -> Option<ProcessExitStatus> {
989 self.try_wait()
990 }
991}
992
993#[cfg(unix)]
999const fn decode_wait_status(raw: libc::c_int) -> ProcessExitStatus {
1000 if libc::WIFEXITED(raw) {
1001 ProcessExitStatus::Exited(libc::WEXITSTATUS(raw))
1002 } else if libc::WIFSIGNALED(raw) {
1003 ProcessExitStatus::Signaled(libc::WTERMSIG(raw))
1004 } else {
1005 ProcessExitStatus::Unknown
1007 }
1008}
1009
1010#[cfg(windows)]
1015pub struct WindowsAsyncPty {
1016 master: rust_pty::WindowsPtyMaster,
1018 child: rust_pty::WindowsPtyChild,
1020 pid: u32,
1022 dimensions: (u16, u16),
1024}
1025
1026#[cfg(windows)]
1027impl WindowsAsyncPty {
1028 pub fn from_handle(handle: WindowsPtyHandle) -> Self {
1032 let pid = handle.child.pid();
1033 let dimensions = handle.dimensions;
1034 Self {
1035 master: handle.master,
1036 child: handle.child,
1037 pid,
1038 dimensions,
1039 }
1040 }
1041
1042 #[must_use]
1044 pub const fn pid(&self) -> u32 {
1045 self.pid
1046 }
1047
1048 #[must_use]
1050 pub const fn dimensions(&self) -> (u16, u16) {
1051 self.dimensions
1052 }
1053
1054 pub fn resize(&mut self, cols: u16, rows: u16) -> Result<()> {
1056 use rust_pty::{PtyMaster, WindowSize};
1057 let size = WindowSize::new(cols, rows);
1058 self.master
1059 .resize(size)
1060 .map_err(|e| ExpectError::Io(io::Error::other(format!("resize failed: {e}"))))?;
1061 self.dimensions = (cols, rows);
1062 Ok(())
1063 }
1064
1065 #[must_use]
1067 pub fn is_running(&self) -> bool {
1068 self.child.is_running()
1069 }
1070
1071 pub fn kill(&mut self) -> Result<()> {
1073 self.child
1074 .kill()
1075 .map_err(|e| ExpectError::Io(io::Error::other(format!("kill failed: {e}"))))
1076 }
1077}
1078
1079#[cfg(windows)]
1080impl ChildExit for WindowsAsyncPty {
1081 fn try_exit_status(&mut self) -> Option<ProcessExitStatus> {
1082 match self.child.try_wait() {
1087 Ok(Some(rust_pty::ExitStatus::Exited(code))) => Some(ProcessExitStatus::Exited(code)),
1088 Ok(Some(rust_pty::ExitStatus::Terminated(code))) => {
1090 Some(ProcessExitStatus::Exited(code as i32))
1091 }
1092 Ok(None) | Err(_) => None,
1094 }
1095 }
1096}
1097
1098#[cfg(windows)]
1099impl AsyncRead for WindowsAsyncPty {
1100 fn poll_read(
1101 mut self: Pin<&mut Self>,
1102 cx: &mut Context<'_>,
1103 buf: &mut ReadBuf<'_>,
1104 ) -> Poll<io::Result<()>> {
1105 Pin::new(&mut self.master).poll_read(cx, buf)
1107 }
1108}
1109
1110#[cfg(windows)]
1111impl AsyncWrite for WindowsAsyncPty {
1112 fn poll_write(
1113 mut self: Pin<&mut Self>,
1114 cx: &mut Context<'_>,
1115 buf: &[u8],
1116 ) -> Poll<io::Result<usize>> {
1117 if matches!(self.child.try_wait(), Ok(Some(_))) {
1119 return Poll::Ready(Err(io::ErrorKind::BrokenPipe.into()));
1120 }
1121 Pin::new(&mut self.master).poll_write(cx, buf)
1122 }
1123
1124 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1125 Pin::new(&mut self.master).poll_flush(cx)
1126 }
1127
1128 fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
1129 Pin::new(&mut self.master).poll_shutdown(cx)
1130 }
1131}
1132
1133#[cfg(windows)]
1134impl std::fmt::Debug for WindowsAsyncPty {
1135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1136 f.debug_struct("WindowsAsyncPty")
1137 .field("pid", &self.pid)
1138 .field("dimensions", &self.dimensions)
1139 .finish_non_exhaustive()
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146
1147 #[test]
1148 fn pty_config_default() {
1149 let config = PtyConfig::default();
1150 assert_eq!(config.dimensions.0, 80);
1151 assert_eq!(config.dimensions.1, 24);
1152 assert_eq!(config.env_mode, EnvMode::Inherit);
1153 }
1154
1155 #[test]
1156 fn pty_config_from_session() {
1157 let session_config = SessionConfig {
1158 dimensions: (120, 40),
1159 ..Default::default()
1160 };
1161
1162 let pty_config = PtyConfig::from(&session_config);
1163 assert_eq!(pty_config.dimensions.0, 120);
1164 assert_eq!(pty_config.dimensions.1, 40);
1165 }
1166
1167 #[cfg(unix)]
1168 #[tokio::test]
1169 async fn spawn_rejects_null_byte_in_command() {
1170 let spawner = PtySpawner::new();
1171 let result = spawner.spawn("test\0command", &[]).await;
1172
1173 assert!(result.is_err());
1174 let err = result.unwrap_err();
1175 let err_str = err.to_string();
1176 assert!(
1177 err_str.contains("null byte"),
1178 "Expected error about null byte, got: {err_str}"
1179 );
1180 }
1181
1182 #[cfg(unix)]
1183 #[tokio::test]
1184 async fn spawn_rejects_null_byte_in_args() {
1185 let spawner = PtySpawner::new();
1186 let result = spawner
1187 .spawn("/bin/echo", &["hello\0world".to_string()])
1188 .await;
1189
1190 assert!(result.is_err());
1191 let err = result.unwrap_err();
1192 let err_str = err.to_string();
1193 assert!(
1194 err_str.contains("null byte"),
1195 "Expected error about null byte, got: {err_str}"
1196 );
1197 }
1198}