1use std::collections::VecDeque;
2use std::ffi::OsString;
3use std::io::{Read, Write};
4use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
5use std::sync::{Arc, Condvar, Mutex};
6use std::thread;
7use std::time::{Duration, Instant};
8
9use portable_pty::CommandBuilder;
10use thiserror::Error;
11
12pub mod reexports {
14 pub use portable_pty;
16}
17
18#[cfg(unix)]
20pub(super) mod pty_posix;
21#[cfg(windows)]
23pub(super) mod pty_windows;
24
25pub mod terminal_input;
27
28#[cfg(windows)]
34pub(super) mod conpty_passthrough;
35
36#[cfg(windows)]
44pub use conpty_passthrough::conpty_api::{current_backend_kind, ConPtyBackendKind};
45
46pub mod backend;
52pub use backend::{PtyChild, PtyMaster, PtySize};
54
55mod native_pty_process;
56pub use native_pty_process::{
58 InteractivePtyOptions, InteractivePtyPumpResult, InteractivePtySession, NativePtyProcess,
59};
60
61#[cfg(feature = "async-process")]
63pub mod async_pty;
64#[cfg(feature = "async-process")]
65pub use async_pty::{AsyncPtyProcess, IdleWaitOutcome};
66
67#[cfg(unix)]
68use pty_posix as pty_platform;
69
70#[derive(Debug, Error)]
72pub enum PtyError {
73 #[error("pseudo-terminal process already started")]
75 AlreadyStarted,
76 #[error("pseudo-terminal process is not running")]
78 NotRunning,
79 #[error("pseudo-terminal timed out")]
81 Timeout,
82 #[error("pseudo-terminal I/O error: {0}")]
84 Io(
85 #[from]
87 std::io::Error,
88 ),
89 #[error("pseudo-terminal spawn failed: {0}")]
91 Spawn(
92 String,
94 ),
95 #[error("pseudo-terminal error: {0}")]
97 Other(
98 String,
100 ),
101}
102
103pub fn is_ignorable_process_control_error(err: &std::io::Error) -> bool {
105 if matches!(
106 err.kind(),
107 std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
108 ) {
109 return true;
110 }
111 #[cfg(unix)]
112 if err.raw_os_error() == Some(libc::ESRCH) {
113 return true;
114 }
115 false
116}
117
118pub struct PtyReadState {
120 pub chunks: VecDeque<Vec<u8>>,
122 pub closed: bool,
124}
125
126pub struct PtyReadShared {
128 pub state: Mutex<PtyReadState>,
130 pub condvar: Condvar,
132}
133
134pub type SharedPtyWriter = Arc<Mutex<Box<dyn Write + Send>>>;
139
140pub struct NativePtyHandles {
141 pub master: Box<dyn crate::pty::backend::PtyMaster>,
147 pub writer: SharedPtyWriter,
155 pub child: Box<dyn crate::pty::backend::PtyChild>,
157 #[cfg(windows)]
159 pub _job: WindowsJobHandle,
160}
161
162#[cfg(windows)]
163pub struct WindowsJobHandle(
165 pub usize,
167);
168
169#[cfg(windows)]
170impl WindowsJobHandle {
171 pub fn assign_pid(&self, pid: u32) -> Result<(), std::io::Error> {
173 use winapi::um::handleapi::CloseHandle;
174 use winapi::um::processthreadsapi::OpenProcess;
175 use winapi::um::winnt::PROCESS_SET_QUOTA;
176 use winapi::um::winnt::PROCESS_TERMINATE;
177
178 let handle = unsafe { OpenProcess(PROCESS_SET_QUOTA | PROCESS_TERMINATE, 0, pid) };
179 if handle.is_null() {
180 return Err(std::io::Error::last_os_error());
181 }
182 let result = unsafe {
183 winapi::um::jobapi2::AssignProcessToJobObject(
184 self.0 as winapi::shared::ntdef::HANDLE,
185 handle,
186 )
187 };
188 unsafe { CloseHandle(handle) };
189 if result == 0 {
190 return Err(std::io::Error::last_os_error());
191 }
192 Ok(())
193 }
194}
195
196#[cfg(windows)]
197impl Drop for WindowsJobHandle {
198 fn drop(&mut self) {
199 unsafe {
200 winapi::um::handleapi::CloseHandle(self.0 as winapi::shared::ntdef::HANDLE);
201 }
202 }
203}
204
205pub struct IdleMonitorState {
207 pub last_reset_at: Instant,
209 pub returncode: Option<i32>,
211 pub interrupted: bool,
213}
214
215pub struct IdleDetectorCore {
218 pub timeout_seconds: f64,
220 pub stability_window_seconds: f64,
222 pub sample_interval_seconds: f64,
224 pub reset_on_input: bool,
226 pub reset_on_output: bool,
228 pub count_control_churn_as_output: bool,
230 pub enabled: Arc<AtomicBool>,
232 pub state: Mutex<IdleMonitorState>,
234 pub condvar: Condvar,
236}
237
238impl IdleDetectorCore {
239 pub fn record_input(&self, byte_count: usize) {
241 if !self.reset_on_input || byte_count == 0 {
242 return;
243 }
244 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
245 guard.last_reset_at = Instant::now();
246 self.condvar.notify_all();
247 }
248
249 pub fn record_output(&self, data: &[u8]) {
251 if !self.reset_on_output || data.is_empty() {
252 return;
253 }
254 let control_bytes = control_churn_bytes(data);
255 let visible_output_bytes = data.len().saturating_sub(control_bytes);
256 let active_output =
257 visible_output_bytes > 0 || (self.count_control_churn_as_output && control_bytes > 0);
258 if !active_output {
259 return;
260 }
261 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
262 guard.last_reset_at = Instant::now();
263 self.condvar.notify_all();
264 }
265
266 pub fn mark_exit(&self, returncode: i32, interrupted: bool) {
268 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
269 guard.returncode = Some(returncode);
270 guard.interrupted = interrupted;
271 self.condvar.notify_all();
272 }
273
274 pub fn enabled(&self) -> bool {
276 self.enabled.load(Ordering::Acquire)
277 }
278
279 pub fn set_enabled(&self, enabled: bool) {
281 let was_enabled = self.enabled.swap(enabled, Ordering::AcqRel);
282 if enabled && !was_enabled {
283 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
284 guard.last_reset_at = Instant::now();
285 }
286 self.condvar.notify_all();
287 }
288
289 pub fn wait(&self, timeout: Option<f64>) -> (bool, String, f64, Option<i32>) {
291 let started = Instant::now();
292 let overall_timeout = timeout.map(Duration::from_secs_f64);
293 let min_idle = self.timeout_seconds.max(self.stability_window_seconds);
294 let sample_interval = Duration::from_secs_f64(self.sample_interval_seconds.max(0.001));
295
296 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
297 loop {
298 let now = Instant::now();
299 let idle_for = now.duration_since(guard.last_reset_at).as_secs_f64();
300
301 if let Some(returncode) = guard.returncode {
302 let reason = if guard.interrupted {
303 "interrupt"
304 } else {
305 "process_exit"
306 };
307 return (false, reason.to_string(), idle_for, Some(returncode));
308 }
309
310 let enabled = self.enabled.load(Ordering::Acquire);
311 if enabled && idle_for >= min_idle {
312 return (true, "idle_timeout".to_string(), idle_for, None);
313 }
314
315 if let Some(limit) = overall_timeout {
316 if now.duration_since(started) >= limit {
317 return (false, "timeout".to_string(), idle_for, None);
318 }
319 }
320
321 let idle_remaining = if enabled {
322 (min_idle - idle_for).max(0.0)
323 } else {
324 sample_interval.as_secs_f64()
325 };
326 let mut wait_for =
327 sample_interval.min(Duration::from_secs_f64(idle_remaining.max(0.001)));
328 if let Some(limit) = overall_timeout {
329 let elapsed = now.duration_since(started);
330 if elapsed < limit {
331 let remaining = limit - elapsed;
332 wait_for = wait_for.min(remaining);
333 }
334 }
335 let result = self
336 .condvar
337 .wait_timeout(guard, wait_for)
338 .expect("idle monitor mutex poisoned");
339 guard = result.0;
340 }
341 }
342}
343
344pub fn control_churn_bytes(data: &[u8]) -> usize {
348 let mut total = 0;
349 let mut index = 0;
350 while index < data.len() {
351 let byte = data[index];
352 if byte == 0x1B {
353 let start = index;
354 index += 1;
355 if index < data.len() && data[index] == b'[' {
356 index += 1;
357 while index < data.len() {
358 let current = data[index];
359 index += 1;
360 if (0x40..=0x7E).contains(¤t) {
361 break;
362 }
363 }
364 }
365 total += index - start;
366 continue;
367 }
368 if matches!(byte, 0x08 | 0x0D | 0x7F) {
369 total += 1;
370 }
371 index += 1;
372 }
373 total
374}
375
376pub fn command_builder_from_argv(argv: &[String]) -> CommandBuilder {
378 let mut command = CommandBuilder::new(&argv[0]);
379 if argv.len() > 1 {
380 command.args(
381 argv[1..]
382 .iter()
383 .map(OsString::from)
384 .collect::<Vec<OsString>>(),
385 );
386 }
387 command
388}
389
390#[inline(never)]
392pub fn spawn_pty_reader(
393 mut reader: Box<dyn Read + Send>,
394 shared: Arc<PtyReadShared>,
395 echo: Arc<AtomicBool>,
396 idle_detector: Arc<Mutex<Option<Arc<IdleDetectorCore>>>>,
397 output_bytes_total: Arc<AtomicUsize>,
398 control_churn_bytes_total: Arc<AtomicUsize>,
399) {
400 crate::rp_rust_debug_scope!("running_process::spawn_pty_reader");
401 let idle_detector_snapshot = idle_detector
402 .lock()
403 .expect("idle detector mutex poisoned")
404 .clone();
405 let mut chunk = vec![0_u8; 65536];
406 loop {
407 match reader.read(&mut chunk) {
408 Ok(0) => break,
409 Ok(n) => {
410 let data = &chunk[..n];
411
412 let churn = control_churn_bytes(data);
413 let visible = data.len().saturating_sub(churn);
414 output_bytes_total.fetch_add(visible, Ordering::Relaxed);
415 control_churn_bytes_total.fetch_add(churn, Ordering::Relaxed);
416
417 if echo.load(Ordering::Relaxed) {
418 let _ = std::io::stdout().write_all(data);
419 let _ = std::io::stdout().flush();
420 }
421
422 if let Some(ref detector) = idle_detector_snapshot {
423 detector.record_output(data);
424 }
425
426 let mut guard = shared.state.lock().expect("pty read mutex poisoned");
427 guard.chunks.push_back(data.to_vec());
428 shared.condvar.notify_all();
429 }
430 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
431 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
432 thread::sleep(Duration::from_millis(10));
438 continue;
439 }
440 Err(_) => break,
441 }
442 }
443 let mut guard = shared.state.lock().expect("pty read mutex poisoned");
444 guard.closed = true;
445 shared.condvar.notify_all();
446}
447
448pub fn portable_exit_code(status: portable_pty::ExitStatus) -> i32 {
450 if let Some(signal) = status.signal() {
451 let signal = signal.to_ascii_lowercase();
452 if signal.contains("interrupt") {
453 return -2;
454 }
455 if signal.contains("terminated") {
456 return -15;
457 }
458 if signal.contains("killed") {
459 return -9;
460 }
461 }
462 status.exit_code() as i32
463}
464
465pub fn input_contains_newline(data: &[u8]) -> bool {
467 data.iter().any(|byte| matches!(*byte, b'\r' | b'\n'))
468}
469
470#[cfg(unix)]
471struct PosixTerminalModeGuard {
472 stdin_fd: i32,
473 original_mode: libc::termios,
474}
475
476#[cfg(unix)]
477impl Drop for PosixTerminalModeGuard {
478 fn drop(&mut self) {
479 unsafe {
480 libc::tcsetattr(self.stdin_fd, libc::TCSANOW, &self.original_mode);
481 }
482 }
483}
484
485#[cfg(unix)]
486fn acquire_posix_terminal_mode_guard() -> Result<PosixTerminalModeGuard, std::io::Error> {
487 let stdin_fd = libc::STDIN_FILENO;
488 let mut original_mode = unsafe { std::mem::zeroed::<libc::termios>() };
489 if unsafe { libc::tcgetattr(stdin_fd, &mut original_mode) } != 0 {
490 return Err(std::io::Error::last_os_error());
491 }
492 let mut raw_mode = original_mode;
493 unsafe {
494 libc::cfmakeraw(&mut raw_mode);
495 }
496 if unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw_mode) } != 0 {
497 return Err(std::io::Error::last_os_error());
498 }
499 Ok(PosixTerminalModeGuard {
500 stdin_fd,
501 original_mode,
502 })
503}
504
505#[cfg(unix)]
506#[inline(never)]
508pub(super) fn posix_terminal_input_relay_worker(
509 handles: Arc<Mutex<Option<NativePtyHandles>>>,
510 returncode: Arc<Mutex<Option<i32>>>,
511 input_bytes_total: Arc<AtomicUsize>,
512 newline_events_total: Arc<AtomicUsize>,
513 submit_events_total: Arc<AtomicUsize>,
514 stop: Arc<AtomicBool>,
515 active: Arc<AtomicBool>,
516) {
517 let _terminal_guard = match acquire_posix_terminal_mode_guard() {
518 Ok(guard) => guard,
519 Err(_) => {
520 active.store(false, Ordering::Release);
521 return;
522 }
523 };
524
525 let stdin_fd = libc::STDIN_FILENO;
526 let mut buffer = vec![0_u8; 65536];
527 loop {
528 if stop.load(Ordering::Acquire) {
529 break;
530 }
531 match poll_pty_process(&handles, &returncode) {
532 Ok(Some(_)) => break,
533 Ok(None) => {}
534 Err(_) => break,
535 }
536
537 let mut pollfd = libc::pollfd {
538 fd: stdin_fd,
539 events: libc::POLLIN,
540 revents: 0,
541 };
542 let poll_result = unsafe { libc::poll(&mut pollfd, 1, 50) };
543 if poll_result < 0 {
544 let err = std::io::Error::last_os_error();
545 if err.kind() == std::io::ErrorKind::Interrupted {
546 continue;
547 }
548 break;
549 }
550 if poll_result == 0 || pollfd.revents & libc::POLLIN == 0 {
551 continue;
552 }
553
554 let read_result = unsafe { libc::read(stdin_fd, buffer.as_mut_ptr().cast(), buffer.len()) };
555 if read_result < 0 {
556 let err = std::io::Error::last_os_error();
557 if err.kind() == std::io::ErrorKind::Interrupted {
558 continue;
559 }
560 break;
561 }
562 if read_result == 0 {
563 continue;
564 }
565
566 let mut data = buffer[..read_result as usize].to_vec();
567 loop {
568 let mut drain_pollfd = libc::pollfd {
569 fd: stdin_fd,
570 events: libc::POLLIN,
571 revents: 0,
572 };
573 let drain_ready = unsafe { libc::poll(&mut drain_pollfd, 1, 0) };
574 if drain_ready <= 0 || drain_pollfd.revents & libc::POLLIN == 0 {
575 break;
576 }
577 let drain_result =
578 unsafe { libc::read(stdin_fd, buffer.as_mut_ptr().cast(), buffer.len()) };
579 if drain_result <= 0 {
580 break;
581 }
582 data.extend_from_slice(&buffer[..drain_result as usize]);
583 }
584
585 record_pty_input_metrics(
586 &input_bytes_total,
587 &newline_events_total,
588 &submit_events_total,
589 &data,
590 input_contains_newline(&data),
591 );
592 if write_pty_input(&handles, &data).is_err() {
593 break;
594 }
595 }
596
597 active.store(false, Ordering::Release);
598}
599
600pub fn record_pty_input_metrics(
602 input_bytes_total: &Arc<AtomicUsize>,
603 newline_events_total: &Arc<AtomicUsize>,
604 submit_events_total: &Arc<AtomicUsize>,
605 data: &[u8],
606 submit: bool,
607) {
608 input_bytes_total.fetch_add(data.len(), Ordering::AcqRel);
609 if input_contains_newline(data) {
610 newline_events_total.fetch_add(1, Ordering::AcqRel);
611 }
612 if submit {
613 submit_events_total.fetch_add(1, Ordering::AcqRel);
614 }
615}
616
617pub fn store_pty_returncode(returncode: &Arc<Mutex<Option<i32>>>, code: i32) {
619 *returncode.lock().expect("pty returncode mutex poisoned") = Some(code);
620}
621
622pub fn poll_pty_process(
624 handles: &Arc<Mutex<Option<NativePtyHandles>>>,
625 returncode: &Arc<Mutex<Option<i32>>>,
626) -> Result<Option<i32>, std::io::Error> {
627 let mut guard = handles.lock().expect("pty handles mutex poisoned");
628 let Some(handles) = guard.as_mut() else {
629 return Ok(*returncode.lock().expect("pty returncode mutex poisoned"));
630 };
631 let status = handles.child.try_wait()?;
632 let code = status.map(|c| c as i32);
635 if let Some(code) = code {
636 store_pty_returncode(returncode, code);
637 return Ok(Some(code));
638 }
639 Ok(None)
640}
641
642pub fn write_pty_input(
644 handles: &Arc<Mutex<Option<NativePtyHandles>>>,
645 data: &[u8],
646) -> Result<(), std::io::Error> {
647 let writer = {
653 let guard = handles.lock().expect("pty handles mutex poisoned");
654 let handles = guard.as_ref().ok_or_else(|| {
655 std::io::Error::new(
656 std::io::ErrorKind::NotConnected,
657 "Pseudo-terminal process is not running",
658 )
659 })?;
660 Arc::clone(&handles.writer)
661 };
662 #[cfg(windows)]
663 let payload = pty_windows::input_payload(data);
664 #[cfg(unix)]
665 let payload = pty_platform::input_payload(data);
666 let mut writer = writer.lock().expect("pty writer mutex poisoned");
667 writer.write_all(&payload)?;
668 writer.flush()
669}
670
671#[cfg(windows)]
672pub fn windows_terminal_input_payload(data: &[u8]) -> Vec<u8> {
674 let mut translated = Vec::with_capacity(data.len());
675 let mut index = 0usize;
676 while index < data.len() {
677 let current = data[index];
678 if current == b'\r' {
679 translated.push(current);
680 if index + 1 < data.len() && data[index + 1] == b'\n' {
681 translated.push(b'\n');
682 index += 2;
683 continue;
684 }
685 index += 1;
686 continue;
687 }
688 if current == b'\n' {
689 translated.push(b'\r');
690 index += 1;
691 continue;
692 }
693 translated.push(current);
694 index += 1;
695 }
696 translated
697}
698
699#[cfg(windows)]
700#[inline(never)]
702pub fn assign_child_to_windows_kill_on_close_job(
703 handle: Option<std::os::windows::io::RawHandle>,
704) -> Result<WindowsJobHandle, PtyError> {
705 crate::rp_rust_debug_scope!("running_process::pty::assign_child_to_windows_kill_on_close_job");
706 use std::mem::zeroed;
707
708 use winapi::shared::minwindef::FALSE;
709 use winapi::um::handleapi::INVALID_HANDLE_VALUE;
710 use winapi::um::jobapi2::{
711 AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
712 };
713 use winapi::um::winnt::{
714 JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
715 JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
716 };
717
718 let Some(handle) = handle else {
719 return Err(PtyError::Other(
720 "Pseudo-terminal child does not expose a Windows process handle".into(),
721 ));
722 };
723
724 let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
725 if job.is_null() || job == INVALID_HANDLE_VALUE {
726 return Err(PtyError::Io(std::io::Error::last_os_error()));
727 }
728
729 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
730 info.BasicLimitInformation.LimitFlags =
734 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
735 let result = unsafe {
736 SetInformationJobObject(
737 job,
738 JobObjectExtendedLimitInformation,
739 (&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
740 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
741 )
742 };
743 if result == FALSE {
744 let err = std::io::Error::last_os_error();
745 unsafe {
746 winapi::um::handleapi::CloseHandle(job);
747 }
748 return Err(PtyError::Io(err));
749 }
750
751 let result = unsafe { AssignProcessToJobObject(job, handle.cast()) };
752 if result == FALSE {
753 let err = std::io::Error::last_os_error();
754 unsafe {
755 winapi::um::handleapi::CloseHandle(job);
756 }
757 return Err(PtyError::Io(err));
758 }
759
760 Ok(WindowsJobHandle(job as usize))
761}
762
763#[cfg(windows)]
765#[derive(Debug, Clone)]
766pub struct ChildProcessInfo {
767 pub pid: u32,
769 pub name: String,
771}
772
773#[cfg(windows)]
776pub fn find_child_processes(parent_pid: u32) -> Vec<ChildProcessInfo> {
777 use winapi::um::handleapi::CloseHandle;
778 use winapi::um::tlhelp32::{
779 CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
780 };
781
782 let mut children = Vec::new();
783 let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
784 if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
785 return children;
786 }
787
788 let mut entry: PROCESSENTRY32 = unsafe { std::mem::zeroed() };
789 entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
790
791 if unsafe { Process32First(snapshot, &mut entry) } != 0 {
792 loop {
793 if entry.th32ParentProcessID == parent_pid {
794 let name_bytes = &entry.szExeFile;
795 let name_len = name_bytes
796 .iter()
797 .position(|&b| b == 0)
798 .unwrap_or(name_bytes.len());
799 let name = String::from_utf8_lossy(
800 &name_bytes[..name_len]
801 .iter()
802 .map(|&c| c as u8)
803 .collect::<Vec<u8>>(),
804 )
805 .into_owned();
806 children.push(ChildProcessInfo {
807 pid: entry.th32ProcessID,
808 name,
809 });
810 }
811 if unsafe { Process32Next(snapshot, &mut entry) } == 0 {
812 break;
813 }
814 }
815 }
816
817 unsafe { CloseHandle(snapshot) };
818 children
819}
820
821#[cfg(windows)]
823pub(super) fn conhost_children_of_current_process() -> Vec<u32> {
824 let our_pid = std::process::id();
825 find_child_processes(our_pid)
826 .into_iter()
827 .filter(|c| c.name.eq_ignore_ascii_case("conhost.exe"))
828 .map(|c| c.pid)
829 .collect()
830}
831
832#[cfg(windows)]
836pub(super) fn assign_conpty_conhost_to_job(job: &WindowsJobHandle, before_pids: &[u32]) {
837 let after_pids = conhost_children_of_current_process();
838 for pid in after_pids {
839 if !before_pids.contains(&pid) {
840 let _ = job.assign_pid(pid);
842 }
843 }
844}
845
846#[cfg(windows)]
849#[derive(Debug, Clone)]
850pub struct OrphanConhostInfo {
851 pub pid: u32,
853 pub parent_pid: u32,
855 pub parent_name: String,
857}
858
859#[cfg(windows)]
865pub fn find_orphan_conhosts() -> Vec<OrphanConhostInfo> {
866 use winapi::um::handleapi::CloseHandle;
867 use winapi::um::tlhelp32::{
868 CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
869 };
870
871 let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
872 if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE {
873 return Vec::new();
874 }
875
876 let mut entry: PROCESSENTRY32 = unsafe { std::mem::zeroed() };
877 entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
878
879 let mut all_pids = std::collections::HashSet::new();
881 let mut conhosts: Vec<(u32, u32)> = Vec::new(); let mut parent_names: std::collections::HashMap<u32, String> = std::collections::HashMap::new();
883
884 if unsafe { Process32First(snapshot, &mut entry) } != 0 {
885 loop {
886 let name_bytes = &entry.szExeFile;
887 let name_len = name_bytes
888 .iter()
889 .position(|&b| b == 0)
890 .unwrap_or(name_bytes.len());
891 let name = String::from_utf8_lossy(
892 &name_bytes[..name_len]
893 .iter()
894 .map(|&c| c as u8)
895 .collect::<Vec<u8>>(),
896 )
897 .into_owned();
898
899 all_pids.insert(entry.th32ProcessID);
900 parent_names.insert(entry.th32ProcessID, name.clone());
901
902 if name.eq_ignore_ascii_case("conhost.exe") {
903 conhosts.push((entry.th32ProcessID, entry.th32ParentProcessID));
904 }
905
906 if unsafe { Process32Next(snapshot, &mut entry) } == 0 {
907 break;
908 }
909 }
910 }
911
912 unsafe { CloseHandle(snapshot) };
913
914 conhosts
916 .into_iter()
917 .filter(|&(_, parent_pid)| !all_pids.contains(&parent_pid))
918 .map(|(pid, parent_pid)| OrphanConhostInfo {
919 pid,
920 parent_pid,
921 parent_name: parent_names.get(&parent_pid).cloned().unwrap_or_default(),
922 })
923 .collect()
924}
925
926#[cfg(windows)]
927#[inline(never)]
929pub fn apply_windows_pty_priority(
930 handle: Option<std::os::windows::io::RawHandle>,
931 nice: Option<i32>,
932) -> Result<(), PtyError> {
933 crate::rp_rust_debug_scope!("running_process::pty::apply_windows_pty_priority");
934 use winapi::um::processthreadsapi::SetPriorityClass;
935 use winapi::um::winbase::{
936 ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS,
937 IDLE_PRIORITY_CLASS,
938 };
939
940 let Some(handle) = handle else {
941 return Ok(());
942 };
943 let flags = match nice {
944 Some(value) if value >= 15 => IDLE_PRIORITY_CLASS,
945 Some(value) if value >= 1 => BELOW_NORMAL_PRIORITY_CLASS,
946 Some(value) if value <= -15 => HIGH_PRIORITY_CLASS,
947 Some(value) if value <= -1 => ABOVE_NORMAL_PRIORITY_CLASS,
948 _ => 0,
949 };
950 if flags == 0 {
951 return Ok(());
952 }
953 let result = unsafe { SetPriorityClass(handle.cast(), flags) };
954 if result == 0 {
955 return Err(PtyError::Io(std::io::Error::last_os_error()));
956 }
957 Ok(())
958}
959
960#[cfg(test)]
961mod tests {
962 use super::native_pty_process::resolved_spawn_cwd;
963
964 #[test]
965 fn resolved_spawn_cwd_preserves_explicit_value() {
966 assert_eq!(
967 resolved_spawn_cwd(Some("C:\\temp\\explicit")),
968 Some("C:\\temp\\explicit".to_string())
969 );
970 }
971
972 #[test]
973 fn resolved_spawn_cwd_defaults_to_current_dir_when_unset() {
974 let expected = std::env::current_dir()
975 .ok()
976 .map(|cwd| cwd.to_string_lossy().to_string());
977 assert_eq!(resolved_spawn_cwd(None), expected);
978 }
979}