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 #[cfg(unix)]
89 pub(super) fn finish_unix_teardown(&self, handles: NativePtyHandles) -> Result<(), PtyError> {
90 const CHILD_REAP_TIMEOUT: Duration = Duration::from_secs(2);
91 const CHILD_POLL_INTERVAL: Duration = Duration::from_millis(10);
92 const READER_TEARDOWN_TIMEOUT: Duration = Duration::from_secs(2);
93
94 let NativePtyHandles {
95 master,
96 writer,
97 mut child,
98 } = handles;
99 let process_group = master.process_group_leader();
100
101 let mut control_error = None;
102 if let Some(pid) = process_group {
103 if let Err(err) = crate::unix_signal_process_group(pid, crate::UnixSignal::Kill) {
104 if !is_ignorable_process_control_error(&err) {
105 control_error = Some(err);
106 }
107 }
108 }
109 if let Err(err) = child.kill() {
110 if !is_ignorable_process_control_error(&err) && control_error.is_none() {
111 control_error = Some(err);
112 }
113 }
114 drop(writer);
115
116 let deadline = Instant::now() + CHILD_REAP_TIMEOUT;
117 let code = loop {
118 match child.try_wait() {
119 Ok(Some(status)) => break status as i32,
120 Ok(None) if Instant::now() < deadline => {
121 thread::sleep(CHILD_POLL_INTERVAL);
122 }
123 Ok(None) => break -9,
124 Err(err) => {
125 if control_error.is_none() {
126 control_error = Some(err);
127 }
128 break -9;
129 }
130 }
131 };
132 self.store_returncode(code);
133
134 let reader_worker = self
135 .reader_worker
136 .lock()
137 .expect("pty reader worker mutex poisoned")
138 .take();
139 let (tx, rx) = std::sync::mpsc::channel();
140 thread::spawn(move || {
141 drop(master);
142 drop(child);
143 if let Some(worker) = reader_worker {
144 let _ = worker.join();
145 }
146 let _ = tx.send(());
147 });
148 let _ = rx.recv_timeout(READER_TEARDOWN_TIMEOUT);
149 self.mark_reader_closed();
150
151 match control_error {
152 Some(err) => Err(PtyError::Io(err)),
153 None => Ok(()),
154 }
155 }
156
157 pub fn new(
161 argv: Vec<String>,
162 cwd: Option<String>,
163 env: Option<Vec<(String, String)>>,
164 rows: u16,
165 cols: u16,
166 nice: Option<i32>,
167 ) -> Result<Self, PtyError> {
168 if argv.is_empty() {
169 return Err(PtyError::Other("command cannot be empty".into()));
170 }
171 #[cfg(not(windows))]
172 let _ = nice;
173 Ok(Self {
174 argv,
175 cwd,
176 env,
177 rows,
178 cols,
179 #[cfg(windows)]
180 nice,
181 handles: Arc::new(Mutex::new(None)),
182 reader: Arc::new(PtyReadShared {
183 state: Mutex::new(PtyReadState {
184 chunks: VecDeque::new(),
185 closed: false,
186 }),
187 condvar: Condvar::new(),
188 }),
189 returncode: Arc::new(Mutex::new(None)),
190 input_bytes_total: Arc::new(AtomicUsize::new(0)),
191 newline_events_total: Arc::new(AtomicUsize::new(0)),
192 submit_events_total: Arc::new(AtomicUsize::new(0)),
193 echo: Arc::new(AtomicBool::new(false)),
194 idle_detector: Arc::new(Mutex::new(None)),
195 output_bytes_total: Arc::new(AtomicUsize::new(0)),
196 control_churn_bytes_total: Arc::new(AtomicUsize::new(0)),
197 reader_worker: Mutex::new(None),
198 terminal_input_relay_stop: Arc::new(AtomicBool::new(false)),
199 terminal_input_relay_active: Arc::new(AtomicBool::new(false)),
200 terminal_input_relay_worker: Mutex::new(None),
201 })
202 }
203
204 pub fn mark_reader_closed(&self) {
206 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
207 guard.closed = true;
208 self.reader.condvar.notify_all();
209 }
210
211 pub fn store_returncode(&self, code: i32) {
213 store_pty_returncode(&self.returncode, code);
214 }
215
216 pub fn record_input_metrics(&self, data: &[u8], submit: bool) {
218 record_pty_input_metrics(
219 &self.input_bytes_total,
220 &self.newline_events_total,
221 &self.submit_events_total,
222 data,
223 submit,
224 );
225 }
226
227 pub fn write_impl(&self, data: &[u8], submit: bool) -> Result<(), PtyError> {
229 self.record_input_metrics(data, submit);
230 write_pty_input(&self.handles, data)?;
231 Ok(())
232 }
233
234 pub fn request_terminal_input_relay_stop(&self) {
236 self.terminal_input_relay_stop
237 .store(true, Ordering::Release);
238 self.terminal_input_relay_active
239 .store(false, Ordering::Release);
240 }
241
242 pub fn start_terminal_input_relay_impl(&self) -> Result<(), PtyError> {
244 let mut worker_guard = self
245 .terminal_input_relay_worker
246 .lock()
247 .expect("pty terminal input relay mutex poisoned");
248 if worker_guard.is_some() && self.terminal_input_relay_active() {
249 return Ok(());
250 }
251 if self
252 .handles
253 .lock()
254 .expect("pty handles mutex poisoned")
255 .is_none()
256 {
257 return Err(PtyError::NotRunning);
258 }
259
260 self.terminal_input_relay_stop
261 .store(false, Ordering::Release);
262 self.terminal_input_relay_active
263 .store(true, Ordering::Release);
264
265 let handles = Arc::clone(&self.handles);
266 let returncode = Arc::clone(&self.returncode);
267 let input_bytes_total = Arc::clone(&self.input_bytes_total);
268 let newline_events_total = Arc::clone(&self.newline_events_total);
269 let submit_events_total = Arc::clone(&self.submit_events_total);
270 let stop = Arc::clone(&self.terminal_input_relay_stop);
271 let active = Arc::clone(&self.terminal_input_relay_active);
272
273 #[cfg(windows)]
274 {
275 let capture = super::terminal_input::TerminalInputCore::new();
276 capture.start_impl().map_err(PtyError::Io)?;
277 *worker_guard = Some(thread::spawn(move || {
278 loop {
279 if stop.load(Ordering::Acquire) {
280 break;
281 }
282 match poll_pty_process(&handles, &returncode) {
283 Ok(Some(_)) => break,
284 Ok(None) => {}
285 Err(_) => break,
286 }
287 match super::terminal_input::wait_for_terminal_input_event(
288 &capture.state,
289 &capture.condvar,
290 Some(Duration::from_millis(50)),
291 ) {
292 super::terminal_input::TerminalInputWaitOutcome::Event(event) => {
293 record_pty_input_metrics(
294 &input_bytes_total,
295 &newline_events_total,
296 &submit_events_total,
297 &event.data,
298 event.submit,
299 );
300 if write_pty_input(&handles, &event.data).is_err() {
301 break;
302 }
303 }
304 super::terminal_input::TerminalInputWaitOutcome::Timeout => continue,
305 super::terminal_input::TerminalInputWaitOutcome::Closed => break,
306 }
307 }
308 active.store(false, Ordering::Release);
309 let _ = capture.stop_impl();
310 }));
311 Ok(())
312 }
313
314 #[cfg(unix)]
315 {
316 if unsafe { libc::isatty(libc::STDIN_FILENO) } != 1 {
317 self.terminal_input_relay_active
318 .store(false, Ordering::Release);
319 return Ok(());
320 }
321
322 *worker_guard = Some(thread::spawn(move || {
323 posix_terminal_input_relay_worker(
324 handles,
325 returncode,
326 input_bytes_total,
327 newline_events_total,
328 submit_events_total,
329 stop,
330 active,
331 );
332 }));
333 Ok(())
334 }
335 }
336
337 pub fn stop_terminal_input_relay_impl(&self) {
339 self.request_terminal_input_relay_stop();
340 if let Some(worker) = self
341 .terminal_input_relay_worker
342 .lock()
343 .expect("pty terminal input relay mutex poisoned")
344 .take()
345 {
346 let _ = worker.join();
347 }
348 }
349
350 pub fn terminal_input_relay_active(&self) -> bool {
352 self.terminal_input_relay_active.load(Ordering::Acquire)
353 }
354
355 #[inline(never)]
357 pub fn close_impl(&self) -> Result<(), PtyError> {
358 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::close_impl");
359 self.stop_terminal_input_relay_impl();
360 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
361 let Some(handles) = guard.take() else {
362 self.mark_reader_closed();
363 return Ok(());
364 };
365 drop(guard);
366
367 #[cfg(windows)]
368 let NativePtyHandles {
369 master,
370 writer,
371 mut child,
372 _job,
373 } = handles;
374 #[cfg(not(windows))]
375 let NativePtyHandles {
376 master,
377 writer,
378 child,
379 } = handles;
380
381 #[cfg(windows)]
382 {
383 {
384 crate::rp_rust_debug_scope!(
385 "running_process::NativePtyProcess::close_impl.drop_job"
386 );
387 drop(_job);
388 }
389
390 {
391 crate::rp_rust_debug_scope!(
392 "running_process::NativePtyProcess::close_impl.wait_job_exit"
393 );
394 let wait_deadline = Instant::now() + Duration::from_secs(2);
395 loop {
396 match child.try_wait() {
397 Ok(Some(status)) => {
398 let code = status as i32;
399 self.store_returncode(code);
400 break;
401 }
402 Ok(None) if Instant::now() < wait_deadline => {
403 thread::sleep(Duration::from_millis(10));
410 }
411 Ok(None) => {
412 if let Err(err) = child.kill() {
413 if !is_ignorable_process_control_error(&err) {
414 return Err(PtyError::Io(err));
415 }
416 }
417 let kill_deadline = Instant::now() + Duration::from_secs(2);
427 let code = loop {
428 match child.try_wait() {
429 Ok(Some(status)) => break status as i32,
430 Ok(None) if Instant::now() < kill_deadline => {
431 thread::sleep(Duration::from_millis(10));
432 }
433 _ => break -9,
434 }
435 };
436 self.store_returncode(code);
437 break;
438 }
439 Err(_) => {
440 self.store_returncode(-9);
441 break;
442 }
443 }
444 }
445 }
446 {
447 crate::rp_rust_debug_scope!(
448 "running_process::NativePtyProcess::close_impl.drop_writer"
449 );
450 drop(writer);
451 }
452 {
462 crate::rp_rust_debug_scope!(
463 "running_process::NativePtyProcess::close_impl.bounded_teardown"
464 );
465 let reader_worker = self
466 .reader_worker
467 .lock()
468 .expect("pty reader worker mutex poisoned")
469 .take();
470 let (tx, rx) = std::sync::mpsc::channel();
471 std::thread::spawn(move || {
472 drop(master);
473 drop(child);
474 if let Some(worker) = reader_worker {
475 let _ = worker.join();
476 }
477 let _ = tx.send(());
478 });
479 let _ = rx.recv_timeout(Duration::from_secs(2));
480 }
481 self.mark_reader_closed();
482 Ok(())
483 }
484
485 #[cfg(not(windows))]
486 {
487 self.finish_unix_teardown(NativePtyHandles {
488 master,
489 writer,
490 child,
491 })
492 }
493 }
494
495 #[inline(never)]
497 pub fn close_nonblocking(&self) {
498 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::close_nonblocking");
499 #[cfg(windows)]
500 self.request_terminal_input_relay_stop();
501 let Ok(mut guard) = self.handles.lock() else {
502 return;
503 };
504 let Some(handles) = guard.take() else {
505 self.mark_reader_closed();
506 return;
507 };
508 drop(guard);
509
510 #[cfg(windows)]
511 let NativePtyHandles {
512 master,
513 writer,
514 mut child,
515 _job,
516 } = handles;
517 #[cfg(not(windows))]
518 let NativePtyHandles {
519 master,
520 writer,
521 mut child,
522 } = handles;
523
524 if let Err(err) = child.kill() {
525 if !is_ignorable_process_control_error(&err) {
526 return;
527 }
528 }
529 drop(writer);
530 #[cfg(windows)]
538 std::thread::spawn(move || {
539 drop(master);
540 drop(child);
541 drop(_job);
542 });
543 #[cfg(not(windows))]
544 {
545 drop(master);
546 drop(child);
547 }
548 self.mark_reader_closed();
549 }
550
551 pub fn start_impl(&self) -> Result<(), PtyError> {
553 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::start");
554 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
555 if guard.is_some() {
556 return Err(PtyError::AlreadyStarted);
557 }
558
559 #[cfg(windows)]
562 let conhost_pids_before = conhost_children_of_current_process();
563
564 let (mut master, slave) = Backend::openpty(PtySize {
565 rows: self.rows,
566 cols: self.cols,
567 pixel_width: 0,
568 pixel_height: 0,
569 })
570 .map_err(|e| PtyError::Spawn(e.to_string()))?;
571
572 let argv: Vec<std::ffi::OsString> =
574 self.argv.iter().map(std::ffi::OsString::from).collect();
575 let cwd = resolved_spawn_cwd(self.cwd.as_deref());
576 let env: Option<Vec<(std::ffi::OsString, std::ffi::OsString)>> =
577 self.env.as_ref().map(|e| {
578 e.iter()
579 .map(|(k, v)| (std::ffi::OsString::from(k), std::ffi::OsString::from(v)))
580 .collect()
581 });
582
583 let reader = master
584 .try_clone_reader()
585 .map_err(|e| PtyError::Spawn(e.to_string()))?;
586 let writer = master
587 .take_writer()
588 .map_err(|e| PtyError::Spawn(e.to_string()))?;
589 let cwd_path = cwd.as_deref().map(std::path::Path::new);
590 let child = slave
591 .spawn(&argv, cwd_path, env.as_deref())
592 .map_err(|e| PtyError::Spawn(e.to_string()))?;
593 #[cfg(windows)]
596 let job = assign_child_to_windows_kill_on_close_job(PtyChild::as_raw_handle(&child))?;
597 #[cfg(windows)]
598 assign_conpty_conhost_to_job(&job, &conhost_pids_before);
599 #[cfg(windows)]
600 apply_windows_pty_priority(PtyChild::as_raw_handle(&child), self.nice)?;
601 let shared = Arc::clone(&self.reader);
602 let echo = Arc::clone(&self.echo);
603 let idle_detector = Arc::clone(&self.idle_detector);
604 let output_bytes = Arc::clone(&self.output_bytes_total);
605 let churn_bytes = Arc::clone(&self.control_churn_bytes_total);
606 let reader_worker = thread::spawn(move || {
607 spawn_pty_reader(
608 reader,
609 shared,
610 echo,
611 idle_detector,
612 output_bytes,
613 churn_bytes,
614 );
615 });
616 *self
617 .reader_worker
618 .lock()
619 .expect("pty reader worker mutex poisoned") = Some(reader_worker);
620
621 *guard = Some(NativePtyHandles {
622 master: Box::new(master) as Box<dyn PtyMaster>,
623 writer: Arc::new(Mutex::new(writer)),
626 child: Box::new(child) as Box<dyn PtyChild>,
627 #[cfg(windows)]
628 _job: job,
629 });
630 Ok(())
631 }
632
633 pub fn respond_to_queries_impl(&self, data: &[u8]) -> Result<(), PtyError> {
635 #[cfg(windows)]
636 {
637 pty_windows::respond_to_queries(self, data)
638 }
639
640 #[cfg(unix)]
641 {
642 pty_platform::respond_to_queries(self, data)
643 }
644 }
645
646 pub fn resize_impl(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
648 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::resize");
649 let guard = self.handles.lock().expect("pty handles mutex poisoned");
650 if let Some(handles) = guard.as_ref() {
651 #[cfg(windows)]
652 {
653 let _ = (rows, cols, handles);
654 return Ok(());
658 }
659
660 #[cfg(not(windows))]
661 handles
662 .master
663 .resize(PtySize {
664 rows,
665 cols,
666 pixel_width: 0,
667 pixel_height: 0,
668 })
669 .map_err(|e| PtyError::Other(e.to_string()))?;
670 }
671 Ok(())
672 }
673
674 pub fn send_interrupt_impl(&self) -> Result<(), PtyError> {
676 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::send_interrupt");
677 #[cfg(windows)]
678 {
679 pty_windows::send_interrupt(self)
680 }
681
682 #[cfg(unix)]
683 {
684 pty_platform::send_interrupt(self)
685 }
686 }
687
688 pub fn wait_impl(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
692 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::wait");
693 if let Some(code) = *self
695 .returncode
696 .lock()
697 .expect("pty returncode mutex poisoned")
698 {
699 return Ok(code);
700 }
701 let start = Instant::now();
702 loop {
703 if let Some(code) = poll_pty_process(&self.handles, &self.returncode)? {
704 return Ok(code);
705 }
706 if timeout.is_some_and(|limit| start.elapsed() >= Duration::from_secs_f64(limit)) {
707 return Err(PtyError::Timeout);
708 }
709 thread::sleep(Duration::from_millis(10));
713 }
714 }
715
716 pub fn terminate_impl(&self) -> Result<(), PtyError> {
718 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::terminate");
719 #[cfg(windows)]
720 {
721 if self
722 .handles
723 .lock()
724 .expect("pty handles mutex poisoned")
725 .is_none()
726 {
727 return Err(PtyError::NotRunning);
728 }
729 self.close_impl()
730 }
731
732 #[cfg(unix)]
733 {
734 pty_platform::terminate(self)
735 }
736 }
737
738 pub fn kill_impl(&self) -> Result<(), PtyError> {
740 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::kill");
741 #[cfg(windows)]
742 {
743 if self
744 .handles
745 .lock()
746 .expect("pty handles mutex poisoned")
747 .is_none()
748 {
749 return Err(PtyError::NotRunning);
750 }
751 self.close_impl()
752 }
753
754 #[cfg(unix)]
755 {
756 pty_platform::kill(self)
757 }
758 }
759
760 pub fn terminate_tree_impl(&self) -> Result<(), PtyError> {
762 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::terminate_tree");
763 #[cfg(windows)]
764 {
765 pty_windows::terminate_tree(self)
766 }
767
768 #[cfg(unix)]
769 {
770 pty_platform::terminate_tree(self)
771 }
772 }
773
774 pub fn kill_tree_impl(&self) -> Result<(), PtyError> {
776 crate::rp_rust_debug_scope!("running_process::NativePtyProcess::kill_tree");
777 #[cfg(windows)]
778 {
779 pty_windows::kill_tree(self)
780 }
781
782 #[cfg(unix)]
783 {
784 pty_platform::kill_tree(self)
785 }
786 }
787
788 pub fn pid(&self) -> Result<Option<u32>, PtyError> {
790 let guard = self.handles.lock().expect("pty handles mutex poisoned");
791 if let Some(handles) = guard.as_ref() {
792 #[cfg(unix)]
793 if let Some(pid) = handles.master.process_group_leader() {
794 if let Ok(pid) = u32::try_from(pid) {
795 return Ok(Some(pid));
796 }
797 }
798 return Ok(Some(handles.child.pid()));
799 }
800 Ok(None)
801 }
802
803 pub fn read_chunk_impl(&self, timeout: Option<f64>) -> Result<Option<Vec<u8>>, PtyError> {
806 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
807 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
808 loop {
809 if let Some(chunk) = guard.chunks.pop_front() {
810 return Ok(Some(chunk));
811 }
812 if guard.closed {
813 return Err(PtyError::Other("Pseudo-terminal stream is closed".into()));
814 }
815 match deadline {
816 Some(deadline) => {
817 let now = Instant::now();
818 if now >= deadline {
819 return Ok(None); }
821 let wait = deadline.saturating_duration_since(now);
822 let result = self
823 .reader
824 .condvar
825 .wait_timeout(guard, wait)
826 .expect("pty read mutex poisoned");
827 guard = result.0;
828 }
829 None => {
830 guard = self
831 .reader
832 .condvar
833 .wait(guard)
834 .expect("pty read mutex poisoned");
835 }
836 }
837 }
838 }
839
840 pub fn wait_for_reader_closed_impl(&self, timeout: Option<f64>) -> bool {
842 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
843 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
844 loop {
845 if guard.closed {
846 return true;
847 }
848 match deadline {
849 Some(deadline) => {
850 let now = Instant::now();
851 if now >= deadline {
852 return false;
853 }
854 let wait = deadline.saturating_duration_since(now);
855 let result = self
856 .reader
857 .condvar
858 .wait_timeout(guard, wait)
859 .expect("pty read mutex poisoned");
860 guard = result.0;
861 }
862 None => {
863 guard = self
864 .reader
865 .condvar
866 .wait(guard)
867 .expect("pty read mutex poisoned");
868 }
869 }
870 }
871 }
872
873 pub fn wait_and_drain_impl(
875 &self,
876 timeout: Option<f64>,
877 drain_timeout: f64,
878 ) -> Result<i32, PtyError> {
879 let code = self.wait_impl(timeout)?;
880 let deadline = Instant::now() + Duration::from_secs_f64(drain_timeout.max(0.0));
881 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
882 while !guard.closed {
883 let remaining = deadline.saturating_duration_since(Instant::now());
884 if remaining.is_zero() {
885 break;
886 }
887 let result = self
888 .reader
889 .condvar
890 .wait_timeout(guard, remaining)
891 .expect("pty read mutex poisoned");
892 guard = result.0;
893 }
894 Ok(code)
895 }
896
897 pub fn set_echo(&self, enabled: bool) {
899 self.echo.store(enabled, Ordering::Release);
900 }
901
902 pub fn echo_enabled(&self) -> bool {
904 self.echo.load(Ordering::Acquire)
905 }
906
907 pub fn attach_idle_detector(&self, detector: &Arc<IdleDetectorCore>) {
909 let mut guard = self
910 .idle_detector
911 .lock()
912 .expect("idle detector mutex poisoned");
913 *guard = Some(Arc::clone(detector));
914 }
915
916 pub fn detach_idle_detector(&self) {
918 let mut guard = self
919 .idle_detector
920 .lock()
921 .expect("idle detector mutex poisoned");
922 *guard = None;
923 }
924
925 pub fn pty_input_bytes_total(&self) -> usize {
927 self.input_bytes_total.load(Ordering::Acquire)
928 }
929
930 pub fn pty_newline_events_total(&self) -> usize {
932 self.newline_events_total.load(Ordering::Acquire)
933 }
934
935 pub fn pty_submit_events_total(&self) -> usize {
937 self.submit_events_total.load(Ordering::Acquire)
938 }
939
940 pub fn pty_output_bytes_total(&self) -> usize {
942 self.output_bytes_total.load(Ordering::Acquire)
943 }
944
945 pub fn pty_control_churn_bytes_total(&self) -> usize {
947 self.control_churn_bytes_total.load(Ordering::Acquire)
948 }
949}
950
951#[derive(Debug, Clone, Copy)]
956pub struct InteractivePtyOptions {
957 pub echo_output: bool,
959 pub relay_terminal_input: bool,
961 pub respond_to_queries: bool,
963}
964
965impl Default for InteractivePtyOptions {
966 fn default() -> Self {
967 Self {
968 echo_output: true,
969 relay_terminal_input: true,
970 respond_to_queries: true,
971 }
972 }
973}
974
975#[derive(Debug, Default)]
977pub struct InteractivePtyPumpResult {
978 pub chunks: Vec<Vec<u8>>,
980 pub stream_closed: bool,
982}
983
984pub struct InteractivePtySession {
989 process: NativePtyProcess,
990 options: InteractivePtyOptions,
991}
992
993impl InteractivePtySession {
994 pub fn new(process: NativePtyProcess) -> Self {
996 Self::with_options(process, InteractivePtyOptions::default())
997 }
998
999 pub fn with_options(process: NativePtyProcess, options: InteractivePtyOptions) -> Self {
1001 Self { process, options }
1002 }
1003
1004 pub fn process(&self) -> &NativePtyProcess {
1006 &self.process
1007 }
1008
1009 pub fn start(&self) -> Result<(), PtyError> {
1011 self.process.set_echo(self.options.echo_output);
1012 self.process.start_impl()?;
1013 if self.options.relay_terminal_input {
1014 self.process.start_terminal_input_relay_impl()?;
1015 }
1016 Ok(())
1017 }
1018
1019 pub fn pump_output(
1024 &self,
1025 timeout: Option<f64>,
1026 consume_all: bool,
1027 ) -> Result<InteractivePtyPumpResult, PtyError> {
1028 let mut pumped = InteractivePtyPumpResult::default();
1029 let mut next_timeout = timeout;
1030 loop {
1031 match self.process.read_chunk_impl(next_timeout) {
1032 Ok(Some(chunk)) => {
1033 if self.options.respond_to_queries {
1034 self.process.respond_to_queries_impl(&chunk)?;
1035 }
1036 pumped.chunks.push(chunk);
1037 if !consume_all {
1038 break;
1039 }
1040 next_timeout = Some(0.0);
1041 }
1042 Ok(None) => break,
1043 Err(PtyError::Other(message)) if message == "Pseudo-terminal stream is closed" => {
1044 pumped.stream_closed = true;
1045 break;
1046 }
1047 Err(err) => return Err(err),
1048 }
1049 }
1050 Ok(pumped)
1051 }
1052
1053 pub fn resize(&self, rows: u16, cols: u16) -> Result<(), PtyError> {
1055 self.process.resize_impl(rows, cols)
1056 }
1057
1058 pub fn send_interrupt(&self) -> Result<(), PtyError> {
1060 self.process.send_interrupt_impl()
1061 }
1062
1063 pub fn wait(&self, timeout: Option<f64>) -> Result<i32, PtyError> {
1065 self.process.wait_impl(timeout)
1066 }
1067
1068 pub fn wait_and_drain(
1070 &self,
1071 timeout: Option<f64>,
1072 drain_timeout: f64,
1073 ) -> Result<i32, PtyError> {
1074 self.process.wait_and_drain_impl(timeout, drain_timeout)
1075 }
1076
1077 pub fn terminate(&self) -> Result<(), PtyError> {
1079 self.process.terminate_impl()
1080 }
1081
1082 pub fn kill(&self) -> Result<(), PtyError> {
1084 self.process.kill_impl()
1085 }
1086
1087 pub fn close(&self) -> Result<(), PtyError> {
1089 self.process.close_impl()
1090 }
1091}
1092
1093impl Drop for NativePtyProcess {
1094 fn drop(&mut self) {
1095 self.close_nonblocking();
1096 }
1097}