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