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