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