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
7use portable_pty::{native_pty_system, PtySize};
8
9use super::{
10 command_builder_from_argv, is_ignorable_process_control_error, poll_pty_process,
11 portable_exit_code, record_pty_input_metrics, spawn_pty_reader, store_pty_returncode,
12 write_pty_input, IdleDetectorCore, NativePtyHandles, PtyError, PtyReadShared, PtyReadState,
13};
14#[cfg(unix)]
15use super::posix_terminal_input_relay_worker;
16#[cfg(windows)]
17use super::{
18 apply_windows_pty_priority, assign_child_to_windows_kill_on_close_job,
19 assign_conpty_conhost_to_job, conhost_children_of_current_process,
20};
21
22#[cfg(unix)]
23use super::pty_posix as pty_platform;
24#[cfg(windows)]
25use super::pty_windows;
26
27pub struct NativePtyProcess {
28 pub argv: Vec<String>,
29 pub cwd: Option<String>,
30 pub env: Option<Vec<(String, String)>>,
31 pub rows: u16,
32 pub cols: u16,
33 #[cfg(windows)]
34 pub nice: Option<i32>,
35 pub handles: Arc<Mutex<Option<NativePtyHandles>>>,
36 pub reader: Arc<PtyReadShared>,
37 pub returncode: Arc<Mutex<Option<i32>>>,
38 pub input_bytes_total: Arc<AtomicUsize>,
39 pub newline_events_total: Arc<AtomicUsize>,
40 pub submit_events_total: Arc<AtomicUsize>,
41 pub echo: Arc<AtomicBool>,
43 pub idle_detector: Arc<Mutex<Option<Arc<IdleDetectorCore>>>>,
45 pub output_bytes_total: Arc<AtomicUsize>,
47 pub control_churn_bytes_total: Arc<AtomicUsize>,
49 pub reader_worker: Mutex<Option<thread::JoinHandle<()>>>,
50 pub terminal_input_relay_stop: Arc<AtomicBool>,
51 pub terminal_input_relay_active: Arc<AtomicBool>,
52 pub terminal_input_relay_worker: Mutex<Option<thread::JoinHandle<()>>>,
53}
54
55pub(super) fn resolved_spawn_cwd(cwd: Option<&str>) -> Option<String> {
56 cwd.map(str::to_owned).or_else(|| {
57 std::env::current_dir()
58 .ok()
59 .map(|cwd| cwd.to_string_lossy().to_string())
60 })
61}
62
63impl NativePtyProcess {
64 pub fn new(
65 argv: Vec<String>,
66 cwd: Option<String>,
67 env: Option<Vec<(String, String)>>,
68 rows: u16,
69 cols: u16,
70 nice: Option<i32>,
71 ) -> Result<Self, PtyError> {
72 if argv.is_empty() {
73 return Err(PtyError::Other("command cannot be empty".into()));
74 }
75 #[cfg(not(windows))]
76 let _ = nice;
77 Ok(Self {
78 argv,
79 cwd,
80 env,
81 rows,
82 cols,
83 #[cfg(windows)]
84 nice,
85 handles: Arc::new(Mutex::new(None)),
86 reader: Arc::new(PtyReadShared {
87 state: Mutex::new(PtyReadState {
88 chunks: VecDeque::new(),
89 closed: false,
90 }),
91 condvar: Condvar::new(),
92 }),
93 returncode: Arc::new(Mutex::new(None)),
94 input_bytes_total: Arc::new(AtomicUsize::new(0)),
95 newline_events_total: Arc::new(AtomicUsize::new(0)),
96 submit_events_total: Arc::new(AtomicUsize::new(0)),
97 echo: Arc::new(AtomicBool::new(false)),
98 idle_detector: Arc::new(Mutex::new(None)),
99 output_bytes_total: Arc::new(AtomicUsize::new(0)),
100 control_churn_bytes_total: Arc::new(AtomicUsize::new(0)),
101 reader_worker: Mutex::new(None),
102 terminal_input_relay_stop: Arc::new(AtomicBool::new(false)),
103 terminal_input_relay_active: Arc::new(AtomicBool::new(false)),
104 terminal_input_relay_worker: Mutex::new(None),
105 })
106 }
107
108 pub fn mark_reader_closed(&self) {
109 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
110 guard.closed = true;
111 self.reader.condvar.notify_all();
112 }
113
114 pub fn store_returncode(&self, code: i32) {
115 store_pty_returncode(&self.returncode, code);
116 }
117
118 pub(super) fn join_reader_worker(&self) {
119 if let Some(worker) = self
120 .reader_worker
121 .lock()
122 .expect("pty reader worker mutex poisoned")
123 .take()
124 {
125 let _ = worker.join();
126 }
127 }
128
129 pub fn record_input_metrics(&self, data: &[u8], submit: bool) {
130 record_pty_input_metrics(
131 &self.input_bytes_total,
132 &self.newline_events_total,
133 &self.submit_events_total,
134 data,
135 submit,
136 );
137 }
138
139 pub fn write_impl(&self, data: &[u8], submit: bool) -> Result<(), PtyError> {
140 self.record_input_metrics(data, submit);
141 write_pty_input(&self.handles, data)?;
142 Ok(())
143 }
144
145 pub fn request_terminal_input_relay_stop(&self) {
146 self.terminal_input_relay_stop
147 .store(true, Ordering::Release);
148 self.terminal_input_relay_active
149 .store(false, Ordering::Release);
150 }
151
152 pub fn start_terminal_input_relay_impl(&self) -> Result<(), PtyError> {
153 let mut worker_guard = self
154 .terminal_input_relay_worker
155 .lock()
156 .expect("pty terminal input relay mutex poisoned");
157 if worker_guard.is_some() && self.terminal_input_relay_active() {
158 return Ok(());
159 }
160 if self
161 .handles
162 .lock()
163 .expect("pty handles mutex poisoned")
164 .is_none()
165 {
166 return Err(PtyError::NotRunning);
167 }
168
169 self.terminal_input_relay_stop
170 .store(false, Ordering::Release);
171 self.terminal_input_relay_active
172 .store(true, Ordering::Release);
173
174 let handles = Arc::clone(&self.handles);
175 let returncode = Arc::clone(&self.returncode);
176 let input_bytes_total = Arc::clone(&self.input_bytes_total);
177 let newline_events_total = Arc::clone(&self.newline_events_total);
178 let submit_events_total = Arc::clone(&self.submit_events_total);
179 let stop = Arc::clone(&self.terminal_input_relay_stop);
180 let active = Arc::clone(&self.terminal_input_relay_active);
181
182 #[cfg(windows)]
183 {
184 let capture = super::terminal_input::TerminalInputCore::new();
185 capture.start_impl().map_err(PtyError::Io)?;
186 *worker_guard = Some(thread::spawn(move || {
187 loop {
188 if stop.load(Ordering::Acquire) {
189 break;
190 }
191 match poll_pty_process(&handles, &returncode) {
192 Ok(Some(_)) => break,
193 Ok(None) => {}
194 Err(_) => break,
195 }
196 match super::terminal_input::wait_for_terminal_input_event(
197 &capture.state,
198 &capture.condvar,
199 Some(Duration::from_millis(50)),
200 ) {
201 super::terminal_input::TerminalInputWaitOutcome::Event(event) => {
202 record_pty_input_metrics(
203 &input_bytes_total,
204 &newline_events_total,
205 &submit_events_total,
206 &event.data,
207 event.submit,
208 );
209 if write_pty_input(&handles, &event.data).is_err() {
210 break;
211 }
212 }
213 super::terminal_input::TerminalInputWaitOutcome::Timeout => continue,
214 super::terminal_input::TerminalInputWaitOutcome::Closed => break,
215 }
216 }
217 active.store(false, Ordering::Release);
218 let _ = capture.stop_impl();
219 }));
220 Ok(())
221 }
222
223 #[cfg(unix)]
224 {
225 if unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 {
226 self.terminal_input_relay_active
227 .store(false, Ordering::Release);
228 return Ok(());
229 }
230
231 *worker_guard = Some(thread::spawn(move || {
232 posix_terminal_input_relay_worker(
233 handles,
234 returncode,
235 input_bytes_total,
236 newline_events_total,
237 submit_events_total,
238 stop,
239 active,
240 );
241 }));
242 Ok(())
243 }
244 }
245
246 pub fn stop_terminal_input_relay_impl(&self) {
247 self.request_terminal_input_relay_stop();
248 if let Some(worker) = self
249 .terminal_input_relay_worker
250 .lock()
251 .expect("pty terminal input relay mutex poisoned")
252 .take()
253 {
254 let _ = worker.join();
255 }
256 }
257
258 pub fn terminal_input_relay_active(&self) -> bool {
259 self.terminal_input_relay_active.load(Ordering::Acquire)
260 }
261
262 #[inline(never)]
264 pub fn close_impl(&self) -> Result<(), PtyError> {
265 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::close_impl");
266 self.stop_terminal_input_relay_impl();
267 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
268 let Some(handles) = guard.take() else {
269 self.mark_reader_closed();
270 return Ok(());
271 };
272 drop(guard);
273
274 #[cfg(windows)]
275 let NativePtyHandles {
276 master,
277 writer,
278 mut child,
279 _job,
280 } = handles;
281 #[cfg(not(windows))]
282 let NativePtyHandles {
283 master,
284 writer,
285 mut child,
286 } = handles;
287
288 #[cfg(windows)]
289 {
290 {
291 crate::rp_rust_debug_scope!(
292 "running_process_core::NativePtyProcess::close_impl.drop_job"
293 );
294 drop(_job);
295 }
296
297 {
298 crate::rp_rust_debug_scope!(
299 "running_process_core::NativePtyProcess::close_impl.wait_job_exit"
300 );
301 let wait_deadline = Instant::now() + Duration::from_secs(2);
302 loop {
303 match child.try_wait() {
304 Ok(Some(status)) => {
305 let code = portable_exit_code(status);
306 self.store_returncode(code);
307 break;
308 }
309 Ok(None) if Instant::now() < wait_deadline => {
310 thread::sleep(Duration::from_millis(10));
311 }
312 Ok(None) => {
313 if let Err(err) = child.kill() {
314 if !is_ignorable_process_control_error(&err) {
315 return Err(PtyError::Io(err));
316 }
317 }
318 let code = match child.wait() {
319 Ok(status) => portable_exit_code(status),
320 Err(_) => -9,
321 };
322 self.store_returncode(code);
323 break;
324 }
325 Err(_) => {
326 self.store_returncode(-9);
327 break;
328 }
329 }
330 }
331 }
332 {
333 crate::rp_rust_debug_scope!(
334 "running_process_core::NativePtyProcess::close_impl.drop_writer"
335 );
336 drop(writer);
337 }
338 {
339 crate::rp_rust_debug_scope!(
340 "running_process_core::NativePtyProcess::close_impl.drop_master"
341 );
342 drop(master);
343 }
344 drop(child);
345 {
346 crate::rp_rust_debug_scope!(
347 "running_process_core::NativePtyProcess::close_impl.join_reader"
348 );
349 self.join_reader_worker();
350 }
351 self.mark_reader_closed();
352 Ok(())
353 }
354
355 #[cfg(not(windows))]
356 {
357 drop(writer);
358 drop(master);
359
360 let code = {
361 crate::rp_rust_debug_scope!(
362 "running_process_core::NativePtyProcess::close_impl.wait_child"
363 );
364 match child.wait() {
365 Ok(status) => portable_exit_code(status),
366 Err(_) => -9,
367 }
368 };
369 drop(child);
370
371 self.store_returncode(code);
372 {
373 crate::rp_rust_debug_scope!(
374 "running_process_core::NativePtyProcess::close_impl.join_reader"
375 );
376 self.join_reader_worker();
377 }
378 self.mark_reader_closed();
379 Ok(())
380 }
381 }
382
383 #[inline(never)]
385 pub fn close_nonblocking(&self) {
386 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::close_nonblocking");
387 #[cfg(windows)]
388 self.request_terminal_input_relay_stop();
389 let Ok(mut guard) = self.handles.lock() else {
390 return;
391 };
392 let Some(handles) = guard.take() else {
393 self.mark_reader_closed();
394 return;
395 };
396 drop(guard);
397
398 #[cfg(windows)]
399 let NativePtyHandles {
400 master,
401 writer,
402 mut child,
403 _job,
404 } = handles;
405 #[cfg(not(windows))]
406 let NativePtyHandles {
407 master,
408 writer,
409 mut child,
410 } = handles;
411
412 if let Err(err) = child.kill() {
413 if !is_ignorable_process_control_error(&err) {
414 return;
415 }
416 }
417 drop(writer);
418 drop(master);
419 drop(child);
420 #[cfg(windows)]
421 drop(_job);
422 self.mark_reader_closed();
423 }
424
425 pub fn start_impl(&self) -> Result<(), PtyError> {
426 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::start");
427 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
428 if guard.is_some() {
429 return Err(PtyError::AlreadyStarted);
430 }
431
432 #[cfg(windows)]
435 let conhost_pids_before = conhost_children_of_current_process();
436
437 let pty_system = native_pty_system();
438 let pair = pty_system
439 .openpty(PtySize {
440 rows: self.rows,
441 cols: self.cols,
442 pixel_width: 0,
443 pixel_height: 0,
444 })
445 .map_err(|e| PtyError::Spawn(e.to_string()))?;
446
447 let mut cmd = command_builder_from_argv(&self.argv);
448 let cwd = resolved_spawn_cwd(self.cwd.as_deref());
449 if let Some(cwd) = &cwd {
450 cmd.cwd(cwd);
451 }
452 if let Some(env) = &self.env {
453 cmd.env_clear();
454 for (key, value) in env {
455 cmd.env(key, value);
456 }
457 }
458
459 let reader = pair
460 .master
461 .try_clone_reader()
462 .map_err(|e| PtyError::Spawn(e.to_string()))?;
463 let writer = pair
464 .master
465 .take_writer()
466 .map_err(|e| PtyError::Spawn(e.to_string()))?;
467 let child = pair
468 .slave
469 .spawn_command(cmd)
470 .map_err(|e| PtyError::Spawn(e.to_string()))?;
471 #[cfg(windows)]
472 let job = assign_child_to_windows_kill_on_close_job(child.as_raw_handle())?;
473 #[cfg(windows)]
474 assign_conpty_conhost_to_job(&job, &conhost_pids_before);
475 #[cfg(windows)]
476 apply_windows_pty_priority(child.as_raw_handle(), self.nice)?;
477 let shared = Arc::clone(&self.reader);
478 let echo = Arc::clone(&self.echo);
479 let idle_detector = Arc::clone(&self.idle_detector);
480 let output_bytes = Arc::clone(&self.output_bytes_total);
481 let churn_bytes = Arc::clone(&self.control_churn_bytes_total);
482 let reader_worker = thread::spawn(move || {
483 spawn_pty_reader(
484 reader,
485 shared,
486 echo,
487 idle_detector,
488 output_bytes,
489 churn_bytes,
490 );
491 });
492 *self
493 .reader_worker
494 .lock()
495 .expect("pty reader worker mutex poisoned") = Some(reader_worker);
496
497 *guard = Some(NativePtyHandles {
498 master: pair.master,
499 writer,
500 child,
501 #[cfg(windows)]
502 _job: job,
503 });
504 Ok(())
505 }
506
507 pub fn respond_to_queries_impl(&self, data: &[u8]) -> Result<(), PtyError> {
508 #[cfg(windows)]
509 {
510 pty_windows::respond_to_queries(self, data)
511 }
512
513 #[cfg(unix)]
514 {
515 pty_platform::respond_to_queries(self, data)
516 }
517 }
518
519 pub fn resize_impl(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
520 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::resize");
521 let guard = self.handles.lock().expect("pty handles mutex poisoned");
522 if let Some(handles) = guard.as_ref() {
523 #[cfg(windows)]
524 {
525 let _ = (rows, cols, handles);
526 return Ok(());
530 }
531
532 #[cfg(not(windows))]
533 handles
534 .master
535 .resize(PtySize {
536 rows,
537 cols,
538 pixel_width: 0,
539 pixel_height: 0,
540 })
541 .map_err(|e| PtyError::Other(e.to_string()))?;
542 }
543 Ok(())
544 }
545
546 pub fn send_interrupt_impl(&self) -> Result<(), PtyError> {
547 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::send_interrupt");
548 #[cfg(windows)]
549 {
550 pty_windows::send_interrupt(self)
551 }
552
553 #[cfg(unix)]
554 {
555 pty_platform::send_interrupt(self)
556 }
557 }
558
559 pub fn wait_impl(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
560 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::wait");
561 if let Some(code) = *self
563 .returncode
564 .lock()
565 .expect("pty returncode mutex poisoned")
566 {
567 return Ok(code);
568 }
569 let start = Instant::now();
570 loop {
571 if let Some(code) = poll_pty_process(&self.handles, &self.returncode)? {
572 return Ok(code);
573 }
574 if timeout.is_some_and(|limit| start.elapsed() >= Duration::from_secs_f64(limit)) {
575 return Err(PtyError::Timeout);
576 }
577 thread::sleep(Duration::from_millis(10));
578 }
579 }
580
581 pub fn terminate_impl(&self) -> Result<(), PtyError> {
582 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::terminate");
583 #[cfg(windows)]
584 {
585 if self
586 .handles
587 .lock()
588 .expect("pty handles mutex poisoned")
589 .is_none()
590 {
591 return Err(PtyError::NotRunning);
592 }
593 self.close_impl()
594 }
595
596 #[cfg(unix)]
597 {
598 pty_platform::terminate(self)
599 }
600 }
601
602 pub fn kill_impl(&self) -> Result<(), PtyError> {
603 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::kill");
604 #[cfg(windows)]
605 {
606 if self
607 .handles
608 .lock()
609 .expect("pty handles mutex poisoned")
610 .is_none()
611 {
612 return Err(PtyError::NotRunning);
613 }
614 self.close_impl()
615 }
616
617 #[cfg(unix)]
618 {
619 pty_platform::kill(self)
620 }
621 }
622
623 pub fn terminate_tree_impl(&self) -> Result<(), PtyError> {
624 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::terminate_tree");
625 #[cfg(windows)]
626 {
627 pty_windows::terminate_tree(self)
628 }
629
630 #[cfg(unix)]
631 {
632 pty_platform::terminate_tree(self)
633 }
634 }
635
636 pub fn kill_tree_impl(&self) -> Result<(), PtyError> {
637 crate::rp_rust_debug_scope!("running_process_core::NativePtyProcess::kill_tree");
638 #[cfg(windows)]
639 {
640 pty_windows::kill_tree(self)
641 }
642
643 #[cfg(unix)]
644 {
645 pty_platform::kill_tree(self)
646 }
647 }
648
649 pub fn pid(&self) -> Result<Option<u32>, PtyError> {
651 let guard = self.handles.lock().expect("pty handles mutex poisoned");
652 if let Some(handles) = guard.as_ref() {
653 #[cfg(unix)]
654 if let Some(pid) = handles.master.process_group_leader() {
655 if let Ok(pid) = u32::try_from(pid) {
656 return Ok(Some(pid));
657 }
658 }
659 return Ok(handles.child.process_id());
660 }
661 Ok(None)
662 }
663
664 pub fn read_chunk_impl(&self, timeout: Option<f64>) -> Result<Option<Vec<u8>>, PtyError> {
667 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
668 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
669 loop {
670 if let Some(chunk) = guard.chunks.pop_front() {
671 return Ok(Some(chunk));
672 }
673 if guard.closed {
674 return Err(PtyError::Other("Pseudo-terminal stream is closed".into()));
675 }
676 match deadline {
677 Some(deadline) => {
678 let now = Instant::now();
679 if now >= deadline {
680 return Ok(None); }
682 let wait = deadline.saturating_duration_since(now);
683 let result = self
684 .reader
685 .condvar
686 .wait_timeout(guard, wait)
687 .expect("pty read mutex poisoned");
688 guard = result.0;
689 }
690 None => {
691 guard = self
692 .reader
693 .condvar
694 .wait(guard)
695 .expect("pty read mutex poisoned");
696 }
697 }
698 }
699 }
700
701 pub fn wait_for_reader_closed_impl(&self, timeout: Option<f64>) -> bool {
703 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
704 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
705 loop {
706 if guard.closed {
707 return true;
708 }
709 match deadline {
710 Some(deadline) => {
711 let now = Instant::now();
712 if now >= deadline {
713 return false;
714 }
715 let wait = deadline.saturating_duration_since(now);
716 let result = self
717 .reader
718 .condvar
719 .wait_timeout(guard, wait)
720 .expect("pty read mutex poisoned");
721 guard = result.0;
722 }
723 None => {
724 guard = self
725 .reader
726 .condvar
727 .wait(guard)
728 .expect("pty read mutex poisoned");
729 }
730 }
731 }
732 }
733
734 pub fn wait_and_drain_impl(
736 &self,
737 timeout: Option<f64>,
738 drain_timeout: f64,
739 ) -> Result<i32, PtyError> {
740 let code = self.wait_impl(timeout)?;
741 let deadline = Instant::now() + Duration::from_secs_f64(drain_timeout.max(0.0));
742 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
743 while !guard.closed {
744 let remaining = deadline.saturating_duration_since(Instant::now());
745 if remaining.is_zero() {
746 break;
747 }
748 let result = self
749 .reader
750 .condvar
751 .wait_timeout(guard, remaining)
752 .expect("pty read mutex poisoned");
753 guard = result.0;
754 }
755 Ok(code)
756 }
757
758 pub fn set_echo(&self, enabled: bool) {
759 self.echo.store(enabled, Ordering::Release);
760 }
761
762 pub fn echo_enabled(&self) -> bool {
763 self.echo.load(Ordering::Acquire)
764 }
765
766 pub fn attach_idle_detector(&self, detector: &Arc<IdleDetectorCore>) {
767 let mut guard = self
768 .idle_detector
769 .lock()
770 .expect("idle detector mutex poisoned");
771 *guard = Some(Arc::clone(detector));
772 }
773
774 pub fn detach_idle_detector(&self) {
775 let mut guard = self
776 .idle_detector
777 .lock()
778 .expect("idle detector mutex poisoned");
779 *guard = None;
780 }
781
782 pub fn pty_input_bytes_total(&self) -> usize {
783 self.input_bytes_total.load(Ordering::Acquire)
784 }
785
786 pub fn pty_newline_events_total(&self) -> usize {
787 self.newline_events_total.load(Ordering::Acquire)
788 }
789
790 pub fn pty_submit_events_total(&self) -> usize {
791 self.submit_events_total.load(Ordering::Acquire)
792 }
793
794 pub fn pty_output_bytes_total(&self) -> usize {
795 self.output_bytes_total.load(Ordering::Acquire)
796 }
797
798 pub fn pty_control_churn_bytes_total(&self) -> usize {
799 self.control_churn_bytes_total.load(Ordering::Acquire)
800 }
801}
802
803#[derive(Debug, Clone, Copy)]
808pub struct InteractivePtyOptions {
809 pub echo_output: bool,
810 pub relay_terminal_input: bool,
811 pub respond_to_queries: bool,
812}
813
814impl Default for InteractivePtyOptions {
815 fn default() -> Self {
816 Self {
817 echo_output: true,
818 relay_terminal_input: true,
819 respond_to_queries: true,
820 }
821 }
822}
823
824#[derive(Debug, Default)]
825pub struct InteractivePtyPumpResult {
826 pub chunks: Vec<Vec<u8>>,
827 pub stream_closed: bool,
828}
829
830pub struct InteractivePtySession {
835 process: NativePtyProcess,
836 options: InteractivePtyOptions,
837}
838
839impl InteractivePtySession {
840 pub fn new(process: NativePtyProcess) -> Self {
841 Self::with_options(process, InteractivePtyOptions::default())
842 }
843
844 pub fn with_options(process: NativePtyProcess, options: InteractivePtyOptions) -> Self {
845 Self { process, options }
846 }
847
848 pub fn process(&self) -> &NativePtyProcess {
849 &self.process
850 }
851
852 pub fn start(&self) -> Result<(), PtyError> {
853 self.process.set_echo(self.options.echo_output);
854 self.process.start_impl()?;
855 if self.options.relay_terminal_input {
856 self.process.start_terminal_input_relay_impl()?;
857 }
858 Ok(())
859 }
860
861 pub fn pump_output(
862 &self,
863 timeout: Option<f64>,
864 consume_all: bool,
865 ) -> Result<InteractivePtyPumpResult, PtyError> {
866 let mut pumped = InteractivePtyPumpResult::default();
867 let mut next_timeout = timeout;
868 loop {
869 match self.process.read_chunk_impl(next_timeout) {
870 Ok(Some(chunk)) => {
871 if self.options.respond_to_queries {
872 self.process.respond_to_queries_impl(&chunk)?;
873 }
874 pumped.chunks.push(chunk);
875 if !consume_all {
876 break;
877 }
878 next_timeout = Some(0.0);
879 }
880 Ok(None) => break,
881 Err(PtyError::Other(message)) if message == "Pseudo-terminal stream is closed" => {
882 pumped.stream_closed = true;
883 break;
884 }
885 Err(err) => return Err(err),
886 }
887 }
888 Ok(pumped)
889 }
890
891 pub fn resize(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
892 self.process.resize_impl(rows, cols)
893 }
894
895 pub fn send_interrupt(&self) -> Result<(), PtyError> {
896 self.process.send_interrupt_impl()
897 }
898
899 pub fn wait(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
900 self.process.wait_impl(timeout)
901 }
902
903 pub fn wait_and_drain(
904 &self,
905 timeout: Option<f64>,
906 drain_timeout: f64,
907 ) -> Result<i32, PtyError> {
908 self.process.wait_and_drain_impl(timeout, drain_timeout)
909 }
910
911 pub fn terminate(&self) -> Result<(), PtyError> {
912 self.process.terminate_impl()
913 }
914
915 pub fn kill(&self) -> Result<(), PtyError> {
916 self.process.kill_impl()
917 }
918
919 pub fn close(&self) -> Result<(), PtyError> {
920 self.process.close_impl()
921 }
922}
923
924impl Drop for NativePtyProcess {
925 fn drop(&mut self) {
926 self.close_nonblocking();
927 }
928}