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