1use std::collections::{HashMap, VecDeque};
2use std::ffi::OsString;
3#[cfg(windows)]
4use std::fs;
5#[cfg(windows)]
6use std::fs::OpenOptions;
7use std::io::{Read, Write};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10use std::sync::Arc;
11use std::sync::{Condvar, Mutex, OnceLock};
12use std::thread;
13use std::time::Duration;
14use std::time::Instant;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
18use pyo3::exceptions::{PyRuntimeError, PyTimeoutError, PyValueError};
19use pyo3::prelude::*;
20use pyo3::types::{PyBytes, PyDict, PyList, PyString};
21use regex::Regex;
22use running_process_core::{
23 find_processes_by_originator, render_rust_debug_traces, CommandSpec, ContainedChild,
24 ContainedProcessGroup, Containment, NativeProcess, OriginatorProcessInfo, ProcessConfig,
25 ProcessError, ReadStatus, StderrMode, StdinMode, StreamEvent, StreamKind,
26};
27#[cfg(unix)]
28use running_process_core::{
29 unix_set_priority, unix_signal_process, unix_signal_process_group, UnixSignal,
30};
31use sysinfo::{Pid, ProcessRefreshKind, Signal, System, UpdateKind};
32
33mod daemon_client;
34#[cfg(unix)]
35mod pty_posix;
36#[cfg(windows)]
37mod pty_windows;
38mod public_symbols;
39
40#[cfg(unix)]
41use pty_posix as pty_platform;
42
43#[cfg(windows)]
44const NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV: &str =
45 "RUNNING_PROCESS_NATIVE_TERMINAL_INPUT_TRACE_PATH";
46
47fn to_py_err(err: impl std::fmt::Display) -> PyErr {
48 PyRuntimeError::new_err(err.to_string())
49}
50
51fn is_ignorable_process_control_error(err: &std::io::Error) -> bool {
52 if matches!(
53 err.kind(),
54 std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
55 ) {
56 return true;
57 }
58 #[cfg(unix)]
59 if err.raw_os_error() == Some(libc::ESRCH) {
60 return true;
61 }
62 false
63}
64
65fn process_err_to_py(err: ProcessError) -> PyErr {
66 match err {
67 ProcessError::Timeout => PyTimeoutError::new_err("process timed out"),
68 other => to_py_err(other),
69 }
70}
71
72fn system_pid(pid: u32) -> Pid {
73 Pid::from_u32(pid)
74}
75
76fn descendant_pids(system: &System, pid: Pid) -> Vec<Pid> {
77 let mut descendants = Vec::new();
78 let mut stack = vec![pid];
79 while let Some(current) = stack.pop() {
80 for (child_pid, process) in system.processes() {
81 if process.parent() == Some(current) {
82 descendants.push(*child_pid);
83 stack.push(*child_pid);
84 }
85 }
86 }
87 descendants
88}
89
90#[derive(Clone)]
91struct ActiveProcessRecord {
92 pid: u32,
93 kind: String,
94 command: String,
95 cwd: Option<String>,
96 started_at: f64,
97}
98
99type TrackedProcessEntry = (u32, f64, String, String, Option<String>);
100type ActiveProcessEntry = (u32, String, String, Option<String>, f64);
101type ExpectDetails = (String, usize, usize, Vec<String>);
102type ExpectResult = (
103 String,
104 String,
105 Option<String>,
106 Option<usize>,
107 Option<usize>,
108 Vec<String>,
109);
110
111fn active_process_registry() -> &'static Mutex<HashMap<u32, ActiveProcessRecord>> {
112 static ACTIVE_PROCESSES: OnceLock<Mutex<HashMap<u32, ActiveProcessRecord>>> = OnceLock::new();
113 ACTIVE_PROCESSES.get_or_init(|| Mutex::new(HashMap::new()))
114}
115
116fn unix_now_seconds() -> f64 {
117 SystemTime::now()
118 .duration_since(UNIX_EPOCH)
119 .map(|duration| duration.as_secs_f64())
120 .unwrap_or(0.0)
121}
122
123#[cfg(windows)]
124fn native_terminal_input_trace_target() -> Option<String> {
125 std::env::var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV)
126 .ok()
127 .map(|value| value.trim().to_string())
128 .filter(|value| !value.is_empty())
129}
130
131#[cfg(windows)]
132fn append_native_terminal_input_trace_line(line: &str) {
133 let Some(target) = native_terminal_input_trace_target() else {
134 return;
135 };
136 if target == "-" {
137 eprintln!("{line}");
138 return;
139 }
140 let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&target) else {
141 return;
142 };
143 let _ = writeln!(file, "{line}");
144}
145
146#[cfg(windows)]
147fn format_terminal_input_bytes(data: &[u8]) -> String {
148 if data.is_empty() {
149 return "[]".to_string();
150 }
151 let parts: Vec<String> = data.iter().map(|byte| format!("{byte:02x}")).collect();
152 format!("[{}]", parts.join(" "))
153}
154
155fn register_active_process(
156 pid: u32,
157 kind: &str,
158 command: &str,
159 cwd: Option<String>,
160 started_at: f64,
161) {
162 let mut registry = active_process_registry()
163 .lock()
164 .expect("active process registry mutex poisoned");
165 registry.insert(
166 pid,
167 ActiveProcessRecord {
168 pid,
169 kind: kind.to_string(),
170 command: command.to_string(),
171 cwd: cwd.clone(),
172 started_at,
173 },
174 );
175 drop(registry); daemon_client::daemon_register(pid, started_at, kind, command, cwd.as_deref());
179}
180
181fn unregister_active_process(pid: u32) {
182 let mut registry = active_process_registry()
183 .lock()
184 .expect("active process registry mutex poisoned");
185 registry.remove(&pid);
186 drop(registry); daemon_client::daemon_unregister(pid);
190}
191
192fn process_created_at(pid: u32) -> Option<f64> {
193 let pid = system_pid(pid);
194 let mut system = System::new();
195 system.refresh_process_specifics(
196 pid,
197 ProcessRefreshKind::new()
198 .with_cpu()
199 .with_disk_usage()
200 .with_memory()
201 .with_exe(UpdateKind::Never),
202 );
203 system
204 .process(pid)
205 .map(|process| process.start_time() as f64)
206}
207
208fn same_process_identity(pid: u32, created_at: f64, tolerance_seconds: f64) -> bool {
209 let Some(actual) = process_created_at(pid) else {
210 return false;
211 };
212 (actual - created_at).abs() <= tolerance_seconds
213}
214
215fn tracked_process_db_path() -> PyResult<PathBuf> {
216 if let Ok(value) = std::env::var("RUNNING_PROCESS_PID_DB") {
217 let trimmed = value.trim();
218 if !trimmed.is_empty() {
219 return Ok(PathBuf::from(trimmed));
220 }
221 }
222
223 #[cfg(windows)]
224 let base_dir = std::env::var_os("LOCALAPPDATA")
225 .map(PathBuf::from)
226 .unwrap_or_else(std::env::temp_dir);
227
228 #[cfg(not(windows))]
229 let base_dir = std::env::var_os("XDG_STATE_HOME")
230 .map(PathBuf::from)
231 .or_else(|| {
232 std::env::var_os("HOME").map(|home| {
233 let mut path = PathBuf::from(home);
234 path.push(".local");
235 path.push("state");
236 path
237 })
238 })
239 .unwrap_or_else(std::env::temp_dir);
240
241 Ok(base_dir
242 .join("running-process")
243 .join("tracked-pids.sqlite3"))
244}
245
246#[pyfunction]
247fn tracked_pid_db_path_py() -> PyResult<String> {
248 Ok(tracked_process_db_path()?.to_string_lossy().into_owned())
249}
250
251#[pyfunction]
252#[pyo3(signature = (pid, created_at, kind, command, cwd=None))]
253fn track_process_pid(
254 pid: u32,
255 created_at: f64,
256 kind: &str,
257 command: &str,
258 cwd: Option<String>,
259) -> PyResult<()> {
260 let _ = created_at;
261 register_active_process(pid, kind, command, cwd, unix_now_seconds());
262 Ok(())
263}
264
265#[pyfunction]
266#[pyo3(signature = (pid, kind, command, cwd=None))]
267fn native_register_process(
268 pid: u32,
269 kind: &str,
270 command: &str,
271 cwd: Option<String>,
272) -> PyResult<()> {
273 register_active_process(pid, kind, command, cwd, unix_now_seconds());
274 Ok(())
275}
276
277#[pyfunction]
278fn untrack_process_pid(pid: u32) -> PyResult<()> {
279 unregister_active_process(pid);
280 Ok(())
281}
282
283#[pyfunction]
284fn native_unregister_process(pid: u32) -> PyResult<()> {
285 unregister_active_process(pid);
286 Ok(())
287}
288
289#[pyfunction]
290fn list_tracked_processes() -> PyResult<Vec<TrackedProcessEntry>> {
291 let registry = active_process_registry()
292 .lock()
293 .expect("active process registry mutex poisoned");
294 let mut entries: Vec<_> = registry
295 .values()
296 .map(|entry| {
297 (
298 entry.pid,
299 process_created_at(entry.pid).unwrap_or(entry.started_at),
300 entry.kind.clone(),
301 entry.command.clone(),
302 entry.cwd.clone(),
303 )
304 })
305 .collect();
306 entries.sort_by(|left, right| {
307 left.1
308 .partial_cmp(&right.1)
309 .unwrap_or(std::cmp::Ordering::Equal)
310 .then_with(|| left.0.cmp(&right.0))
311 });
312 Ok(entries)
313}
314
315fn kill_process_tree_impl(pid: u32, timeout_seconds: f64) {
316 let mut system = System::new_all();
317 let pid = system_pid(pid);
318 let Some(_) = system.process(pid) else {
319 return;
320 };
321
322 let mut kill_order = descendant_pids(&system, pid);
323 kill_order.reverse();
324 kill_order.push(pid);
325
326 for target in &kill_order {
327 if let Some(process) = system.process(*target) {
328 if !process.kill_with(Signal::Kill).unwrap_or(false) {
329 process.kill();
330 }
331 }
332 }
333
334 let deadline = Instant::now()
335 .checked_add(Duration::from_secs_f64(timeout_seconds.max(0.0)))
336 .unwrap_or_else(Instant::now);
337 loop {
338 system.refresh_processes();
339 if kill_order
340 .iter()
341 .all(|target| system.process(*target).is_none())
342 {
343 break;
344 }
345 if Instant::now() >= deadline {
346 break;
347 }
348 thread::sleep(Duration::from_millis(25));
349 }
350}
351
352#[cfg(windows)]
353fn windows_terminal_input_payload(data: &[u8]) -> Vec<u8> {
354 let mut translated = Vec::with_capacity(data.len());
355 let mut index = 0usize;
356 while index < data.len() {
357 let current = data[index];
358 if current == b'\r' {
359 translated.push(current);
360 if index + 1 < data.len() && data[index + 1] == b'\n' {
361 translated.push(b'\n');
362 index += 2;
363 continue;
364 }
365 index += 1;
366 continue;
367 }
368 if current == b'\n' {
369 translated.push(b'\r');
370 index += 1;
371 continue;
372 }
373 translated.push(current);
374 index += 1;
375 }
376 translated
377}
378
379#[cfg(windows)]
380fn native_terminal_input_mode(original_mode: u32) -> u32 {
381 use winapi::um::wincon::{
382 ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
383 ENABLE_QUICK_EDIT_MODE, ENABLE_WINDOW_INPUT,
384 };
385
386 (original_mode | ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT)
387 & !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE)
388}
389
390#[cfg(windows)]
391fn terminal_input_modifier_parameter(shift: bool, alt: bool, ctrl: bool) -> Option<u8> {
392 let value = 1 + u8::from(shift) + (u8::from(alt) * 2) + (u8::from(ctrl) * 4);
393 (value > 1).then_some(value)
394}
395
396#[cfg(windows)]
397fn repeat_terminal_input_bytes(chunk: &[u8], repeat_count: u16) -> Vec<u8> {
398 let repeat = usize::from(repeat_count.max(1));
399 let mut output = Vec::with_capacity(chunk.len() * repeat);
400 for _ in 0..repeat {
401 output.extend_from_slice(chunk);
402 }
403 output
404}
405
406#[cfg(windows)]
407fn repeated_modified_sequence(base: &[u8], modifier: Option<u8>, repeat_count: u16) -> Vec<u8> {
408 if let Some(value) = modifier {
409 let base_text = std::str::from_utf8(base).expect("VT sequence literal must be utf-8");
410 let body = base_text
411 .strip_prefix("\x1b[")
412 .expect("VT sequence literal must start with CSI");
413 let sequence = format!("\x1b[1;{value}{body}");
414 repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
415 } else {
416 repeat_terminal_input_bytes(base, repeat_count)
417 }
418}
419
420#[cfg(windows)]
421fn repeated_tilde_sequence(number: u8, modifier: Option<u8>, repeat_count: u16) -> Vec<u8> {
422 if let Some(value) = modifier {
423 let sequence = format!("\x1b[{number};{value}~");
424 repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
425 } else {
426 let sequence = format!("\x1b[{number}~");
427 repeat_terminal_input_bytes(sequence.as_bytes(), repeat_count)
428 }
429}
430
431#[cfg(windows)]
432fn control_character_for_unicode(unicode: u16) -> Option<u8> {
433 let upper = char::from_u32(u32::from(unicode))?.to_ascii_uppercase();
434 match upper {
435 '@' | ' ' => Some(0x00),
436 'A'..='Z' => Some((upper as u8) - b'@'),
437 '[' => Some(0x1B),
438 '\\' => Some(0x1C),
439 ']' => Some(0x1D),
440 '^' => Some(0x1E),
441 '_' => Some(0x1F),
442 _ => None,
443 }
444}
445
446#[cfg(windows)]
447fn trace_translated_console_key_event(
448 record: &winapi::um::wincontypes::KEY_EVENT_RECORD,
449 event: TerminalInputEventRecord,
450) -> TerminalInputEventRecord {
451 append_native_terminal_input_trace_line(&format!(
452 "[{:.6}] native_terminal_input raw bKeyDown={} vk={:#06x} scan={:#06x} unicode={:#06x} control={:#010x} repeat={} translated bytes={} submit={} shift={} ctrl={} alt={}",
453 unix_now_seconds(),
454 record.bKeyDown,
455 record.wVirtualKeyCode,
456 record.wVirtualScanCode,
457 unsafe { *record.uChar.UnicodeChar() },
458 record.dwControlKeyState,
459 record.wRepeatCount.max(1),
460 format_terminal_input_bytes(&event.data),
461 event.submit,
462 event.shift,
463 event.ctrl,
464 event.alt,
465 ));
466 event
467}
468
469#[cfg(windows)]
470fn translate_console_key_event(
471 record: &winapi::um::wincontypes::KEY_EVENT_RECORD,
472) -> Option<TerminalInputEventRecord> {
473 use winapi::um::wincontypes::{
474 LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
475 };
476 use winapi::um::winuser::{
477 VK_BACK, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_HOME, VK_INSERT, VK_LEFT, VK_NEXT,
478 VK_PRIOR, VK_RETURN, VK_RIGHT, VK_TAB, VK_UP,
479 };
480
481 if record.bKeyDown == 0 {
482 append_native_terminal_input_trace_line(&format!(
483 "[{:.6}] native_terminal_input raw bKeyDown=0 vk={:#06x} scan={:#06x} unicode={:#06x} control={:#010x} repeat={} translated=ignored",
484 unix_now_seconds(),
485 record.wVirtualKeyCode,
486 record.wVirtualScanCode,
487 unsafe { *record.uChar.UnicodeChar() },
488 record.dwControlKeyState,
489 record.wRepeatCount,
490 ));
491 return None;
492 }
493
494 let repeat_count = record.wRepeatCount.max(1);
495 let modifiers = record.dwControlKeyState;
496 let shift = modifiers & SHIFT_PRESSED != 0;
497 let alt = modifiers & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0;
498 let ctrl = modifiers & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0;
499 let virtual_key_code = record.wVirtualKeyCode;
500 let unicode = unsafe { *record.uChar.UnicodeChar() };
501
502 if shift && !ctrl && !alt && virtual_key_code as i32 == VK_RETURN {
506 return Some(trace_translated_console_key_event(
507 record,
508 TerminalInputEventRecord {
509 data: repeat_terminal_input_bytes(b"\x1b[13;2u", repeat_count),
510 submit: false,
511 shift,
512 ctrl,
513 alt,
514 virtual_key_code,
515 repeat_count,
516 },
517 ));
518 }
519
520 let mut data = if ctrl {
521 control_character_for_unicode(unicode)
522 .map(|byte| repeat_terminal_input_bytes(&[byte], repeat_count))
523 .unwrap_or_default()
524 } else {
525 Vec::new()
526 };
527
528 if data.is_empty() && unicode != 0 {
529 if let Some(character) = char::from_u32(u32::from(unicode)) {
530 let text: String = std::iter::repeat_n(character, usize::from(repeat_count)).collect();
531 data = text.into_bytes();
532 }
533 }
534
535 if data.is_empty() {
536 let modifier = terminal_input_modifier_parameter(shift, alt, ctrl);
537 let sequence = match virtual_key_code as i32 {
538 VK_BACK => Some(b"\x08".as_slice()),
539 VK_TAB if shift => Some(b"\x1b[Z".as_slice()),
540 VK_TAB => Some(b"\t".as_slice()),
541 VK_RETURN => Some(b"\r".as_slice()),
542 VK_ESCAPE => Some(b"\x1b".as_slice()),
543 VK_UP => {
544 return Some(trace_translated_console_key_event(
545 record,
546 TerminalInputEventRecord {
547 data: repeated_modified_sequence(b"\x1b[A", modifier, repeat_count),
548 submit: false,
549 shift,
550 ctrl,
551 alt,
552 virtual_key_code,
553 repeat_count,
554 },
555 ));
556 }
557 VK_DOWN => {
558 return Some(trace_translated_console_key_event(
559 record,
560 TerminalInputEventRecord {
561 data: repeated_modified_sequence(b"\x1b[B", modifier, repeat_count),
562 submit: false,
563 shift,
564 ctrl,
565 alt,
566 virtual_key_code,
567 repeat_count,
568 },
569 ));
570 }
571 VK_RIGHT => {
572 return Some(trace_translated_console_key_event(
573 record,
574 TerminalInputEventRecord {
575 data: repeated_modified_sequence(b"\x1b[C", modifier, repeat_count),
576 submit: false,
577 shift,
578 ctrl,
579 alt,
580 virtual_key_code,
581 repeat_count,
582 },
583 ));
584 }
585 VK_LEFT => {
586 return Some(trace_translated_console_key_event(
587 record,
588 TerminalInputEventRecord {
589 data: repeated_modified_sequence(b"\x1b[D", modifier, repeat_count),
590 submit: false,
591 shift,
592 ctrl,
593 alt,
594 virtual_key_code,
595 repeat_count,
596 },
597 ));
598 }
599 VK_HOME => {
600 return Some(trace_translated_console_key_event(
601 record,
602 TerminalInputEventRecord {
603 data: repeated_modified_sequence(b"\x1b[H", modifier, repeat_count),
604 submit: false,
605 shift,
606 ctrl,
607 alt,
608 virtual_key_code,
609 repeat_count,
610 },
611 ));
612 }
613 VK_END => {
614 return Some(trace_translated_console_key_event(
615 record,
616 TerminalInputEventRecord {
617 data: repeated_modified_sequence(b"\x1b[F", modifier, repeat_count),
618 submit: false,
619 shift,
620 ctrl,
621 alt,
622 virtual_key_code,
623 repeat_count,
624 },
625 ));
626 }
627 VK_INSERT => {
628 return Some(trace_translated_console_key_event(
629 record,
630 TerminalInputEventRecord {
631 data: repeated_tilde_sequence(2, modifier, repeat_count),
632 submit: false,
633 shift,
634 ctrl,
635 alt,
636 virtual_key_code,
637 repeat_count,
638 },
639 ));
640 }
641 VK_DELETE => {
642 return Some(trace_translated_console_key_event(
643 record,
644 TerminalInputEventRecord {
645 data: repeated_tilde_sequence(3, modifier, repeat_count),
646 submit: false,
647 shift,
648 ctrl,
649 alt,
650 virtual_key_code,
651 repeat_count,
652 },
653 ));
654 }
655 VK_PRIOR => {
656 return Some(trace_translated_console_key_event(
657 record,
658 TerminalInputEventRecord {
659 data: repeated_tilde_sequence(5, modifier, repeat_count),
660 submit: false,
661 shift,
662 ctrl,
663 alt,
664 virtual_key_code,
665 repeat_count,
666 },
667 ));
668 }
669 VK_NEXT => {
670 return Some(trace_translated_console_key_event(
671 record,
672 TerminalInputEventRecord {
673 data: repeated_tilde_sequence(6, modifier, repeat_count),
674 submit: false,
675 shift,
676 ctrl,
677 alt,
678 virtual_key_code,
679 repeat_count,
680 },
681 ));
682 }
683 _ => None,
684 };
685 data = sequence.map(|chunk| repeat_terminal_input_bytes(chunk, repeat_count))?;
686 }
687
688 if alt && !data.starts_with(b"\x1b[") && !data.starts_with(b"\x1bO") {
689 let mut prefixed = Vec::with_capacity(data.len() + 1);
690 prefixed.push(0x1B);
691 prefixed.extend_from_slice(&data);
692 data = prefixed;
693 }
694
695 let event = TerminalInputEventRecord {
696 data,
697 submit: virtual_key_code as i32 == VK_RETURN && !shift,
698 shift,
699 ctrl,
700 alt,
701 virtual_key_code,
702 repeat_count,
703 };
704 Some(trace_translated_console_key_event(record, event))
705}
706
707#[cfg(windows)]
708fn native_terminal_input_worker(
709 input_handle: usize,
710 state: Arc<Mutex<TerminalInputState>>,
711 condvar: Arc<Condvar>,
712 stop: Arc<AtomicBool>,
713 capturing: Arc<AtomicBool>,
714) {
715 use winapi::shared::minwindef::DWORD;
716 use winapi::shared::winerror::WAIT_TIMEOUT;
717 use winapi::um::consoleapi::ReadConsoleInputW;
718 use winapi::um::synchapi::WaitForSingleObject;
719 use winapi::um::winbase::WAIT_OBJECT_0;
720 use winapi::um::wincontypes::{INPUT_RECORD, KEY_EVENT};
721 use winapi::um::winnt::HANDLE;
722
723 let handle = input_handle as HANDLE;
724 let mut records: [INPUT_RECORD; 16] = unsafe { std::mem::zeroed() };
725 append_native_terminal_input_trace_line(&format!(
726 "[{:.6}] native_terminal_input worker_start handle={input_handle}",
727 unix_now_seconds(),
728 ));
729
730 while !stop.load(Ordering::Acquire) {
731 let wait_result = unsafe { WaitForSingleObject(handle, 50) };
732 match wait_result {
733 WAIT_OBJECT_0 => {
734 let mut read_count: DWORD = 0;
735 let ok = unsafe {
736 ReadConsoleInputW(
737 handle,
738 records.as_mut_ptr(),
739 records.len() as DWORD,
740 &mut read_count,
741 )
742 };
743 if ok == 0 {
744 append_native_terminal_input_trace_line(&format!(
745 "[{:.6}] native_terminal_input read_console_input_failed handle={input_handle}",
746 unix_now_seconds(),
747 ));
748 break;
749 }
750 for record in records.iter().take(read_count as usize) {
751 if record.EventType != KEY_EVENT {
752 continue;
753 }
754 let key_event = unsafe { record.Event.KeyEvent() };
755 if let Some(event) = translate_console_key_event(key_event) {
756 let mut guard = state.lock().expect("terminal input mutex poisoned");
757 guard.events.push_back(event);
758 drop(guard);
759 condvar.notify_all();
760 }
761 }
762 }
763 WAIT_TIMEOUT => continue,
764 _ => {
765 append_native_terminal_input_trace_line(&format!(
766 "[{:.6}] native_terminal_input wait_result={wait_result} handle={input_handle}",
767 unix_now_seconds(),
768 ));
769 break;
770 }
771 }
772 }
773
774 capturing.store(false, Ordering::Release);
775 let mut guard = state.lock().expect("terminal input mutex poisoned");
776 guard.closed = true;
777 condvar.notify_all();
778 drop(guard);
779 append_native_terminal_input_trace_line(&format!(
780 "[{:.6}] native_terminal_input worker_stop handle={input_handle}",
781 unix_now_seconds(),
782 ));
783}
784
785#[pyfunction]
786fn native_get_process_tree_info(pid: u32) -> String {
787 let system = System::new_all();
788 let pid = system_pid(pid);
789 let Some(process) = system.process(pid) else {
790 return format!("Could not get process info for PID {}", pid.as_u32());
791 };
792
793 let mut info = vec![
794 format!("Process {} ({})", pid.as_u32(), process.name()),
795 format!("Status: {:?}", process.status()),
796 ];
797 let children = descendant_pids(&system, pid);
798 if !children.is_empty() {
799 info.push("Child processes:".to_string());
800 for child_pid in children {
801 if let Some(child) = system.process(child_pid) {
802 info.push(format!(" Child {} ({})", child_pid.as_u32(), child.name()));
803 }
804 }
805 }
806 info.join("\n")
807}
808
809#[pyfunction]
810#[pyo3(signature = (pid, timeout_seconds=3.0))]
811fn native_kill_process_tree(pid: u32, timeout_seconds: f64) {
812 kill_process_tree_impl(pid, timeout_seconds);
813}
814
815#[pyfunction]
816fn native_process_created_at(pid: u32) -> Option<f64> {
817 process_created_at(pid)
818}
819
820#[pyfunction]
821#[pyo3(signature = (pid, created_at, tolerance_seconds=1.0))]
822fn native_is_same_process(pid: u32, created_at: f64, tolerance_seconds: f64) -> bool {
823 same_process_identity(pid, created_at, tolerance_seconds)
824}
825
826#[pyfunction]
827#[pyo3(signature = (tolerance_seconds=1.0, kill_timeout_seconds=3.0))]
828fn native_cleanup_tracked_processes(
829 tolerance_seconds: f64,
830 kill_timeout_seconds: f64,
831) -> PyResult<Vec<TrackedProcessEntry>> {
832 let entries = list_tracked_processes()?;
833
834 let mut killed = Vec::new();
835 for entry in entries {
836 let pid = entry.0;
837 if !same_process_identity(pid, entry.1, tolerance_seconds) {
838 unregister_active_process(pid);
839 continue;
840 }
841 kill_process_tree_impl(pid, kill_timeout_seconds);
842 unregister_active_process(pid);
843 killed.push(entry);
844 }
845 Ok(killed)
846}
847
848#[pyfunction]
849fn native_list_active_processes() -> Vec<ActiveProcessEntry> {
850 let registry = active_process_registry()
851 .lock()
852 .expect("active process registry mutex poisoned");
853 let mut items: Vec<_> = registry
854 .values()
855 .map(|entry| {
856 (
857 entry.pid,
858 entry.kind.clone(),
859 entry.command.clone(),
860 entry.cwd.clone(),
861 entry.started_at,
862 )
863 })
864 .collect();
865 items.sort_by(|left, right| {
866 left.4
867 .partial_cmp(&right.4)
868 .unwrap_or(std::cmp::Ordering::Equal)
869 .then_with(|| left.0.cmp(&right.0))
870 });
871 items
872}
873
874#[pyfunction]
875#[inline(never)]
876fn native_apply_process_nice(pid: u32, nice: i32) -> PyResult<()> {
877 public_symbols::rp_native_apply_process_nice_public(pid, nice)
878}
879
880fn native_apply_process_nice_impl(pid: u32, nice: i32) -> PyResult<()> {
881 running_process_core::rp_rust_debug_scope!("running_process_py::native_apply_process_nice");
882 #[cfg(windows)]
883 {
884 public_symbols::rp_windows_apply_process_priority_public(pid, nice)
885 }
886
887 #[cfg(unix)]
888 {
889 unix_set_priority(pid, nice).map_err(to_py_err)
890 }
891}
892
893#[cfg(windows)]
894fn windows_apply_process_priority_impl(pid: u32, nice: i32) -> PyResult<()> {
895 running_process_core::rp_rust_debug_scope!(
896 "running_process_py::windows_apply_process_priority"
897 );
898 use winapi::um::handleapi::CloseHandle;
899 use winapi::um::processthreadsapi::{OpenProcess, SetPriorityClass};
900 use winapi::um::winbase::{
901 ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS,
902 IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS,
903 };
904 use winapi::um::winnt::{PROCESS_QUERY_INFORMATION, PROCESS_SET_INFORMATION};
905
906 let priority_class = if nice >= 15 {
907 IDLE_PRIORITY_CLASS
908 } else if nice >= 1 {
909 BELOW_NORMAL_PRIORITY_CLASS
910 } else if nice <= -15 {
911 HIGH_PRIORITY_CLASS
912 } else if nice <= -1 {
913 ABOVE_NORMAL_PRIORITY_CLASS
914 } else {
915 NORMAL_PRIORITY_CLASS
916 };
917
918 let handle =
919 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION, 0, pid) };
920 if handle.is_null() {
921 return Err(to_py_err(std::io::Error::last_os_error()));
922 }
923 let result = unsafe { SetPriorityClass(handle, priority_class) };
924 let close_result = unsafe { CloseHandle(handle) };
925 if close_result == 0 {
926 return Err(to_py_err(std::io::Error::last_os_error()));
927 }
928 if result == 0 {
929 return Err(to_py_err(std::io::Error::last_os_error()));
930 }
931 Ok(())
932}
933
934#[cfg(windows)]
935fn windows_generate_console_ctrl_break_impl(pid: u32, creationflags: Option<u32>) -> PyResult<()> {
936 running_process_core::rp_rust_debug_scope!(
937 "running_process_py::windows_generate_console_ctrl_break"
938 );
939 use winapi::um::wincon::{GenerateConsoleCtrlEvent, CTRL_BREAK_EVENT};
940
941 let new_process_group =
942 creationflags.unwrap_or(0) & winapi::um::winbase::CREATE_NEW_PROCESS_GROUP;
943 if new_process_group == 0 {
944 return Err(PyRuntimeError::new_err(
945 "send_interrupt on Windows requires CREATE_NEW_PROCESS_GROUP",
946 ));
947 }
948 let result = unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) };
949 if result == 0 {
950 return Err(to_py_err(std::io::Error::last_os_error()));
951 }
952 Ok(())
953}
954
955#[pyfunction]
956fn native_windows_terminal_input_bytes(py: Python<'_>, data: &[u8]) -> Py<PyAny> {
957 #[cfg(windows)]
958 let payload = windows_terminal_input_payload(data);
959 #[cfg(not(windows))]
960 let payload = data.to_vec();
961 PyBytes::new(py, &payload).into_any().unbind()
962}
963
964#[pyfunction]
965fn native_dump_rust_debug_traces() -> String {
966 render_rust_debug_traces()
967}
968
969#[pyfunction]
970fn native_test_capture_rust_debug_trace() -> String {
971 #[inline(never)]
972 fn inner() -> String {
973 running_process_core::rp_rust_debug_scope!(
974 "running_process_py::native_test_capture_rust_debug_trace::inner"
975 );
976 render_rust_debug_traces()
977 }
978
979 #[inline(never)]
980 fn outer() -> String {
981 running_process_core::rp_rust_debug_scope!(
982 "running_process_py::native_test_capture_rust_debug_trace::outer"
983 );
984 inner()
985 }
986
987 outer()
988}
989
990#[cfg(windows)]
991#[no_mangle]
992#[inline(never)]
993pub fn running_process_py_debug_hang_inner(release_path: &std::path::Path) -> PyResult<()> {
994 running_process_core::rp_rust_debug_scope!("running_process_py::debug_hang_inner");
995 while !release_path.exists() {
996 std::hint::spin_loop();
997 }
998 Ok(())
999}
1000
1001#[cfg(windows)]
1002#[no_mangle]
1003#[inline(never)]
1004pub fn running_process_py_debug_hang_outer(
1005 ready_path: &std::path::Path,
1006 release_path: &std::path::Path,
1007) -> PyResult<()> {
1008 running_process_core::rp_rust_debug_scope!("running_process_py::debug_hang_outer");
1009 fs::write(ready_path, b"ready").map_err(to_py_err)?;
1010 running_process_py_debug_hang_inner(release_path)
1011}
1012
1013#[pyfunction]
1014#[cfg(windows)]
1015#[inline(never)]
1016fn native_test_hang_in_rust(ready_path: String, release_path: String) -> PyResult<()> {
1017 running_process_core::rp_rust_debug_scope!("running_process_py::native_test_hang_in_rust");
1018 running_process_py_debug_hang_outer(
1019 std::path::Path::new(&ready_path),
1020 std::path::Path::new(&release_path),
1021 )
1022}
1023
1024#[pymethods]
1025impl NativeProcessMetrics {
1026 #[new]
1027 fn new(pid: u32) -> Self {
1028 let pid = system_pid(pid);
1029 let mut system = System::new();
1030 system.refresh_process_specifics(
1031 pid,
1032 ProcessRefreshKind::new()
1033 .with_cpu()
1034 .with_disk_usage()
1035 .with_memory()
1036 .with_exe(UpdateKind::Never),
1037 );
1038 Self {
1039 pid,
1040 system: Mutex::new(system),
1041 }
1042 }
1043
1044 fn prime(&self) {
1045 let mut system = self.system.lock().expect("process metrics mutex poisoned");
1046 system.refresh_process_specifics(
1047 self.pid,
1048 ProcessRefreshKind::new()
1049 .with_cpu()
1050 .with_disk_usage()
1051 .with_memory()
1052 .with_exe(UpdateKind::Never),
1053 );
1054 }
1055
1056 fn sample(&self) -> (bool, f32, u64, u64) {
1057 let mut system = self.system.lock().expect("process metrics mutex poisoned");
1058 system.refresh_process_specifics(
1059 self.pid,
1060 ProcessRefreshKind::new()
1061 .with_cpu()
1062 .with_disk_usage()
1063 .with_memory()
1064 .with_exe(UpdateKind::Never),
1065 );
1066 let Some(process) = system.process(self.pid) else {
1067 return (false, 0.0, 0, 0);
1068 };
1069 let disk = process.disk_usage();
1070 (
1071 true,
1072 process.cpu_usage(),
1073 disk.total_read_bytes
1074 .saturating_add(disk.total_written_bytes),
1075 0,
1076 )
1077 }
1078}
1079
1080struct PtyReadState {
1081 chunks: VecDeque<Vec<u8>>,
1082 closed: bool,
1083}
1084
1085struct PtyReadShared {
1086 state: Mutex<PtyReadState>,
1087 condvar: Condvar,
1088}
1089
1090struct NativePtyHandles {
1091 master: Box<dyn MasterPty + Send>,
1092 writer: Box<dyn Write + Send>,
1093 child: Box<dyn portable_pty::Child + Send + Sync>,
1094 #[cfg(windows)]
1095 _job: WindowsJobHandle,
1096}
1097
1098#[pyclass]
1099struct NativeProcessMetrics {
1100 pid: Pid,
1101 system: Mutex<System>,
1102}
1103
1104#[pyclass]
1105struct NativePtyProcess {
1106 argv: Vec<String>,
1107 cwd: Option<String>,
1108 env: Option<Vec<(String, String)>>,
1109 rows: u16,
1110 cols: u16,
1111 #[cfg(windows)]
1112 nice: Option<i32>,
1113 handles: Arc<Mutex<Option<NativePtyHandles>>>,
1114 reader: Arc<PtyReadShared>,
1115 returncode: Arc<Mutex<Option<i32>>>,
1116 input_bytes_total: Arc<AtomicUsize>,
1117 newline_events_total: Arc<AtomicUsize>,
1118 submit_events_total: Arc<AtomicUsize>,
1119 echo: Arc<AtomicBool>,
1121 idle_detector: Arc<Mutex<Option<Arc<IdleDetectorCore>>>>,
1123 output_bytes_total: Arc<AtomicUsize>,
1125 control_churn_bytes_total: Arc<AtomicUsize>,
1127 #[cfg(windows)]
1128 terminal_input_relay_stop: Arc<AtomicBool>,
1129 #[cfg(windows)]
1130 terminal_input_relay_active: Arc<AtomicBool>,
1131 #[cfg(windows)]
1132 terminal_input_relay_worker: Mutex<Option<thread::JoinHandle<()>>>,
1133}
1134
1135impl NativePtyProcess {
1136 fn mark_reader_closed(&self) {
1137 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
1138 guard.closed = true;
1139 self.reader.condvar.notify_all();
1140 }
1141
1142 fn store_returncode(&self, code: i32) {
1143 store_pty_returncode(&self.returncode, code);
1144 }
1145
1146 fn record_input_metrics(&self, data: &[u8], submit: bool) {
1147 record_pty_input_metrics(
1148 &self.input_bytes_total,
1149 &self.newline_events_total,
1150 &self.submit_events_total,
1151 data,
1152 submit,
1153 );
1154 }
1155
1156 fn write_impl(&self, data: &[u8], submit: bool) -> PyResult<()> {
1157 self.record_input_metrics(data, submit);
1158 write_pty_input(&self.handles, data).map_err(to_py_err)
1159 }
1160
1161 #[cfg(windows)]
1162 fn request_terminal_input_relay_stop(&self) {
1163 self.terminal_input_relay_stop
1164 .store(true, Ordering::Release);
1165 self.terminal_input_relay_active
1166 .store(false, Ordering::Release);
1167 }
1168
1169 #[cfg(windows)]
1170 fn stop_terminal_input_relay_impl(&self) {
1171 self.request_terminal_input_relay_stop();
1172 if let Some(worker) = self
1173 .terminal_input_relay_worker
1174 .lock()
1175 .expect("pty terminal input relay mutex poisoned")
1176 .take()
1177 {
1178 let _ = worker.join();
1179 }
1180 }
1181
1182 #[cfg(not(windows))]
1183 fn stop_terminal_input_relay_impl(&self) {}
1184
1185 #[cfg(windows)]
1186 fn start_terminal_input_relay_impl(&self) -> PyResult<()> {
1187 let mut worker_guard = self
1188 .terminal_input_relay_worker
1189 .lock()
1190 .expect("pty terminal input relay mutex poisoned");
1191 if worker_guard.is_some() && self.terminal_input_relay_active.load(Ordering::Acquire) {
1192 return Ok(());
1193 }
1194 if self
1195 .handles
1196 .lock()
1197 .expect("pty handles mutex poisoned")
1198 .is_none()
1199 {
1200 return Err(PyRuntimeError::new_err(
1201 "Pseudo-terminal process is not running",
1202 ));
1203 }
1204
1205 let capture = NativeTerminalInput::new();
1206 capture.start_impl()?;
1207
1208 self.terminal_input_relay_stop
1209 .store(false, Ordering::Release);
1210 self.terminal_input_relay_active
1211 .store(true, Ordering::Release);
1212
1213 let handles = Arc::clone(&self.handles);
1214 let returncode = Arc::clone(&self.returncode);
1215 let input_bytes_total = Arc::clone(&self.input_bytes_total);
1216 let newline_events_total = Arc::clone(&self.newline_events_total);
1217 let submit_events_total = Arc::clone(&self.submit_events_total);
1218 let stop = Arc::clone(&self.terminal_input_relay_stop);
1219 let active = Arc::clone(&self.terminal_input_relay_active);
1220
1221 *worker_guard = Some(thread::spawn(move || {
1222 loop {
1223 if stop.load(Ordering::Acquire) {
1224 break;
1225 }
1226 match poll_pty_process(&handles, &returncode) {
1227 Ok(Some(_)) => break,
1228 Ok(None) => {}
1229 Err(_) => break,
1230 }
1231 match wait_for_terminal_input_event(
1232 &capture.state,
1233 &capture.condvar,
1234 Some(Duration::from_millis(50)),
1235 ) {
1236 TerminalInputWaitOutcome::Event(event) => {
1237 record_pty_input_metrics(
1238 &input_bytes_total,
1239 &newline_events_total,
1240 &submit_events_total,
1241 &event.data,
1242 event.submit,
1243 );
1244 if write_pty_input(&handles, &event.data).is_err() {
1245 break;
1246 }
1247 }
1248 TerminalInputWaitOutcome::Timeout => continue,
1249 TerminalInputWaitOutcome::Closed => break,
1250 }
1251 }
1252 stop.store(true, Ordering::Release);
1253 active.store(false, Ordering::Release);
1254 let _ = capture.stop_impl();
1255 }));
1256 Ok(())
1257 }
1258
1259 #[cfg(not(windows))]
1260 fn start_terminal_input_relay_impl(&self) -> PyResult<()> {
1261 Err(PyRuntimeError::new_err(
1262 "Native PTY terminal input relay is only available on Windows consoles",
1263 ))
1264 }
1265
1266 #[inline(never)]
1275 fn close_impl(&self) -> PyResult<()> {
1276 running_process_core::rp_rust_debug_scope!(
1277 "running_process_py::NativePtyProcess::close_impl"
1278 );
1279 self.stop_terminal_input_relay_impl();
1280 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
1281 let Some(handles) = guard.take() else {
1282 self.mark_reader_closed();
1283 return Ok(());
1284 };
1285 drop(guard);
1288
1289 #[cfg(windows)]
1290 let NativePtyHandles {
1291 master,
1292 writer,
1293 mut child,
1294 _job,
1295 } = handles;
1296 #[cfg(not(windows))]
1297 let NativePtyHandles {
1298 master,
1299 writer,
1300 mut child,
1301 } = handles;
1302
1303 if let Err(err) = child.kill() {
1308 if !is_ignorable_process_control_error(&err) {
1309 return Err(to_py_err(err));
1310 }
1311 }
1312
1313 drop(writer);
1317 drop(master);
1318
1319 let code = match child.wait() {
1325 Ok(status) => portable_exit_code(status),
1326 Err(_) => -9,
1327 };
1328 drop(child);
1329 #[cfg(windows)]
1330 drop(_job);
1331
1332 self.store_returncode(code);
1333 self.mark_reader_closed();
1334 Ok(())
1335 }
1336
1337 #[inline(never)]
1346 fn close_nonblocking(&self) {
1347 running_process_core::rp_rust_debug_scope!(
1348 "running_process_py::NativePtyProcess::close_nonblocking"
1349 );
1350 #[cfg(windows)]
1351 self.request_terminal_input_relay_stop();
1352 let Ok(mut guard) = self.handles.lock() else {
1353 return;
1354 };
1355 let Some(handles) = guard.take() else {
1356 self.mark_reader_closed();
1357 return;
1358 };
1359 drop(guard);
1360
1361 #[cfg(windows)]
1362 let NativePtyHandles {
1363 master,
1364 writer,
1365 mut child,
1366 _job,
1367 } = handles;
1368 #[cfg(not(windows))]
1369 let NativePtyHandles {
1370 master,
1371 writer,
1372 mut child,
1373 } = handles;
1374
1375 if let Err(err) = child.kill() {
1376 if !is_ignorable_process_control_error(&err) {
1377 return;
1378 }
1379 }
1380 drop(writer);
1382 drop(master);
1383 drop(child);
1387 #[cfg(windows)]
1388 drop(_job);
1389 self.mark_reader_closed();
1390 }
1391
1392 fn start_impl(&self) -> PyResult<()> {
1393 running_process_core::rp_rust_debug_scope!("running_process_py::NativePtyProcess::start");
1394 let mut guard = self.handles.lock().expect("pty handles mutex poisoned");
1395 if guard.is_some() {
1396 return Err(PyRuntimeError::new_err(
1397 "Pseudo-terminal process already started",
1398 ));
1399 }
1400
1401 let pty_system = native_pty_system();
1402 let pair = pty_system
1403 .openpty(PtySize {
1404 rows: self.rows,
1405 cols: self.cols,
1406 pixel_width: 0,
1407 pixel_height: 0,
1408 })
1409 .map_err(to_py_err)?;
1410
1411 let mut cmd = command_builder_from_argv(&self.argv);
1412 if let Some(cwd) = &self.cwd {
1413 cmd.cwd(cwd);
1414 }
1415 if let Some(env) = &self.env {
1416 cmd.env_clear();
1417 for (key, value) in env {
1418 cmd.env(key, value);
1419 }
1420 }
1421
1422 let reader = pair.master.try_clone_reader().map_err(to_py_err)?;
1423 let writer = pair.master.take_writer().map_err(to_py_err)?;
1424 let child = pair.slave.spawn_command(cmd).map_err(to_py_err)?;
1425 #[cfg(windows)]
1426 let job = public_symbols::rp_py_assign_child_to_windows_kill_on_close_job_public(
1427 child.as_raw_handle(),
1428 )?;
1429 #[cfg(windows)]
1430 public_symbols::rp_apply_windows_pty_priority_public(child.as_raw_handle(), self.nice)?;
1431 let shared = Arc::clone(&self.reader);
1432 let echo = Arc::clone(&self.echo);
1433 let idle_detector = Arc::clone(&self.idle_detector);
1434 let output_bytes = Arc::clone(&self.output_bytes_total);
1435 let churn_bytes = Arc::clone(&self.control_churn_bytes_total);
1436 thread::spawn(move || {
1437 spawn_pty_reader(
1438 reader,
1439 shared,
1440 echo,
1441 idle_detector,
1442 output_bytes,
1443 churn_bytes,
1444 );
1445 });
1446
1447 *guard = Some(NativePtyHandles {
1448 master: pair.master,
1449 writer,
1450 child,
1451 #[cfg(windows)]
1452 _job: job,
1453 });
1454 Ok(())
1455 }
1456
1457 fn respond_to_queries_impl(&self, data: &[u8]) -> PyResult<()> {
1458 #[cfg(windows)]
1459 {
1460 public_symbols::rp_pty_windows_respond_to_queries_public(self, data)
1461 }
1462
1463 #[cfg(unix)]
1464 {
1465 pty_platform::respond_to_queries(self, data)
1466 }
1467 }
1468
1469 fn resize_impl(&self, rows: u16, cols: u16) -> PyResult<()> {
1470 running_process_core::rp_rust_debug_scope!("running_process_py::NativePtyProcess::resize");
1471 let guard = self.handles.lock().expect("pty handles mutex poisoned");
1472 if let Some(handles) = guard.as_ref() {
1473 handles
1474 .master
1475 .resize(PtySize {
1476 rows,
1477 cols,
1478 pixel_width: 0,
1479 pixel_height: 0,
1480 })
1481 .map_err(to_py_err)?;
1482 }
1483 Ok(())
1484 }
1485
1486 fn send_interrupt_impl(&self) -> PyResult<()> {
1487 running_process_core::rp_rust_debug_scope!(
1488 "running_process_py::NativePtyProcess::send_interrupt"
1489 );
1490 #[cfg(windows)]
1491 {
1492 public_symbols::rp_pty_windows_send_interrupt_public(self)
1493 }
1494
1495 #[cfg(unix)]
1496 {
1497 pty_platform::send_interrupt(self)
1498 }
1499 }
1500
1501 fn wait_impl(&self, timeout: Option<f64>) -> PyResult<i32> {
1502 running_process_core::rp_rust_debug_scope!("running_process_py::NativePtyProcess::wait");
1503 let start = Instant::now();
1504 loop {
1505 if let Some(code) = self.poll()? {
1506 return Ok(code);
1507 }
1508 if timeout.is_some_and(|limit| start.elapsed() >= Duration::from_secs_f64(limit)) {
1509 return Err(PyTimeoutError::new_err("Pseudo-terminal process timed out"));
1510 }
1511 thread::sleep(Duration::from_millis(10));
1512 }
1513 }
1514
1515 fn terminate_impl(&self) -> PyResult<()> {
1516 running_process_core::rp_rust_debug_scope!(
1517 "running_process_py::NativePtyProcess::terminate"
1518 );
1519 #[cfg(windows)]
1520 {
1521 public_symbols::rp_pty_windows_terminate_public(self)
1522 }
1523
1524 #[cfg(unix)]
1525 {
1526 pty_platform::terminate(self)
1527 }
1528 }
1529
1530 fn kill_impl(&self) -> PyResult<()> {
1531 running_process_core::rp_rust_debug_scope!("running_process_py::NativePtyProcess::kill");
1532 #[cfg(windows)]
1533 {
1534 public_symbols::rp_pty_windows_kill_public(self)
1535 }
1536
1537 #[cfg(unix)]
1538 {
1539 pty_platform::kill(self)
1540 }
1541 }
1542
1543 fn terminate_tree_impl(&self) -> PyResult<()> {
1544 running_process_core::rp_rust_debug_scope!(
1545 "running_process_py::NativePtyProcess::terminate_tree"
1546 );
1547 #[cfg(windows)]
1548 {
1549 public_symbols::rp_pty_windows_terminate_tree_public(self)
1550 }
1551
1552 #[cfg(unix)]
1553 {
1554 pty_platform::terminate_tree(self)
1555 }
1556 }
1557
1558 fn kill_tree_impl(&self) -> PyResult<()> {
1559 running_process_core::rp_rust_debug_scope!(
1560 "running_process_py::NativePtyProcess::kill_tree"
1561 );
1562 #[cfg(windows)]
1563 {
1564 public_symbols::rp_pty_windows_kill_tree_public(self)
1565 }
1566
1567 #[cfg(unix)]
1568 {
1569 pty_platform::kill_tree(self)
1570 }
1571 }
1572}
1573
1574#[cfg(windows)]
1575struct WindowsJobHandle(usize);
1576
1577#[cfg(windows)]
1578impl Drop for WindowsJobHandle {
1579 fn drop(&mut self) {
1580 unsafe {
1581 winapi::um::handleapi::CloseHandle(self.0 as winapi::shared::ntdef::HANDLE);
1582 }
1583 }
1584}
1585
1586fn parse_command(command: &Bound<'_, PyAny>, shell: bool) -> PyResult<CommandSpec> {
1587 if let Ok(command) = command.extract::<String>() {
1588 if !shell {
1589 return Err(PyValueError::new_err(
1590 "String commands require shell=True. Use shell=True or provide command as list[str].",
1591 ));
1592 }
1593 return Ok(CommandSpec::Shell(command));
1594 }
1595
1596 if let Ok(command) = command.downcast::<PyList>() {
1597 let argv = command.extract::<Vec<String>>()?;
1598 if argv.is_empty() {
1599 return Err(PyValueError::new_err("command cannot be empty"));
1600 }
1601 if shell {
1602 return Ok(CommandSpec::Shell(argv.join(" ")));
1603 }
1604 return Ok(CommandSpec::Argv(argv));
1605 }
1606
1607 Err(PyValueError::new_err(
1608 "command must be either a string or a list[str]",
1609 ))
1610}
1611
1612fn stream_kind(name: &str) -> PyResult<StreamKind> {
1613 match name {
1614 "stdout" => Ok(StreamKind::Stdout),
1615 "stderr" => Ok(StreamKind::Stderr),
1616 _ => Err(PyValueError::new_err("stream must be 'stdout' or 'stderr'")),
1617 }
1618}
1619
1620fn stdin_mode(name: &str) -> PyResult<StdinMode> {
1621 match name {
1622 "inherit" => Ok(StdinMode::Inherit),
1623 "piped" => Ok(StdinMode::Piped),
1624 "null" => Ok(StdinMode::Null),
1625 _ => Err(PyValueError::new_err(
1626 "stdin_mode must be 'inherit', 'piped', or 'null'",
1627 )),
1628 }
1629}
1630
1631fn stderr_mode(name: &str) -> PyResult<StderrMode> {
1632 match name {
1633 "stdout" => Ok(StderrMode::Stdout),
1634 "pipe" => Ok(StderrMode::Pipe),
1635 _ => Err(PyValueError::new_err(
1636 "stderr_mode must be 'stdout' or 'pipe'",
1637 )),
1638 }
1639}
1640
1641#[pyclass]
1642struct NativeRunningProcess {
1643 inner: NativeProcess,
1644 text: bool,
1645 encoding: Option<String>,
1646 errors: Option<String>,
1647 #[cfg(windows)]
1648 creationflags: Option<u32>,
1649 #[cfg(unix)]
1650 create_process_group: bool,
1651}
1652
1653enum NativeProcessBackend {
1654 Running(NativeRunningProcess),
1655 Pty(NativePtyProcess),
1656}
1657
1658#[pyclass(name = "NativeProcess")]
1659struct PyNativeProcess {
1660 backend: NativeProcessBackend,
1661}
1662
1663#[pyclass]
1664#[derive(Clone)]
1665struct NativeSignalBool {
1666 value: Arc<AtomicBool>,
1667 write_lock: Arc<Mutex<()>>,
1668}
1669
1670struct IdleMonitorState {
1671 last_reset_at: Instant,
1672 returncode: Option<i32>,
1673 interrupted: bool,
1674}
1675
1676struct IdleDetectorCore {
1679 timeout_seconds: f64,
1680 stability_window_seconds: f64,
1681 sample_interval_seconds: f64,
1682 reset_on_input: bool,
1683 reset_on_output: bool,
1684 count_control_churn_as_output: bool,
1685 enabled: Arc<AtomicBool>,
1686 state: Mutex<IdleMonitorState>,
1687 condvar: Condvar,
1688}
1689
1690impl IdleDetectorCore {
1691 fn record_input(&self, byte_count: usize) {
1692 if !self.reset_on_input || byte_count == 0 {
1693 return;
1694 }
1695 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
1696 guard.last_reset_at = Instant::now();
1697 self.condvar.notify_all();
1698 }
1699
1700 fn record_output(&self, data: &[u8]) {
1701 if !self.reset_on_output || data.is_empty() {
1702 return;
1703 }
1704 let control_bytes = control_churn_bytes(data);
1705 let visible_output_bytes = data.len().saturating_sub(control_bytes);
1706 let active_output =
1707 visible_output_bytes > 0 || (self.count_control_churn_as_output && control_bytes > 0);
1708 if !active_output {
1709 return;
1710 }
1711 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
1712 guard.last_reset_at = Instant::now();
1713 self.condvar.notify_all();
1714 }
1715
1716 fn mark_exit(&self, returncode: i32, interrupted: bool) {
1717 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
1718 guard.returncode = Some(returncode);
1719 guard.interrupted = interrupted;
1720 self.condvar.notify_all();
1721 }
1722
1723 fn enabled(&self) -> bool {
1724 self.enabled.load(Ordering::Acquire)
1725 }
1726
1727 fn set_enabled(&self, enabled: bool) {
1728 let was_enabled = self.enabled.swap(enabled, Ordering::AcqRel);
1729 if enabled && !was_enabled {
1730 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
1731 guard.last_reset_at = Instant::now();
1732 }
1733 self.condvar.notify_all();
1734 }
1735
1736 fn wait(&self, timeout: Option<f64>) -> (bool, String, f64, Option<i32>) {
1737 let started = Instant::now();
1738 let overall_timeout = timeout.map(Duration::from_secs_f64);
1739 let min_idle = self.timeout_seconds.max(self.stability_window_seconds);
1740 let sample_interval = Duration::from_secs_f64(self.sample_interval_seconds.max(0.001));
1741
1742 let mut guard = self.state.lock().expect("idle monitor mutex poisoned");
1743 loop {
1744 let now = Instant::now();
1745 let idle_for = now.duration_since(guard.last_reset_at).as_secs_f64();
1746
1747 if let Some(returncode) = guard.returncode {
1748 let reason = if guard.interrupted {
1749 "interrupt"
1750 } else {
1751 "process_exit"
1752 };
1753 return (false, reason.to_string(), idle_for, Some(returncode));
1754 }
1755
1756 let enabled = self.enabled.load(Ordering::Acquire);
1757 if enabled && idle_for >= min_idle {
1758 return (true, "idle_timeout".to_string(), idle_for, None);
1759 }
1760
1761 if let Some(limit) = overall_timeout {
1762 if now.duration_since(started) >= limit {
1763 return (false, "timeout".to_string(), idle_for, None);
1764 }
1765 }
1766
1767 let idle_remaining = if enabled {
1768 (min_idle - idle_for).max(0.0)
1769 } else {
1770 sample_interval.as_secs_f64()
1771 };
1772 let mut wait_for =
1773 sample_interval.min(Duration::from_secs_f64(idle_remaining.max(0.001)));
1774 if let Some(limit) = overall_timeout {
1775 let elapsed = now.duration_since(started);
1776 if elapsed < limit {
1777 let remaining = limit - elapsed;
1778 wait_for = wait_for.min(remaining);
1779 }
1780 }
1781 let result = self
1782 .condvar
1783 .wait_timeout(guard, wait_for)
1784 .expect("idle monitor mutex poisoned");
1785 guard = result.0;
1786 }
1787 }
1788}
1789
1790#[pyclass]
1791struct NativeIdleDetector {
1792 core: Arc<IdleDetectorCore>,
1793}
1794
1795struct PtyBufferState {
1796 chunks: VecDeque<Vec<u8>>,
1797 history: Vec<u8>,
1798 history_bytes: usize,
1799 closed: bool,
1800}
1801
1802#[pyclass]
1803struct NativePtyBuffer {
1804 text: bool,
1805 encoding: String,
1806 errors: String,
1807 state: Mutex<PtyBufferState>,
1808 condvar: Condvar,
1809}
1810
1811#[derive(Clone)]
1812struct TerminalInputEventRecord {
1813 data: Vec<u8>,
1814 submit: bool,
1815 shift: bool,
1816 ctrl: bool,
1817 alt: bool,
1818 virtual_key_code: u16,
1819 repeat_count: u16,
1820}
1821
1822struct TerminalInputState {
1823 events: VecDeque<TerminalInputEventRecord>,
1824 closed: bool,
1825}
1826
1827#[cfg(windows)]
1828struct ActiveTerminalInputCapture {
1829 input_handle: usize,
1830 original_mode: u32,
1831 active_mode: u32,
1832}
1833
1834#[cfg(windows)]
1835enum TerminalInputWaitOutcome {
1836 Event(TerminalInputEventRecord),
1837 Closed,
1838 Timeout,
1839}
1840
1841#[cfg(windows)]
1842fn wait_for_terminal_input_event(
1843 state: &Arc<Mutex<TerminalInputState>>,
1844 condvar: &Arc<Condvar>,
1845 timeout: Option<Duration>,
1846) -> TerminalInputWaitOutcome {
1847 let deadline = timeout.map(|limit| Instant::now() + limit);
1848 let mut guard = state.lock().expect("terminal input mutex poisoned");
1849 loop {
1850 if let Some(event) = guard.events.pop_front() {
1851 return TerminalInputWaitOutcome::Event(event);
1852 }
1853 if guard.closed {
1854 return TerminalInputWaitOutcome::Closed;
1855 }
1856 match deadline {
1857 Some(deadline) => {
1858 let now = Instant::now();
1859 if now >= deadline {
1860 return TerminalInputWaitOutcome::Timeout;
1861 }
1862 let wait = deadline.saturating_duration_since(now);
1863 let result = condvar
1864 .wait_timeout(guard, wait)
1865 .expect("terminal input mutex poisoned");
1866 guard = result.0;
1867 }
1868 None => {
1869 guard = condvar.wait(guard).expect("terminal input mutex poisoned");
1870 }
1871 }
1872 }
1873}
1874
1875fn input_contains_newline(data: &[u8]) -> bool {
1876 data.iter().any(|byte| matches!(*byte, b'\r' | b'\n'))
1877}
1878
1879fn record_pty_input_metrics(
1880 input_bytes_total: &Arc<AtomicUsize>,
1881 newline_events_total: &Arc<AtomicUsize>,
1882 submit_events_total: &Arc<AtomicUsize>,
1883 data: &[u8],
1884 submit: bool,
1885) {
1886 input_bytes_total.fetch_add(data.len(), Ordering::AcqRel);
1887 if input_contains_newline(data) {
1888 newline_events_total.fetch_add(1, Ordering::AcqRel);
1889 }
1890 if submit {
1891 submit_events_total.fetch_add(1, Ordering::AcqRel);
1892 }
1893}
1894
1895fn store_pty_returncode(returncode: &Arc<Mutex<Option<i32>>>, code: i32) {
1896 *returncode.lock().expect("pty returncode mutex poisoned") = Some(code);
1897}
1898
1899fn poll_pty_process(
1900 handles: &Arc<Mutex<Option<NativePtyHandles>>>,
1901 returncode: &Arc<Mutex<Option<i32>>>,
1902) -> Result<Option<i32>, std::io::Error> {
1903 let mut guard = handles.lock().expect("pty handles mutex poisoned");
1904 let Some(handles) = guard.as_mut() else {
1905 return Ok(*returncode.lock().expect("pty returncode mutex poisoned"));
1906 };
1907 let status = handles.child.try_wait()?;
1908 let code = status.map(portable_exit_code);
1909 if let Some(code) = code {
1910 store_pty_returncode(returncode, code);
1911 return Ok(Some(code));
1912 }
1913 Ok(None)
1914}
1915
1916fn write_pty_input(
1917 handles: &Arc<Mutex<Option<NativePtyHandles>>>,
1918 data: &[u8],
1919) -> Result<(), std::io::Error> {
1920 let mut guard = handles.lock().expect("pty handles mutex poisoned");
1921 let handles = guard.as_mut().ok_or_else(|| {
1922 std::io::Error::new(
1923 std::io::ErrorKind::NotConnected,
1924 "Pseudo-terminal process is not running",
1925 )
1926 })?;
1927 #[cfg(windows)]
1928 let payload = public_symbols::rp_pty_windows_input_payload_public(data);
1929 #[cfg(unix)]
1930 let payload = pty_platform::input_payload(data);
1931 handles.writer.write_all(&payload)?;
1932 handles.writer.flush()
1933}
1934
1935#[pyclass]
1936#[derive(Clone)]
1937struct NativeTerminalInputEvent {
1938 data: Vec<u8>,
1939 submit: bool,
1940 shift: bool,
1941 ctrl: bool,
1942 alt: bool,
1943 virtual_key_code: u16,
1944 repeat_count: u16,
1945}
1946
1947#[pyclass]
1948struct NativeTerminalInput {
1949 state: Arc<Mutex<TerminalInputState>>,
1950 condvar: Arc<Condvar>,
1951 stop: Arc<AtomicBool>,
1952 capturing: Arc<AtomicBool>,
1953 worker: Mutex<Option<thread::JoinHandle<()>>>,
1954 #[cfg(windows)]
1955 console: Mutex<Option<ActiveTerminalInputCapture>>,
1956}
1957
1958#[pymethods]
1959impl NativeRunningProcess {
1960 #[new]
1961 #[allow(clippy::too_many_arguments)]
1962 #[pyo3(signature = (command, cwd=None, shell=false, capture=true, env=None, creationflags=None, text=true, encoding=None, errors=None, stdin_mode_name="inherit", stderr_mode_name="stdout", nice=None, create_process_group=false))]
1963 fn new(
1964 command: &Bound<'_, PyAny>,
1965 cwd: Option<String>,
1966 shell: bool,
1967 capture: bool,
1968 env: Option<Bound<'_, PyDict>>,
1969 creationflags: Option<u32>,
1970 text: bool,
1971 encoding: Option<String>,
1972 errors: Option<String>,
1973 stdin_mode_name: &str,
1974 stderr_mode_name: &str,
1975 nice: Option<i32>,
1976 create_process_group: bool,
1977 ) -> PyResult<Self> {
1978 let parsed = parse_command(command, shell)?;
1979 let env_pairs = env
1980 .map(|mapping| {
1981 mapping
1982 .iter()
1983 .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
1984 .collect::<PyResult<Vec<(String, String)>>>()
1985 })
1986 .transpose()?;
1987
1988 Ok(Self {
1989 inner: NativeProcess::new(ProcessConfig {
1990 command: parsed,
1991 cwd: cwd.map(PathBuf::from),
1992 env: env_pairs,
1993 capture,
1994 stderr_mode: stderr_mode(stderr_mode_name)?,
1995 creationflags,
1996 create_process_group,
1997 stdin_mode: stdin_mode(stdin_mode_name)?,
1998 nice,
1999 containment: None,
2000 }),
2001 text,
2002 encoding,
2003 errors,
2004 #[cfg(windows)]
2005 creationflags,
2006 #[cfg(unix)]
2007 create_process_group,
2008 })
2009 }
2010
2011 #[inline(never)]
2012 fn start(&self) -> PyResult<()> {
2013 public_symbols::rp_native_running_process_start_public(self)
2014 }
2015
2016 fn poll(&self) -> PyResult<Option<i32>> {
2017 self.inner.poll().map_err(to_py_err)
2018 }
2019
2020 #[pyo3(signature = (timeout=None))]
2021 #[inline(never)]
2022 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
2023 public_symbols::rp_native_running_process_wait_public(self, py, timeout)
2024 }
2025
2026 #[inline(never)]
2027 fn kill(&self) -> PyResult<()> {
2028 public_symbols::rp_native_running_process_kill_public(self)
2029 }
2030
2031 #[inline(never)]
2032 fn terminate(&self) -> PyResult<()> {
2033 public_symbols::rp_native_running_process_terminate_public(self)
2034 }
2035
2036 #[inline(never)]
2037 fn close(&self, py: Python<'_>) -> PyResult<()> {
2038 public_symbols::rp_native_running_process_close_public(self, py)
2039 }
2040
2041 fn terminate_group(&self) -> PyResult<()> {
2042 #[cfg(unix)]
2043 {
2044 let pid = self
2045 .inner
2046 .pid()
2047 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
2048 if self.create_process_group {
2049 unix_signal_process_group(pid as i32, UnixSignal::Terminate).map_err(to_py_err)?;
2050 return Ok(());
2051 }
2052 }
2053 self.inner.terminate().map_err(to_py_err)
2054 }
2055
2056 fn write_stdin(&self, data: &[u8]) -> PyResult<()> {
2057 self.inner.write_stdin(data).map_err(to_py_err)
2058 }
2059
2060 #[getter]
2061 fn pid(&self) -> Option<u32> {
2062 self.inner.pid()
2063 }
2064
2065 #[getter]
2066 fn returncode(&self) -> Option<i32> {
2067 self.inner.returncode()
2068 }
2069
2070 #[inline(never)]
2071 fn send_interrupt(&self) -> PyResult<()> {
2072 public_symbols::rp_native_running_process_send_interrupt_public(self)
2073 }
2074
2075 fn kill_group(&self) -> PyResult<()> {
2076 #[cfg(unix)]
2077 {
2078 let pid = self
2079 .inner
2080 .pid()
2081 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
2082 if self.create_process_group {
2083 unix_signal_process_group(pid as i32, UnixSignal::Kill).map_err(to_py_err)?;
2084 return Ok(());
2085 }
2086 }
2087 self.inner.kill().map_err(to_py_err)
2088 }
2089
2090 fn has_pending_combined(&self) -> bool {
2091 self.inner.has_pending_combined()
2092 }
2093
2094 fn has_pending_stream(&self, stream: &str) -> PyResult<bool> {
2095 Ok(self.inner.has_pending_stream(stream_kind(stream)?))
2096 }
2097
2098 fn drain_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
2099 self.inner
2100 .drain_combined()
2101 .into_iter()
2102 .map(|event| {
2103 Ok((
2104 event.stream.as_str().to_string(),
2105 self.decode_line(py, &event.line)?,
2106 ))
2107 })
2108 .collect()
2109 }
2110
2111 fn drain_stream(&self, py: Python<'_>, stream: &str) -> PyResult<Vec<Py<PyAny>>> {
2112 self.inner
2113 .drain_stream(stream_kind(stream)?)
2114 .into_iter()
2115 .map(|line| self.decode_line(py, &line))
2116 .collect()
2117 }
2118
2119 #[pyo3(signature = (timeout=None))]
2120 fn take_combined_line(
2121 &self,
2122 py: Python<'_>,
2123 timeout: Option<f64>,
2124 ) -> PyResult<(String, Option<String>, Option<Py<PyAny>>)> {
2125 match self
2126 .inner
2127 .read_combined(timeout.map(Duration::from_secs_f64))
2128 {
2129 ReadStatus::Line(StreamEvent { stream, line }) => Ok((
2130 "line".into(),
2131 Some(stream.as_str().into()),
2132 Some(self.decode_line(py, &line)?),
2133 )),
2134 ReadStatus::Timeout => Ok(("timeout".into(), None, None)),
2135 ReadStatus::Eof => Ok(("eof".into(), None, None)),
2136 }
2137 }
2138
2139 #[pyo3(signature = (stream, timeout=None))]
2140 fn take_stream_line(
2141 &self,
2142 py: Python<'_>,
2143 stream: &str,
2144 timeout: Option<f64>,
2145 ) -> PyResult<(String, Option<Py<PyAny>>)> {
2146 match self
2147 .inner
2148 .read_stream(stream_kind(stream)?, timeout.map(Duration::from_secs_f64))
2149 {
2150 ReadStatus::Line(line) => Ok(("line".into(), Some(self.decode_line(py, &line)?))),
2151 ReadStatus::Timeout => Ok(("timeout".into(), None)),
2152 ReadStatus::Eof => Ok(("eof".into(), None)),
2153 }
2154 }
2155
2156 fn captured_stdout(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
2157 self.inner
2158 .captured_stdout()
2159 .into_iter()
2160 .map(|line| self.decode_line(py, &line))
2161 .collect()
2162 }
2163
2164 fn captured_stderr(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
2165 self.inner
2166 .captured_stderr()
2167 .into_iter()
2168 .map(|line| self.decode_line(py, &line))
2169 .collect()
2170 }
2171
2172 fn captured_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
2173 self.inner
2174 .captured_combined()
2175 .into_iter()
2176 .map(|event| {
2177 Ok((
2178 event.stream.as_str().to_string(),
2179 self.decode_line(py, &event.line)?,
2180 ))
2181 })
2182 .collect()
2183 }
2184
2185 fn captured_stream_bytes(&self, stream: &str) -> PyResult<usize> {
2186 Ok(self.inner.captured_stream_bytes(stream_kind(stream)?))
2187 }
2188
2189 fn captured_combined_bytes(&self) -> usize {
2190 self.inner.captured_combined_bytes()
2191 }
2192
2193 fn clear_captured_stream(&self, stream: &str) -> PyResult<usize> {
2194 Ok(self.inner.clear_captured_stream(stream_kind(stream)?))
2195 }
2196
2197 fn clear_captured_combined(&self) -> usize {
2198 self.inner.clear_captured_combined()
2199 }
2200
2201 #[pyo3(signature = (stream, pattern, is_regex=false, timeout=None))]
2202 fn expect(
2203 &self,
2204 py: Python<'_>,
2205 stream: &str,
2206 pattern: &str,
2207 is_regex: bool,
2208 timeout: Option<f64>,
2209 ) -> PyResult<ExpectResult> {
2210 let stream_kind = if stream == "combined" {
2211 None
2212 } else {
2213 Some(stream_kind(stream)?)
2214 };
2215 let mut buffer = match stream_kind {
2216 Some(kind) => self.captured_stream_text(py, kind)?,
2217 None => self.captured_combined_text(py)?,
2218 };
2219 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
2220
2221 loop {
2222 if let Some((matched, start, end, groups)) =
2223 self.find_expect_match(&buffer, pattern, is_regex)?
2224 {
2225 return Ok((
2226 "match".to_string(),
2227 buffer,
2228 Some(matched),
2229 Some(start),
2230 Some(end),
2231 groups,
2232 ));
2233 }
2234
2235 let wait_timeout = deadline.map(|limit| {
2236 let now = Instant::now();
2237 if now >= limit {
2238 Duration::from_secs(0)
2239 } else {
2240 limit
2241 .saturating_duration_since(now)
2242 .min(Duration::from_millis(100))
2243 }
2244 });
2245 if deadline.is_some_and(|limit| Instant::now() >= limit) {
2246 return Ok(("timeout".to_string(), buffer, None, None, None, Vec::new()));
2247 }
2248
2249 match self.read_status_text(stream_kind, wait_timeout)? {
2250 ReadStatus::Line(line) => {
2251 let decoded = self.decode_line_to_string(py, &line)?;
2252 buffer.push_str(&decoded);
2253 buffer.push('\n');
2254 }
2255 ReadStatus::Timeout => {
2256 continue;
2258 }
2259 ReadStatus::Eof => {
2260 return Ok(("eof".to_string(), buffer, None, None, None, Vec::new()));
2261 }
2262 }
2263 }
2264 }
2265
2266 #[staticmethod]
2267 fn is_pty_available() -> bool {
2268 false
2269 }
2270}
2271
2272#[pymethods]
2273impl PyNativeProcess {
2274 #[new]
2275 #[allow(clippy::too_many_arguments)]
2276 #[pyo3(signature = (command, cwd=None, shell=false, capture=true, env=None, creationflags=None, text=true, encoding=None, errors=None, stdin_mode_name="inherit", stderr_mode_name="stdout", nice=None, create_process_group=false))]
2277 fn new(
2278 command: &Bound<'_, PyAny>,
2279 cwd: Option<String>,
2280 shell: bool,
2281 capture: bool,
2282 env: Option<Bound<'_, PyDict>>,
2283 creationflags: Option<u32>,
2284 text: bool,
2285 encoding: Option<String>,
2286 errors: Option<String>,
2287 stdin_mode_name: &str,
2288 stderr_mode_name: &str,
2289 nice: Option<i32>,
2290 create_process_group: bool,
2291 ) -> PyResult<Self> {
2292 Ok(Self {
2293 backend: NativeProcessBackend::Running(NativeRunningProcess::new(
2294 command,
2295 cwd,
2296 shell,
2297 capture,
2298 env,
2299 creationflags,
2300 text,
2301 encoding,
2302 errors,
2303 stdin_mode_name,
2304 stderr_mode_name,
2305 nice,
2306 create_process_group,
2307 )?),
2308 })
2309 }
2310
2311 #[staticmethod]
2312 #[pyo3(signature = (argv, cwd=None, env=None, rows=24, cols=80, nice=None))]
2313 fn for_pty(
2314 argv: Vec<String>,
2315 cwd: Option<String>,
2316 env: Option<Bound<'_, PyDict>>,
2317 rows: u16,
2318 cols: u16,
2319 nice: Option<i32>,
2320 ) -> PyResult<Self> {
2321 Ok(Self {
2322 backend: NativeProcessBackend::Pty(NativePtyProcess::new(
2323 argv, cwd, env, rows, cols, nice,
2324 )?),
2325 })
2326 }
2327
2328 fn start(&self) -> PyResult<()> {
2329 match &self.backend {
2330 NativeProcessBackend::Running(process) => process.start(),
2331 NativeProcessBackend::Pty(process) => process.start(),
2332 }
2333 }
2334
2335 fn poll(&self) -> PyResult<Option<i32>> {
2336 match &self.backend {
2337 NativeProcessBackend::Running(process) => process.poll(),
2338 NativeProcessBackend::Pty(process) => process.poll(),
2339 }
2340 }
2341
2342 #[pyo3(signature = (timeout=None))]
2343 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
2344 match &self.backend {
2345 NativeProcessBackend::Running(process) => process.wait(py, timeout),
2346 NativeProcessBackend::Pty(process) => py.allow_threads(|| process.wait(timeout)),
2347 }
2348 }
2349
2350 fn kill(&self) -> PyResult<()> {
2351 match &self.backend {
2352 NativeProcessBackend::Running(process) => process.kill(),
2353 NativeProcessBackend::Pty(process) => process.kill(),
2354 }
2355 }
2356
2357 fn terminate(&self) -> PyResult<()> {
2358 match &self.backend {
2359 NativeProcessBackend::Running(process) => process.terminate(),
2360 NativeProcessBackend::Pty(process) => process.terminate(),
2361 }
2362 }
2363
2364 fn terminate_group(&self) -> PyResult<()> {
2365 match &self.backend {
2366 NativeProcessBackend::Running(process) => process.terminate_group(),
2367 NativeProcessBackend::Pty(process) => process.terminate_tree(),
2368 }
2369 }
2370
2371 fn kill_group(&self) -> PyResult<()> {
2372 match &self.backend {
2373 NativeProcessBackend::Running(process) => process.kill_group(),
2374 NativeProcessBackend::Pty(process) => process.kill_tree(),
2375 }
2376 }
2377
2378 fn has_pending_combined(&self) -> PyResult<bool> {
2379 match &self.backend {
2380 NativeProcessBackend::Running(process) => Ok(process.has_pending_combined()),
2381 NativeProcessBackend::Pty(_) => Ok(false),
2382 }
2383 }
2384
2385 fn has_pending_stream(&self, stream: &str) -> PyResult<bool> {
2386 match &self.backend {
2387 NativeProcessBackend::Running(process) => process.has_pending_stream(stream),
2388 NativeProcessBackend::Pty(_) => Ok(false),
2389 }
2390 }
2391
2392 fn drain_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
2393 match &self.backend {
2394 NativeProcessBackend::Running(process) => process.drain_combined(py),
2395 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
2396 }
2397 }
2398
2399 fn drain_stream(&self, py: Python<'_>, stream: &str) -> PyResult<Vec<Py<PyAny>>> {
2400 match &self.backend {
2401 NativeProcessBackend::Running(process) => process.drain_stream(py, stream),
2402 NativeProcessBackend::Pty(_) => {
2403 let _ = stream;
2404 Ok(Vec::new())
2405 }
2406 }
2407 }
2408
2409 #[pyo3(signature = (timeout=None))]
2410 fn take_combined_line(
2411 &self,
2412 py: Python<'_>,
2413 timeout: Option<f64>,
2414 ) -> PyResult<(String, Option<String>, Option<Py<PyAny>>)> {
2415 match &self.backend {
2416 NativeProcessBackend::Running(process) => process.take_combined_line(py, timeout),
2417 NativeProcessBackend::Pty(_) => Ok(("eof".into(), None, None)),
2418 }
2419 }
2420
2421 #[pyo3(signature = (stream, timeout=None))]
2422 fn take_stream_line(
2423 &self,
2424 py: Python<'_>,
2425 stream: &str,
2426 timeout: Option<f64>,
2427 ) -> PyResult<(String, Option<Py<PyAny>>)> {
2428 match &self.backend {
2429 NativeProcessBackend::Running(process) => process.take_stream_line(py, stream, timeout),
2430 NativeProcessBackend::Pty(_) => {
2431 let _ = (py, stream, timeout);
2432 Ok(("eof".into(), None))
2433 }
2434 }
2435 }
2436
2437 fn captured_stdout(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
2438 match &self.backend {
2439 NativeProcessBackend::Running(process) => process.captured_stdout(py),
2440 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
2441 }
2442 }
2443
2444 fn captured_stderr(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
2445 match &self.backend {
2446 NativeProcessBackend::Running(process) => process.captured_stderr(py),
2447 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
2448 }
2449 }
2450
2451 fn captured_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
2452 match &self.backend {
2453 NativeProcessBackend::Running(process) => process.captured_combined(py),
2454 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
2455 }
2456 }
2457
2458 fn captured_stream_bytes(&self, stream: &str) -> PyResult<usize> {
2459 match &self.backend {
2460 NativeProcessBackend::Running(process) => process.captured_stream_bytes(stream),
2461 NativeProcessBackend::Pty(_) => Ok(0),
2462 }
2463 }
2464
2465 fn captured_combined_bytes(&self) -> PyResult<usize> {
2466 match &self.backend {
2467 NativeProcessBackend::Running(process) => Ok(process.captured_combined_bytes()),
2468 NativeProcessBackend::Pty(_) => Ok(0),
2469 }
2470 }
2471
2472 fn clear_captured_stream(&self, stream: &str) -> PyResult<usize> {
2473 match &self.backend {
2474 NativeProcessBackend::Running(process) => process.clear_captured_stream(stream),
2475 NativeProcessBackend::Pty(_) => Ok(0),
2476 }
2477 }
2478
2479 fn clear_captured_combined(&self) -> PyResult<usize> {
2480 match &self.backend {
2481 NativeProcessBackend::Running(process) => Ok(process.clear_captured_combined()),
2482 NativeProcessBackend::Pty(_) => Ok(0),
2483 }
2484 }
2485
2486 fn write_stdin(&self, data: &[u8]) -> PyResult<()> {
2487 match &self.backend {
2488 NativeProcessBackend::Running(process) => process.write_stdin(data),
2489 NativeProcessBackend::Pty(process) => process.write(data, false),
2490 }
2491 }
2492
2493 #[pyo3(signature = (data, submit=false))]
2494 fn write(&self, data: &[u8], submit: bool) -> PyResult<()> {
2495 match &self.backend {
2496 NativeProcessBackend::Running(process) => process.write_stdin(data),
2497 NativeProcessBackend::Pty(process) => process.write(data, submit),
2498 }
2499 }
2500
2501 #[pyo3(signature = (timeout=None))]
2502 fn read_chunk(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
2503 match &self.backend {
2504 NativeProcessBackend::Pty(process) => process.read_chunk(py, timeout),
2505 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2506 "read_chunk is only available for PTY-backed NativeProcess",
2507 )),
2508 }
2509 }
2510
2511 #[pyo3(signature = (timeout=None))]
2512 fn wait_for_pty_reader_closed(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<bool> {
2513 match &self.backend {
2514 NativeProcessBackend::Pty(process) => process.wait_for_reader_closed(py, timeout),
2515 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2516 "wait_for_pty_reader_closed is only available for PTY-backed NativeProcess",
2517 )),
2518 }
2519 }
2520
2521 fn respond_to_queries(&self, data: &[u8]) -> PyResult<()> {
2522 match &self.backend {
2523 NativeProcessBackend::Pty(process) => process.respond_to_queries(data),
2524 NativeProcessBackend::Running(_) => Ok(()),
2525 }
2526 }
2527
2528 fn resize(&self, rows: u16, cols: u16) -> PyResult<()> {
2529 match &self.backend {
2530 NativeProcessBackend::Pty(process) => process.resize(rows, cols),
2531 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2532 "resize is only available for PTY-backed NativeProcess",
2533 )),
2534 }
2535 }
2536
2537 fn send_interrupt(&self) -> PyResult<()> {
2538 match &self.backend {
2539 NativeProcessBackend::Running(process) => process.send_interrupt(),
2540 NativeProcessBackend::Pty(process) => process.send_interrupt(),
2541 }
2542 }
2543
2544 #[pyo3(signature = (stream, pattern, is_regex=false, timeout=None))]
2545 fn expect(
2546 &self,
2547 py: Python<'_>,
2548 stream: &str,
2549 pattern: &str,
2550 is_regex: bool,
2551 timeout: Option<f64>,
2552 ) -> PyResult<ExpectResult> {
2553 match &self.backend {
2554 NativeProcessBackend::Running(process) => {
2555 process.expect(py, stream, pattern, is_regex, timeout)
2556 }
2557 NativeProcessBackend::Pty(_) => Err(PyRuntimeError::new_err(
2558 "expect is only available for subprocess-backed NativeProcess",
2559 )),
2560 }
2561 }
2562
2563 fn close(&self, py: Python<'_>) -> PyResult<()> {
2564 match &self.backend {
2565 NativeProcessBackend::Running(process) => process.close(py),
2566 NativeProcessBackend::Pty(process) => process.close(py),
2567 }
2568 }
2569
2570 fn start_terminal_input_relay(&self) -> PyResult<()> {
2571 match &self.backend {
2572 NativeProcessBackend::Pty(process) => process.start_terminal_input_relay(),
2573 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2574 "terminal input relay is only available for PTY-backed NativeProcess",
2575 )),
2576 }
2577 }
2578
2579 fn stop_terminal_input_relay(&self) -> PyResult<()> {
2580 match &self.backend {
2581 NativeProcessBackend::Pty(process) => {
2582 process.stop_terminal_input_relay();
2583 Ok(())
2584 }
2585 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2586 "terminal input relay is only available for PTY-backed NativeProcess",
2587 )),
2588 }
2589 }
2590
2591 fn terminal_input_relay_active(&self) -> PyResult<bool> {
2592 match &self.backend {
2593 NativeProcessBackend::Pty(process) => Ok(process.terminal_input_relay_active()),
2594 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2595 "terminal input relay is only available for PTY-backed NativeProcess",
2596 )),
2597 }
2598 }
2599
2600 fn pty_input_bytes_total(&self) -> PyResult<usize> {
2601 match &self.backend {
2602 NativeProcessBackend::Pty(process) => Ok(process.pty_input_bytes_total()),
2603 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2604 "PTY input metrics are only available for PTY-backed NativeProcess",
2605 )),
2606 }
2607 }
2608
2609 fn pty_newline_events_total(&self) -> PyResult<usize> {
2610 match &self.backend {
2611 NativeProcessBackend::Pty(process) => Ok(process.pty_newline_events_total()),
2612 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2613 "PTY input metrics are only available for PTY-backed NativeProcess",
2614 )),
2615 }
2616 }
2617
2618 fn pty_submit_events_total(&self) -> PyResult<usize> {
2619 match &self.backend {
2620 NativeProcessBackend::Pty(process) => Ok(process.pty_submit_events_total()),
2621 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2622 "PTY input metrics are only available for PTY-backed NativeProcess",
2623 )),
2624 }
2625 }
2626
2627 #[getter]
2628 fn pid(&self) -> PyResult<Option<u32>> {
2629 match &self.backend {
2630 NativeProcessBackend::Running(process) => Ok(process.pid()),
2631 NativeProcessBackend::Pty(process) => process.pid(),
2632 }
2633 }
2634
2635 #[getter]
2636 fn returncode(&self) -> PyResult<Option<i32>> {
2637 match &self.backend {
2638 NativeProcessBackend::Running(process) => Ok(process.returncode()),
2639 NativeProcessBackend::Pty(process) => Ok(*process
2640 .returncode
2641 .lock()
2642 .expect("pty returncode mutex poisoned")),
2643 }
2644 }
2645
2646 fn is_pty(&self) -> bool {
2647 matches!(self.backend, NativeProcessBackend::Pty(_))
2648 }
2649
2650 #[pyo3(signature = (timeout=None, drain_timeout=2.0))]
2652 fn wait_and_drain(
2653 &self,
2654 py: Python<'_>,
2655 timeout: Option<f64>,
2656 drain_timeout: f64,
2657 ) -> PyResult<i32> {
2658 match &self.backend {
2659 NativeProcessBackend::Pty(process) => {
2660 process.wait_and_drain(py, timeout, drain_timeout)
2661 }
2662 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
2663 "wait_and_drain is only available for PTY-backed NativeProcess",
2664 )),
2665 }
2666 }
2667}
2668
2669#[pymethods]
2670impl NativePtyProcess {
2671 #[new]
2672 #[pyo3(signature = (argv, cwd=None, env=None, rows=24, cols=80, nice=None))]
2673 fn new(
2674 argv: Vec<String>,
2675 cwd: Option<String>,
2676 env: Option<Bound<'_, PyDict>>,
2677 rows: u16,
2678 cols: u16,
2679 nice: Option<i32>,
2680 ) -> PyResult<Self> {
2681 if argv.is_empty() {
2682 return Err(PyValueError::new_err("command cannot be empty"));
2683 }
2684 #[cfg(not(windows))]
2685 let _ = nice;
2686 let env_pairs = env
2687 .map(|mapping| {
2688 mapping
2689 .iter()
2690 .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
2691 .collect::<PyResult<Vec<(String, String)>>>()
2692 })
2693 .transpose()?;
2694 Ok(Self {
2695 argv,
2696 cwd,
2697 env: env_pairs,
2698 rows,
2699 cols,
2700 #[cfg(windows)]
2701 nice,
2702 handles: Arc::new(Mutex::new(None)),
2703 reader: Arc::new(PtyReadShared {
2704 state: Mutex::new(PtyReadState {
2705 chunks: VecDeque::new(),
2706 closed: false,
2707 }),
2708 condvar: Condvar::new(),
2709 }),
2710 returncode: Arc::new(Mutex::new(None)),
2711 input_bytes_total: Arc::new(AtomicUsize::new(0)),
2712 newline_events_total: Arc::new(AtomicUsize::new(0)),
2713 submit_events_total: Arc::new(AtomicUsize::new(0)),
2714 echo: Arc::new(AtomicBool::new(false)),
2715 idle_detector: Arc::new(Mutex::new(None)),
2716 output_bytes_total: Arc::new(AtomicUsize::new(0)),
2717 control_churn_bytes_total: Arc::new(AtomicUsize::new(0)),
2718 #[cfg(windows)]
2719 terminal_input_relay_stop: Arc::new(AtomicBool::new(false)),
2720 #[cfg(windows)]
2721 terminal_input_relay_active: Arc::new(AtomicBool::new(false)),
2722 #[cfg(windows)]
2723 terminal_input_relay_worker: Mutex::new(None),
2724 })
2725 }
2726
2727 #[inline(never)]
2728 fn start(&self) -> PyResult<()> {
2729 public_symbols::rp_native_pty_process_start_public(self)
2730 }
2731
2732 #[pyo3(signature = (timeout=None))]
2733 fn read_chunk(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
2734 enum WaitOutcome {
2742 Chunk(Vec<u8>),
2743 Closed,
2744 Timeout,
2745 }
2746
2747 let outcome = py.allow_threads(|| -> WaitOutcome {
2748 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
2749 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
2750 loop {
2751 if let Some(chunk) = guard.chunks.pop_front() {
2752 return WaitOutcome::Chunk(chunk);
2753 }
2754 if guard.closed {
2755 return WaitOutcome::Closed;
2756 }
2757 match deadline {
2758 Some(deadline) => {
2759 let now = Instant::now();
2760 if now >= deadline {
2761 return WaitOutcome::Timeout;
2762 }
2763 let wait = deadline.saturating_duration_since(now);
2764 let result = self
2765 .reader
2766 .condvar
2767 .wait_timeout(guard, wait)
2768 .expect("pty read mutex poisoned");
2769 guard = result.0;
2770 }
2771 None => {
2772 guard = self
2773 .reader
2774 .condvar
2775 .wait(guard)
2776 .expect("pty read mutex poisoned");
2777 }
2778 }
2779 }
2780 });
2781
2782 match outcome {
2783 WaitOutcome::Chunk(chunk) => Ok(PyBytes::new(py, &chunk).into_any().unbind()),
2784 WaitOutcome::Closed => Err(PyRuntimeError::new_err("Pseudo-terminal stream is closed")),
2785 WaitOutcome::Timeout => Err(PyTimeoutError::new_err(
2786 "No pseudo-terminal output available before timeout",
2787 )),
2788 }
2789 }
2790
2791 #[pyo3(signature = (timeout=None))]
2792 fn wait_for_reader_closed(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<bool> {
2793 let closed = py.allow_threads(|| {
2794 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
2795 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
2796 loop {
2797 if guard.closed {
2798 return true;
2799 }
2800 match deadline {
2801 Some(deadline) => {
2802 let now = Instant::now();
2803 if now >= deadline {
2804 return false;
2805 }
2806 let wait = deadline.saturating_duration_since(now);
2807 let result = self
2808 .reader
2809 .condvar
2810 .wait_timeout(guard, wait)
2811 .expect("pty read mutex poisoned");
2812 guard = result.0;
2813 }
2814 None => {
2815 guard = self
2816 .reader
2817 .condvar
2818 .wait(guard)
2819 .expect("pty read mutex poisoned");
2820 }
2821 }
2822 }
2823 });
2824 Ok(closed)
2825 }
2826
2827 #[pyo3(signature = (data, submit=false))]
2828 fn write(&self, data: &[u8], submit: bool) -> PyResult<()> {
2829 self.write_impl(data, submit)
2830 }
2831
2832 fn respond_to_queries(&self, data: &[u8]) -> PyResult<()> {
2833 public_symbols::rp_native_pty_process_respond_to_queries_public(self, data)
2834 }
2835
2836 #[inline(never)]
2837 fn resize(&self, rows: u16, cols: u16) -> PyResult<()> {
2838 public_symbols::rp_native_pty_process_resize_public(self, rows, cols)
2839 }
2840
2841 #[inline(never)]
2842 fn send_interrupt(&self) -> PyResult<()> {
2843 public_symbols::rp_native_pty_process_send_interrupt_public(self)
2844 }
2845
2846 fn poll(&self) -> PyResult<Option<i32>> {
2847 poll_pty_process(&self.handles, &self.returncode).map_err(to_py_err)
2848 }
2849
2850 #[pyo3(signature = (timeout=None))]
2851 #[inline(never)]
2852 fn wait(&self, timeout: Option<f64>) -> PyResult<i32> {
2853 public_symbols::rp_native_pty_process_wait_public(self, timeout)
2854 }
2855
2856 #[inline(never)]
2857 fn terminate(&self) -> PyResult<()> {
2858 public_symbols::rp_native_pty_process_terminate_public(self)
2859 }
2860
2861 #[inline(never)]
2862 fn kill(&self) -> PyResult<()> {
2863 public_symbols::rp_native_pty_process_kill_public(self)
2864 }
2865
2866 #[inline(never)]
2867 fn terminate_tree(&self) -> PyResult<()> {
2868 public_symbols::rp_native_pty_process_terminate_tree_public(self)
2869 }
2870
2871 #[inline(never)]
2872 fn kill_tree(&self) -> PyResult<()> {
2873 public_symbols::rp_native_pty_process_kill_tree_public(self)
2874 }
2875
2876 fn start_terminal_input_relay(&self) -> PyResult<()> {
2877 self.start_terminal_input_relay_impl()
2878 }
2879
2880 fn stop_terminal_input_relay(&self) {
2881 self.stop_terminal_input_relay_impl();
2882 }
2883
2884 fn terminal_input_relay_active(&self) -> bool {
2885 #[cfg(windows)]
2886 {
2887 self.terminal_input_relay_active.load(Ordering::Acquire)
2888 }
2889
2890 #[cfg(not(windows))]
2891 {
2892 false
2893 }
2894 }
2895
2896 fn pty_input_bytes_total(&self) -> usize {
2897 self.input_bytes_total.load(Ordering::Acquire)
2898 }
2899
2900 fn pty_newline_events_total(&self) -> usize {
2901 self.newline_events_total.load(Ordering::Acquire)
2902 }
2903
2904 fn pty_submit_events_total(&self) -> usize {
2905 self.submit_events_total.load(Ordering::Acquire)
2906 }
2907
2908 fn pty_output_bytes_total(&self) -> usize {
2910 self.output_bytes_total.load(Ordering::Acquire)
2911 }
2912
2913 fn pty_control_churn_bytes_total(&self) -> usize {
2915 self.control_churn_bytes_total.load(Ordering::Acquire)
2916 }
2917
2918 #[pyo3(signature = (timeout=None, drain_timeout=2.0))]
2921 fn wait_and_drain(
2922 &self,
2923 py: Python<'_>,
2924 timeout: Option<f64>,
2925 drain_timeout: f64,
2926 ) -> PyResult<i32> {
2927 py.allow_threads(|| {
2928 let code = self.wait_impl(timeout)?;
2930 let deadline = Instant::now() + Duration::from_secs_f64(drain_timeout.max(0.0));
2932 let mut guard = self.reader.state.lock().expect("pty read mutex poisoned");
2933 while !guard.closed {
2934 let remaining = deadline.saturating_duration_since(Instant::now());
2935 if remaining.is_zero() {
2936 break;
2937 }
2938 let result = self
2939 .reader
2940 .condvar
2941 .wait_timeout(guard, remaining)
2942 .expect("pty read mutex poisoned");
2943 guard = result.0;
2944 }
2945 Ok(code)
2946 })
2947 }
2948
2949 fn set_echo(&self, enabled: bool) {
2951 self.echo.store(enabled, Ordering::Release);
2952 }
2953
2954 fn echo_enabled(&self) -> bool {
2955 self.echo.load(Ordering::Acquire)
2956 }
2957
2958 fn attach_idle_detector(&self, detector: &NativeIdleDetector) {
2960 let mut guard = self
2961 .idle_detector
2962 .lock()
2963 .expect("idle detector mutex poisoned");
2964 *guard = Some(Arc::clone(&detector.core));
2965 }
2966
2967 fn detach_idle_detector(&self) {
2969 let mut guard = self
2970 .idle_detector
2971 .lock()
2972 .expect("idle detector mutex poisoned");
2973 *guard = None;
2974 }
2975
2976 #[pyo3(signature = (detector, timeout=None))]
2979 fn wait_for_idle(
2980 &self,
2981 py: Python<'_>,
2982 detector: &NativeIdleDetector,
2983 timeout: Option<f64>,
2984 ) -> PyResult<(bool, String, f64, Option<i32>)> {
2985 {
2987 let mut guard = self
2988 .idle_detector
2989 .lock()
2990 .expect("idle detector mutex poisoned");
2991 *guard = Some(Arc::clone(&detector.core));
2992 }
2993
2994 let handles = Arc::clone(&self.handles);
2996 let returncode = Arc::clone(&self.returncode);
2997 let core = Arc::clone(&detector.core);
2998 let exit_watcher = thread::spawn(move || loop {
2999 match poll_pty_process(&handles, &returncode) {
3000 Ok(Some(code)) => {
3001 let interrupted = code == -2 || code == 130;
3003 core.mark_exit(code, interrupted);
3004 return;
3005 }
3006 Ok(None) => {}
3007 Err(_) => return,
3008 }
3009 thread::sleep(Duration::from_millis(1));
3010 });
3011
3012 let result = py.allow_threads(|| detector.core.wait(timeout));
3014
3015 {
3017 let mut guard = self
3018 .idle_detector
3019 .lock()
3020 .expect("idle detector mutex poisoned");
3021 *guard = None;
3022 }
3023 let _ = exit_watcher.join();
3024 Ok(result)
3025 }
3026
3027 #[getter]
3028 fn pid(&self) -> PyResult<Option<u32>> {
3029 let guard = self.handles.lock().expect("pty handles mutex poisoned");
3030 if let Some(handles) = guard.as_ref() {
3031 #[cfg(unix)]
3032 if let Some(pid) = handles.master.process_group_leader() {
3033 if let Ok(pid) = u32::try_from(pid) {
3034 return Ok(Some(pid));
3035 }
3036 }
3037 return Ok(handles.child.process_id());
3038 }
3039 Ok(None)
3040 }
3041
3042 fn close(&self, py: Python<'_>) -> PyResult<()> {
3043 public_symbols::rp_native_pty_process_close_public(self, py)
3047 }
3048}
3049
3050impl Drop for NativePtyProcess {
3051 fn drop(&mut self) {
3052 public_symbols::rp_native_pty_process_close_nonblocking_public(self);
3057 }
3058}
3059
3060#[pymethods]
3061impl NativeSignalBool {
3062 #[new]
3063 #[pyo3(signature = (value=false))]
3064 fn new(value: bool) -> Self {
3065 Self {
3066 value: Arc::new(AtomicBool::new(value)),
3067 write_lock: Arc::new(Mutex::new(())),
3068 }
3069 }
3070
3071 #[getter]
3072 fn value(&self) -> bool {
3073 self.load_nolock()
3074 }
3075
3076 #[setter]
3077 fn set_value(&self, value: bool) {
3078 self.store_locked(value);
3079 }
3080
3081 fn load_nolock(&self) -> bool {
3082 self.value.load(Ordering::Acquire)
3083 }
3084
3085 fn store_locked(&self, value: bool) {
3086 let _guard = self.write_lock.lock().expect("signal bool mutex poisoned");
3087 self.value.store(value, Ordering::Release);
3088 }
3089
3090 fn compare_and_swap_locked(&self, current: bool, new: bool) -> bool {
3091 let _guard = self.write_lock.lock().expect("signal bool mutex poisoned");
3092 self.value
3093 .compare_exchange(current, new, Ordering::AcqRel, Ordering::Acquire)
3094 .is_ok()
3095 }
3096}
3097
3098#[pymethods]
3099impl NativePtyBuffer {
3100 #[new]
3101 #[pyo3(signature = (text=false, encoding="utf-8", errors="replace"))]
3102 fn new(text: bool, encoding: &str, errors: &str) -> Self {
3103 Self {
3104 text,
3105 encoding: encoding.to_string(),
3106 errors: errors.to_string(),
3107 state: Mutex::new(PtyBufferState {
3108 chunks: VecDeque::new(),
3109 history: Vec::new(),
3110 history_bytes: 0,
3111 closed: false,
3112 }),
3113 condvar: Condvar::new(),
3114 }
3115 }
3116
3117 fn available(&self) -> bool {
3118 !self
3119 .state
3120 .lock()
3121 .expect("pty buffer mutex poisoned")
3122 .chunks
3123 .is_empty()
3124 }
3125
3126 fn record_output(&self, data: &[u8]) {
3127 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3128 guard.history_bytes += data.len();
3129 guard.history.extend_from_slice(data);
3130 guard.chunks.push_back(data.to_vec());
3131 self.condvar.notify_all();
3132 }
3133
3134 fn close(&self) {
3135 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3136 guard.closed = true;
3137 self.condvar.notify_all();
3138 }
3139
3140 #[pyo3(signature = (timeout=None))]
3141 fn read(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
3142 enum WaitOutcome {
3146 Chunk(Vec<u8>),
3147 Closed,
3148 Timeout,
3149 }
3150
3151 let outcome = py.allow_threads(|| -> WaitOutcome {
3152 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
3153 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3154 loop {
3155 if let Some(chunk) = guard.chunks.pop_front() {
3156 return WaitOutcome::Chunk(chunk);
3157 }
3158 if guard.closed {
3159 return WaitOutcome::Closed;
3160 }
3161 match deadline {
3162 Some(deadline) => {
3163 let now = Instant::now();
3164 if now >= deadline {
3165 return WaitOutcome::Timeout;
3166 }
3167 let wait = deadline.saturating_duration_since(now);
3168 let result = self
3169 .condvar
3170 .wait_timeout(guard, wait)
3171 .expect("pty buffer mutex poisoned");
3172 guard = result.0;
3173 }
3174 None => {
3175 guard = self.condvar.wait(guard).expect("pty buffer mutex poisoned");
3176 }
3177 }
3178 }
3179 });
3180
3181 match outcome {
3182 WaitOutcome::Chunk(chunk) => self.decode_chunk(py, &chunk),
3183 WaitOutcome::Closed => Err(PyRuntimeError::new_err("Pseudo-terminal stream is closed")),
3184 WaitOutcome::Timeout => Err(PyTimeoutError::new_err(
3185 "No pseudo-terminal output available before timeout",
3186 )),
3187 }
3188 }
3189
3190 fn read_non_blocking(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
3191 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3192 if let Some(chunk) = guard.chunks.pop_front() {
3193 return self.decode_chunk(py, &chunk).map(Some);
3194 }
3195 if guard.closed {
3196 return Err(PyRuntimeError::new_err("Pseudo-terminal stream is closed"));
3197 }
3198 Ok(None)
3199 }
3200
3201 fn drain(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
3202 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3203 guard
3204 .chunks
3205 .drain(..)
3206 .map(|chunk| self.decode_chunk(py, &chunk))
3207 .collect()
3208 }
3209
3210 fn output(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
3211 let guard = self.state.lock().expect("pty buffer mutex poisoned");
3212 self.decode_chunk(py, &guard.history)
3213 }
3214
3215 fn output_since(&self, py: Python<'_>, start: usize) -> PyResult<Py<PyAny>> {
3216 let guard = self.state.lock().expect("pty buffer mutex poisoned");
3217 let start = start.min(guard.history.len());
3218 self.decode_chunk(py, &guard.history[start..])
3219 }
3220
3221 fn history_bytes(&self) -> usize {
3222 self.state
3223 .lock()
3224 .expect("pty buffer mutex poisoned")
3225 .history_bytes
3226 }
3227
3228 fn clear_history(&self) -> usize {
3229 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
3230 let released = guard.history_bytes;
3231 guard.history.clear();
3232 guard.history_bytes = 0;
3233 released
3234 }
3235}
3236
3237impl NativeTerminalInput {
3238 fn next_event(&self) -> Option<TerminalInputEventRecord> {
3239 self.state
3240 .lock()
3241 .expect("terminal input mutex poisoned")
3242 .events
3243 .pop_front()
3244 }
3245
3246 fn event_to_py(
3247 py: Python<'_>,
3248 event: TerminalInputEventRecord,
3249 ) -> PyResult<Py<NativeTerminalInputEvent>> {
3250 Py::new(
3251 py,
3252 NativeTerminalInputEvent {
3253 data: event.data,
3254 submit: event.submit,
3255 shift: event.shift,
3256 ctrl: event.ctrl,
3257 alt: event.alt,
3258 virtual_key_code: event.virtual_key_code,
3259 repeat_count: event.repeat_count,
3260 },
3261 )
3262 }
3263
3264 fn wait_for_event(
3265 &self,
3266 py: Python<'_>,
3267 timeout: Option<f64>,
3268 ) -> PyResult<TerminalInputEventRecord> {
3269 enum WaitOutcome {
3270 Event(TerminalInputEventRecord),
3271 Closed,
3272 Timeout,
3273 }
3274
3275 let state = Arc::clone(&self.state);
3276 let condvar = Arc::clone(&self.condvar);
3277 let outcome = py.allow_threads(move || -> WaitOutcome {
3278 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
3279 let mut guard = state.lock().expect("terminal input mutex poisoned");
3280 loop {
3281 if let Some(event) = guard.events.pop_front() {
3282 return WaitOutcome::Event(event);
3283 }
3284 if guard.closed {
3285 return WaitOutcome::Closed;
3286 }
3287 match deadline {
3288 Some(deadline) => {
3289 let now = Instant::now();
3290 if now >= deadline {
3291 return WaitOutcome::Timeout;
3292 }
3293 let wait = deadline.saturating_duration_since(now);
3294 let result = condvar
3295 .wait_timeout(guard, wait)
3296 .expect("terminal input mutex poisoned");
3297 guard = result.0;
3298 }
3299 None => {
3300 guard = condvar.wait(guard).expect("terminal input mutex poisoned");
3301 }
3302 }
3303 }
3304 });
3305
3306 match outcome {
3307 WaitOutcome::Event(event) => Ok(event),
3308 WaitOutcome::Closed => Err(PyRuntimeError::new_err("Native terminal input is closed")),
3309 WaitOutcome::Timeout => Err(PyTimeoutError::new_err(
3310 "No terminal input available before timeout",
3311 )),
3312 }
3313 }
3314
3315 fn stop_impl(&self) -> PyResult<()> {
3316 self.stop.store(true, Ordering::Release);
3317 #[cfg(windows)]
3318 append_native_terminal_input_trace_line(&format!(
3319 "[{:.6}] native_terminal_input stop_requested",
3320 unix_now_seconds(),
3321 ));
3322 if let Some(worker) = self
3323 .worker
3324 .lock()
3325 .expect("terminal input worker mutex poisoned")
3326 .take()
3327 {
3328 let _ = worker.join();
3329 }
3330 self.capturing.store(false, Ordering::Release);
3331
3332 #[cfg(windows)]
3333 let restore_result = {
3334 use winapi::um::consoleapi::SetConsoleMode;
3335 use winapi::um::winnt::HANDLE;
3336
3337 let console = self
3338 .console
3339 .lock()
3340 .expect("terminal input console mutex poisoned")
3341 .take();
3342 console.map(|capture| unsafe {
3343 SetConsoleMode(capture.input_handle as HANDLE, capture.original_mode)
3344 })
3345 };
3346
3347 let mut guard = self.state.lock().expect("terminal input mutex poisoned");
3348 guard.closed = true;
3349 self.condvar.notify_all();
3350 drop(guard);
3351
3352 #[cfg(windows)]
3353 if let Some(result) = restore_result {
3354 if result == 0 {
3355 return Err(to_py_err(std::io::Error::last_os_error()));
3356 }
3357 }
3358 Ok(())
3359 }
3360
3361 #[cfg(windows)]
3362 fn start_impl(&self) -> PyResult<()> {
3363 use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
3364 use winapi::um::handleapi::INVALID_HANDLE_VALUE;
3365 use winapi::um::processenv::GetStdHandle;
3366 use winapi::um::winbase::STD_INPUT_HANDLE;
3367
3368 let mut worker_guard = self
3369 .worker
3370 .lock()
3371 .expect("terminal input worker mutex poisoned");
3372 if worker_guard.is_some() {
3373 return Ok(());
3374 }
3375
3376 let input_handle = unsafe { GetStdHandle(STD_INPUT_HANDLE) };
3377 if input_handle.is_null() || input_handle == INVALID_HANDLE_VALUE {
3378 return Err(to_py_err(std::io::Error::last_os_error()));
3379 }
3380
3381 let mut original_mode = 0u32;
3382 let got_mode = unsafe { GetConsoleMode(input_handle, &mut original_mode) };
3383 if got_mode == 0 {
3384 return Err(PyRuntimeError::new_err(
3385 "NativeTerminalInput requires an attached Windows console stdin",
3386 ));
3387 }
3388
3389 let active_mode = native_terminal_input_mode(original_mode);
3390 let set_mode = unsafe { SetConsoleMode(input_handle, active_mode) };
3391 if set_mode == 0 {
3392 return Err(to_py_err(std::io::Error::last_os_error()));
3393 }
3394 append_native_terminal_input_trace_line(&format!(
3395 "[{:.6}] native_terminal_input start handle={} original_mode={:#010x} active_mode={:#010x}",
3396 unix_now_seconds(),
3397 input_handle as usize,
3398 original_mode,
3399 active_mode,
3400 ));
3401
3402 self.stop.store(false, Ordering::Release);
3403 self.capturing.store(true, Ordering::Release);
3404 {
3405 let mut state = self.state.lock().expect("terminal input mutex poisoned");
3406 state.events.clear();
3407 state.closed = false;
3408 }
3409 *self
3410 .console
3411 .lock()
3412 .expect("terminal input console mutex poisoned") = Some(ActiveTerminalInputCapture {
3413 input_handle: input_handle as usize,
3414 original_mode,
3415 active_mode,
3416 });
3417
3418 let state = Arc::clone(&self.state);
3419 let condvar = Arc::clone(&self.condvar);
3420 let stop = Arc::clone(&self.stop);
3421 let capturing = Arc::clone(&self.capturing);
3422 let input_handle_raw = input_handle as usize;
3423 *worker_guard = Some(thread::spawn(move || {
3424 native_terminal_input_worker(input_handle_raw, state, condvar, stop, capturing);
3425 }));
3426 Ok(())
3427 }
3428}
3429
3430#[pymethods]
3431impl NativeTerminalInputEvent {
3432 #[getter]
3433 fn data(&self, py: Python<'_>) -> Py<PyAny> {
3434 PyBytes::new(py, &self.data).into_any().unbind()
3435 }
3436
3437 #[getter]
3438 fn submit(&self) -> bool {
3439 self.submit
3440 }
3441
3442 #[getter]
3443 fn shift(&self) -> bool {
3444 self.shift
3445 }
3446
3447 #[getter]
3448 fn ctrl(&self) -> bool {
3449 self.ctrl
3450 }
3451
3452 #[getter]
3453 fn alt(&self) -> bool {
3454 self.alt
3455 }
3456
3457 #[getter]
3458 fn virtual_key_code(&self) -> u16 {
3459 self.virtual_key_code
3460 }
3461
3462 #[getter]
3463 fn repeat_count(&self) -> u16 {
3464 self.repeat_count
3465 }
3466
3467 fn __repr__(&self) -> String {
3468 format!(
3469 "NativeTerminalInputEvent(data={:?}, submit={}, shift={}, ctrl={}, alt={}, virtual_key_code={}, repeat_count={})",
3470 self.data,
3471 self.submit,
3472 self.shift,
3473 self.ctrl,
3474 self.alt,
3475 self.virtual_key_code,
3476 self.repeat_count,
3477 )
3478 }
3479}
3480
3481#[pymethods]
3482impl NativeTerminalInput {
3483 #[new]
3484 fn new() -> Self {
3485 Self {
3486 state: Arc::new(Mutex::new(TerminalInputState {
3487 events: VecDeque::new(),
3488 closed: true,
3489 })),
3490 condvar: Arc::new(Condvar::new()),
3491 stop: Arc::new(AtomicBool::new(false)),
3492 capturing: Arc::new(AtomicBool::new(false)),
3493 worker: Mutex::new(None),
3494 #[cfg(windows)]
3495 console: Mutex::new(None),
3496 }
3497 }
3498
3499 fn start(&self) -> PyResult<()> {
3500 #[cfg(windows)]
3501 {
3502 self.start_impl()
3503 }
3504
3505 #[cfg(not(windows))]
3506 {
3507 Err(PyRuntimeError::new_err(
3508 "NativeTerminalInput is only available on Windows consoles",
3509 ))
3510 }
3511 }
3512
3513 fn stop(&self, py: Python<'_>) -> PyResult<()> {
3514 py.allow_threads(|| self.stop_impl())
3515 }
3516
3517 fn close(&self, py: Python<'_>) -> PyResult<()> {
3518 py.allow_threads(|| self.stop_impl())
3519 }
3520
3521 fn available(&self) -> bool {
3522 !self
3523 .state
3524 .lock()
3525 .expect("terminal input mutex poisoned")
3526 .events
3527 .is_empty()
3528 }
3529
3530 #[getter]
3531 fn capturing(&self) -> bool {
3532 self.capturing.load(Ordering::Acquire)
3533 }
3534
3535 #[getter]
3536 fn original_console_mode(&self) -> Option<u32> {
3537 #[cfg(windows)]
3538 {
3539 return self
3540 .console
3541 .lock()
3542 .expect("terminal input console mutex poisoned")
3543 .as_ref()
3544 .map(|capture| capture.original_mode);
3545 }
3546
3547 #[cfg(not(windows))]
3548 {
3549 None
3550 }
3551 }
3552
3553 #[getter]
3554 fn active_console_mode(&self) -> Option<u32> {
3555 #[cfg(windows)]
3556 {
3557 return self
3558 .console
3559 .lock()
3560 .expect("terminal input console mutex poisoned")
3561 .as_ref()
3562 .map(|capture| capture.active_mode);
3563 }
3564
3565 #[cfg(not(windows))]
3566 {
3567 None
3568 }
3569 }
3570
3571 #[pyo3(signature = (timeout=None))]
3572 fn read_event(
3573 &self,
3574 py: Python<'_>,
3575 timeout: Option<f64>,
3576 ) -> PyResult<Py<NativeTerminalInputEvent>> {
3577 let event = self.wait_for_event(py, timeout)?;
3578 Self::event_to_py(py, event)
3579 }
3580
3581 fn read_event_non_blocking(
3582 &self,
3583 py: Python<'_>,
3584 ) -> PyResult<Option<Py<NativeTerminalInputEvent>>> {
3585 if let Some(event) = self.next_event() {
3586 return Self::event_to_py(py, event).map(Some);
3587 }
3588 if self
3589 .state
3590 .lock()
3591 .expect("terminal input mutex poisoned")
3592 .closed
3593 {
3594 return Err(PyRuntimeError::new_err("Native terminal input is closed"));
3595 }
3596 Ok(None)
3597 }
3598
3599 #[pyo3(signature = (timeout=None))]
3600 fn read(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
3601 let event = self.wait_for_event(py, timeout)?;
3602 Ok(PyBytes::new(py, &event.data).into_any().unbind())
3603 }
3604
3605 fn read_non_blocking(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
3606 if let Some(event) = self.next_event() {
3607 return Ok(Some(PyBytes::new(py, &event.data).into_any().unbind()));
3608 }
3609 if self
3610 .state
3611 .lock()
3612 .expect("terminal input mutex poisoned")
3613 .closed
3614 {
3615 return Err(PyRuntimeError::new_err("Native terminal input is closed"));
3616 }
3617 Ok(None)
3618 }
3619
3620 fn drain(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
3621 let mut guard = self.state.lock().expect("terminal input mutex poisoned");
3622 guard
3623 .events
3624 .drain(..)
3625 .map(|event| PyBytes::new(py, &event.data).into_any().unbind())
3626 .collect()
3627 }
3628
3629 fn drain_events(&self, py: Python<'_>) -> PyResult<Vec<Py<NativeTerminalInputEvent>>> {
3630 let mut guard = self.state.lock().expect("terminal input mutex poisoned");
3631 guard
3632 .events
3633 .drain(..)
3634 .map(|event| Self::event_to_py(py, event))
3635 .collect()
3636 }
3637}
3638
3639impl Drop for NativeTerminalInput {
3640 fn drop(&mut self) {
3641 let _ = self.stop_impl();
3642 }
3643}
3644
3645impl NativeRunningProcess {
3646 fn start_impl(&self) -> PyResult<()> {
3647 running_process_core::rp_rust_debug_scope!(
3648 "running_process_py::NativeRunningProcess::start"
3649 );
3650 self.inner.start().map_err(to_py_err)
3651 }
3652
3653 fn wait_impl(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
3654 running_process_core::rp_rust_debug_scope!(
3655 "running_process_py::NativeRunningProcess::wait"
3656 );
3657 py.allow_threads(|| {
3658 self.inner
3659 .wait(timeout.map(Duration::from_secs_f64))
3660 .map_err(process_err_to_py)
3661 })
3662 }
3663
3664 fn kill_impl(&self) -> PyResult<()> {
3665 running_process_core::rp_rust_debug_scope!(
3666 "running_process_py::NativeRunningProcess::kill"
3667 );
3668 self.inner.kill().map_err(to_py_err)
3669 }
3670
3671 fn terminate_impl(&self) -> PyResult<()> {
3672 running_process_core::rp_rust_debug_scope!(
3673 "running_process_py::NativeRunningProcess::terminate"
3674 );
3675 self.inner.terminate().map_err(to_py_err)
3676 }
3677
3678 fn close_impl(&self, py: Python<'_>) -> PyResult<()> {
3679 running_process_core::rp_rust_debug_scope!(
3680 "running_process_py::NativeRunningProcess::close"
3681 );
3682 py.allow_threads(|| self.inner.close().map_err(process_err_to_py))
3683 }
3684
3685 fn send_interrupt_impl(&self) -> PyResult<()> {
3686 running_process_core::rp_rust_debug_scope!(
3687 "running_process_py::NativeRunningProcess::send_interrupt"
3688 );
3689 let pid = self
3690 .inner
3691 .pid()
3692 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
3693
3694 #[cfg(windows)]
3695 {
3696 public_symbols::rp_windows_generate_console_ctrl_break_public(pid, self.creationflags)
3697 }
3698
3699 #[cfg(unix)]
3700 {
3701 if self.create_process_group {
3702 unix_signal_process_group(pid as i32, UnixSignal::Interrupt).map_err(to_py_err)?;
3703 } else {
3704 unix_signal_process(pid, UnixSignal::Interrupt).map_err(to_py_err)?;
3705 }
3706 Ok(())
3707 }
3708 }
3709
3710 fn decode_line_to_string(&self, py: Python<'_>, line: &[u8]) -> PyResult<String> {
3711 if !self.text {
3712 return Ok(String::from_utf8_lossy(line).into_owned());
3713 }
3714 PyBytes::new(py, line)
3715 .call_method1(
3716 "decode",
3717 (
3718 self.encoding.as_deref().unwrap_or("utf-8"),
3719 self.errors.as_deref().unwrap_or("replace"),
3720 ),
3721 )?
3722 .extract()
3723 }
3724
3725 fn captured_stream_text(&self, py: Python<'_>, stream: StreamKind) -> PyResult<String> {
3726 let lines = match stream {
3727 StreamKind::Stdout => self.inner.captured_stdout(),
3728 StreamKind::Stderr => self.inner.captured_stderr(),
3729 };
3730 let mut text = String::new();
3731 for (index, line) in lines.iter().enumerate() {
3732 if index > 0 {
3733 text.push('\n');
3734 }
3735 text.push_str(&self.decode_line_to_string(py, line)?);
3736 }
3737 Ok(text)
3738 }
3739
3740 fn captured_combined_text(&self, py: Python<'_>) -> PyResult<String> {
3741 let lines = self.inner.captured_combined();
3742 let mut text = String::new();
3743 for (index, event) in lines.iter().enumerate() {
3744 if index > 0 {
3745 text.push('\n');
3746 }
3747 text.push_str(&self.decode_line_to_string(py, &event.line)?);
3748 }
3749 Ok(text)
3750 }
3751
3752 fn read_status_text(
3753 &self,
3754 stream: Option<StreamKind>,
3755 timeout: Option<Duration>,
3756 ) -> PyResult<ReadStatus<Vec<u8>>> {
3757 Ok(match stream {
3758 Some(kind) => self.inner.read_stream(kind, timeout),
3759 None => match self.inner.read_combined(timeout) {
3760 ReadStatus::Line(StreamEvent { line, .. }) => ReadStatus::Line(line),
3761 ReadStatus::Timeout => ReadStatus::Timeout,
3762 ReadStatus::Eof => ReadStatus::Eof,
3763 },
3764 })
3765 }
3766
3767 fn find_expect_match(
3768 &self,
3769 buffer: &str,
3770 pattern: &str,
3771 is_regex: bool,
3772 ) -> PyResult<Option<ExpectDetails>> {
3773 if !is_regex {
3774 let Some(start) = buffer.find(pattern) else {
3775 return Ok(None);
3776 };
3777 return Ok(Some((
3778 pattern.to_string(),
3779 start,
3780 start + pattern.len(),
3781 Vec::new(),
3782 )));
3783 }
3784
3785 let regex = Regex::new(pattern).map_err(to_py_err)?;
3786 let Some(captures) = regex.captures(buffer) else {
3787 return Ok(None);
3788 };
3789 let whole = captures
3790 .get(0)
3791 .ok_or_else(|| PyRuntimeError::new_err("regex capture missing group 0"))?;
3792 let groups = captures
3793 .iter()
3794 .skip(1)
3795 .map(|group| {
3796 group
3797 .map(|value| value.as_str().to_string())
3798 .unwrap_or_default()
3799 })
3800 .collect();
3801 Ok(Some((
3802 whole.as_str().to_string(),
3803 whole.start(),
3804 whole.end(),
3805 groups,
3806 )))
3807 }
3808
3809 fn decode_line(&self, py: Python<'_>, line: &[u8]) -> PyResult<Py<PyAny>> {
3810 if !self.text {
3811 return Ok(PyBytes::new(py, line).into_any().unbind());
3812 }
3813 Ok(PyBytes::new(py, line)
3814 .call_method1(
3815 "decode",
3816 (
3817 self.encoding.as_deref().unwrap_or("utf-8"),
3818 self.errors.as_deref().unwrap_or("replace"),
3819 ),
3820 )?
3821 .into_any()
3822 .unbind())
3823 }
3824}
3825
3826impl NativePtyBuffer {
3827 fn decode_chunk(&self, py: Python<'_>, line: &[u8]) -> PyResult<Py<PyAny>> {
3828 if !self.text {
3829 return Ok(PyBytes::new(py, line).into_any().unbind());
3830 }
3831 Ok(PyBytes::new(py, line)
3832 .call_method1("decode", (&self.encoding, &self.errors))?
3833 .into_any()
3834 .unbind())
3835 }
3836}
3837
3838#[pymethods]
3839impl NativeIdleDetector {
3840 #[new]
3841 #[allow(clippy::too_many_arguments)]
3842 #[pyo3(signature = (timeout_seconds, stability_window_seconds, sample_interval_seconds, enabled_signal, reset_on_input=true, reset_on_output=true, count_control_churn_as_output=true, initial_idle_for_seconds=0.0))]
3843 fn new(
3844 py: Python<'_>,
3845 timeout_seconds: f64,
3846 stability_window_seconds: f64,
3847 sample_interval_seconds: f64,
3848 enabled_signal: Py<NativeSignalBool>,
3849 reset_on_input: bool,
3850 reset_on_output: bool,
3851 count_control_churn_as_output: bool,
3852 initial_idle_for_seconds: f64,
3853 ) -> Self {
3854 let now = Instant::now();
3855 let initial_idle_for_seconds = initial_idle_for_seconds.max(0.0);
3856 let last_reset_at = now
3857 .checked_sub(Duration::from_secs_f64(initial_idle_for_seconds))
3858 .unwrap_or(now);
3859 let enabled = enabled_signal.borrow(py).value.clone();
3860 Self {
3861 core: Arc::new(IdleDetectorCore {
3862 timeout_seconds,
3863 stability_window_seconds,
3864 sample_interval_seconds,
3865 reset_on_input,
3866 reset_on_output,
3867 count_control_churn_as_output,
3868 enabled,
3869 state: Mutex::new(IdleMonitorState {
3870 last_reset_at,
3871 returncode: None,
3872 interrupted: false,
3873 }),
3874 condvar: Condvar::new(),
3875 }),
3876 }
3877 }
3878
3879 #[getter]
3880 fn enabled(&self) -> bool {
3881 self.core.enabled()
3882 }
3883
3884 #[setter]
3885 fn set_enabled(&self, enabled: bool) {
3886 self.core.set_enabled(enabled);
3887 }
3888
3889 fn record_input(&self, byte_count: usize) {
3890 self.core.record_input(byte_count);
3891 }
3892
3893 fn record_output(&self, data: &[u8]) {
3894 self.core.record_output(data);
3895 }
3896
3897 fn mark_exit(&self, returncode: i32, interrupted: bool) {
3898 self.core.mark_exit(returncode, interrupted);
3899 }
3900
3901 #[pyo3(signature = (timeout=None))]
3902 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> (bool, String, f64, Option<i32>) {
3903 py.allow_threads(|| self.core.wait(timeout))
3904 }
3905}
3906
3907fn control_churn_bytes(data: &[u8]) -> usize {
3908 let mut total = 0;
3909 let mut index = 0;
3910 while index < data.len() {
3911 let byte = data[index];
3912 if byte == 0x1B {
3913 let start = index;
3914 index += 1;
3915 if index < data.len() && data[index] == b'[' {
3916 index += 1;
3917 while index < data.len() {
3918 let current = data[index];
3919 index += 1;
3920 if (0x40..=0x7E).contains(¤t) {
3921 break;
3922 }
3923 }
3924 }
3925 total += index - start;
3926 continue;
3927 }
3928 if matches!(byte, 0x08 | 0x0D | 0x7F) {
3929 total += 1;
3930 }
3931 index += 1;
3932 }
3933 total
3934}
3935
3936fn command_builder_from_argv(argv: &[String]) -> CommandBuilder {
3937 let mut command = CommandBuilder::new(&argv[0]);
3938 if argv.len() > 1 {
3939 command.args(
3940 argv[1..]
3941 .iter()
3942 .map(OsString::from)
3943 .collect::<Vec<OsString>>(),
3944 );
3945 }
3946 command
3947}
3948
3949#[inline(never)]
3950fn spawn_pty_reader(
3951 mut reader: Box<dyn Read + Send>,
3952 shared: Arc<PtyReadShared>,
3953 echo: Arc<AtomicBool>,
3954 idle_detector: Arc<Mutex<Option<Arc<IdleDetectorCore>>>>,
3955 output_bytes_total: Arc<AtomicUsize>,
3956 control_churn_bytes_total: Arc<AtomicUsize>,
3957) {
3958 running_process_core::rp_rust_debug_scope!("running_process_py::spawn_pty_reader");
3959 let mut chunk = [0_u8; 4096];
3960 loop {
3961 match reader.read(&mut chunk) {
3962 Ok(0) => break,
3963 Ok(n) => {
3964 let data = &chunk[..n];
3965
3966 let churn = control_churn_bytes(data);
3968 let visible = data.len().saturating_sub(churn);
3969 output_bytes_total.fetch_add(visible, Ordering::Relaxed);
3970 control_churn_bytes_total.fetch_add(churn, Ordering::Relaxed);
3971
3972 if echo.load(Ordering::Relaxed) {
3974 let _ = std::io::stdout().write_all(data);
3975 let _ = std::io::stdout().flush();
3976 }
3977
3978 if let Some(detector) = idle_detector
3980 .lock()
3981 .expect("idle detector mutex poisoned")
3982 .as_ref()
3983 {
3984 detector.record_output(data);
3985 }
3986
3987 let mut guard = shared.state.lock().expect("pty read mutex poisoned");
3988 guard.chunks.push_back(data.to_vec());
3989 shared.condvar.notify_all();
3990 }
3991 Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
3992 Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
3993 thread::sleep(Duration::from_millis(10));
3994 continue;
3995 }
3996 Err(_) => break,
3997 }
3998 }
3999 let mut guard = shared.state.lock().expect("pty read mutex poisoned");
4000 guard.closed = true;
4001 shared.condvar.notify_all();
4002}
4003
4004fn portable_exit_code(status: portable_pty::ExitStatus) -> i32 {
4005 if let Some(signal) = status.signal() {
4006 let signal = signal.to_ascii_lowercase();
4007 if signal.contains("interrupt") {
4008 return -2;
4009 }
4010 if signal.contains("terminated") {
4011 return -15;
4012 }
4013 if signal.contains("killed") {
4014 return -9;
4015 }
4016 }
4017 status.exit_code() as i32
4018}
4019
4020#[cfg(windows)]
4021#[inline(never)]
4022fn assign_child_to_windows_kill_on_close_job(
4023 handle: Option<std::os::windows::io::RawHandle>,
4024) -> PyResult<WindowsJobHandle> {
4025 running_process_core::rp_rust_debug_scope!(
4026 "running_process_py::assign_child_to_windows_kill_on_close_job"
4027 );
4028 use std::mem::zeroed;
4029
4030 use winapi::shared::minwindef::FALSE;
4031 use winapi::um::handleapi::INVALID_HANDLE_VALUE;
4032 use winapi::um::jobapi2::{
4033 AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
4034 };
4035 use winapi::um::winnt::{
4036 JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
4037 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
4038 };
4039
4040 let Some(handle) = handle else {
4041 return Err(PyRuntimeError::new_err(
4042 "Pseudo-terminal child does not expose a Windows process handle",
4043 ));
4044 };
4045
4046 let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
4047 if job.is_null() || job == INVALID_HANDLE_VALUE {
4048 return Err(to_py_err(std::io::Error::last_os_error()));
4049 }
4050
4051 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
4052 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
4053 let result = unsafe {
4054 SetInformationJobObject(
4055 job,
4056 JobObjectExtendedLimitInformation,
4057 (&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
4058 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
4059 )
4060 };
4061 if result == FALSE {
4062 let err = std::io::Error::last_os_error();
4063 unsafe {
4064 winapi::um::handleapi::CloseHandle(job);
4065 }
4066 return Err(to_py_err(err));
4067 }
4068
4069 let result = unsafe { AssignProcessToJobObject(job, handle.cast()) };
4070 if result == FALSE {
4071 let err = std::io::Error::last_os_error();
4072 unsafe {
4073 winapi::um::handleapi::CloseHandle(job);
4074 }
4075 return Err(to_py_err(err));
4076 }
4077
4078 Ok(WindowsJobHandle(job as usize))
4079}
4080
4081#[cfg(windows)]
4082#[inline(never)]
4083fn apply_windows_pty_priority(
4084 handle: Option<std::os::windows::io::RawHandle>,
4085 nice: Option<i32>,
4086) -> PyResult<()> {
4087 running_process_core::rp_rust_debug_scope!("running_process_py::apply_windows_pty_priority");
4088 use winapi::um::processthreadsapi::SetPriorityClass;
4089 use winapi::um::winbase::{
4090 ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS,
4091 IDLE_PRIORITY_CLASS,
4092 };
4093
4094 let Some(handle) = handle else {
4095 return Ok(());
4096 };
4097 let flags = match nice {
4098 Some(value) if value >= 15 => IDLE_PRIORITY_CLASS,
4099 Some(value) if value >= 1 => BELOW_NORMAL_PRIORITY_CLASS,
4100 Some(value) if value <= -15 => HIGH_PRIORITY_CLASS,
4101 Some(value) if value <= -1 => ABOVE_NORMAL_PRIORITY_CLASS,
4102 _ => 0,
4103 };
4104 if flags == 0 {
4105 return Ok(());
4106 }
4107 let result = unsafe { SetPriorityClass(handle.cast(), flags) };
4108 if result == 0 {
4109 return Err(to_py_err(std::io::Error::last_os_error()));
4110 }
4111 Ok(())
4112}
4113
4114#[cfg(test)]
4115mod tests {
4116 use super::*;
4117
4118 #[cfg(windows)]
4119 use winapi::um::wincon::{
4120 ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
4121 ENABLE_QUICK_EDIT_MODE, ENABLE_WINDOW_INPUT,
4122 };
4123 #[cfg(windows)]
4124 use winapi::um::wincontypes::{
4125 KEY_EVENT_RECORD, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, SHIFT_PRESSED,
4126 };
4127 #[cfg(windows)]
4128 use winapi::um::winuser::{VK_RETURN, VK_TAB, VK_UP};
4129
4130 #[cfg(windows)]
4131 fn key_event(
4132 virtual_key_code: u16,
4133 unicode: u16,
4134 control_key_state: u32,
4135 repeat_count: u16,
4136 ) -> KEY_EVENT_RECORD {
4137 let mut event: KEY_EVENT_RECORD = unsafe { std::mem::zeroed() };
4138 event.bKeyDown = 1;
4139 event.wRepeatCount = repeat_count;
4140 event.wVirtualKeyCode = virtual_key_code;
4141 event.wVirtualScanCode = 0;
4142 event.dwControlKeyState = control_key_state;
4143 unsafe {
4144 *event.uChar.UnicodeChar_mut() = unicode;
4145 }
4146 event
4147 }
4148
4149 #[test]
4150 #[cfg(windows)]
4151 fn native_terminal_input_mode_disables_cooked_console_flags() {
4152 let original_mode =
4153 ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE;
4154
4155 let active_mode = native_terminal_input_mode(original_mode);
4156
4157 assert_eq!(active_mode & ENABLE_ECHO_INPUT, 0);
4158 assert_eq!(active_mode & ENABLE_LINE_INPUT, 0);
4159 assert_eq!(active_mode & ENABLE_PROCESSED_INPUT, 0);
4160 assert_eq!(active_mode & ENABLE_QUICK_EDIT_MODE, 0);
4161 assert_ne!(active_mode & ENABLE_EXTENDED_FLAGS, 0);
4162 assert_ne!(active_mode & ENABLE_WINDOW_INPUT, 0);
4163 }
4164
4165 #[test]
4166 #[cfg(windows)]
4167 fn translate_terminal_input_preserves_submit_hint_for_enter() {
4168 let event = translate_console_key_event(&key_event(VK_RETURN as u16, '\r' as u16, 0, 1))
4169 .expect("enter should translate");
4170 assert_eq!(event.data, b"\r");
4171 assert!(event.submit);
4172 }
4173
4174 #[test]
4175 #[cfg(windows)]
4176 fn translate_terminal_input_keeps_shift_enter_non_submit() {
4177 let event = translate_console_key_event(&key_event(
4178 VK_RETURN as u16,
4179 '\r' as u16,
4180 SHIFT_PRESSED,
4181 1,
4182 ))
4183 .expect("shift-enter should translate");
4184 assert_eq!(event.data, b"\x1b[13;2u");
4187 assert!(!event.submit);
4188 assert!(event.shift);
4189 }
4190
4191 #[test]
4192 #[cfg(windows)]
4193 fn translate_terminal_input_encodes_shift_tab() {
4194 let event = translate_console_key_event(&key_event(VK_TAB as u16, 0, SHIFT_PRESSED, 1))
4195 .expect("shift-tab should translate");
4196 assert_eq!(event.data, b"\x1b[Z");
4197 assert!(!event.submit);
4198 }
4199
4200 #[test]
4201 #[cfg(windows)]
4202 fn translate_terminal_input_encodes_modified_arrows() {
4203 let event = translate_console_key_event(&key_event(
4204 VK_UP as u16,
4205 0,
4206 SHIFT_PRESSED | LEFT_CTRL_PRESSED,
4207 1,
4208 ))
4209 .expect("modified arrow should translate");
4210 assert_eq!(event.data, b"\x1b[1;6A");
4211 }
4212
4213 #[test]
4214 #[cfg(windows)]
4215 fn translate_terminal_input_encodes_alt_printable_with_escape_prefix() {
4216 let event =
4217 translate_console_key_event(&key_event(b'X' as u16, 'x' as u16, LEFT_ALT_PRESSED, 1))
4218 .expect("alt printable should translate");
4219 assert_eq!(event.data, b"\x1bx");
4220 }
4221
4222 #[test]
4223 #[cfg(windows)]
4224 fn translate_terminal_input_encodes_ctrl_printable_as_control_character() {
4225 let event =
4226 translate_console_key_event(&key_event(b'C' as u16, 'c' as u16, LEFT_CTRL_PRESSED, 1))
4227 .expect("ctrl-c should translate");
4228 assert_eq!(event.data, [0x03]);
4229 }
4230
4231 #[test]
4232 #[cfg(windows)]
4233 fn translate_terminal_input_ignores_keyup_events() {
4234 let mut event = key_event(VK_RETURN as u16, '\r' as u16, 0, 1);
4235 event.bKeyDown = 0;
4236 assert!(translate_console_key_event(&event).is_none());
4237 }
4238
4239 #[test]
4242 fn control_churn_bytes_empty() {
4243 assert_eq!(control_churn_bytes(b""), 0);
4244 }
4245
4246 #[test]
4247 fn control_churn_bytes_plain_text() {
4248 assert_eq!(control_churn_bytes(b"hello world"), 0);
4249 }
4250
4251 #[test]
4252 fn control_churn_bytes_ansi_csi_sequence() {
4253 assert_eq!(control_churn_bytes(b"\x1b[31mhello\x1b[0m"), 9);
4255 }
4256
4257 #[test]
4258 fn control_churn_bytes_backspace_cr_del() {
4259 assert_eq!(control_churn_bytes(b"\x08\x0D\x7F"), 3);
4260 }
4261
4262 #[test]
4263 fn control_churn_bytes_bare_escape() {
4264 assert_eq!(control_churn_bytes(b"\x1b"), 1);
4266 }
4267
4268 #[test]
4269 fn control_churn_bytes_mixed() {
4270 assert_eq!(control_churn_bytes(b"ok\x1b[Jmore\x08"), 4);
4272 }
4273
4274 #[test]
4277 fn input_contains_newline_cr() {
4278 assert!(input_contains_newline(b"hello\rworld"));
4279 }
4280
4281 #[test]
4282 fn input_contains_newline_lf() {
4283 assert!(input_contains_newline(b"hello\nworld"));
4284 }
4285
4286 #[test]
4287 fn input_contains_newline_none() {
4288 assert!(!input_contains_newline(b"hello world"));
4289 }
4290
4291 #[test]
4292 fn input_contains_newline_empty() {
4293 assert!(!input_contains_newline(b""));
4294 }
4295
4296 #[test]
4299 fn ignorable_error_not_found() {
4300 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
4301 assert!(is_ignorable_process_control_error(&err));
4302 }
4303
4304 #[test]
4305 fn ignorable_error_invalid_input() {
4306 let err = std::io::Error::new(std::io::ErrorKind::InvalidInput, "bad input");
4307 assert!(is_ignorable_process_control_error(&err));
4308 }
4309
4310 #[test]
4311 fn ignorable_error_permission_denied_is_not_ignorable() {
4312 let err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
4313 assert!(!is_ignorable_process_control_error(&err));
4314 }
4315
4316 #[test]
4317 #[cfg(unix)]
4318 fn ignorable_error_esrch() {
4319 let err = std::io::Error::from_raw_os_error(libc::ESRCH);
4320 assert!(is_ignorable_process_control_error(&err));
4321 }
4322
4323 #[test]
4326 #[cfg(windows)]
4327 fn windows_terminal_input_payload_passthrough() {
4328 let result = windows_terminal_input_payload(b"hello");
4329 assert_eq!(result, b"hello");
4330 }
4331
4332 #[test]
4333 #[cfg(windows)]
4334 fn windows_terminal_input_payload_lone_lf_becomes_cr() {
4335 let result = windows_terminal_input_payload(b"\n");
4336 assert_eq!(result, b"\r");
4337 }
4338
4339 #[test]
4340 #[cfg(windows)]
4341 fn windows_terminal_input_payload_crlf_preserved() {
4342 let result = windows_terminal_input_payload(b"\r\n");
4343 assert_eq!(result, b"\r\n");
4344 }
4345
4346 #[test]
4347 #[cfg(windows)]
4348 fn windows_terminal_input_payload_lone_cr_preserved() {
4349 let result = windows_terminal_input_payload(b"\r");
4350 assert_eq!(result, b"\r");
4351 }
4352
4353 #[test]
4354 #[cfg(windows)]
4355 fn terminal_input_modifier_none() {
4356 assert!(terminal_input_modifier_parameter(false, false, false).is_none());
4357 }
4358
4359 #[test]
4360 #[cfg(windows)]
4361 fn terminal_input_modifier_shift() {
4362 assert_eq!(
4363 terminal_input_modifier_parameter(true, false, false),
4364 Some(2)
4365 );
4366 }
4367
4368 #[test]
4369 #[cfg(windows)]
4370 fn terminal_input_modifier_alt() {
4371 assert_eq!(
4372 terminal_input_modifier_parameter(false, true, false),
4373 Some(3)
4374 );
4375 }
4376
4377 #[test]
4378 #[cfg(windows)]
4379 fn terminal_input_modifier_ctrl() {
4380 assert_eq!(
4381 terminal_input_modifier_parameter(false, false, true),
4382 Some(5)
4383 );
4384 }
4385
4386 #[test]
4387 #[cfg(windows)]
4388 fn terminal_input_modifier_shift_ctrl() {
4389 assert_eq!(
4390 terminal_input_modifier_parameter(true, false, true),
4391 Some(6)
4392 );
4393 }
4394
4395 #[test]
4396 #[cfg(windows)]
4397 fn control_character_for_unicode_letters() {
4398 assert_eq!(control_character_for_unicode('A' as u16), Some(0x01));
4399 assert_eq!(control_character_for_unicode('C' as u16), Some(0x03));
4400 assert_eq!(control_character_for_unicode('Z' as u16), Some(0x1A));
4401 }
4402
4403 #[test]
4404 #[cfg(windows)]
4405 fn control_character_for_unicode_special() {
4406 assert_eq!(control_character_for_unicode('@' as u16), Some(0x00));
4407 assert_eq!(control_character_for_unicode('[' as u16), Some(0x1B));
4408 }
4409
4410 #[test]
4411 #[cfg(windows)]
4412 fn control_character_for_unicode_digit_returns_none() {
4413 assert!(control_character_for_unicode('1' as u16).is_none());
4414 }
4415
4416 #[test]
4417 #[cfg(windows)]
4418 fn format_terminal_input_bytes_empty() {
4419 assert_eq!(format_terminal_input_bytes(b""), "[]");
4420 }
4421
4422 #[test]
4423 #[cfg(windows)]
4424 fn format_terminal_input_bytes_multi() {
4425 assert_eq!(format_terminal_input_bytes(&[0x41, 0x42]), "[41 42]");
4426 }
4427
4428 #[test]
4429 #[cfg(windows)]
4430 fn repeated_tilde_sequence_no_modifier() {
4431 assert_eq!(repeated_tilde_sequence(3, None, 1), b"\x1b[3~");
4432 }
4433
4434 #[test]
4435 #[cfg(windows)]
4436 fn repeated_tilde_sequence_with_modifier() {
4437 assert_eq!(repeated_tilde_sequence(3, Some(2), 1), b"\x1b[3;2~");
4438 }
4439
4440 #[test]
4441 #[cfg(windows)]
4442 fn repeated_tilde_sequence_repeated() {
4443 let result = repeated_tilde_sequence(3, None, 3);
4444 assert_eq!(result, b"\x1b[3~\x1b[3~\x1b[3~");
4445 }
4446
4447 #[test]
4448 #[cfg(windows)]
4449 fn repeated_modified_sequence_no_modifier() {
4450 let result = repeated_modified_sequence(b"\x1b[A", None, 1);
4451 assert_eq!(result, b"\x1b[A");
4452 }
4453
4454 #[test]
4455 #[cfg(windows)]
4456 fn repeated_modified_sequence_with_modifier() {
4457 let result = repeated_modified_sequence(b"\x1b[A", Some(2), 1);
4459 assert_eq!(result, b"\x1b[1;2A");
4460 }
4461
4462 #[test]
4463 #[cfg(windows)]
4464 fn repeated_modified_sequence_repeated() {
4465 let result = repeated_modified_sequence(b"\x1b[A", None, 2);
4466 assert_eq!(result, b"\x1b[A\x1b[A");
4467 }
4468
4469 #[test]
4470 #[cfg(windows)]
4471 fn repeat_terminal_input_bytes_single() {
4472 let result = repeat_terminal_input_bytes(b"\r", 1);
4473 assert_eq!(result, b"\r");
4474 }
4475
4476 #[test]
4477 #[cfg(windows)]
4478 fn repeat_terminal_input_bytes_multiple() {
4479 let result = repeat_terminal_input_bytes(b"ab", 3);
4480 assert_eq!(result, b"ababab");
4481 }
4482
4483 #[test]
4484 #[cfg(windows)]
4485 fn repeat_terminal_input_bytes_zero_clamps_to_one() {
4486 let result = repeat_terminal_input_bytes(b"x", 0);
4487 assert_eq!(result, b"x");
4488 }
4489
4490 #[test]
4493 #[cfg(windows)]
4494 fn translate_console_key_home() {
4495 use winapi::um::winuser::VK_HOME;
4496 let event = translate_console_key_event(&key_event(VK_HOME as u16, 0, 0, 1))
4497 .expect("VK_HOME should translate");
4498 assert_eq!(event.data, b"\x1b[H");
4499 assert!(!event.submit);
4500 }
4501
4502 #[test]
4503 #[cfg(windows)]
4504 fn translate_console_key_end() {
4505 use winapi::um::winuser::VK_END;
4506 let event = translate_console_key_event(&key_event(VK_END as u16, 0, 0, 1))
4507 .expect("VK_END should translate");
4508 assert_eq!(event.data, b"\x1b[F");
4509 assert!(!event.submit);
4510 }
4511
4512 #[test]
4513 #[cfg(windows)]
4514 fn translate_console_key_insert() {
4515 use winapi::um::winuser::VK_INSERT;
4516 let event = translate_console_key_event(&key_event(VK_INSERT as u16, 0, 0, 1))
4517 .expect("VK_INSERT should translate");
4518 assert_eq!(event.data, b"\x1b[2~");
4519 assert!(!event.submit);
4520 }
4521
4522 #[test]
4523 #[cfg(windows)]
4524 fn translate_console_key_delete() {
4525 use winapi::um::winuser::VK_DELETE;
4526 let event = translate_console_key_event(&key_event(VK_DELETE as u16, 0, 0, 1))
4527 .expect("VK_DELETE should translate");
4528 assert_eq!(event.data, b"\x1b[3~");
4529 assert!(!event.submit);
4530 }
4531
4532 #[test]
4533 #[cfg(windows)]
4534 fn translate_console_key_page_up() {
4535 use winapi::um::winuser::VK_PRIOR;
4536 let event = translate_console_key_event(&key_event(VK_PRIOR as u16, 0, 0, 1))
4537 .expect("VK_PRIOR should translate");
4538 assert_eq!(event.data, b"\x1b[5~");
4539 assert!(!event.submit);
4540 }
4541
4542 #[test]
4543 #[cfg(windows)]
4544 fn translate_console_key_page_down() {
4545 use winapi::um::winuser::VK_NEXT;
4546 let event = translate_console_key_event(&key_event(VK_NEXT as u16, 0, 0, 1))
4547 .expect("VK_NEXT should translate");
4548 assert_eq!(event.data, b"\x1b[6~");
4549 assert!(!event.submit);
4550 }
4551
4552 #[test]
4553 #[cfg(windows)]
4554 fn translate_console_key_shift_home() {
4555 use winapi::um::winuser::VK_HOME;
4556 let event = translate_console_key_event(&key_event(VK_HOME as u16, 0, SHIFT_PRESSED, 1))
4557 .expect("Shift+Home should translate");
4558 assert_eq!(event.data, b"\x1b[1;2H");
4559 assert!(event.shift);
4560 }
4561
4562 #[test]
4563 #[cfg(windows)]
4564 fn translate_console_key_shift_end() {
4565 use winapi::um::winuser::VK_END;
4566 let event = translate_console_key_event(&key_event(VK_END as u16, 0, SHIFT_PRESSED, 1))
4567 .expect("Shift+End should translate");
4568 assert_eq!(event.data, b"\x1b[1;2F");
4569 assert!(event.shift);
4570 }
4571
4572 #[test]
4573 #[cfg(windows)]
4574 fn translate_console_key_ctrl_home() {
4575 use winapi::um::winuser::VK_HOME;
4576 let event =
4577 translate_console_key_event(&key_event(VK_HOME as u16, 0, LEFT_CTRL_PRESSED, 1))
4578 .expect("Ctrl+Home should translate");
4579 assert_eq!(event.data, b"\x1b[1;5H");
4580 assert!(event.ctrl);
4581 }
4582
4583 #[test]
4584 #[cfg(windows)]
4585 fn translate_console_key_shift_delete() {
4586 use winapi::um::winuser::VK_DELETE;
4587 let event = translate_console_key_event(&key_event(VK_DELETE as u16, 0, SHIFT_PRESSED, 1))
4588 .expect("Shift+Delete should translate");
4589 assert_eq!(event.data, b"\x1b[3;2~");
4590 assert!(event.shift);
4591 }
4592
4593 #[test]
4594 #[cfg(windows)]
4595 fn translate_console_key_ctrl_page_up() {
4596 use winapi::um::winuser::VK_PRIOR;
4597 let event =
4598 translate_console_key_event(&key_event(VK_PRIOR as u16, 0, LEFT_CTRL_PRESSED, 1))
4599 .expect("Ctrl+PageUp should translate");
4600 assert_eq!(event.data, b"\x1b[5;5~");
4601 assert!(event.ctrl);
4602 }
4603
4604 #[test]
4605 #[cfg(windows)]
4606 fn translate_console_key_backspace() {
4607 use winapi::um::winuser::VK_BACK;
4608 let event = translate_console_key_event(&key_event(VK_BACK as u16, 0x08, 0, 1))
4609 .expect("Backspace should translate");
4610 assert_eq!(event.data, b"\x08");
4611 }
4612
4613 #[test]
4614 #[cfg(windows)]
4615 fn translate_console_key_escape() {
4616 use winapi::um::winuser::VK_ESCAPE;
4617 let event = translate_console_key_event(&key_event(VK_ESCAPE as u16, 0x1b, 0, 1))
4618 .expect("Escape should translate");
4619 assert_eq!(event.data, b"\x1b");
4620 }
4621
4622 #[test]
4623 #[cfg(windows)]
4624 fn translate_console_key_tab() {
4625 let event = translate_console_key_event(&key_event(VK_TAB as u16, 0, 0, 1))
4626 .expect("Tab should translate");
4627 assert_eq!(event.data, b"\t");
4628 }
4629
4630 #[test]
4631 #[cfg(windows)]
4632 fn translate_console_key_plain_enter_is_submit() {
4633 let event = translate_console_key_event(&key_event(VK_RETURN as u16, '\r' as u16, 0, 1))
4634 .expect("Enter should translate");
4635 assert_eq!(event.data, b"\r");
4636 assert!(event.submit);
4637 assert!(!event.shift);
4638 }
4639
4640 #[test]
4641 #[cfg(windows)]
4642 fn translate_console_key_unicode_printable() {
4643 let event = translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, 0, 1))
4645 .expect("printable should translate");
4646 assert_eq!(event.data, b"a");
4647 }
4648
4649 #[test]
4650 #[cfg(windows)]
4651 fn translate_console_key_unicode_repeated() {
4652 let event = translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, 0, 3))
4653 .expect("repeated printable should translate");
4654 assert_eq!(event.data, b"aaa");
4655 }
4656
4657 #[test]
4658 #[cfg(windows)]
4659 fn translate_console_key_down_arrow() {
4660 use winapi::um::winuser::VK_DOWN;
4661 let event = translate_console_key_event(&key_event(VK_DOWN as u16, 0, 0, 1))
4662 .expect("Down arrow should translate");
4663 assert_eq!(event.data, b"\x1b[B");
4664 }
4665
4666 #[test]
4667 #[cfg(windows)]
4668 fn translate_console_key_right_arrow() {
4669 use winapi::um::winuser::VK_RIGHT;
4670 let event = translate_console_key_event(&key_event(VK_RIGHT as u16, 0, 0, 1))
4671 .expect("Right arrow should translate");
4672 assert_eq!(event.data, b"\x1b[C");
4673 }
4674
4675 #[test]
4676 #[cfg(windows)]
4677 fn translate_console_key_left_arrow() {
4678 use winapi::um::winuser::VK_LEFT;
4679 let event = translate_console_key_event(&key_event(VK_LEFT as u16, 0, 0, 1))
4680 .expect("Left arrow should translate");
4681 assert_eq!(event.data, b"\x1b[D");
4682 }
4683
4684 #[test]
4685 #[cfg(windows)]
4686 fn translate_console_key_unknown_vk_no_unicode_returns_none() {
4687 let result = translate_console_key_event(&key_event(0xFF, 0, 0, 1));
4689 assert!(result.is_none());
4690 }
4691
4692 #[test]
4693 #[cfg(windows)]
4694 fn translate_console_key_alt_escape_prefix() {
4695 let event =
4697 translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, LEFT_ALT_PRESSED, 1))
4698 .expect("Alt+a should translate");
4699 assert_eq!(event.data, b"\x1ba");
4700 assert!(event.alt);
4701 }
4702
4703 #[test]
4704 #[cfg(windows)]
4705 fn translate_console_key_ctrl_a() {
4706 let event =
4707 translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, LEFT_CTRL_PRESSED, 1))
4708 .expect("Ctrl+A should translate");
4709 assert_eq!(event.data, [0x01]); assert!(event.ctrl);
4711 }
4712
4713 #[test]
4714 #[cfg(windows)]
4715 fn translate_console_key_ctrl_z() {
4716 let event =
4717 translate_console_key_event(&key_event(b'Z' as u16, 'z' as u16, LEFT_CTRL_PRESSED, 1))
4718 .expect("Ctrl+Z should translate");
4719 assert_eq!(event.data, [0x1A]); assert!(event.ctrl);
4721 }
4722
4723 #[test]
4726 fn signal_bool_default_false() {
4727 let sb = NativeSignalBool::new(false);
4728 assert!(!sb.load_nolock());
4729 }
4730
4731 #[test]
4732 fn signal_bool_default_true() {
4733 let sb = NativeSignalBool::new(true);
4734 assert!(sb.load_nolock());
4735 }
4736
4737 #[test]
4738 fn signal_bool_store_and_load() {
4739 let sb = NativeSignalBool::new(false);
4740 sb.store_locked(true);
4741 assert!(sb.load_nolock());
4742 sb.store_locked(false);
4743 assert!(!sb.load_nolock());
4744 }
4745
4746 #[test]
4747 fn signal_bool_compare_and_swap_success() {
4748 let sb = NativeSignalBool::new(false);
4749 assert!(sb.compare_and_swap_locked(false, true));
4750 assert!(sb.load_nolock());
4751 }
4752
4753 #[test]
4754 fn signal_bool_compare_and_swap_failure() {
4755 let sb = NativeSignalBool::new(false);
4756 assert!(!sb.compare_and_swap_locked(true, false));
4757 assert!(!sb.load_nolock());
4758 }
4759
4760 #[test]
4763 fn pty_buffer_available_empty() {
4764 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4765 assert!(!buf.available());
4766 }
4767
4768 #[test]
4769 fn pty_buffer_record_and_available() {
4770 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4771 buf.record_output(b"hello");
4772 assert!(buf.available());
4773 }
4774
4775 #[test]
4776 fn pty_buffer_history_bytes_and_clear() {
4777 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4778 buf.record_output(b"hello");
4779 buf.record_output(b"world");
4780 assert_eq!(buf.history_bytes(), 10);
4781 let released = buf.clear_history();
4782 assert_eq!(released, 10);
4783 assert_eq!(buf.history_bytes(), 0);
4784 }
4785
4786 #[test]
4787 fn pty_buffer_close() {
4788 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4789 buf.close();
4790 }
4793
4794 #[test]
4797 fn pty_buffer_drain_returns_recorded_chunks() {
4798 pyo3::prepare_freethreaded_python();
4799 pyo3::Python::with_gil(|py| {
4800 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4801 buf.record_output(b"chunk1");
4802 buf.record_output(b"chunk2");
4803 let drained = buf.drain(py).unwrap();
4804 assert_eq!(drained.len(), 2);
4805 assert!(!buf.available());
4806 });
4807 }
4808
4809 #[test]
4810 fn pty_buffer_output_returns_full_history() {
4811 pyo3::prepare_freethreaded_python();
4812 pyo3::Python::with_gil(|py| {
4813 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
4814 buf.record_output(b"hello ");
4815 buf.record_output(b"world");
4816 let output = buf.output(py).unwrap();
4817 let text: String = output.extract(py).unwrap();
4818 assert_eq!(text, "hello world");
4819 });
4820 }
4821
4822 #[test]
4823 fn pty_buffer_output_since_offset() {
4824 pyo3::prepare_freethreaded_python();
4825 pyo3::Python::with_gil(|py| {
4826 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
4827 buf.record_output(b"hello ");
4828 buf.record_output(b"world");
4829 let output = buf.output_since(py, 6).unwrap();
4830 let text: String = output.extract(py).unwrap();
4831 assert_eq!(text, "world");
4832 });
4833 }
4834
4835 #[test]
4836 fn pty_buffer_read_non_blocking_empty() {
4837 pyo3::prepare_freethreaded_python();
4838 pyo3::Python::with_gil(|py| {
4839 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4840 let result = buf.read_non_blocking(py).unwrap();
4841 assert!(result.is_none());
4842 });
4843 }
4844
4845 #[test]
4846 fn pty_buffer_read_non_blocking_with_data() {
4847 pyo3::prepare_freethreaded_python();
4848 pyo3::Python::with_gil(|py| {
4849 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4850 buf.record_output(b"data");
4851 let result = buf.read_non_blocking(py).unwrap();
4852 assert!(result.is_some());
4853 });
4854 }
4855
4856 #[test]
4857 fn pty_buffer_read_closed_returns_error() {
4858 pyo3::prepare_freethreaded_python();
4859 pyo3::Python::with_gil(|py| {
4860 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4861 buf.close();
4862 let result = buf.read_non_blocking(py);
4863 assert!(result.is_err());
4864 });
4865 }
4866
4867 #[test]
4868 fn pty_buffer_read_with_timeout() {
4869 pyo3::prepare_freethreaded_python();
4870 pyo3::Python::with_gil(|py| {
4871 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4872 let result = buf.read(py, Some(0.05));
4873 assert!(result.is_err());
4875 });
4876 }
4877
4878 #[test]
4879 fn pty_buffer_text_mode_decodes_utf8() {
4880 pyo3::prepare_freethreaded_python();
4881 pyo3::Python::with_gil(|py| {
4882 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
4883 buf.record_output("héllo".as_bytes());
4884 let result = buf.read_non_blocking(py).unwrap().unwrap();
4885 let text: String = result.extract(py).unwrap();
4886 assert_eq!(text, "héllo");
4887 });
4888 }
4889
4890 #[test]
4891 fn pty_buffer_bytes_mode_returns_bytes() {
4892 pyo3::prepare_freethreaded_python();
4893 pyo3::Python::with_gil(|py| {
4894 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4895 buf.record_output(b"\xff\xfe");
4896 let result = buf.read_non_blocking(py).unwrap().unwrap();
4897 let bytes: Vec<u8> = result.extract(py).unwrap();
4898 assert_eq!(bytes, vec![0xff, 0xfe]);
4899 });
4900 }
4901
4902 fn make_idle_detector(
4905 py: pyo3::Python<'_>,
4906 timeout_seconds: f64,
4907 enabled: bool,
4908 initial_idle_for: f64,
4909 ) -> NativeIdleDetector {
4910 let signal = pyo3::Py::new(py, NativeSignalBool::new(enabled)).unwrap();
4911 NativeIdleDetector::new(
4912 py,
4913 timeout_seconds,
4914 0.0, 0.01, signal,
4917 true, true, true, initial_idle_for,
4921 )
4922 }
4923
4924 #[test]
4925 fn idle_detector_mark_exit_returns_process_exit() {
4926 pyo3::prepare_freethreaded_python();
4927 pyo3::Python::with_gil(|py| {
4928 let det = make_idle_detector(py, 10.0, true, 0.0);
4929 det.mark_exit(42, false);
4930 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(1.0));
4931 assert!(!triggered);
4932 assert_eq!(reason, "process_exit");
4933 assert_eq!(returncode, Some(42));
4934 });
4935 }
4936
4937 #[test]
4938 fn idle_detector_mark_exit_interrupted() {
4939 pyo3::prepare_freethreaded_python();
4940 pyo3::Python::with_gil(|py| {
4941 let det = make_idle_detector(py, 10.0, true, 0.0);
4942 det.mark_exit(1, true);
4943 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(1.0));
4944 assert!(!triggered);
4945 assert_eq!(reason, "interrupt");
4946 assert_eq!(returncode, Some(1));
4947 });
4948 }
4949
4950 #[test]
4951 fn idle_detector_timeout_when_not_idle() {
4952 pyo3::prepare_freethreaded_python();
4953 pyo3::Python::with_gil(|py| {
4954 let det = make_idle_detector(py, 10.0, true, 0.0);
4955 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(0.05));
4956 assert!(!triggered);
4957 assert_eq!(reason, "timeout");
4958 assert!(returncode.is_none());
4959 });
4960 }
4961
4962 #[test]
4963 fn idle_detector_triggers_when_already_idle() {
4964 pyo3::prepare_freethreaded_python();
4965 pyo3::Python::with_gil(|py| {
4966 let det = make_idle_detector(py, 0.5, true, 1.0);
4969 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(1.0));
4970 assert!(triggered);
4971 assert_eq!(reason, "idle_timeout");
4972 });
4973 }
4974
4975 #[test]
4976 fn idle_detector_disabled_does_not_trigger() {
4977 pyo3::prepare_freethreaded_python();
4978 pyo3::Python::with_gil(|py| {
4979 let det = make_idle_detector(py, 0.01, false, 1.0);
4980 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.1));
4981 assert!(!triggered);
4982 assert_eq!(reason, "timeout");
4983 });
4984 }
4985
4986 #[test]
4987 fn idle_detector_record_input_resets_idle() {
4988 pyo3::prepare_freethreaded_python();
4989 pyo3::Python::with_gil(|py| {
4990 let det = make_idle_detector(py, 0.5, true, 1.0);
4991 det.record_input(5);
4993 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.05));
4995 assert!(!triggered);
4996 assert_eq!(reason, "timeout");
4997 });
4998 }
4999
5000 #[test]
5001 fn idle_detector_record_output_resets_idle() {
5002 pyo3::prepare_freethreaded_python();
5003 pyo3::Python::with_gil(|py| {
5004 let det = make_idle_detector(py, 0.5, true, 1.0);
5005 det.record_output(b"visible output");
5007 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.05));
5008 assert!(!triggered);
5009 assert_eq!(reason, "timeout");
5010 });
5011 }
5012
5013 #[test]
5014 fn idle_detector_control_churn_only_no_reset_when_not_counted() {
5015 pyo3::prepare_freethreaded_python();
5016 pyo3::Python::with_gil(|py| {
5017 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5018 let det = NativeIdleDetector::new(
5019 py, 0.05, 0.0, 0.01, signal, true, true,
5020 false, 1.0, );
5023 det.record_output(b"\x1b[31m");
5025 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
5027 assert!(triggered);
5028 assert_eq!(reason, "idle_timeout");
5029 });
5030 }
5031
5032 #[test]
5035 fn process_registry_register_list_unregister() {
5036 pyo3::prepare_freethreaded_python();
5037 pyo3::Python::with_gil(|_py| {
5038 let test_pid = 99999u32;
5039 native_register_process(test_pid, "test", "test-command", None).unwrap();
5041 let list = native_list_active_processes();
5043 let found = list.iter().any(|(pid, _, _, _, _)| *pid == test_pid);
5044 assert!(found, "registered pid should appear in active list");
5045 native_unregister_process(test_pid).unwrap();
5047 let list = native_list_active_processes();
5048 let found = list.iter().any(|(pid, _, _, _, _)| *pid == test_pid);
5049 assert!(!found, "unregistered pid should not appear in active list");
5050 });
5051 }
5052
5053 #[test]
5056 fn process_metrics_sample_current_process() {
5057 let pid = std::process::id();
5058 let metrics = NativeProcessMetrics::new(pid);
5059 metrics.prime();
5060 let (exists, _cpu, _disk, _extra) = metrics.sample();
5061 assert!(exists, "current process should exist");
5062 }
5063
5064 #[test]
5065 fn process_metrics_nonexistent_process() {
5066 let metrics = NativeProcessMetrics::new(99999999);
5067 metrics.prime();
5068 let (exists, _cpu, _disk, _extra) = metrics.sample();
5069 assert!(!exists, "nonexistent pid should not exist");
5070 }
5071
5072 #[test]
5075 fn portable_exit_code_normal_exit_zero() {
5076 let status = portable_pty::ExitStatus::with_exit_code(0);
5077 assert_eq!(portable_exit_code(status), 0);
5078 }
5079
5080 #[test]
5081 fn portable_exit_code_normal_exit_nonzero() {
5082 let status = portable_pty::ExitStatus::with_exit_code(42);
5083 assert_eq!(portable_exit_code(status), 42);
5084 }
5085
5086 #[test]
5089 fn record_pty_input_metrics_basic() {
5090 let input_bytes = Arc::new(AtomicUsize::new(0));
5091 let newline_events = Arc::new(AtomicUsize::new(0));
5092 let submit_events = Arc::new(AtomicUsize::new(0));
5093
5094 record_pty_input_metrics(
5095 &input_bytes,
5096 &newline_events,
5097 &submit_events,
5098 b"hello",
5099 false,
5100 );
5101
5102 assert_eq!(input_bytes.load(Ordering::Acquire), 5);
5103 assert_eq!(newline_events.load(Ordering::Acquire), 0);
5104 assert_eq!(submit_events.load(Ordering::Acquire), 0);
5105 }
5106
5107 #[test]
5108 fn record_pty_input_metrics_with_newline() {
5109 let input_bytes = Arc::new(AtomicUsize::new(0));
5110 let newline_events = Arc::new(AtomicUsize::new(0));
5111 let submit_events = Arc::new(AtomicUsize::new(0));
5112
5113 record_pty_input_metrics(
5114 &input_bytes,
5115 &newline_events,
5116 &submit_events,
5117 b"hello\n",
5118 false,
5119 );
5120
5121 assert_eq!(input_bytes.load(Ordering::Acquire), 6);
5122 assert_eq!(newline_events.load(Ordering::Acquire), 1);
5123 assert_eq!(submit_events.load(Ordering::Acquire), 0);
5124 }
5125
5126 #[test]
5127 fn record_pty_input_metrics_with_submit() {
5128 let input_bytes = Arc::new(AtomicUsize::new(0));
5129 let newline_events = Arc::new(AtomicUsize::new(0));
5130 let submit_events = Arc::new(AtomicUsize::new(0));
5131
5132 record_pty_input_metrics(&input_bytes, &newline_events, &submit_events, b"\r", true);
5133
5134 assert_eq!(input_bytes.load(Ordering::Acquire), 1);
5135 assert_eq!(newline_events.load(Ordering::Acquire), 1);
5136 assert_eq!(submit_events.load(Ordering::Acquire), 1);
5137 }
5138
5139 #[test]
5140 fn record_pty_input_metrics_accumulates() {
5141 let input_bytes = Arc::new(AtomicUsize::new(0));
5142 let newline_events = Arc::new(AtomicUsize::new(0));
5143 let submit_events = Arc::new(AtomicUsize::new(0));
5144
5145 record_pty_input_metrics(&input_bytes, &newline_events, &submit_events, b"ab", false);
5146 record_pty_input_metrics(&input_bytes, &newline_events, &submit_events, b"cd\n", true);
5147
5148 assert_eq!(input_bytes.load(Ordering::Acquire), 5);
5149 assert_eq!(newline_events.load(Ordering::Acquire), 1);
5150 assert_eq!(submit_events.load(Ordering::Acquire), 1);
5151 }
5152
5153 #[test]
5156 fn store_pty_returncode_sets_value() {
5157 let returncode = Arc::new(Mutex::new(None));
5158 store_pty_returncode(&returncode, 42);
5159 assert_eq!(*returncode.lock().unwrap(), Some(42));
5160 }
5161
5162 #[test]
5163 fn store_pty_returncode_overwrites() {
5164 let returncode = Arc::new(Mutex::new(Some(1)));
5165 store_pty_returncode(&returncode, 0);
5166 assert_eq!(*returncode.lock().unwrap(), Some(0));
5167 }
5168
5169 #[test]
5172 fn write_pty_input_not_connected() {
5173 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
5174 let result = write_pty_input(&handles, b"hello");
5175 assert!(result.is_err());
5176 let err = result.unwrap_err();
5177 assert_eq!(err.kind(), std::io::ErrorKind::NotConnected);
5178 }
5179
5180 #[test]
5183 fn poll_pty_process_no_handles_returns_stored_code() {
5184 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
5185 let returncode = Arc::new(Mutex::new(Some(42)));
5186 let result = poll_pty_process(&handles, &returncode).unwrap();
5187 assert_eq!(result, Some(42));
5188 }
5189
5190 #[test]
5191 fn poll_pty_process_no_handles_no_code() {
5192 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
5193 let returncode = Arc::new(Mutex::new(None));
5194 let result = poll_pty_process(&handles, &returncode).unwrap();
5195 assert_eq!(result, None);
5196 }
5197
5198 #[test]
5201 fn descendant_pids_returns_empty_for_unknown_pid() {
5202 let system = System::new();
5203 let pid = system_pid(99999999);
5204 let descendants = descendant_pids(&system, pid);
5205 assert!(descendants.is_empty());
5206 }
5207
5208 #[test]
5211 fn unix_now_seconds_returns_positive() {
5212 let now = unix_now_seconds();
5213 assert!(now > 0.0, "unix timestamp should be positive");
5214 }
5215
5216 #[test]
5219 fn same_process_identity_nonexistent_pid() {
5220 assert!(!same_process_identity(99999999, 0.0, 1.0));
5221 }
5222
5223 #[test]
5226 fn tracked_process_db_path_returns_ok() {
5227 let path = tracked_process_db_path();
5228 assert!(path.is_ok());
5229 let path = path.unwrap();
5230 assert!(
5231 path.to_string_lossy().contains("tracked-pids.sqlite3"),
5232 "path should contain expected filename: {:?}",
5233 path
5234 );
5235 }
5236
5237 #[test]
5240 fn command_builder_from_argv_single_arg() {
5241 let argv = vec!["echo".to_string()];
5242 let _cmd = command_builder_from_argv(&argv);
5243 }
5245
5246 #[test]
5247 fn command_builder_from_argv_multi_args() {
5248 let argv = vec!["echo".to_string(), "hello".to_string(), "world".to_string()];
5249 let _cmd = command_builder_from_argv(&argv);
5250 }
5252
5253 #[test]
5256 fn process_err_to_py_timeout() {
5257 pyo3::prepare_freethreaded_python();
5258 pyo3::Python::with_gil(|py| {
5259 let err = process_err_to_py(ProcessError::Timeout);
5260 assert!(err.is_instance_of::<pyo3::exceptions::PyTimeoutError>(py));
5261 });
5262 }
5263
5264 #[test]
5267 fn kill_process_tree_nonexistent_pid_no_panic() {
5268 kill_process_tree_impl(99999999, 0.1);
5270 }
5271
5272 #[test]
5275 fn idle_detector_record_input_zero_bytes_no_reset() {
5276 pyo3::prepare_freethreaded_python();
5277 pyo3::Python::with_gil(|py| {
5278 let det = make_idle_detector(py, 0.05, true, 1.0);
5279 det.record_input(0);
5281 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
5282 assert!(triggered);
5283 assert_eq!(reason, "idle_timeout");
5284 });
5285 }
5286
5287 #[test]
5288 fn idle_detector_record_output_empty_no_reset() {
5289 pyo3::prepare_freethreaded_python();
5290 pyo3::Python::with_gil(|py| {
5291 let det = make_idle_detector(py, 0.05, true, 1.0);
5292 det.record_output(b"");
5294 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
5295 assert!(triggered);
5296 assert_eq!(reason, "idle_timeout");
5297 });
5298 }
5299
5300 #[test]
5301 fn idle_detector_enabled_getter_and_setter() {
5302 pyo3::prepare_freethreaded_python();
5303 pyo3::Python::with_gil(|py| {
5304 let det = make_idle_detector(py, 1.0, true, 0.0);
5305 assert!(det.enabled());
5306 det.set_enabled(false);
5307 assert!(!det.enabled());
5308 det.set_enabled(true);
5309 assert!(det.enabled());
5310 });
5311 }
5312
5313 #[test]
5316 fn pty_buffer_multiple_record_and_drain() {
5317 pyo3::prepare_freethreaded_python();
5318 pyo3::Python::with_gil(|py| {
5319 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
5320 buf.record_output(b"a");
5321 buf.record_output(b"b");
5322 buf.record_output(b"c");
5323 let drained = buf.drain(py).unwrap();
5324 assert_eq!(drained.len(), 3);
5325 assert!(!buf.available());
5326 assert_eq!(buf.history_bytes(), 3);
5328 });
5329 }
5330
5331 #[test]
5332 fn pty_buffer_output_since_beyond_length() {
5333 pyo3::prepare_freethreaded_python();
5334 pyo3::Python::with_gil(|py| {
5335 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
5336 buf.record_output(b"hi");
5337 let output = buf.output_since(py, 999).unwrap();
5338 let text: String = output.extract(py).unwrap();
5339 assert_eq!(text, "");
5340 });
5341 }
5342
5343 #[test]
5344 fn pty_buffer_clear_history_returns_correct_bytes() {
5345 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
5346 buf.record_output(b"hello");
5347 buf.record_output(b"world");
5348 assert_eq!(buf.history_bytes(), 10);
5349 let released = buf.clear_history();
5350 assert_eq!(released, 10);
5351 assert_eq!(buf.history_bytes(), 0);
5352 buf.record_output(b"new");
5354 assert_eq!(buf.history_bytes(), 3);
5355 }
5356
5357 #[test]
5360 fn signal_bool_concurrent_access() {
5361 let sb = NativeSignalBool::new(false);
5362 let sb_clone = sb.clone();
5363
5364 let handle = std::thread::spawn(move || {
5365 sb_clone.store_locked(true);
5366 });
5367 handle.join().unwrap();
5368 assert!(sb.load_nolock());
5369 }
5370
5371 #[test]
5374 fn control_churn_bytes_escape_then_non_bracket() {
5375 assert_eq!(control_churn_bytes(b"\x1bO"), 1);
5377 }
5378
5379 #[test]
5380 fn control_churn_bytes_incomplete_csi() {
5381 assert_eq!(control_churn_bytes(b"\x1b[123"), 5);
5383 }
5384
5385 #[test]
5386 fn control_churn_bytes_multiple_sequences() {
5387 assert_eq!(control_churn_bytes(b"\x1b[H\x1b[2J"), 7);
5389 }
5390
5391 #[cfg(windows)]
5394 mod windows_payload_tests {
5395 use super::*;
5396
5397 #[test]
5398 fn windows_terminal_input_payload_mixed_line_endings() {
5399 let result = windows_terminal_input_payload(b"a\nb\r\nc\rd");
5400 assert_eq!(result, b"a\rb\r\nc\rd");
5401 }
5402
5403 #[test]
5404 fn windows_terminal_input_payload_consecutive_lf() {
5405 let result = windows_terminal_input_payload(b"\n\n");
5406 assert_eq!(result, b"\r\r");
5407 }
5408
5409 #[test]
5410 fn windows_terminal_input_payload_empty() {
5411 let result = windows_terminal_input_payload(b"");
5412 assert!(result.is_empty());
5413 }
5414
5415 #[test]
5416 fn windows_terminal_input_payload_no_line_endings() {
5417 let result = windows_terminal_input_payload(b"hello world");
5418 assert_eq!(result, b"hello world");
5419 }
5420
5421 #[test]
5422 fn format_terminal_input_bytes_single() {
5423 assert_eq!(format_terminal_input_bytes(&[0x0D]), "[0d]");
5424 }
5425
5426 #[test]
5427 fn native_terminal_input_mode_preserves_other_flags() {
5428 let custom_flag = 0x0100; let result = native_terminal_input_mode(custom_flag);
5431 assert_ne!(result & custom_flag, 0);
5433 }
5434 }
5435
5436 #[test]
5439 fn process_registry_register_with_cwd() {
5440 pyo3::prepare_freethreaded_python();
5441 pyo3::Python::with_gil(|_py| {
5442 let test_pid = 99998u32;
5443 native_register_process(test_pid, "test", "test-cmd", Some("/tmp/test".to_string()))
5444 .unwrap();
5445 let list = native_list_active_processes();
5446 let entry = list.iter().find(|(pid, _, _, _, _)| *pid == test_pid);
5447 assert!(entry.is_some());
5448 let (_, kind, cmd, cwd, _) = entry.unwrap();
5449 assert_eq!(kind, "test");
5450 assert_eq!(cmd, "test-cmd");
5451 assert_eq!(cwd.as_deref(), Some("/tmp/test"));
5452 native_unregister_process(test_pid).unwrap();
5453 });
5454 }
5455
5456 #[test]
5457 fn process_registry_double_register_overwrites() {
5458 pyo3::prepare_freethreaded_python();
5459 pyo3::Python::with_gil(|_py| {
5460 let test_pid = 99997u32;
5461 native_register_process(test_pid, "first", "cmd1", None).unwrap();
5462 native_register_process(test_pid, "second", "cmd2", None).unwrap();
5463 let list = native_list_active_processes();
5464 let entries: Vec<_> = list
5465 .iter()
5466 .filter(|(pid, _, _, _, _)| *pid == test_pid)
5467 .collect();
5468 assert_eq!(entries.len(), 1);
5469 assert_eq!(entries[0].1, "second");
5470 native_unregister_process(test_pid).unwrap();
5471 });
5472 }
5473
5474 #[test]
5475 fn process_registry_unregister_nonexistent_no_error() {
5476 pyo3::prepare_freethreaded_python();
5477 pyo3::Python::with_gil(|_py| {
5478 let result = native_unregister_process(99996);
5480 assert!(result.is_ok());
5481 });
5482 }
5483
5484 #[test]
5487 fn list_tracked_processes_returns_ok() {
5488 pyo3::prepare_freethreaded_python();
5489 pyo3::Python::with_gil(|_py| {
5490 let result = list_tracked_processes();
5491 assert!(result.is_ok());
5492 });
5493 }
5494
5495 #[test]
5502 fn non_ignorable_error_connection_refused() {
5503 let err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
5504 assert!(!is_ignorable_process_control_error(&err));
5505 }
5506
5507 #[test]
5510 fn to_py_err_creates_runtime_error() {
5511 pyo3::prepare_freethreaded_python();
5512 let err = to_py_err("test error message");
5513 assert!(err.to_string().contains("test error message"));
5514 }
5515
5516 #[test]
5519 fn process_err_to_py_timeout_is_timeout_error() {
5520 pyo3::prepare_freethreaded_python();
5521 let err = process_err_to_py(running_process_core::ProcessError::Timeout);
5522 pyo3::Python::with_gil(|py| {
5523 assert!(err.is_instance_of::<pyo3::exceptions::PyTimeoutError>(py));
5524 });
5525 }
5526
5527 #[test]
5528 fn process_err_to_py_not_running_is_runtime_error() {
5529 pyo3::prepare_freethreaded_python();
5530 let err = process_err_to_py(running_process_core::ProcessError::NotRunning);
5531 pyo3::Python::with_gil(|py| {
5532 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
5533 });
5534 }
5535
5536 #[test]
5539 fn input_contains_newline_with_cr() {
5540 assert!(input_contains_newline(b"hello\rworld"));
5541 }
5542
5543 #[test]
5544 fn input_contains_newline_with_lf() {
5545 assert!(input_contains_newline(b"hello\nworld"));
5546 }
5547
5548 #[test]
5549 fn input_contains_newline_with_crlf() {
5550 assert!(input_contains_newline(b"hello\r\nworld"));
5551 }
5552
5553 #[test]
5554 fn input_contains_newline_without_newline() {
5555 assert!(!input_contains_newline(b"hello world"));
5556 }
5557
5558 #[test]
5561 fn control_churn_bytes_backspace() {
5562 assert_eq!(control_churn_bytes(b"\x08"), 1);
5563 }
5564
5565 #[test]
5566 fn control_churn_bytes_carriage_return() {
5567 assert_eq!(control_churn_bytes(b"\x0D"), 1);
5568 }
5569
5570 #[test]
5571 fn control_churn_bytes_delete_char() {
5572 assert_eq!(control_churn_bytes(b"\x7F"), 1);
5573 }
5574
5575 #[test]
5576 fn control_churn_bytes_mixed_with_text() {
5577 assert_eq!(control_churn_bytes(b"hello\x0D\x1b[H"), 4);
5578 }
5579
5580 #[test]
5581 fn control_churn_bytes_plain_text_no_churn() {
5582 assert_eq!(control_churn_bytes(b"hello world"), 0);
5583 }
5584
5585 #[test]
5588 fn system_pid_converts_u32() {
5589 let pid = system_pid(12345);
5590 assert_eq!(pid.as_u32(), 12345);
5591 }
5592
5593 #[test]
5596 fn unix_now_seconds_is_recent() {
5597 let now = unix_now_seconds();
5598 assert!(now > 1_577_836_800.0);
5599 }
5600
5601 #[test]
5604 fn idle_detector_wait_idle_timeout_with_initial_idle() {
5605 pyo3::prepare_freethreaded_python();
5606 pyo3::Python::with_gil(|py| {
5607 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5608 let detector =
5609 NativeIdleDetector::new(py, 0.01, 0.01, 0.001, signal, true, true, true, 100.0);
5610 let (idle, reason, _, code) = detector.wait(py, Some(1.0));
5611 assert!(idle);
5612 assert_eq!(reason, "idle_timeout");
5613 assert!(code.is_none());
5614 });
5615 }
5616
5617 #[test]
5618 fn idle_detector_record_output_only_control_churn_with_flag() {
5619 pyo3::prepare_freethreaded_python();
5620 pyo3::Python::with_gil(|py| {
5621 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5622 let detector =
5623 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
5624 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5625 std::thread::sleep(std::time::Duration::from_millis(10));
5626 detector.record_output(b"\x1b[H");
5627 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5628 assert!(state_after > state_before);
5629 });
5630 }
5631
5632 #[test]
5633 fn idle_detector_record_output_only_control_churn_without_flag() {
5634 pyo3::prepare_freethreaded_python();
5635 pyo3::Python::with_gil(|py| {
5636 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5637 let detector =
5638 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, false, 5.0);
5639 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5640 std::thread::sleep(std::time::Duration::from_millis(10));
5641 detector.record_output(b"\x1b[H");
5642 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5643 assert_eq!(state_before, state_after);
5644 });
5645 }
5646
5647 #[test]
5648 fn idle_detector_record_output_not_enabled() {
5649 pyo3::prepare_freethreaded_python();
5650 pyo3::Python::with_gil(|py| {
5651 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5652 let detector =
5653 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, false, true, 5.0);
5654 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5655 std::thread::sleep(std::time::Duration::from_millis(10));
5656 detector.record_output(b"visible");
5657 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5658 assert_eq!(state_before, state_after);
5659 });
5660 }
5661
5662 #[test]
5663 fn idle_detector_record_input_not_enabled() {
5664 pyo3::prepare_freethreaded_python();
5665 pyo3::Python::with_gil(|py| {
5666 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5667 let detector =
5668 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, false, true, true, 5.0);
5669 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5670 std::thread::sleep(std::time::Duration::from_millis(10));
5671 detector.record_input(100);
5672 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5673 assert_eq!(state_before, state_after);
5674 });
5675 }
5676
5677 #[test]
5678 fn idle_detector_record_input_nonzero_bytes_resets() {
5679 pyo3::prepare_freethreaded_python();
5680 pyo3::Python::with_gil(|py| {
5681 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5682 let detector =
5683 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
5684 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5685 std::thread::sleep(std::time::Duration::from_millis(10));
5686 detector.record_input(100);
5687 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5688 assert!(state_after > state_before);
5689 });
5690 }
5691
5692 #[test]
5693 fn idle_detector_record_output_visible_resets() {
5694 pyo3::prepare_freethreaded_python();
5695 pyo3::Python::with_gil(|py| {
5696 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5697 let detector =
5698 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
5699 let state_before = detector.core.state.lock().unwrap().last_reset_at;
5700 std::thread::sleep(std::time::Duration::from_millis(10));
5701 detector.record_output(b"visible output");
5702 let state_after = detector.core.state.lock().unwrap().last_reset_at;
5703 assert!(state_after > state_before);
5704 });
5705 }
5706
5707 #[test]
5708 fn idle_detector_mark_exit_sets_returncode() {
5709 pyo3::prepare_freethreaded_python();
5710 pyo3::Python::with_gil(|py| {
5711 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
5712 let detector =
5713 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 0.0);
5714 detector.mark_exit(42, false);
5715 let state = detector.core.state.lock().unwrap();
5716 assert_eq!(state.returncode, Some(42));
5717 assert!(!state.interrupted);
5718 });
5719 }
5720
5721 #[test]
5724 fn find_expect_match_literal_found() {
5725 pyo3::prepare_freethreaded_python();
5726 pyo3::Python::with_gil(|py| {
5727 let process = make_test_running_process(py);
5728 let result = process
5729 .find_expect_match("hello world", "world", false)
5730 .unwrap();
5731 assert!(result.is_some());
5732 let (matched, start, end, groups) = result.unwrap();
5733 assert_eq!(matched, "world");
5734 assert_eq!(start, 6);
5735 assert_eq!(end, 11);
5736 assert!(groups.is_empty());
5737 });
5738 }
5739
5740 #[test]
5741 fn find_expect_match_literal_not_found() {
5742 pyo3::prepare_freethreaded_python();
5743 pyo3::Python::with_gil(|py| {
5744 let process = make_test_running_process(py);
5745 let result = process
5746 .find_expect_match("hello world", "missing", false)
5747 .unwrap();
5748 assert!(result.is_none());
5749 });
5750 }
5751
5752 #[test]
5753 fn find_expect_match_regex_found() {
5754 pyo3::prepare_freethreaded_python();
5755 pyo3::Python::with_gil(|py| {
5756 let process = make_test_running_process(py);
5757 let result = process
5758 .find_expect_match("hello 123 world", r"\d+", true)
5759 .unwrap();
5760 assert!(result.is_some());
5761 let (matched, start, end, _) = result.unwrap();
5762 assert_eq!(matched, "123");
5763 assert_eq!(start, 6);
5764 assert_eq!(end, 9);
5765 });
5766 }
5767
5768 #[test]
5769 fn find_expect_match_regex_with_groups() {
5770 pyo3::prepare_freethreaded_python();
5771 pyo3::Python::with_gil(|py| {
5772 let process = make_test_running_process(py);
5773 let result = process
5774 .find_expect_match("hello 123 world", r"(\d+) (\w+)", true)
5775 .unwrap();
5776 assert!(result.is_some());
5777 let (_, _, _, groups) = result.unwrap();
5778 assert_eq!(groups.len(), 2);
5779 assert_eq!(groups[0], "123");
5780 assert_eq!(groups[1], "world");
5781 });
5782 }
5783
5784 #[test]
5785 fn find_expect_match_regex_not_found() {
5786 pyo3::prepare_freethreaded_python();
5787 pyo3::Python::with_gil(|py| {
5788 let process = make_test_running_process(py);
5789 let result = process
5790 .find_expect_match("hello world", r"\d+", true)
5791 .unwrap();
5792 assert!(result.is_none());
5793 });
5794 }
5795
5796 #[test]
5797 fn find_expect_match_invalid_regex_errors() {
5798 pyo3::prepare_freethreaded_python();
5799 pyo3::Python::with_gil(|py| {
5800 let process = make_test_running_process(py);
5801 let result = process.find_expect_match("test", r"[invalid", true);
5802 assert!(result.is_err());
5803 });
5804 }
5805
5806 fn make_test_running_process(py: Python<'_>) -> NativeRunningProcess {
5807 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5808 NativeRunningProcess::new(
5809 cmd.as_any(),
5810 None,
5811 false,
5812 true,
5813 None,
5814 None,
5815 true,
5816 None,
5817 None,
5818 "inherit",
5819 "stdout",
5820 None,
5821 false,
5822 )
5823 .unwrap()
5824 }
5825
5826 #[test]
5829 fn parse_command_string_with_shell() {
5830 pyo3::prepare_freethreaded_python();
5831 pyo3::Python::with_gil(|py| {
5832 let cmd = pyo3::types::PyString::new(py, "echo hello");
5833 let result = parse_command(cmd.as_any(), true).unwrap();
5834 assert!(matches!(result, CommandSpec::Shell(ref s) if s == "echo hello"));
5835 });
5836 }
5837
5838 #[test]
5839 fn parse_command_string_without_shell_errors() {
5840 pyo3::prepare_freethreaded_python();
5841 pyo3::Python::with_gil(|py| {
5842 let cmd = pyo3::types::PyString::new(py, "echo hello");
5843 let result = parse_command(cmd.as_any(), false);
5844 assert!(result.is_err());
5845 });
5846 }
5847
5848 #[test]
5849 fn parse_command_list_without_shell() {
5850 pyo3::prepare_freethreaded_python();
5851 pyo3::Python::with_gil(|py| {
5852 let cmd = pyo3::types::PyList::new(py, ["echo", "hello"]).unwrap();
5853 let result = parse_command(cmd.as_any(), false).unwrap();
5854 assert!(matches!(result, CommandSpec::Argv(ref v) if v.len() == 2));
5855 });
5856 }
5857
5858 #[test]
5859 fn parse_command_list_with_shell_joins() {
5860 pyo3::prepare_freethreaded_python();
5861 pyo3::Python::with_gil(|py| {
5862 let cmd = pyo3::types::PyList::new(py, ["echo", "hello"]).unwrap();
5863 let result = parse_command(cmd.as_any(), true).unwrap();
5864 assert!(matches!(result, CommandSpec::Shell(ref s) if s == "echo hello"));
5865 });
5866 }
5867
5868 #[test]
5869 fn parse_command_empty_list_errors() {
5870 pyo3::prepare_freethreaded_python();
5871 pyo3::Python::with_gil(|py| {
5872 let cmd = pyo3::types::PyList::empty(py);
5873 let result = parse_command(cmd.as_any(), false);
5874 assert!(result.is_err());
5875 });
5876 }
5877
5878 #[test]
5879 fn parse_command_invalid_type_errors() {
5880 pyo3::prepare_freethreaded_python();
5881 pyo3::Python::with_gil(|py| {
5882 let cmd = 42i32.into_pyobject(py).unwrap();
5883 let result = parse_command(cmd.as_any(), false);
5884 assert!(result.is_err());
5885 });
5886 }
5887
5888 #[test]
5891 fn stream_kind_stdout() {
5892 let result = stream_kind("stdout").unwrap();
5893 assert_eq!(result, StreamKind::Stdout);
5894 }
5895
5896 #[test]
5897 fn stream_kind_stderr() {
5898 let result = stream_kind("stderr").unwrap();
5899 assert_eq!(result, StreamKind::Stderr);
5900 }
5901
5902 #[test]
5903 fn stream_kind_invalid() {
5904 let result = stream_kind("invalid");
5905 assert!(result.is_err());
5906 }
5907
5908 #[test]
5911 fn stdin_mode_inherit() {
5912 assert_eq!(stdin_mode("inherit").unwrap(), StdinMode::Inherit);
5913 }
5914
5915 #[test]
5916 fn stdin_mode_piped() {
5917 assert_eq!(stdin_mode("piped").unwrap(), StdinMode::Piped);
5918 }
5919
5920 #[test]
5921 fn stdin_mode_null() {
5922 assert_eq!(stdin_mode("null").unwrap(), StdinMode::Null);
5923 }
5924
5925 #[test]
5926 fn stdin_mode_invalid() {
5927 assert!(stdin_mode("invalid").is_err());
5928 }
5929
5930 #[test]
5933 fn stderr_mode_stdout() {
5934 assert_eq!(stderr_mode("stdout").unwrap(), StderrMode::Stdout);
5935 }
5936
5937 #[test]
5938 fn stderr_mode_pipe() {
5939 assert_eq!(stderr_mode("pipe").unwrap(), StderrMode::Pipe);
5940 }
5941
5942 #[test]
5943 fn stderr_mode_invalid() {
5944 assert!(stderr_mode("invalid").is_err());
5945 }
5946
5947 #[cfg(windows)]
5950 mod windows_additional_tests {
5951 use super::*;
5952 use winapi::um::winuser::VK_F1;
5953
5954 #[test]
5957 fn control_char_at_sign() {
5958 assert_eq!(control_character_for_unicode('@' as u16), Some(0x00));
5959 }
5960
5961 #[test]
5962 fn control_char_space() {
5963 assert_eq!(control_character_for_unicode(' ' as u16), Some(0x00));
5964 }
5965
5966 #[test]
5967 fn control_char_a() {
5968 assert_eq!(control_character_for_unicode('a' as u16), Some(0x01));
5969 }
5970
5971 #[test]
5972 fn control_char_z() {
5973 assert_eq!(control_character_for_unicode('z' as u16), Some(0x1A));
5974 }
5975
5976 #[test]
5977 fn control_char_bracket() {
5978 assert_eq!(control_character_for_unicode('[' as u16), Some(0x1B));
5979 }
5980
5981 #[test]
5982 fn control_char_backslash() {
5983 assert_eq!(control_character_for_unicode('\\' as u16), Some(0x1C));
5984 }
5985
5986 #[test]
5987 fn control_char_close_bracket() {
5988 assert_eq!(control_character_for_unicode(']' as u16), Some(0x1D));
5989 }
5990
5991 #[test]
5992 fn control_char_caret() {
5993 assert_eq!(control_character_for_unicode('^' as u16), Some(0x1E));
5994 }
5995
5996 #[test]
5997 fn control_char_underscore() {
5998 assert_eq!(control_character_for_unicode('_' as u16), Some(0x1F));
5999 }
6000
6001 #[test]
6002 fn control_char_digit_returns_none() {
6003 assert_eq!(control_character_for_unicode('0' as u16), None);
6004 }
6005
6006 #[test]
6007 fn control_char_exclamation_returns_none() {
6008 assert_eq!(control_character_for_unicode('!' as u16), None);
6009 }
6010
6011 #[test]
6014 fn modifier_param_no_modifiers_returns_none() {
6015 assert_eq!(terminal_input_modifier_parameter(false, false, false), None);
6016 }
6017
6018 #[test]
6019 fn modifier_param_shift_only() {
6020 assert_eq!(
6021 terminal_input_modifier_parameter(true, false, false),
6022 Some(2)
6023 );
6024 }
6025
6026 #[test]
6027 fn modifier_param_alt_only() {
6028 assert_eq!(
6029 terminal_input_modifier_parameter(false, true, false),
6030 Some(3)
6031 );
6032 }
6033
6034 #[test]
6035 fn modifier_param_ctrl_only() {
6036 assert_eq!(
6037 terminal_input_modifier_parameter(false, false, true),
6038 Some(5)
6039 );
6040 }
6041
6042 #[test]
6043 fn modifier_param_shift_ctrl() {
6044 assert_eq!(
6045 terminal_input_modifier_parameter(true, false, true),
6046 Some(6)
6047 );
6048 }
6049
6050 #[test]
6051 fn modifier_param_shift_alt() {
6052 assert_eq!(
6053 terminal_input_modifier_parameter(true, true, false),
6054 Some(4)
6055 );
6056 }
6057
6058 #[test]
6059 fn modifier_param_all_modifiers() {
6060 assert_eq!(terminal_input_modifier_parameter(true, true, true), Some(8));
6061 }
6062
6063 #[test]
6066 fn tilde_sequence_no_modifier() {
6067 let result = repeated_tilde_sequence(3, None, 1);
6068 assert_eq!(result, b"\x1b[3~");
6069 }
6070
6071 #[test]
6072 fn tilde_sequence_with_modifier() {
6073 let result = repeated_tilde_sequence(3, Some(2), 1);
6074 assert_eq!(result, b"\x1b[3;2~");
6075 }
6076
6077 #[test]
6078 fn tilde_sequence_repeated() {
6079 let result = repeated_tilde_sequence(3, None, 3);
6080 assert_eq!(result, b"\x1b[3~\x1b[3~\x1b[3~");
6081 }
6082
6083 #[test]
6086 fn modified_sequence_no_modifier() {
6087 let result = repeated_modified_sequence(b"\x1b[A", None, 1);
6088 assert_eq!(result, b"\x1b[A");
6089 }
6090
6091 #[test]
6092 fn modified_sequence_with_modifier() {
6093 let result = repeated_modified_sequence(b"\x1b[A", Some(2), 1);
6094 assert_eq!(result, b"\x1b[1;2A");
6095 }
6096
6097 #[test]
6098 fn modified_sequence_repeated_with_modifier() {
6099 let result = repeated_modified_sequence(b"\x1b[A", Some(5), 2);
6100 assert_eq!(result, b"\x1b[1;5A\x1b[1;5A");
6101 }
6102
6103 #[test]
6106 fn format_bytes_empty() {
6107 assert_eq!(format_terminal_input_bytes(&[]), "[]");
6108 }
6109
6110 #[test]
6111 fn format_bytes_multiple() {
6112 assert_eq!(
6113 format_terminal_input_bytes(&[0x1B, 0x5B, 0x41]),
6114 "[1b 5b 41]"
6115 );
6116 }
6117
6118 #[test]
6121 fn trace_target_empty_env_returns_none() {
6122 std::env::remove_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV);
6123 assert!(native_terminal_input_trace_target().is_none());
6124 }
6125
6126 #[test]
6127 fn trace_target_whitespace_env_returns_none() {
6128 std::env::set_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV, " ");
6129 assert!(native_terminal_input_trace_target().is_none());
6130 std::env::remove_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV);
6131 }
6132
6133 #[test]
6134 fn trace_target_valid_env_returns_value() {
6135 std::env::set_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV, "/tmp/trace.log");
6136 let result = native_terminal_input_trace_target();
6137 assert_eq!(result, Some("/tmp/trace.log".to_string()));
6138 std::env::remove_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV);
6139 }
6140
6141 #[test]
6144 fn translate_key_up_event_returns_none() {
6145 let mut event: KEY_EVENT_RECORD = unsafe { std::mem::zeroed() };
6146 event.bKeyDown = 0;
6147 event.wVirtualKeyCode = VK_RETURN as u16;
6148 let result = translate_console_key_event(&event);
6149 assert!(result.is_none());
6150 }
6151
6152 #[test]
6155 fn translate_f1_key_returns_none() {
6156 let event = key_event(VK_F1 as u16, 0, 0, 1);
6157 let result = translate_console_key_event(&event);
6158 assert!(result.is_none());
6159 }
6160
6161 #[test]
6164 fn translate_alt_a_has_escape_prefix() {
6165 let event = key_event('a' as u16, 'a' as u16, LEFT_ALT_PRESSED, 1);
6166 let result = translate_console_key_event(&event).unwrap();
6167 assert!(result.data.starts_with(b"\x1b"));
6168 assert!(result.alt);
6169 }
6170
6171 #[test]
6174 fn translate_ctrl_c_produces_etx() {
6175 let event = key_event('C' as u16, 'c' as u16, LEFT_CTRL_PRESSED, 1);
6176 let result = translate_console_key_event(&event).unwrap();
6177 assert_eq!(result.data, &[0x03]);
6178 assert!(result.ctrl);
6179 }
6180 }
6181
6182 #[test]
6185 fn terminal_input_new_starts_closed() {
6186 let input = NativeTerminalInput::new();
6187 assert!(!input.capturing());
6188 let state = input.state.lock().unwrap();
6189 assert!(state.closed);
6190 assert!(state.events.is_empty());
6191 }
6192
6193 #[test]
6194 fn terminal_input_available_false_when_empty() {
6195 let input = NativeTerminalInput::new();
6196 assert!(!input.available());
6197 }
6198
6199 #[test]
6200 fn terminal_input_next_event_none_when_empty() {
6201 let input = NativeTerminalInput::new();
6202 assert!(input.next_event().is_none());
6203 }
6204
6205 #[test]
6206 fn terminal_input_inject_and_consume_event() {
6207 let input = NativeTerminalInput::new();
6208 {
6209 let mut state = input.state.lock().unwrap();
6210 state.events.push_back(TerminalInputEventRecord {
6211 data: b"test".to_vec(),
6212 submit: false,
6213 shift: false,
6214 ctrl: false,
6215 alt: false,
6216 virtual_key_code: 0,
6217 repeat_count: 1,
6218 });
6219 }
6220 assert!(input.available());
6221 let event = input.next_event().unwrap();
6222 assert_eq!(event.data, b"test");
6223 assert!(!input.available());
6224 }
6225
6226 #[test]
6227 #[cfg(not(windows))]
6228 fn terminal_input_start_errors_on_non_windows() {
6229 pyo3::prepare_freethreaded_python();
6230 let input = NativeTerminalInput::new();
6231 let result = input.start();
6232 assert!(result.is_err());
6233 }
6234
6235 #[test]
6238 fn terminal_input_event_repr() {
6239 let event = NativeTerminalInputEvent {
6240 data: vec![0x0D],
6241 submit: true,
6242 shift: false,
6243 ctrl: false,
6244 alt: false,
6245 virtual_key_code: 13,
6246 repeat_count: 1,
6247 };
6248 let repr = event.__repr__();
6249 assert!(repr.contains("submit=true"));
6250 assert!(repr.contains("virtual_key_code=13"));
6251 }
6252
6253 #[test]
6256 fn tracked_process_db_path_with_env() {
6257 pyo3::prepare_freethreaded_python();
6258 std::env::set_var("RUNNING_PROCESS_PID_DB", "/custom/path/db.sqlite3");
6259 let result = tracked_process_db_path().unwrap();
6260 assert_eq!(result, std::path::PathBuf::from("/custom/path/db.sqlite3"));
6261 std::env::remove_var("RUNNING_PROCESS_PID_DB");
6262 }
6263
6264 #[test]
6265 fn tracked_process_db_path_empty_env_falls_back() {
6266 pyo3::prepare_freethreaded_python();
6267 std::env::set_var("RUNNING_PROCESS_PID_DB", " ");
6268 let result = tracked_process_db_path().unwrap();
6269 assert!(!result.to_str().unwrap().trim().is_empty());
6270 std::env::remove_var("RUNNING_PROCESS_PID_DB");
6271 }
6272
6273 #[test]
6276 #[cfg(not(windows))]
6277 fn pty_process_start_terminal_input_relay_errors_on_non_windows() {
6278 pyo3::prepare_freethreaded_python();
6279 pyo3::Python::with_gil(|_py| {
6280 let argv = vec!["echo".to_string(), "test".to_string()];
6281 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6282 let result = process.start_terminal_input_relay_impl();
6283 assert!(result.is_err());
6284 });
6285 }
6286
6287 #[test]
6288 #[cfg(not(windows))]
6289 fn pty_process_terminal_input_relay_active_false_on_non_windows() {
6290 pyo3::prepare_freethreaded_python();
6291 pyo3::Python::with_gil(|_py| {
6292 let argv = vec!["echo".to_string(), "test".to_string()];
6293 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6294 assert!(!process.terminal_input_relay_active());
6295 });
6296 }
6297
6298 #[test]
6301 fn process_metrics_sample_nonexistent_pid() {
6302 pyo3::prepare_freethreaded_python();
6303 let metrics = NativeProcessMetrics::new(999999);
6304 let (alive, cpu, io, _) = metrics.sample();
6305 assert!(!alive);
6306 assert_eq!(cpu, 0.0);
6307 assert_eq!(io, 0);
6308 }
6309
6310 #[test]
6311 fn process_metrics_prime_no_panic() {
6312 pyo3::prepare_freethreaded_python();
6313 let metrics = NativeProcessMetrics::new(999999);
6314 metrics.prime();
6315 }
6316
6317 #[test]
6320 fn active_process_record_clone() {
6321 let record = ActiveProcessRecord {
6322 pid: 1234,
6323 kind: "test".to_string(),
6324 command: "echo".to_string(),
6325 cwd: Some("/tmp".to_string()),
6326 started_at: 1000.0,
6327 };
6328 let cloned = record.clone();
6329 assert_eq!(cloned.pid, 1234);
6330 assert_eq!(cloned.kind, "test");
6331 assert_eq!(cloned.command, "echo");
6332 assert_eq!(cloned.cwd, Some("/tmp".to_string()));
6333 }
6334
6335 #[test]
6338 fn pty_process_empty_argv_errors() {
6339 pyo3::prepare_freethreaded_python();
6340 pyo3::Python::with_gil(|_py| {
6341 let result = NativePtyProcess::new(vec![], None, None, 24, 80, None);
6342 assert!(result.is_err());
6343 });
6344 }
6345
6346 #[test]
6349 #[cfg(not(windows))]
6350 fn pty_process_start_already_started_errors() {
6351 pyo3::prepare_freethreaded_python();
6352 pyo3::Python::with_gil(|_py| {
6353 let argv = vec![
6354 "python".to_string(),
6355 "-c".to_string(),
6356 "import time; time.sleep(0.1)".to_string(),
6357 ];
6358 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6359 process.start_impl().unwrap();
6360 let result = process.start_impl();
6361 assert!(result.is_err());
6362 let _ = process.close_impl();
6363 });
6364 }
6365
6366 #[test]
6369 fn pty_buffer_new_defaults() {
6370 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6371 assert!(!buf.available());
6372 assert_eq!(buf.history_bytes(), 0);
6373 }
6374
6375 #[test]
6376 fn pty_buffer_record_output_makes_available() {
6377 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6378 buf.record_output(b"hello");
6379 assert!(buf.available());
6380 }
6381
6382 #[test]
6383 fn pty_buffer_history_bytes_accumulates() {
6384 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6385 buf.record_output(b"hello");
6386 assert_eq!(buf.history_bytes(), 5);
6387 buf.record_output(b" world");
6388 assert_eq!(buf.history_bytes(), 11);
6389 }
6390
6391 #[test]
6392 fn pty_buffer_clear_history_resets_to_zero() {
6393 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6394 buf.record_output(b"data");
6395 let released = buf.clear_history();
6396 assert_eq!(released, 4);
6397 assert_eq!(buf.history_bytes(), 0);
6398 }
6399
6400 #[test]
6401 fn pty_buffer_close_sets_closed_flag() {
6402 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6403 buf.close();
6404 let state = buf.state.lock().unwrap();
6405 assert!(state.closed);
6406 }
6407
6408 #[test]
6409 fn pty_buffer_record_multiple_chunks_all_available() {
6410 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
6411 buf.record_output(b"a");
6412 buf.record_output(b"bb");
6413 buf.record_output(b"ccc");
6414 assert_eq!(buf.history_bytes(), 6);
6415 let state = buf.state.lock().unwrap();
6416 assert_eq!(state.chunks.len(), 3);
6417 }
6418
6419 #[test]
6422 fn pty_process_pid_none_before_start() {
6423 pyo3::prepare_freethreaded_python();
6424 pyo3::Python::with_gil(|_py| {
6425 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6426 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6427 assert!(process.pid().unwrap().is_none());
6428 });
6429 }
6430
6431 #[test]
6432 #[cfg(not(windows))]
6433 fn pty_process_lifecycle_start_wait_close() {
6434 pyo3::prepare_freethreaded_python();
6435 pyo3::Python::with_gil(|_py| {
6436 let argv = vec![
6437 "python".to_string(),
6438 "-c".to_string(),
6439 "print('hello')".to_string(),
6440 ];
6441 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6442 process.start_impl().unwrap();
6443 assert!(process.pid().unwrap().is_some());
6444 let code = process.wait_impl(Some(10.0)).unwrap();
6445 assert_eq!(code, 0);
6446 let _ = process.close_impl();
6447 });
6448 }
6449
6450 #[test]
6451 #[cfg(not(windows))]
6452 fn pty_process_poll_none_while_running() {
6453 pyo3::prepare_freethreaded_python();
6454 pyo3::Python::with_gil(|_py| {
6455 let argv = vec![
6456 "python".to_string(),
6457 "-c".to_string(),
6458 "import time; time.sleep(5)".to_string(),
6459 ];
6460 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6461 process.start_impl().unwrap();
6462 assert!(process.poll().unwrap().is_none());
6463 let _ = process.close_impl();
6464 });
6465 }
6466
6467 #[test]
6468 #[cfg(not(windows))]
6469 fn pty_process_nonzero_exit_code() {
6470 pyo3::prepare_freethreaded_python();
6471 pyo3::Python::with_gil(|_py| {
6472 let argv = vec![
6473 "python".to_string(),
6474 "-c".to_string(),
6475 "import sys; sys.exit(42)".to_string(),
6476 ];
6477 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6478 process.start_impl().unwrap();
6479 let code = process.wait_impl(Some(10.0)).unwrap();
6480 assert_eq!(code, 42);
6481 let _ = process.close_impl();
6482 });
6483 }
6484
6485 #[test]
6486 fn pty_process_write_before_start_errors() {
6487 pyo3::prepare_freethreaded_python();
6488 pyo3::Python::with_gil(|_py| {
6489 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6490 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6491 assert!(process.write_impl(b"test", false).is_err());
6492 });
6493 }
6494
6495 #[test]
6496 #[cfg(not(windows))]
6497 fn pty_process_input_metrics_tracked() {
6498 pyo3::prepare_freethreaded_python();
6499 pyo3::Python::with_gil(|_py| {
6500 let argv = vec![
6501 "python".to_string(),
6502 "-c".to_string(),
6503 "import time; time.sleep(2)".to_string(),
6504 ];
6505 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6506 process.start_impl().unwrap();
6507 assert_eq!(process.pty_input_bytes_total(), 0);
6508 let _ = process.write_impl(b"hello\n", false);
6509 assert_eq!(process.pty_input_bytes_total(), 6);
6510 assert_eq!(process.pty_newline_events_total(), 1);
6511 let _ = process.write_impl(b"x", true);
6512 assert_eq!(process.pty_submit_events_total(), 1);
6513 let _ = process.close_impl();
6514 });
6515 }
6516
6517 #[test]
6518 #[cfg(not(windows))]
6519 fn pty_process_resize_while_running() {
6520 pyo3::prepare_freethreaded_python();
6521 pyo3::Python::with_gil(|_py| {
6522 let argv = vec![
6523 "python".to_string(),
6524 "-c".to_string(),
6525 "import time; time.sleep(2)".to_string(),
6526 ];
6527 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6528 process.start_impl().unwrap();
6529 assert!(process.resize_impl(40, 120).is_ok());
6530 let _ = process.close_impl();
6531 });
6532 }
6533
6534 #[test]
6535 #[cfg(not(windows))]
6536 fn pty_process_kill_running_process() {
6537 pyo3::prepare_freethreaded_python();
6538 pyo3::Python::with_gil(|_py| {
6539 let argv = vec![
6540 "python".to_string(),
6541 "-c".to_string(),
6542 "import time; time.sleep(0.1)".to_string(),
6543 ];
6544 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6545 process.start_impl().unwrap();
6546 assert!(process.kill_impl().is_ok());
6547 });
6548 }
6549
6550 #[test]
6551 #[cfg(not(windows))]
6552 fn pty_process_terminate_running_process() {
6553 pyo3::prepare_freethreaded_python();
6554 pyo3::Python::with_gil(|_py| {
6555 let argv = vec![
6556 "python".to_string(),
6557 "-c".to_string(),
6558 "import time; time.sleep(0.1)".to_string(),
6559 ];
6560 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6561 process.start_impl().unwrap();
6562 assert!(process.terminate_impl().is_ok());
6563 let _ = process.close_impl();
6564 });
6565 }
6566
6567 #[test]
6568 #[cfg(not(windows))]
6569 fn pty_process_close_already_closed_is_noop() {
6570 pyo3::prepare_freethreaded_python();
6571 pyo3::Python::with_gil(|_py| {
6572 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6573 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6574 process.start_impl().unwrap();
6575 let _ = process.wait_impl(Some(10.0));
6576 let _ = process.close_impl();
6577 assert!(process.close_impl().is_ok());
6578 });
6579 }
6580
6581 #[test]
6582 #[cfg(not(windows))]
6583 fn pty_process_wait_timeout_errors() {
6584 pyo3::prepare_freethreaded_python();
6585 pyo3::Python::with_gil(|_py| {
6586 let argv = vec![
6587 "python".to_string(),
6588 "-c".to_string(),
6589 "import time; time.sleep(0.1)".to_string(),
6590 ];
6591 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6592 process.start_impl().unwrap();
6593 assert!(process.wait_impl(Some(0.1)).is_err());
6594 let _ = process.close_impl();
6595 });
6596 }
6597
6598 #[test]
6599 fn pty_process_send_interrupt_before_start_errors() {
6600 pyo3::prepare_freethreaded_python();
6601 pyo3::Python::with_gil(|_py| {
6602 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6603 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6604 assert!(process.send_interrupt_impl().is_err());
6605 });
6606 }
6607
6608 #[test]
6609 fn pty_process_terminate_before_start_errors() {
6610 pyo3::prepare_freethreaded_python();
6611 pyo3::Python::with_gil(|_py| {
6612 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6613 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6614 assert!(process.terminate_impl().is_err());
6615 });
6616 }
6617
6618 #[test]
6619 fn pty_process_kill_before_start_errors() {
6620 pyo3::prepare_freethreaded_python();
6621 pyo3::Python::with_gil(|_py| {
6622 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6623 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6624 assert!(process.kill_impl().is_err());
6625 });
6626 }
6627
6628 #[test]
6631 fn kill_process_tree_nonexistent_pid_is_noop() {
6632 kill_process_tree_impl(999999, 0.5);
6633 }
6634
6635 #[test]
6636 fn get_process_tree_info_current_pid() {
6637 let pid = std::process::id();
6638 let info = native_get_process_tree_info(pid);
6639 assert!(info.contains(&format!("{}", pid)));
6640 }
6641
6642 #[test]
6643 fn get_process_tree_info_nonexistent_pid() {
6644 let info = native_get_process_tree_info(999999);
6645 assert!(info.contains("Could not get process info"));
6646 }
6647
6648 #[test]
6649 fn register_and_list_active_processes() {
6650 let fake_pid = 777777u32;
6651 register_active_process(
6652 fake_pid,
6653 "test",
6654 "echo hello",
6655 Some("/tmp".to_string()),
6656 1000.0,
6657 );
6658 let items = native_list_active_processes();
6659 assert!(items.iter().any(|e| e.0 == fake_pid));
6660 unregister_active_process(fake_pid);
6661 let items = native_list_active_processes();
6662 assert!(!items.iter().any(|e| e.0 == fake_pid));
6663 }
6664
6665 #[test]
6666 fn process_created_at_current_process_returns_some() {
6667 let created = process_created_at(std::process::id());
6668 assert!(created.is_some());
6669 assert!(created.unwrap() > 0.0);
6670 }
6671
6672 #[test]
6673 fn process_created_at_nonexistent_returns_none() {
6674 assert!(process_created_at(999999).is_none());
6675 }
6676
6677 #[test]
6678 fn same_process_identity_current_process_matches() {
6679 let pid = std::process::id();
6680 let created = process_created_at(pid).unwrap();
6681 assert!(same_process_identity(pid, created, 2.0));
6682 }
6683
6684 #[test]
6685 fn same_process_identity_wrong_time_no_match() {
6686 assert!(!same_process_identity(std::process::id(), 0.0, 1.0));
6687 }
6688
6689 #[test]
6690 #[cfg(windows)]
6691 fn windows_apply_process_priority_current_pid_ok() {
6692 pyo3::prepare_freethreaded_python();
6693 assert!(windows_apply_process_priority_impl(std::process::id(), 0).is_ok());
6694 }
6695
6696 #[test]
6697 #[cfg(windows)]
6698 fn windows_apply_process_priority_nonexistent_errors() {
6699 pyo3::prepare_freethreaded_python();
6700 assert!(windows_apply_process_priority_impl(999999, 0).is_err());
6701 }
6702
6703 #[test]
6704 fn signal_bool_new_default_false() {
6705 assert!(!NativeSignalBool::new(false).load_nolock());
6706 }
6707
6708 #[test]
6709 fn signal_bool_new_true() {
6710 assert!(NativeSignalBool::new(true).load_nolock());
6711 }
6712
6713 #[test]
6714 fn signal_bool_store_locked_changes_value() {
6715 let sb = NativeSignalBool::new(false);
6716 sb.store_locked(true);
6717 assert!(sb.load_nolock());
6718 }
6719
6720 #[test]
6721 fn signal_bool_compare_and_swap_success_iter3() {
6722 let sb = NativeSignalBool::new(false);
6723 assert!(sb.compare_and_swap_locked(false, true));
6724 assert!(sb.load_nolock());
6725 }
6726
6727 #[test]
6728 fn idle_monitor_state_initial_values() {
6729 let state = IdleMonitorState {
6730 last_reset_at: Instant::now(),
6731 returncode: None,
6732 interrupted: false,
6733 };
6734 assert!(state.returncode.is_none());
6735 assert!(!state.interrupted);
6736 }
6737
6738 #[test]
6739 #[cfg(windows)]
6740 fn terminal_input_wait_returns_event_immediately() {
6741 let state = Arc::new(Mutex::new(TerminalInputState {
6742 events: {
6743 let mut q = VecDeque::new();
6744 q.push_back(TerminalInputEventRecord {
6745 data: b"x".to_vec(),
6746 submit: false,
6747 shift: false,
6748 ctrl: false,
6749 alt: false,
6750 virtual_key_code: 0,
6751 repeat_count: 1,
6752 });
6753 q
6754 },
6755 closed: false,
6756 }));
6757 let condvar = Arc::new(Condvar::new());
6758 match wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(100))) {
6759 TerminalInputWaitOutcome::Event(e) => assert_eq!(e.data, b"x"),
6760 _ => panic!("expected Event"),
6761 }
6762 }
6763
6764 #[test]
6765 #[cfg(windows)]
6766 fn terminal_input_wait_returns_closed() {
6767 let state = Arc::new(Mutex::new(TerminalInputState {
6768 events: VecDeque::new(),
6769 closed: true,
6770 }));
6771 let condvar = Arc::new(Condvar::new());
6772 assert!(matches!(
6773 wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(100))),
6774 TerminalInputWaitOutcome::Closed
6775 ));
6776 }
6777
6778 #[test]
6779 #[cfg(windows)]
6780 fn terminal_input_wait_returns_timeout() {
6781 let state = Arc::new(Mutex::new(TerminalInputState {
6782 events: VecDeque::new(),
6783 closed: false,
6784 }));
6785 let condvar = Arc::new(Condvar::new());
6786 assert!(matches!(
6787 wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(50))),
6788 TerminalInputWaitOutcome::Timeout
6789 ));
6790 }
6791
6792 #[test]
6793 fn native_running_process_is_pty_available_false() {
6794 assert!(!NativeRunningProcess::is_pty_available());
6795 }
6796
6797 #[test]
6798 #[cfg(not(windows))]
6799 fn posix_input_payload_passthrough() {
6800 assert_eq!(pty_platform::input_payload(b"hello\n"), b"hello\n");
6801 }
6802
6803 #[test]
6816 #[cfg(windows)]
6817 fn pty_process_start_and_close_windows() {
6818 pyo3::prepare_freethreaded_python();
6819 pyo3::Python::with_gil(|_py| {
6820 let argv = vec![
6821 "python".to_string(),
6822 "-c".to_string(),
6823 "print('hello')".to_string(),
6824 ];
6825 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6826 process.start_impl().unwrap();
6827 assert!(process.pid().unwrap().is_some());
6828 assert!(process.close_impl().is_ok());
6830 });
6831 }
6832
6833 #[test]
6834 #[cfg(windows)]
6835 fn pty_process_poll_none_while_running_windows() {
6836 pyo3::prepare_freethreaded_python();
6837 pyo3::Python::with_gil(|_py| {
6838 let argv = vec![
6839 "python".to_string(),
6840 "-c".to_string(),
6841 "import time; time.sleep(5)".to_string(),
6842 ];
6843 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6844 process.start_impl().unwrap();
6845 assert!(process.poll().unwrap().is_none());
6846 let _ = process.close_impl();
6847 });
6848 }
6849
6850 #[test]
6851 #[cfg(windows)]
6852 fn pty_process_kill_running_process_windows() {
6853 pyo3::prepare_freethreaded_python();
6854 pyo3::Python::with_gil(|_py| {
6855 let argv = vec![
6856 "python".to_string(),
6857 "-c".to_string(),
6858 "import time; time.sleep(0.1)".to_string(),
6859 ];
6860 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6861 process.start_impl().unwrap();
6862 assert!(process.kill_impl().is_ok());
6863 });
6864 }
6865
6866 #[test]
6867 #[cfg(windows)]
6868 fn pty_process_terminate_running_process_windows() {
6869 pyo3::prepare_freethreaded_python();
6870 pyo3::Python::with_gil(|_py| {
6871 let argv = vec![
6872 "python".to_string(),
6873 "-c".to_string(),
6874 "import time; time.sleep(0.1)".to_string(),
6875 ];
6876 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6877 process.start_impl().unwrap();
6878 assert!(process.terminate_impl().is_ok());
6880 });
6881 }
6882
6883 #[test]
6884 #[cfg(windows)]
6885 fn pty_process_close_not_started_is_ok_windows() {
6886 pyo3::prepare_freethreaded_python();
6887 pyo3::Python::with_gil(|_py| {
6888 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6889 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6890 assert!(process.close_impl().is_ok());
6892 });
6893 }
6894
6895 #[test]
6896 #[cfg(windows)]
6897 fn pty_process_start_already_started_errors_windows() {
6898 pyo3::prepare_freethreaded_python();
6899 pyo3::Python::with_gil(|_py| {
6900 let argv = vec![
6901 "python".to_string(),
6902 "-c".to_string(),
6903 "import time; time.sleep(0.1)".to_string(),
6904 ];
6905 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6906 process.start_impl().unwrap();
6907 let result = process.start_impl();
6908 assert!(result.is_err());
6909 let _ = process.close_impl();
6910 });
6911 }
6912
6913 #[test]
6914 #[cfg(windows)]
6915 fn pty_process_resize_while_running_windows() {
6916 pyo3::prepare_freethreaded_python();
6917 pyo3::Python::with_gil(|_py| {
6918 let argv = vec![
6919 "python".to_string(),
6920 "-c".to_string(),
6921 "import time; time.sleep(2)".to_string(),
6922 ];
6923 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6924 process.start_impl().unwrap();
6925 assert!(process.resize_impl(40, 120).is_ok());
6926 let _ = process.close_impl();
6927 });
6928 }
6929
6930 #[test]
6931 #[cfg(windows)]
6932 fn pty_process_write_windows() {
6933 pyo3::prepare_freethreaded_python();
6934 pyo3::Python::with_gil(|_py| {
6935 let argv = vec![
6936 "python".to_string(),
6937 "-c".to_string(),
6938 "import time; time.sleep(2)".to_string(),
6939 ];
6940 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6941 process.start_impl().unwrap();
6942 let _ = process.write_impl(b"hello\n", false);
6943 assert!(process.pty_input_bytes_total() >= 6);
6944 assert!(process.pty_newline_events_total() >= 1);
6945 let _ = process.close_impl();
6946 });
6947 }
6948
6949 #[test]
6950 #[cfg(windows)]
6951 fn pty_process_input_metrics_tracked_windows() {
6952 pyo3::prepare_freethreaded_python();
6953 pyo3::Python::with_gil(|_py| {
6954 let argv = vec![
6955 "python".to_string(),
6956 "-c".to_string(),
6957 "import time; time.sleep(2)".to_string(),
6958 ];
6959 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6960 process.start_impl().unwrap();
6961 assert_eq!(process.pty_input_bytes_total(), 0);
6962 let _ = process.write_impl(b"hello\n", false);
6963 assert_eq!(process.pty_input_bytes_total(), 6);
6964 assert_eq!(process.pty_newline_events_total(), 1);
6965 let _ = process.write_impl(b"x", true);
6966 assert_eq!(process.pty_submit_events_total(), 1);
6967 let _ = process.close_impl();
6968 });
6969 }
6970
6971 #[test]
6972 #[cfg(windows)]
6973 fn pty_process_send_interrupt_windows() {
6974 pyo3::prepare_freethreaded_python();
6975 pyo3::Python::with_gil(|_py| {
6976 let argv = vec![
6977 "python".to_string(),
6978 "-c".to_string(),
6979 "import time; time.sleep(0.1)".to_string(),
6980 ];
6981 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
6982 process.start_impl().unwrap();
6983 assert!(process.send_interrupt_impl().is_ok());
6985 let _ = process.close_impl();
6986 });
6987 }
6988
6989 #[test]
6990 #[cfg(windows)]
6991 fn pty_process_with_cwd_windows() {
6992 pyo3::prepare_freethreaded_python();
6993 pyo3::Python::with_gil(|_py| {
6994 let tmp = std::env::temp_dir();
6995 let cwd = tmp.to_str().unwrap().to_string();
6996 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
6997 let process = NativePtyProcess::new(argv, Some(cwd), None, 24, 80, None).unwrap();
6998 process.start_impl().unwrap();
6999 assert!(process.close_impl().is_ok());
7000 });
7001 }
7002
7003 #[test]
7004 #[cfg(windows)]
7005 fn pty_process_with_env_windows() {
7006 pyo3::prepare_freethreaded_python();
7007 pyo3::Python::with_gil(|py| {
7008 let env = pyo3::types::PyDict::new(py);
7009 if let Ok(path) = std::env::var("PATH") {
7010 env.set_item("PATH", &path).unwrap();
7011 }
7012 if let Ok(root) = std::env::var("SystemRoot") {
7013 env.set_item("SystemRoot", &root).unwrap();
7014 }
7015 env.set_item("RP_TEST_PTY", "test_value").unwrap();
7016 let argv = vec![
7017 "python".to_string(),
7018 "-c".to_string(),
7019 "import os; print(os.environ.get('RP_TEST_PTY', 'MISSING'))".to_string(),
7020 ];
7021 let process = NativePtyProcess::new(argv, None, Some(env), 24, 80, None).unwrap();
7022 process.start_impl().unwrap();
7023 assert!(process.close_impl().is_ok());
7024 });
7025 }
7026
7027 #[test]
7030 #[cfg(windows)]
7031 fn pty_process_terminal_input_relay_not_active_initially() {
7032 pyo3::prepare_freethreaded_python();
7033 pyo3::Python::with_gil(|_py| {
7034 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
7035 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
7036 assert!(!process.terminal_input_relay_active());
7037 });
7038 }
7039
7040 #[test]
7041 #[cfg(windows)]
7042 fn pty_process_stop_terminal_input_relay_noop_when_not_started() {
7043 pyo3::prepare_freethreaded_python();
7044 pyo3::Python::with_gil(|_py| {
7045 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
7046 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
7047 process.stop_terminal_input_relay_impl(); });
7049 }
7050
7051 #[test]
7054 #[cfg(windows)]
7055 fn assign_child_to_job_null_handle_errors() {
7056 pyo3::prepare_freethreaded_python();
7057 let result = assign_child_to_windows_kill_on_close_job(None);
7058 assert!(result.is_err());
7059 }
7060
7061 #[test]
7062 #[cfg(windows)]
7063 fn apply_windows_pty_priority_none_handle_ok() {
7064 pyo3::prepare_freethreaded_python();
7065 assert!(apply_windows_pty_priority(None, Some(5)).is_ok());
7067 assert!(apply_windows_pty_priority(None, None).is_ok());
7068 }
7069
7070 #[test]
7071 #[cfg(windows)]
7072 fn apply_windows_pty_priority_zero_nice_noop() {
7073 pyo3::prepare_freethreaded_python();
7074 use std::os::windows::io::AsRawHandle;
7076 let current = std::process::Command::new("cmd")
7077 .args(["/C", "echo"])
7078 .stdout(std::process::Stdio::null())
7079 .spawn()
7080 .unwrap();
7081 let handle = current.as_raw_handle();
7082 assert!(apply_windows_pty_priority(Some(handle), Some(0)).is_ok());
7083 assert!(apply_windows_pty_priority(Some(handle), None).is_ok());
7084 }
7085
7086 #[test]
7089 fn running_process_start_wait_lifecycle() {
7090 pyo3::prepare_freethreaded_python();
7091 pyo3::Python::with_gil(|py| {
7092 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('hello')"]).unwrap();
7093 let process = NativeRunningProcess::new(
7094 cmd.as_any(),
7095 None,
7096 false,
7097 true,
7098 None,
7099 None,
7100 true,
7101 None,
7102 None,
7103 "inherit",
7104 "stdout",
7105 None,
7106 false,
7107 )
7108 .unwrap();
7109 process.start_impl().unwrap();
7110 assert!(process.inner.pid().is_some());
7111 let code = process.wait_impl(py, Some(10.0)).unwrap();
7112 assert_eq!(code, 0);
7113 });
7114 }
7115
7116 #[test]
7117 fn running_process_kill_running() {
7118 pyo3::prepare_freethreaded_python();
7119 pyo3::Python::with_gil(|py| {
7120 let cmd =
7121 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
7122 .unwrap();
7123 let process = NativeRunningProcess::new(
7124 cmd.as_any(),
7125 None,
7126 false,
7127 false,
7128 None,
7129 None,
7130 true,
7131 None,
7132 None,
7133 "inherit",
7134 "stdout",
7135 None,
7136 false,
7137 )
7138 .unwrap();
7139 process.start_impl().unwrap();
7140 assert!(process.kill_impl().is_ok());
7141 });
7142 }
7143
7144 #[test]
7145 fn running_process_terminate_running() {
7146 pyo3::prepare_freethreaded_python();
7147 pyo3::Python::with_gil(|py| {
7148 let cmd =
7149 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
7150 .unwrap();
7151 let process = NativeRunningProcess::new(
7152 cmd.as_any(),
7153 None,
7154 false,
7155 false,
7156 None,
7157 None,
7158 true,
7159 None,
7160 None,
7161 "inherit",
7162 "stdout",
7163 None,
7164 false,
7165 )
7166 .unwrap();
7167 process.start_impl().unwrap();
7168 assert!(process.terminate_impl().is_ok());
7169 });
7170 }
7171
7172 #[test]
7173 fn running_process_close_finished() {
7174 pyo3::prepare_freethreaded_python();
7175 pyo3::Python::with_gil(|py| {
7176 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "pass"]).unwrap();
7177 let process = NativeRunningProcess::new(
7178 cmd.as_any(),
7179 None,
7180 false,
7181 false,
7182 None,
7183 None,
7184 true,
7185 None,
7186 None,
7187 "inherit",
7188 "stdout",
7189 None,
7190 false,
7191 )
7192 .unwrap();
7193 process.start_impl().unwrap();
7194 let _ = process.wait_impl(py, Some(10.0));
7195 assert!(process.close_impl(py).is_ok());
7196 });
7197 }
7198
7199 #[test]
7200 fn running_process_close_running() {
7201 pyo3::prepare_freethreaded_python();
7202 pyo3::Python::with_gil(|py| {
7203 let cmd =
7204 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
7205 .unwrap();
7206 let process = NativeRunningProcess::new(
7207 cmd.as_any(),
7208 None,
7209 false,
7210 false,
7211 None,
7212 None,
7213 true,
7214 None,
7215 None,
7216 "inherit",
7217 "stdout",
7218 None,
7219 false,
7220 )
7221 .unwrap();
7222 process.start_impl().unwrap();
7223 assert!(process.close_impl(py).is_ok());
7224 });
7225 }
7226
7227 #[test]
7230 fn running_process_decode_line_text_mode() {
7231 pyo3::prepare_freethreaded_python();
7232 pyo3::Python::with_gil(|py| {
7233 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
7234 let process = NativeRunningProcess::new(
7235 cmd.as_any(),
7236 None,
7237 false,
7238 true,
7239 None,
7240 None,
7241 true, None,
7243 None,
7244 "inherit",
7245 "stdout",
7246 None,
7247 false,
7248 )
7249 .unwrap();
7250 let result = process.decode_line_to_string(py, b"hello world").unwrap();
7251 assert_eq!(result, "hello world");
7252 });
7253 }
7254
7255 #[test]
7256 fn running_process_decode_line_binary_mode() {
7257 pyo3::prepare_freethreaded_python();
7258 pyo3::Python::with_gil(|py| {
7259 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
7260 let process = NativeRunningProcess::new(
7261 cmd.as_any(),
7262 None,
7263 false,
7264 true,
7265 None,
7266 None,
7267 false, None,
7269 None,
7270 "inherit",
7271 "stdout",
7272 None,
7273 false,
7274 )
7275 .unwrap();
7276 let result = process.decode_line_to_string(py, b"\xff\xfe").unwrap();
7277 assert!(!result.is_empty());
7279 });
7280 }
7281
7282 #[test]
7283 fn running_process_decode_line_custom_encoding() {
7284 pyo3::prepare_freethreaded_python();
7285 pyo3::Python::with_gil(|py| {
7286 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
7287 let process = NativeRunningProcess::new(
7288 cmd.as_any(),
7289 None,
7290 false,
7291 true,
7292 None,
7293 None,
7294 true,
7295 Some("ascii".to_string()),
7296 Some("replace".to_string()),
7297 "inherit",
7298 "stdout",
7299 None,
7300 false,
7301 )
7302 .unwrap();
7303 let result = process.decode_line_to_string(py, b"hello").unwrap();
7304 assert_eq!(result, "hello");
7305 });
7306 }
7307
7308 #[test]
7309 fn running_process_captured_stream_text() {
7310 pyo3::prepare_freethreaded_python();
7311 pyo3::Python::with_gil(|py| {
7312 let cmd =
7313 pyo3::types::PyList::new(py, ["python", "-c", "print('line1'); print('line2')"])
7314 .unwrap();
7315 let process = NativeRunningProcess::new(
7316 cmd.as_any(),
7317 None,
7318 false,
7319 true,
7320 None,
7321 None,
7322 true,
7323 None,
7324 None,
7325 "inherit",
7326 "stdout",
7327 None,
7328 false,
7329 )
7330 .unwrap();
7331 process.start_impl().unwrap();
7332 let _ = process.wait_impl(py, Some(10.0));
7333 let text = process
7334 .captured_stream_text(py, StreamKind::Stdout)
7335 .unwrap();
7336 assert!(text.contains("line1"));
7337 assert!(text.contains("line2"));
7338 });
7339 }
7340
7341 #[test]
7342 fn running_process_captured_combined_text() {
7343 pyo3::prepare_freethreaded_python();
7344 pyo3::Python::with_gil(|py| {
7345 let cmd = pyo3::types::PyList::new(
7346 py,
7347 [
7348 "python",
7349 "-c",
7350 "import sys; print('out'); print('err', file=sys.stderr)",
7351 ],
7352 )
7353 .unwrap();
7354 let process = NativeRunningProcess::new(
7355 cmd.as_any(),
7356 None,
7357 false,
7358 true,
7359 None,
7360 None,
7361 true,
7362 None,
7363 None,
7364 "inherit",
7365 "pipe",
7366 None,
7367 false,
7368 )
7369 .unwrap();
7370 process.start_impl().unwrap();
7371 let _ = process.wait_impl(py, Some(10.0));
7372 let text = process.captured_combined_text(py).unwrap();
7373 assert!(text.contains("out"));
7374 assert!(text.contains("err"));
7375 });
7376 }
7377
7378 #[test]
7379 fn running_process_read_status_text_stream() {
7380 pyo3::prepare_freethreaded_python();
7381 pyo3::Python::with_gil(|py| {
7382 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('data')"]).unwrap();
7383 let process = NativeRunningProcess::new(
7384 cmd.as_any(),
7385 None,
7386 false,
7387 true,
7388 None,
7389 None,
7390 true,
7391 None,
7392 None,
7393 "inherit",
7394 "stdout",
7395 None,
7396 false,
7397 )
7398 .unwrap();
7399 process.start_impl().unwrap();
7400 let _ = process.wait_impl(py, Some(10.0));
7401 std::thread::sleep(Duration::from_millis(50));
7402 let status = process
7404 .read_status_text(Some(StreamKind::Stdout), Some(Duration::from_millis(100)));
7405 assert!(status.is_ok());
7406 });
7407 }
7408
7409 #[test]
7410 fn running_process_read_status_text_combined() {
7411 pyo3::prepare_freethreaded_python();
7412 pyo3::Python::with_gil(|py| {
7413 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('data')"]).unwrap();
7414 let process = NativeRunningProcess::new(
7415 cmd.as_any(),
7416 None,
7417 false,
7418 true,
7419 None,
7420 None,
7421 true,
7422 None,
7423 None,
7424 "inherit",
7425 "stdout",
7426 None,
7427 false,
7428 )
7429 .unwrap();
7430 process.start_impl().unwrap();
7431 let _ = process.wait_impl(py, Some(10.0));
7432 std::thread::sleep(Duration::from_millis(50));
7433 let status = process.read_status_text(None, Some(Duration::from_millis(100)));
7435 assert!(status.is_ok());
7436 });
7437 }
7438
7439 #[test]
7440 fn running_process_decode_line_returns_bytes_in_binary_mode() {
7441 pyo3::prepare_freethreaded_python();
7442 pyo3::Python::with_gil(|py| {
7443 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
7444 let process = NativeRunningProcess::new(
7445 cmd.as_any(),
7446 None,
7447 false,
7448 true,
7449 None,
7450 None,
7451 false, None,
7453 None,
7454 "inherit",
7455 "stdout",
7456 None,
7457 false,
7458 )
7459 .unwrap();
7460 let result = process.decode_line(py, b"hello").unwrap();
7461 let bytes: Vec<u8> = result.extract(py).unwrap();
7463 assert_eq!(bytes, b"hello");
7464 });
7465 }
7466
7467 #[test]
7468 fn running_process_decode_line_returns_string_in_text_mode() {
7469 pyo3::prepare_freethreaded_python();
7470 pyo3::Python::with_gil(|py| {
7471 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
7472 let process = NativeRunningProcess::new(
7473 cmd.as_any(),
7474 None,
7475 false,
7476 true,
7477 None,
7478 None,
7479 true, None,
7481 None,
7482 "inherit",
7483 "stdout",
7484 None,
7485 false,
7486 )
7487 .unwrap();
7488 let result = process.decode_line(py, b"hello").unwrap();
7489 let text: String = result.extract(py).unwrap();
7490 assert_eq!(text, "hello");
7491 });
7492 }
7493
7494 #[test]
7497 fn pty_buffer_decode_chunk_text_mode() {
7498 pyo3::prepare_freethreaded_python();
7499 pyo3::Python::with_gil(|py| {
7500 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
7501 let result = buf.decode_chunk(py, b"hello").unwrap();
7502 let text: String = result.extract(py).unwrap();
7503 assert_eq!(text, "hello");
7504 });
7505 }
7506
7507 #[test]
7508 fn pty_buffer_decode_chunk_binary_mode() {
7509 pyo3::prepare_freethreaded_python();
7510 pyo3::Python::with_gil(|py| {
7511 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
7512 let result = buf.decode_chunk(py, b"\xff\xfe").unwrap();
7513 let bytes: Vec<u8> = result.extract(py).unwrap();
7514 assert_eq!(bytes, vec![0xff, 0xfe]);
7515 });
7516 }
7517
7518 #[test]
7521 fn pty_process_mark_reader_closed() {
7522 pyo3::prepare_freethreaded_python();
7523 pyo3::Python::with_gil(|_py| {
7524 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
7525 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
7526 assert!(!process.reader.state.lock().unwrap().closed);
7528 process.mark_reader_closed();
7529 assert!(process.reader.state.lock().unwrap().closed);
7530 });
7531 }
7532
7533 #[test]
7534 fn pty_process_store_returncode_sets_value() {
7535 pyo3::prepare_freethreaded_python();
7536 pyo3::Python::with_gil(|_py| {
7537 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
7538 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
7539 assert!(process.returncode.lock().unwrap().is_none());
7540 process.store_returncode(42);
7541 assert_eq!(*process.returncode.lock().unwrap(), Some(42));
7542 });
7543 }
7544
7545 #[test]
7546 fn pty_process_record_input_metrics_tracks_data() {
7547 pyo3::prepare_freethreaded_python();
7548 pyo3::Python::with_gil(|_py| {
7549 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
7550 let process = NativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
7551 assert_eq!(process.pty_input_bytes_total(), 0);
7552 process.record_input_metrics(b"hello\n", false);
7553 assert_eq!(process.pty_input_bytes_total(), 6);
7554 assert_eq!(process.pty_newline_events_total(), 1);
7555 assert_eq!(process.pty_submit_events_total(), 0);
7556 process.record_input_metrics(b"\r", true);
7557 assert_eq!(process.pty_submit_events_total(), 1);
7558 });
7559 }
7560
7561 #[test]
7564 fn process_err_to_py_already_started_is_runtime_error() {
7565 pyo3::prepare_freethreaded_python();
7566 let err = process_err_to_py(running_process_core::ProcessError::AlreadyStarted);
7567 pyo3::Python::with_gil(|py| {
7568 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7569 });
7570 }
7571
7572 #[test]
7573 fn process_err_to_py_stdin_unavailable_is_runtime_error() {
7574 pyo3::prepare_freethreaded_python();
7575 let err = process_err_to_py(running_process_core::ProcessError::StdinUnavailable);
7576 pyo3::Python::with_gil(|py| {
7577 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7578 });
7579 }
7580
7581 #[test]
7582 fn process_err_to_py_spawn_is_runtime_error() {
7583 pyo3::prepare_freethreaded_python();
7584 let err = process_err_to_py(running_process_core::ProcessError::Spawn(
7585 std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
7586 ));
7587 pyo3::Python::with_gil(|py| {
7588 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7589 });
7590 }
7591
7592 #[test]
7593 fn process_err_to_py_io_is_runtime_error() {
7594 pyo3::prepare_freethreaded_python();
7595 let err = process_err_to_py(running_process_core::ProcessError::Io(std::io::Error::new(
7596 std::io::ErrorKind::BrokenPipe,
7597 "broken pipe",
7598 )));
7599 pyo3::Python::with_gil(|py| {
7600 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
7601 });
7602 }
7603
7604 #[test]
7607 fn running_process_piped_stdin() {
7608 pyo3::prepare_freethreaded_python();
7609 pyo3::Python::with_gil(|py| {
7610 let cmd = pyo3::types::PyList::new(
7611 py,
7612 [
7613 "python",
7614 "-c",
7615 "import sys; data=sys.stdin.buffer.read(); sys.stdout.buffer.write(data[::-1])",
7616 ],
7617 )
7618 .unwrap();
7619 let process = NativeRunningProcess::new(
7620 cmd.as_any(),
7621 None,
7622 false,
7623 true,
7624 None,
7625 None,
7626 true,
7627 None,
7628 None,
7629 "piped",
7630 "stdout",
7631 None,
7632 false,
7633 )
7634 .unwrap();
7635 process.start_impl().unwrap();
7636 process.inner.write_stdin(b"abc").unwrap();
7637 let code = process.wait_impl(py, Some(10.0)).unwrap();
7638 assert_eq!(code, 0);
7639 });
7640 }
7641
7642 #[test]
7645 fn running_process_shell_mode() {
7646 pyo3::prepare_freethreaded_python();
7647 pyo3::Python::with_gil(|py| {
7648 let cmd = pyo3::types::PyString::new(py, "echo shell-mode-test");
7649 let process = NativeRunningProcess::new(
7650 cmd.as_any(),
7651 None,
7652 true, true,
7654 None,
7655 None,
7656 true,
7657 None,
7658 None,
7659 "inherit",
7660 "stdout",
7661 None,
7662 false,
7663 )
7664 .unwrap();
7665 process.start_impl().unwrap();
7666 let code = process.wait_impl(py, Some(10.0)).unwrap();
7667 assert_eq!(code, 0);
7668 });
7669 }
7670
7671 #[test]
7674 fn running_process_send_interrupt_before_start_errors() {
7675 pyo3::prepare_freethreaded_python();
7676 pyo3::Python::with_gil(|py| {
7677 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "pass"]).unwrap();
7678 let process = NativeRunningProcess::new(
7679 cmd.as_any(),
7680 None,
7681 false,
7682 false,
7683 None,
7684 None,
7685 true,
7686 None,
7687 None,
7688 "inherit",
7689 "stdout",
7690 None,
7691 false,
7692 )
7693 .unwrap();
7694 assert!(process.send_interrupt_impl().is_err());
7695 });
7696 }
7697
7698 #[test]
7701 fn terminal_input_inject_multiple_events() {
7702 let input = NativeTerminalInput::new();
7703 {
7704 let mut state = input.state.lock().unwrap();
7705 for i in 0..5 {
7706 state.events.push_back(TerminalInputEventRecord {
7707 data: vec![b'a' + i],
7708 submit: false,
7709 shift: false,
7710 ctrl: false,
7711 alt: false,
7712 virtual_key_code: 0,
7713 repeat_count: 1,
7714 });
7715 }
7716 }
7717 assert!(input.available());
7718 let mut count = 0;
7719 while input.next_event().is_some() {
7720 count += 1;
7721 }
7722 assert_eq!(count, 5);
7723 assert!(!input.available());
7724 }
7725
7726 #[test]
7727 fn terminal_input_capturing_false_initially() {
7728 let input = NativeTerminalInput::new();
7729 assert!(!input.capturing());
7730 }
7731
7732 #[test]
7735 fn terminal_input_event_fields() {
7736 let event = NativeTerminalInputEvent {
7737 data: vec![0x1B, 0x5B, 0x41],
7738 submit: false,
7739 shift: true,
7740 ctrl: true,
7741 alt: false,
7742 virtual_key_code: 38,
7743 repeat_count: 2,
7744 };
7745 assert_eq!(event.data, vec![0x1B, 0x5B, 0x41]);
7746 assert!(!event.submit);
7747 assert!(event.shift);
7748 assert!(event.ctrl);
7749 assert!(!event.alt);
7750 assert_eq!(event.virtual_key_code, 38);
7751 assert_eq!(event.repeat_count, 2);
7752 let repr = event.__repr__();
7754 assert!(repr.contains("shift=true"));
7755 assert!(repr.contains("ctrl=true"));
7756 assert!(repr.contains("alt=false"));
7757 }
7758
7759 #[test]
7762 fn spawn_pty_reader_reads_data_and_closes() {
7763 let shared = Arc::new(PtyReadShared {
7764 state: Mutex::new(PtyReadState {
7765 chunks: VecDeque::new(),
7766 closed: false,
7767 }),
7768 condvar: Condvar::new(),
7769 });
7770
7771 let data = b"hello from reader\n";
7772 let reader: Box<dyn std::io::Read + Send> = Box::new(std::io::Cursor::new(data.to_vec()));
7773 let echo = Arc::new(AtomicBool::new(false));
7774 let idle = Arc::new(Mutex::new(None));
7775 let out_bytes = Arc::new(AtomicUsize::new(0));
7776 let churn_bytes = Arc::new(AtomicUsize::new(0));
7777 spawn_pty_reader(
7778 reader,
7779 Arc::clone(&shared),
7780 echo,
7781 idle,
7782 out_bytes,
7783 churn_bytes,
7784 );
7785
7786 let deadline = Instant::now() + Duration::from_secs(5);
7788 loop {
7789 let state = shared.state.lock().unwrap();
7790 if state.closed {
7791 break;
7792 }
7793 drop(state);
7794 assert!(Instant::now() < deadline, "reader thread did not close");
7795 std::thread::sleep(Duration::from_millis(10));
7796 }
7797
7798 let state = shared.state.lock().unwrap();
7799 assert!(state.closed);
7800 assert!(!state.chunks.is_empty());
7801 }
7802
7803 #[test]
7804 fn spawn_pty_reader_empty_input_closes() {
7805 let shared = Arc::new(PtyReadShared {
7806 state: Mutex::new(PtyReadState {
7807 chunks: VecDeque::new(),
7808 closed: false,
7809 }),
7810 condvar: Condvar::new(),
7811 });
7812
7813 let reader: Box<dyn std::io::Read + Send> = Box::new(std::io::Cursor::new(Vec::new()));
7814 let echo = Arc::new(AtomicBool::new(false));
7815 let idle = Arc::new(Mutex::new(None));
7816 let out_bytes = Arc::new(AtomicUsize::new(0));
7817 let churn_bytes = Arc::new(AtomicUsize::new(0));
7818 spawn_pty_reader(
7819 reader,
7820 Arc::clone(&shared),
7821 echo,
7822 idle,
7823 out_bytes,
7824 churn_bytes,
7825 );
7826
7827 let deadline = Instant::now() + Duration::from_secs(5);
7828 loop {
7829 let state = shared.state.lock().unwrap();
7830 if state.closed {
7831 break;
7832 }
7833 drop(state);
7834 assert!(Instant::now() < deadline, "reader thread did not close");
7835 std::thread::sleep(Duration::from_millis(10));
7836 }
7837
7838 let state = shared.state.lock().unwrap();
7839 assert!(state.closed);
7840 assert!(state.chunks.is_empty());
7841 }
7842
7843 #[test]
7846 #[cfg(windows)]
7847 fn windows_generate_console_ctrl_break_nonexistent_pid() {
7848 pyo3::prepare_freethreaded_python();
7849 let result = windows_generate_console_ctrl_break_impl(999999, None);
7851 assert!(result.is_err());
7852 }
7853
7854 #[test]
7857 fn running_process_with_env() {
7858 pyo3::prepare_freethreaded_python();
7859 pyo3::Python::with_gil(|py| {
7860 let env = pyo3::types::PyDict::new(py);
7861 if let Ok(path) = std::env::var("PATH") {
7862 env.set_item("PATH", &path).unwrap();
7863 }
7864 #[cfg(windows)]
7865 if let Ok(root) = std::env::var("SystemRoot") {
7866 env.set_item("SystemRoot", &root).unwrap();
7867 }
7868 env.set_item("RP_TEST_VAR", "test_value").unwrap();
7869
7870 let cmd = pyo3::types::PyList::new(
7871 py,
7872 [
7873 "python",
7874 "-c",
7875 "import os; print(os.environ.get('RP_TEST_VAR', 'MISSING'))",
7876 ],
7877 )
7878 .unwrap();
7879 let process = NativeRunningProcess::new(
7880 cmd.as_any(),
7881 None,
7882 false,
7883 true,
7884 Some(env),
7885 None,
7886 true,
7887 None,
7888 None,
7889 "inherit",
7890 "stdout",
7891 None,
7892 false,
7893 )
7894 .unwrap();
7895 process.start_impl().unwrap();
7896 let code = process.wait_impl(py, Some(10.0)).unwrap();
7897 assert_eq!(code, 0);
7898 let text = process
7899 .captured_stream_text(py, StreamKind::Stdout)
7900 .unwrap();
7901 assert!(text.contains("test_value"));
7902 });
7903 }
7904
7905 #[test]
7908 #[cfg(windows)]
7909 fn windows_pty_input_payload_via_module() {
7910 assert_eq!(pty_windows::input_payload(b"hello"), b"hello");
7911 assert_eq!(pty_windows::input_payload(b"\n"), b"\r");
7912 }
7913}
7914
7915#[pyclass]
7919#[derive(Clone, Copy)]
7920struct PyContainment {
7921 inner: Containment,
7922}
7923
7924#[pymethods]
7925impl PyContainment {
7926 #[staticmethod]
7928 fn contained() -> Self {
7929 Self {
7930 inner: Containment::Contained,
7931 }
7932 }
7933
7934 #[staticmethod]
7936 fn detached() -> Self {
7937 Self {
7938 inner: Containment::Detached,
7939 }
7940 }
7941
7942 fn __repr__(&self) -> String {
7943 match self.inner {
7944 Containment::Contained => "Containment.Contained".to_string(),
7945 Containment::Detached => "Containment.Detached".to_string(),
7946 }
7947 }
7948}
7949
7950#[pyclass(name = "ContainedProcessGroup")]
7952struct PyContainedProcessGroup {
7953 inner: Option<ContainedProcessGroup>,
7954 children: Vec<ContainedChild>,
7955}
7956
7957#[pymethods]
7958impl PyContainedProcessGroup {
7959 #[new]
7960 #[pyo3(signature = (originator=None))]
7961 fn new(originator: Option<String>) -> PyResult<Self> {
7962 let group = match originator {
7963 Some(ref orig) => ContainedProcessGroup::with_originator(orig).map_err(to_py_err)?,
7964 None => ContainedProcessGroup::new().map_err(to_py_err)?,
7965 };
7966 Ok(Self {
7967 inner: Some(group),
7968 children: Vec::new(),
7969 })
7970 }
7971
7972 #[getter]
7973 fn originator(&self) -> Option<String> {
7974 self.inner.as_ref()?.originator().map(String::from)
7975 }
7976
7977 #[getter]
7978 fn originator_value(&self) -> Option<String> {
7979 self.inner.as_ref()?.originator_value()
7980 }
7981
7982 fn spawn(&mut self, argv: Vec<String>) -> PyResult<u32> {
7984 let group = self
7985 .inner
7986 .as_ref()
7987 .ok_or_else(|| PyRuntimeError::new_err("group already closed"))?;
7988 if argv.is_empty() {
7989 return Err(PyValueError::new_err("argv must not be empty"));
7990 }
7991 let mut cmd = std::process::Command::new(&argv[0]);
7992 if argv.len() > 1 {
7993 cmd.args(&argv[1..]);
7994 }
7995 let contained = group.spawn(&mut cmd).map_err(to_py_err)?;
7996 let pid = contained.child.id();
7997 self.children.push(contained);
7998 Ok(pid)
7999 }
8000
8001 fn spawn_detached(&mut self, argv: Vec<String>) -> PyResult<u32> {
8003 let group = self
8004 .inner
8005 .as_ref()
8006 .ok_or_else(|| PyRuntimeError::new_err("group already closed"))?;
8007 if argv.is_empty() {
8008 return Err(PyValueError::new_err("argv must not be empty"));
8009 }
8010 let mut cmd = std::process::Command::new(&argv[0]);
8011 if argv.len() > 1 {
8012 cmd.args(&argv[1..]);
8013 }
8014 let contained = group.spawn_detached(&mut cmd).map_err(to_py_err)?;
8015 let pid = contained.child.id();
8016 self.children.push(contained);
8017 Ok(pid)
8018 }
8019
8020 fn close(&mut self) {
8022 self.inner.take();
8023 }
8024
8025 fn __enter__(slf: Py<Self>) -> Py<Self> {
8027 slf
8028 }
8029
8030 #[pyo3(signature = (_exc_type=None, _exc_val=None, _exc_tb=None))]
8032 fn __exit__(
8033 &mut self,
8034 _exc_type: Option<&Bound<'_, PyAny>>,
8035 _exc_val: Option<&Bound<'_, PyAny>>,
8036 _exc_tb: Option<&Bound<'_, PyAny>>,
8037 ) {
8038 self.close();
8039 }
8040}
8041
8042#[pyclass(name = "OriginatorProcessInfo")]
8045#[derive(Clone)]
8046struct PyOriginatorProcessInfo {
8047 #[pyo3(get)]
8048 pid: u32,
8049 #[pyo3(get)]
8050 name: String,
8051 #[pyo3(get)]
8052 command: String,
8053 #[pyo3(get)]
8054 originator: String,
8055 #[pyo3(get)]
8056 parent_pid: u32,
8057 #[pyo3(get)]
8058 parent_alive: bool,
8059}
8060
8061#[pymethods]
8062impl PyOriginatorProcessInfo {
8063 fn __repr__(&self) -> String {
8064 format!(
8065 "OriginatorProcessInfo(pid={}, name={:?}, originator={:?}, parent_pid={}, parent_alive={})",
8066 self.pid, self.name, self.originator, self.parent_pid, self.parent_alive
8067 )
8068 }
8069}
8070
8071impl From<OriginatorProcessInfo> for PyOriginatorProcessInfo {
8072 fn from(info: OriginatorProcessInfo) -> Self {
8073 Self {
8074 pid: info.pid,
8075 name: info.name,
8076 command: info.command,
8077 originator: info.originator,
8078 parent_pid: info.parent_pid,
8079 parent_alive: info.parent_alive,
8080 }
8081 }
8082}
8083
8084#[pyfunction]
8087fn py_find_processes_by_originator(tool: &str) -> Vec<PyOriginatorProcessInfo> {
8088 find_processes_by_originator(tool)
8089 .into_iter()
8090 .map(PyOriginatorProcessInfo::from)
8091 .collect()
8092}
8093
8094#[pymodule]
8095fn _native(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
8096 module.add_class::<PyNativeProcess>()?;
8097 module.add_class::<NativeRunningProcess>()?;
8098 module.add_class::<PyContainedProcessGroup>()?;
8099 module.add_class::<PyContainment>()?;
8100 module.add_class::<PyOriginatorProcessInfo>()?;
8101 module.add_function(wrap_pyfunction!(py_find_processes_by_originator, module)?)?;
8102 module.add_class::<NativePtyProcess>()?;
8103 module.add_class::<NativeProcessMetrics>()?;
8104 module.add_class::<NativeSignalBool>()?;
8105 module.add_class::<NativeIdleDetector>()?;
8106 module.add_class::<NativePtyBuffer>()?;
8107 module.add_class::<NativeTerminalInput>()?;
8108 module.add_class::<NativeTerminalInputEvent>()?;
8109 module.add_function(wrap_pyfunction!(tracked_pid_db_path_py, module)?)?;
8110 module.add_function(wrap_pyfunction!(track_process_pid, module)?)?;
8111 module.add_function(wrap_pyfunction!(untrack_process_pid, module)?)?;
8112 module.add_function(wrap_pyfunction!(native_register_process, module)?)?;
8113 module.add_function(wrap_pyfunction!(native_unregister_process, module)?)?;
8114 module.add_function(wrap_pyfunction!(list_tracked_processes, module)?)?;
8115 module.add_function(wrap_pyfunction!(native_list_active_processes, module)?)?;
8116 module.add_function(wrap_pyfunction!(native_get_process_tree_info, module)?)?;
8117 module.add_function(wrap_pyfunction!(native_kill_process_tree, module)?)?;
8118 module.add_function(wrap_pyfunction!(native_process_created_at, module)?)?;
8119 module.add_function(wrap_pyfunction!(native_is_same_process, module)?)?;
8120 module.add_function(wrap_pyfunction!(native_cleanup_tracked_processes, module)?)?;
8121 module.add_function(wrap_pyfunction!(native_apply_process_nice, module)?)?;
8122 module.add_function(wrap_pyfunction!(
8123 native_windows_terminal_input_bytes,
8124 module
8125 )?)?;
8126 module.add_function(wrap_pyfunction!(native_dump_rust_debug_traces, module)?)?;
8127 module.add_function(wrap_pyfunction!(
8128 native_test_capture_rust_debug_trace,
8129 module
8130 )?)?;
8131 #[cfg(windows)]
8132 module.add_function(wrap_pyfunction!(native_test_hang_in_rust, module)?)?;
8133 module.add("VERSION", PyString::new(_py, env!("CARGO_PKG_VERSION")))?;
8134 Ok(())
8135}