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