1use std::collections::VecDeque;
2use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
3use std::sync::{Arc, Condvar, Mutex};
4use std::thread;
5use std::time::{Duration, Instant};
6
7#[cfg(unix)]
8use super::backend::PtySlave;
9use super::backend::{Backend, PtyBackend, PtyChild, PtyMaster, PtySize};
10#[cfg(unix)]
11use super::posix_terminal_input_relay_worker;
12#[cfg(windows)]
13use super::{
14 apply_windows_pty_priority, assign_child_to_windows_kill_on_close_job,
15 assign_conpty_conhost_to_job, conhost_children_of_current_process,
16};
17use super::{
18 is_ignorable_process_control_error, poll_pty_process, record_pty_input_metrics,
19 spawn_pty_reader, store_pty_returncode, write_pty_input, IdleDetectorCore, NativePtyHandles,
20 PtyError, PtyReadShared, PtyReadState,
21};
22
23#[cfg(unix)]
24use super::pty_posix as pty_platform;
25#[cfg(windows)]
26use super::pty_windows;
27
28pub struct NativePtyProcess {
34 pub argv: Vec<String>,
36 pub cwd: Option<String>,
38 pub env: Option<Vec<(String, String)>>,
40 pub rows: u16,
42 pub cols: u16,
44 #[cfg(windows)]
46 pub nice: Option<i32>,
47 pub handles: Arc<Mutex<Option<NativePtyHandles>>>,
49 pub reader: Arc<PtyReadShared>,
51 pub returncode: Arc<Mutex<Option<i32>>>,
53 pub input_bytes_total: Arc<AtomicUsize>,
55 pub newline_events_total: Arc<AtomicUsize>,
57 pub submit_events_total: Arc<AtomicUsize>,
59 pub echo: Arc<AtomicBool>,
61 pub idle_detector: Arc<Mutex<Option<Arc<IdleDetectorCore>>>>,
63 pub output_bytes_total: Arc<AtomicUsize>,
65 pub control_churn_bytes_total: Arc<AtomicUsize>,
67 pub reader_worker: Mutex<Option<thread::JoinHandle<()>>>,
69 pub terminal_input_relay_stop: Arc<AtomicBool>,
71 pub terminal_input_relay_active: Arc<AtomicBool>,
73 pub terminal_input_relay_worker: Mutex<Option<thread::JoinHandle<()>>>,
75}
76
77pub(super) fn resolved_spawn_cwd(cwd: Option<&str>) -> Option<String> {
78 cwd.map(str::to_owned).or_else(|| {
79 std::env::current_dir()
80 .ok()
81 .map(|cwd| cwd.to_string_lossy().to_string())
82 })
83}
84
85impl NativePtyProcess {
86 pub fn new(
90 argv: Vec<String>,
91 cwd: Option<String>,
92 env: Option<Vec<(String, String)>>,
93 rows: u16,
94 cols: u16,
95 nice: Option<i32>,
96 ) -> Result<Self, PtyError> {
97 if argv.is_empty() {
98 return Err(PtyError::Other("command cannot be empty".into()));
99 }
100 #[cfg(not(windows))]
101 let _ = nice;
102 Ok(Self {
103 argv,
104 cwd,
105 env,
106 rows,
107 cols,
108 #[cfg(windows)]
109 nice,
110 handles: Arc::new(Mutex::new(None)),
111 reader: Arc::new(PtyReadShared {
112 state: Mutex::new(PtyReadState {
113 chunks: VecDeque::new(),
114 closed: false,
115 }),
116 condvar: Condvar::new(),
117 }),
118 returncode: Arc::new(Mutex::new(None)),
119 input_bytes_total: Arc::new(AtomicUsize::new(0)),
120 newline_events_total: Arc::new(AtomicUsize::new(0)),
121 submit_events_total: Arc::new(AtomicUsize::new(0)),
122 echo: Arc::new(AtomicBool::new(false)),
123 idle_detector: Arc::new(Mutex::new(None)),
124 output_bytes_total: Arc::new(AtomicUsize::new(0)),
125 control_churn_bytes_total: Arc::new(AtomicUsize::new(0)),
126 reader_worker: Mutex::new(None),
127 terminal_input_relay_stop: Arc::new(AtomicBool::new(false)),
128 terminal_input_relay_active: Arc::new(AtomicBool::new(false)),
129 terminal_input_relay_worker: Mutex::new(None),
130 })
131 }
132
133 pub fn mark_reader_closed(&self) {
135 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
136 guard.closed = true;
137 self.reader.condvar.notify_all();
138 }
139
140 pub fn store_returncode(&self, code: i32) {
142 store_pty_returncode(&self.returncode, code);
143 }
144
145 #[cfg(not(windows))]
149 pub(super) fn join_reader_worker(&self) {
150 if let Some(worker) = self
151 .reader_worker
152 .lock()
153 .expect("pty reader worker mutex poisoned")
154 .take()
155 {
156 let _ = worker.join();
157 }
158 }
159
160 pub fn record_input_metrics(&self, data: &[u8], submit: bool) {
162 record_pty_input_metrics(
163 &self.input_bytes_total,
164 &self.newline_events_total,
165 &self.submit_events_total,
166 data,
167 submit,
168 );
169 }
170
171 pub fn write_impl(&self, data: &[u8], submit: bool) -> Result<(), PtyError> {
173 self.record_input_metrics(data, submit);
174 write_pty_input(&self.handles, data)?;
175 Ok(())
176 }
177
178 pub fn request_terminal_input_relay_stop(&self) {
180 self.terminal_input_relay_stop
181 .store(true, Ordering::Release);
182 self.terminal_input_relay_active
183 .store(false, Ordering::Release);
184 }
185
186 pub fn start_terminal_input_relay_impl(&self) -> Result<(), PtyError> {
188 let mut worker_guard = self
189 .terminal_input_relay_worker
190 .lock()
191 .expect("pty terminal input relay mutex poisoned");
192 if worker_guard.is_some() && self.terminal_input_relay_active() {
193 return Ok(());
194 }
195 if self
196 .handles
197 .lock()
198 .expect("pty handles mutex poisoned")
199 .is_none()
200 {
201 return Err(PtyError::NotRunning);
202 }
203
204 self.terminal_input_relay_stop
205 .store(false, Ordering::Release);
206 self.terminal_input_relay_active
207 .store(true, Ordering::Release);
208
209 let handles = Arc::clone(&self.handles);
210 let returncode = Arc::clone(&self.returncode);
211 let input_bytes_total = Arc::clone(&self.input_bytes_total);
212 let newline_events_total = Arc::clone(&self.newline_events_total);
213 let submit_events_total = Arc::clone(&self.submit_events_total);
214 let stop = Arc::clone(&self.terminal_input_relay_stop);
215 let active = Arc::clone(&self.terminal_input_relay_active);
216
217 #[cfg(windows)]
218 {
219 let capture = super::terminal_input::TerminalInputCore::new();
220 capture.start_impl().map_err(PtyError::Io)?;
221 *worker_guard = Some(thread::spawn(move || {
222 loop {
223 if stop.load(Ordering::Acquire) {
224 break;
225 }
226 match poll_pty_process(&handles, &returncode) {
227 Ok(Some(_)) => break,
228 Ok(None) => {}
229 Err(_) => break,
230 }
231 match super::terminal_input::wait_for_terminal_input_event(
232 &capture.state,
233 &capture.condvar,
234 Some(Duration::from_millis(50)),
235 ) {
236 super::terminal_input::TerminalInputWaitOutcome::Event(event) => {
237 record_pty_input_metrics(
238 &input_bytes_total,
239 &newline_events_total,
240 &submit_events_total,
241 &event.data,
242 event.submit,
243 );
244 if write_pty_input(&handles, &event.data).is_err() {
245 break;
246 }
247 }
248 super::terminal_input::TerminalInputWaitOutcome::Timeout => continue,
249 super::terminal_input::TerminalInputWaitOutcome::Closed => break,
250 }
251 }
252 active.store(false, Ordering::Release);
253 let _ = capture.stop_impl();
254 }));
255 Ok(())
256 }
257
258 #[cfg(unix)]
259 {
260 if unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 {
261 self.terminal_input_relay_active
262 .store(false, Ordering::Release);
263 return Ok(());
264 }
265
266 *worker_guard = Some(thread::spawn(move || {
267 posix_terminal_input_relay_worker(
268 handles,
269 returncode,
270 input_bytes_total,
271 newline_events_total,
272 submit_events_total,
273 stop,
274 active,
275 );
276 }));
277 Ok(())
278 }
279 }
280
281 pub fn stop_terminal_input_relay_impl(&self) {
283 self.request_terminal_input_relay_stop();
284 if let Some(worker) = self
285 .terminal_input_relay_worker
286 .lock()
287 .expect("pty terminal input relay mutex poisoned")
288 .take()
289 {
290 let _ = worker.join();
291 }
292 }
293
294 pub fn terminal_input_relay_active(&self) -> bool {
296 self.terminal_input_relay_active.load(Ordering::Acquire)
297 }
298
299 #[inline(never)]
301 pub fn close_impl(&self) -> Result<(), PtyError> {
302 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::close_impl");
303 self.stop_terminal_input_relay_impl();
304 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
305 let Some(handles) = guard.take() else {
306 self.mark_reader_closed();
307 return Ok(());
308 };
309 drop(guard);
310
311 #[cfg(windows)]
312 let NativePtyHandles {
313 master,
314 writer,
315 mut child,
316 _job,
317 } = handles;
318 #[cfg(not(windows))]
319 let NativePtyHandles {
320 master,
321 writer,
322 mut child,
323 } = handles;
324
325 #[cfg(windows)]
326 {
327 {
328 crate::rp_rust_debug_scope!(
329 "running_process::NativePtyProcess::close_impl.drop_job"
330 );
331 drop(_job);
332 }
333
334 {
335 crate::rp_rust_debug_scope!(
336 "running_process::NativePtyProcess::close_impl.wait_job_exit"
337 );
338 let wait_deadline = Instant::now() + Duration::from_secs(2);
339 loop {
340 match child.try_wait() {
341 Ok(Some(status)) => {
342 let code = status as i32;
343 self.store_returncode(code);
344 break;
345 }
346 Ok(None) if Instant::now() < wait_deadline => {
347 thread::sleep(Duration::from_millis(10));
354 }
355 Ok(None) => {
356 if let Err(err) = child.kill() {
357 if !is_ignorable_process_control_error(&err) {
358 return Err(PtyError::Io(err));
359 }
360 }
361 let kill_deadline = Instant::now() + Duration::from_secs(2);
371 let code = loop {
372 match child.try_wait() {
373 Ok(Some(status)) => break status as i32,
374 Ok(None) if Instant::now() < kill_deadline => {
375 thread::sleep(Duration::from_millis(10));
376 }
377 _ => break -9,
378 }
379 };
380 self.store_returncode(code);
381 break;
382 }
383 Err(_) => {
384 self.store_returncode(-9);
385 break;
386 }
387 }
388 }
389 }
390 {
391 crate::rp_rust_debug_scope!(
392 "running_process::NativePtyProcess::close_impl.drop_writer"
393 );
394 drop(writer);
395 }
396 {
406 crate::rp_rust_debug_scope!(
407 "running_process::NativePtyProcess::close_impl.bounded_teardown"
408 );
409 let reader_worker = self
410 .reader_worker
411 .lock()
412 .expect("pty reader worker mutex poisoned")
413 .take();
414 let (tx, rx) = std::sync::mpsc::channel();
415 std::thread::spawn(move || {
416 drop(master);
417 drop(child);
418 if let Some(worker) = reader_worker {
419 let _ = worker.join();
420 }
421 let _ = tx.send(());
422 });
423 let _ = rx.recv_timeout(Duration::from_secs(2));
424 }
425 self.mark_reader_closed();
426 Ok(())
427 }
428
429 #[cfg(not(windows))]
430 {
431 drop(writer);
432 drop(master);
433
434 let code = {
435 crate::rp_rust_debug_scope!(
436 "running_process::NativePtyProcess::close_impl.wait_child"
437 );
438 match child.wait() {
439 Ok(status) => status as i32,
440 Err(_) => -9,
441 }
442 };
443 drop(child);
444
445 self.store_returncode(code);
446 {
447 crate::rp_rust_debug_scope!(
448 "running_process::NativePtyProcess::close_impl.join_reader"
449 );
450 self.join_reader_worker();
451 }
452 self.mark_reader_closed();
453 Ok(())
454 }
455 }
456
457 #[inline(never)]
459 pub fn close_nonblocking(&self) {
460 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::close_nonblocking");
461 #[cfg(windows)]
462 self.request_terminal_input_relay_stop();
463 let Ok(mut guard) = self.handles.lock() else {
464 return;
465 };
466 let Some(handles) = guard.take() else {
467 self.mark_reader_closed();
468 return;
469 };
470 drop(guard);
471
472 #[cfg(windows)]
473 let NativePtyHandles {
474 master,
475 writer,
476 mut child,
477 _job,
478 } = handles;
479 #[cfg(not(windows))]
480 let NativePtyHandles {
481 master,
482 writer,
483 mut child,
484 } = handles;
485
486 if let Err(err) = child.kill() {
487 if !is_ignorable_process_control_error(&err) {
488 return;
489 }
490 }
491 drop(writer);
492 #[cfg(windows)]
500 std::thread::spawn(move || {
501 drop(master);
502 drop(child);
503 drop(_job);
504 });
505 #[cfg(not(windows))]
506 {
507 drop(master);
508 drop(child);
509 }
510 self.mark_reader_closed();
511 }
512
513 pub fn start_impl(&self) -> Result<(), PtyError> {
515 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::start");
516 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
517 if guard.is_some() {
518 return Err(PtyError::AlreadyStarted);
519 }
520
521 #[cfg(windows)]
524 let conhost_pids_before = conhost_children_of_current_process();
525
526 let (mut master, slave) = Backend::openpty(PtySize {
527 rows: self.rows,
528 cols: self.cols,
529 pixel_width: 0,
530 pixel_height: 0,
531 })
532 .map_err(|e| PtyError::Spawn(e.to_string()))?;
533
534 let argv: Vec<std::ffi::OsString> =
536 self.argv.iter().map(std::ffi::OsString::from).collect();
537 let cwd = resolved_spawn_cwd(self.cwd.as_deref());
538 let env: Option<Vec<(std::ffi::OsString, std::ffi::OsString)>> =
539 self.env.as_ref().map(|e| {
540 e.iter()
541 .map(|(k, v)| (std::ffi::OsString::from(k), std::ffi::OsString::from(v)))
542 .collect()
543 });
544
545 let reader = master
546 .try_clone_reader()
547 .map_err(|e| PtyError::Spawn(e.to_string()))?;
548 let writer = master
549 .take_writer()
550 .map_err(|e| PtyError::Spawn(e.to_string()))?;
551 let cwd_path = cwd.as_deref().map(std::path::Path::new);
552 let child = slave
553 .spawn(&argv, cwd_path, env.as_deref())
554 .map_err(|e| PtyError::Spawn(e.to_string()))?;
555 #[cfg(windows)]
558 let job = assign_child_to_windows_kill_on_close_job(PtyChild::as_raw_handle(&child))?;
559 #[cfg(windows)]
560 assign_conpty_conhost_to_job(&job, &conhost_pids_before);
561 #[cfg(windows)]
562 apply_windows_pty_priority(PtyChild::as_raw_handle(&child), self.nice)?;
563 let shared = Arc::clone(&self.reader);
564 let echo = Arc::clone(&self.echo);
565 let idle_detector = Arc::clone(&self.idle_detector);
566 let output_bytes = Arc::clone(&self.output_bytes_total);
567 let churn_bytes = Arc::clone(&self.control_churn_bytes_total);
568 let reader_worker = thread::spawn(move || {
569 spawn_pty_reader(
570 reader,
571 shared,
572 echo,
573 idle_detector,
574 output_bytes,
575 churn_bytes,
576 );
577 });
578 *self
579 .reader_worker
580 .lock()
581 .expect("pty reader worker mutex poisoned") = Some(reader_worker);
582
583 *guard = Some(NativePtyHandles {
584 master: Box::new(master) as Box<dyn PtyMaster>,
585 writer: Arc::new(Mutex::new(writer)),
588 child: Box::new(child) as Box<dyn PtyChild>,
589 #[cfg(windows)]
590 _job: job,
591 });
592 Ok(())
593 }
594
595 pub fn respond_to_queries_impl(&self, data: &[u8]) -> Result<(), PtyError> {
597 #[cfg(windows)]
598 {
599 pty_windows::respond_to_queries(self, data)
600 }
601
602 #[cfg(unix)]
603 {
604 pty_platform::respond_to_queries(self, data)
605 }
606 }
607
608 pub fn resize_impl(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
610 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::resize");
611 let guard = self.handles.lock().expect("pty handles mutex poisoned");
612 if let Some(handles) = guard.as_ref() {
613 #[cfg(windows)]
614 {
615 let _ = (rows, cols, handles);
616 return Ok(());
620 }
621
622 #[cfg(not(windows))]
623 handles
624 .master
625 .resize(PtySize {
626 rows,
627 cols,
628 pixel_width: 0,
629 pixel_height: 0,
630 })
631 .map_err(|e| PtyError::Other(e.to_string()))?;
632 }
633 Ok(())
634 }
635
636 pub fn send_interrupt_impl(&self) -> Result<(), PtyError> {
638 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::send_interrupt");
639 #[cfg(windows)]
640 {
641 pty_windows::send_interrupt(self)
642 }
643
644 #[cfg(unix)]
645 {
646 pty_platform::send_interrupt(self)
647 }
648 }
649
650 pub fn wait_impl(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
654 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::wait");
655 if let Some(code) = *self
657 .returncode
658 .lock()
659 .expect("pty returncode mutex poisoned")
660 {
661 return Ok(code);
662 }
663 let start = Instant::now();
664 loop {
665 if let Some(code) = poll_pty_process(&self.handles, &self.returncode)? {
666 return Ok(code);
667 }
668 if timeout.is_some_and(|limit| start.elapsed() >= Duration::from_secs_f64(limit)) {
669 return Err(PtyError::Timeout);
670 }
671 thread::sleep(Duration::from_millis(10));
675 }
676 }
677
678 pub fn terminate_impl(&self) -> Result<(), PtyError> {
680 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::terminate");
681 #[cfg(windows)]
682 {
683 if self
684 .handles
685 .lock()
686 .expect("pty handles mutex poisoned")
687 .is_none()
688 {
689 return Err(PtyError::NotRunning);
690 }
691 self.close_impl()
692 }
693
694 #[cfg(unix)]
695 {
696 pty_platform::terminate(self)
697 }
698 }
699
700 pub fn kill_impl(&self) -> Result<(), PtyError> {
702 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::kill");
703 #[cfg(windows)]
704 {
705 if self
706 .handles
707 .lock()
708 .expect("pty handles mutex poisoned")
709 .is_none()
710 {
711 return Err(PtyError::NotRunning);
712 }
713 self.close_impl()
714 }
715
716 #[cfg(unix)]
717 {
718 pty_platform::kill(self)
719 }
720 }
721
722 pub fn terminate_tree_impl(&self) -> Result<(), PtyError> {
724 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::terminate_tree");
725 #[cfg(windows)]
726 {
727 pty_windows::terminate_tree(self)
728 }
729
730 #[cfg(unix)]
731 {
732 pty_platform::terminate_tree(self)
733 }
734 }
735
736 pub fn kill_tree_impl(&self) -> Result<(), PtyError> {
738 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::kill_tree");
739 #[cfg(windows)]
740 {
741 pty_windows::kill_tree(self)
742 }
743
744 #[cfg(unix)]
745 {
746 pty_platform::kill_tree(self)
747 }
748 }
749
750 pub fn pid(&self) -> Result<Option<u32>, PtyError> {
752 let guard = self.handles.lock().expect("pty handles mutex poisoned");
753 if let Some(handles) = guard.as_ref() {
754 #[cfg(unix)]
755 if let Some(pid) = handles.master.process_group_leader() {
756 if let Ok(pid) = u32::try_from(pid) {
757 return Ok(Some(pid));
758 }
759 }
760 return Ok(Some(handles.child.pid()));
761 }
762 Ok(None)
763 }
764
765 pub fn read_chunk_impl(&self, timeout: Option<f64>) -> Result<Option<Vec<u8>>, PtyError> {
768 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
769 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
770 loop {
771 if let Some(chunk) = guard.chunks.pop_front() {
772 return Ok(Some(chunk));
773 }
774 if guard.closed {
775 return Err(PtyError::Other("Pseudo-terminal stream is closed".into()));
776 }
777 match deadline {
778 Some(deadline) => {
779 let now = Instant::now();
780 if now >= deadline {
781 return Ok(None); }
783 let wait = deadline.saturating_duration_since(now);
784 let result = self
785 .reader
786 .condvar
787 .wait_timeout(guard, wait)
788 .expect("pty read mutex poisoned");
789 guard = result.0;
790 }
791 None => {
792 guard = self
793 .reader
794 .condvar
795 .wait(guard)
796 .expect("pty read mutex poisoned");
797 }
798 }
799 }
800 }
801
802 pub fn wait_for_reader_closed_impl(&self, timeout: Option<f64>) -> bool {
804 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
805 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
806 loop {
807 if guard.closed {
808 return true;
809 }
810 match deadline {
811 Some(deadline) => {
812 let now = Instant::now();
813 if now >= deadline {
814 return false;
815 }
816 let wait = deadline.saturating_duration_since(now);
817 let result = self
818 .reader
819 .condvar
820 .wait_timeout(guard, wait)
821 .expect("pty read mutex poisoned");
822 guard = result.0;
823 }
824 None => {
825 guard = self
826 .reader
827 .condvar
828 .wait(guard)
829 .expect("pty read mutex poisoned");
830 }
831 }
832 }
833 }
834
835 pub fn wait_and_drain_impl(
837 &self,
838 timeout: Option<f64>,
839 drain_timeout: f64,
840 ) -> Result<i32, PtyError> {
841 let code = self.wait_impl(timeout)?;
842 let deadline = Instant::now() + Duration::from_secs_f64(drain_timeout.max(0.0));
843 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
844 while !guard.closed {
845 let remaining = deadline.saturating_duration_since(Instant::now());
846 if remaining.is_zero() {
847 break;
848 }
849 let result = self
850 .reader
851 .condvar
852 .wait_timeout(guard, remaining)
853 .expect("pty read mutex poisoned");
854 guard = result.0;
855 }
856 Ok(code)
857 }
858
859 pub fn set_echo(&self, enabled: bool) {
861 self.echo.store(enabled, Ordering::Release);
862 }
863
864 pub fn echo_enabled(&self) -> bool {
866 self.echo.load(Ordering::Acquire)
867 }
868
869 pub fn attach_idle_detector(&self, detector: &Arc<IdleDetectorCore>) {
871 let mut guard = self
872 .idle_detector
873 .lock()
874 .expect("idle detector mutex poisoned");
875 *guard = Some(Arc::clone(detector));
876 }
877
878 pub fn detach_idle_detector(&self) {
880 let mut guard = self
881 .idle_detector
882 .lock()
883 .expect("idle detector mutex poisoned");
884 *guard = None;
885 }
886
887 pub fn pty_input_bytes_total(&self) -> usize {
889 self.input_bytes_total.load(Ordering::Acquire)
890 }
891
892 pub fn pty_newline_events_total(&self) -> usize {
894 self.newline_events_total.load(Ordering::Acquire)
895 }
896
897 pub fn pty_submit_events_total(&self) -> usize {
899 self.submit_events_total.load(Ordering::Acquire)
900 }
901
902 pub fn pty_output_bytes_total(&self) -> usize {
904 self.output_bytes_total.load(Ordering::Acquire)
905 }
906
907 pub fn pty_control_churn_bytes_total(&self) -> usize {
909 self.control_churn_bytes_total.load(Ordering::Acquire)
910 }
911}
912
913#[derive(Debug, Clone, Copy)]
918pub struct InteractivePtyOptions {
919 pub echo_output: bool,
921 pub relay_terminal_input: bool,
923 pub respond_to_queries: bool,
925}
926
927impl Default for InteractivePtyOptions {
928 fn default() -> Self {
929 Self {
930 echo_output: true,
931 relay_terminal_input: true,
932 respond_to_queries: true,
933 }
934 }
935}
936
937#[derive(Debug, Default)]
939pub struct InteractivePtyPumpResult {
940 pub chunks: Vec<Vec<u8>>,
942 pub stream_closed: bool,
944}
945
946pub struct InteractivePtySession {
951 process: NativePtyProcess,
952 options: InteractivePtyOptions,
953}
954
955impl InteractivePtySession {
956 pub fn new(process: NativePtyProcess) -> Self {
958 Self::with_options(process, InteractivePtyOptions::default())
959 }
960
961 pub fn with_options(process: NativePtyProcess, options: InteractivePtyOptions) -> Self {
963 Self { process, options }
964 }
965
966 pub fn process(&self) -> &NativePtyProcess {
968 &self.process
969 }
970
971 pub fn start(&self) -> Result<(), PtyError> {
973 self.process.set_echo(self.options.echo_output);
974 self.process.start_impl()?;
975 if self.options.relay_terminal_input {
976 self.process.start_terminal_input_relay_impl()?;
977 }
978 Ok(())
979 }
980
981 pub fn pump_output(
986 &self,
987 timeout: Option<f64>,
988 consume_all: bool,
989 ) -> Result<InteractivePtyPumpResult, PtyError> {
990 let mut pumped = InteractivePtyPumpResult::default();
991 let mut next_timeout = timeout;
992 loop {
993 match self.process.read_chunk_impl(next_timeout) {
994 Ok(Some(chunk)) => {
995 if self.options.respond_to_queries {
996 self.process.respond_to_queries_impl(&chunk)?;
997 }
998 pumped.chunks.push(chunk);
999 if !consume_all {
1000 break;
1001 }
1002 next_timeout = Some(0.0);
1003 }
1004 Ok(None) => break,
1005 Err(PtyError::Other(message)) if message == "Pseudo-terminal stream is closed" => {
1006 pumped.stream_closed = true;
1007 break;
1008 }
1009 Err(err) => return Err(err),
1010 }
1011 }
1012 Ok(pumped)
1013 }
1014
1015 pub fn resize(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
1017 self.process.resize_impl(rows, cols)
1018 }
1019
1020 pub fn send_interrupt(&self) -> Result<(), PtyError> {
1022 self.process.send_interrupt_impl()
1023 }
1024
1025 pub fn wait(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
1027 self.process.wait_impl(timeout)
1028 }
1029
1030 pub fn wait_and_drain(
1032 &self,
1033 timeout: Option<f64>,
1034 drain_timeout: f64,
1035 ) -> Result<i32, PtyError> {
1036 self.process.wait_and_drain_impl(timeout, drain_timeout)
1037 }
1038
1039 pub fn terminate(&self) -> Result<(), PtyError> {
1041 self.process.terminate_impl()
1042 }
1043
1044 pub fn kill(&self) -> Result<(), PtyError> {
1046 self.process.kill_impl()
1047 }
1048
1049 pub fn close(&self) -> Result<(), PtyError> {
1051 self.process.close_impl()
1052 }
1053}
1054
1055impl Drop for NativePtyProcess {
1056 fn drop(&mut self) {
1057 self.close_nonblocking();
1058 }
1059}