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