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