1use std::collections::{HashMap, VecDeque};
2#[cfg(windows)]
3use std::fs;
4use std::path::PathBuf;
5#[cfg(test)]
6use std::sync::atomic::AtomicUsize;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::sync::Arc;
9use std::sync::{Condvar, Mutex, OnceLock};
10use std::thread;
11use std::time::Duration;
12use std::time::Instant;
13
14use pyo3::exceptions::{PyRuntimeError, PyTimeoutError, PyValueError};
15use pyo3::prelude::*;
16use pyo3::types::{PyBytes, PyDict, PyList, PyString};
17use regex::Regex;
18#[cfg(all(windows, test))]
19use running_process_core::pty::terminal_input::{
20 wait_for_terminal_input_event, TerminalInputWaitOutcome,
21};
22use running_process_core::pty::{
23 self as core_pty,
24 terminal_input::{
25 self as core_terminal_input, TerminalInputCore, TerminalInputError,
26 TerminalInputEventRecord,
27 },
28 IdleDetectorCore, IdleMonitorState, NativePtyProcess as CoreNativePtyProcess, PtyError,
29};
30use running_process_core::{
31 find_processes_by_originator, render_rust_debug_traces, CommandSpec, ContainedChild,
32 ContainedProcessGroup, Containment, NativeProcess, OriginatorProcessInfo, ProcessConfig,
33 ProcessError, ReadStatus, StderrMode, StdinMode, StreamEvent, StreamKind,
34};
35#[cfg(unix)]
36use running_process_core::{
37 unix_set_priority, unix_signal_process, unix_signal_process_group, UnixSignal,
38};
39use sysinfo::{Pid, ProcessRefreshKind, Signal, System, UpdateKind};
40
41mod daemon_client;
42mod public_symbols;
43
44#[cfg(all(windows, test))]
46use running_process_core::pty::terminal_input::NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV;
47
48fn to_py_err(err: impl std::fmt::Display) -> PyErr {
49 PyRuntimeError::new_err(err.to_string())
50}
51
52#[cfg(test)]
53fn is_ignorable_process_control_error(err: &std::io::Error) -> bool {
54 if matches!(
55 err.kind(),
56 std::io::ErrorKind::NotFound | std::io::ErrorKind::InvalidInput
57 ) {
58 return true;
59 }
60 #[cfg(unix)]
61 if err.raw_os_error() == Some(libc::ESRCH) {
62 return true;
63 }
64 false
65}
66
67fn process_err_to_py(err: ProcessError) -> PyErr {
68 match err {
69 ProcessError::Timeout => PyTimeoutError::new_err("process timed out"),
70 other => to_py_err(other),
71 }
72}
73
74fn system_pid(pid: u32) -> Pid {
75 Pid::from_u32(pid)
76}
77
78fn descendant_pids(system: &System, pid: Pid) -> Vec<Pid> {
79 let mut children_map: HashMap<Pid, Vec<Pid>> = HashMap::new();
81 for (child_pid, process) in system.processes() {
82 if let Some(parent) = process.parent() {
83 children_map.entry(parent).or_default().push(*child_pid);
84 }
85 }
86 let mut descendants = Vec::new();
88 let mut stack = vec![pid];
89 while let Some(current) = stack.pop() {
90 if let Some(children) = children_map.get(¤t) {
91 for &child in children {
92 descendants.push(child);
93 stack.push(child);
94 }
95 }
96 }
97 descendants
98}
99
100#[derive(Clone)]
101struct ActiveProcessRecord {
102 pid: u32,
103 kind: String,
104 command: String,
105 cwd: Option<String>,
106 started_at: f64,
107}
108
109type TrackedProcessEntry = (u32, f64, String, String, Option<String>);
110type ActiveProcessEntry = (u32, String, String, Option<String>, f64);
111type ExpectDetails = (String, usize, usize, Vec<String>);
112type ExpectResult = (
113 String,
114 String,
115 Option<String>,
116 Option<usize>,
117 Option<usize>,
118 Vec<String>,
119);
120
121fn active_process_registry() -> &'static Mutex<HashMap<u32, ActiveProcessRecord>> {
122 static ACTIVE_PROCESSES: OnceLock<Mutex<HashMap<u32, ActiveProcessRecord>>> = OnceLock::new();
123 ACTIVE_PROCESSES.get_or_init(|| Mutex::new(HashMap::new()))
124}
125
126#[cfg(test)]
127fn test_env_lock() -> &'static Mutex<()> {
128 static TEST_ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
129 TEST_ENV_LOCK.get_or_init(|| Mutex::new(()))
130}
131
132#[cfg(test)]
133fn with_locked_env_var<T>(
134 key: &'static str,
135 value: Option<&str>,
136 f: impl FnOnce() -> T + std::panic::UnwindSafe,
137) -> T {
138 let _guard = test_env_lock().lock().unwrap();
139 let previous = std::env::var_os(key);
140 match value {
141 Some(value) => std::env::set_var(key, value),
142 None => std::env::remove_var(key),
143 }
144
145 let result = std::panic::catch_unwind(f);
146
147 match previous {
148 Some(previous) => std::env::set_var(key, previous),
149 None => std::env::remove_var(key),
150 }
151
152 match result {
153 Ok(value) => value,
154 Err(payload) => std::panic::resume_unwind(payload),
155 }
156}
157
158fn unix_now_seconds() -> f64 {
160 core_terminal_input::unix_now_seconds()
161}
162
163fn register_active_process(
167 pid: u32,
168 kind: &str,
169 command: &str,
170 cwd: Option<String>,
171 started_at: f64,
172) {
173 let mut registry = active_process_registry()
174 .lock()
175 .expect("active process registry mutex poisoned");
176 registry.insert(
177 pid,
178 ActiveProcessRecord {
179 pid,
180 kind: kind.to_string(),
181 command: command.to_string(),
182 cwd: cwd.clone(),
183 started_at,
184 },
185 );
186 drop(registry); daemon_client::daemon_register(pid, started_at, kind, command, cwd.as_deref());
190}
191
192fn unregister_active_process(pid: u32) {
193 let mut registry = active_process_registry()
194 .lock()
195 .expect("active process registry mutex poisoned");
196 registry.remove(&pid);
197 drop(registry); daemon_client::daemon_unregister(pid);
201}
202
203fn process_created_at(pid: u32) -> Option<f64> {
204 let pid = system_pid(pid);
205 let mut system = System::new();
206 system.refresh_process_specifics(pid, ProcessRefreshKind::new());
207 system
208 .process(pid)
209 .map(|process| process.start_time() as f64)
210}
211
212fn same_process_identity(pid: u32, created_at: f64, tolerance_seconds: f64) -> bool {
213 let Some(actual) = process_created_at(pid) else {
214 return false;
215 };
216 (actual - created_at).abs() <= tolerance_seconds
217}
218
219fn tracked_process_db_path() -> PyResult<PathBuf> {
220 if let Ok(value) = std::env::var("RUNNING_PROCESS_PID_DB") {
221 let trimmed = value.trim();
222 if !trimmed.is_empty() {
223 return Ok(PathBuf::from(trimmed));
224 }
225 }
226
227 #[cfg(windows)]
228 let base_dir = std::env::var_os("LOCALAPPDATA")
229 .map(PathBuf::from)
230 .unwrap_or_else(std::env::temp_dir);
231
232 #[cfg(not(windows))]
233 let base_dir = std::env::var_os("XDG_STATE_HOME")
234 .map(PathBuf::from)
235 .or_else(|| {
236 std::env::var_os("HOME").map(|home| {
237 let mut path = PathBuf::from(home);
238 path.push(".local");
239 path.push("state");
240 path
241 })
242 })
243 .unwrap_or_else(std::env::temp_dir);
244
245 Ok(base_dir
246 .join("running-process")
247 .join("tracked-pids.sqlite3"))
248}
249
250#[pyfunction]
251fn tracked_pid_db_path_py() -> PyResult<String> {
252 Ok(tracked_process_db_path()?.to_string_lossy().into_owned())
253}
254
255#[pyfunction]
256#[pyo3(signature = (pid, created_at, kind, command, cwd=None))]
257fn track_process_pid(
258 pid: u32,
259 created_at: f64,
260 kind: &str,
261 command: &str,
262 cwd: Option<String>,
263) -> PyResult<()> {
264 let _ = created_at;
265 register_active_process(pid, kind, command, cwd, unix_now_seconds());
266 Ok(())
267}
268
269#[pyfunction]
270#[pyo3(signature = (pid, kind, command, cwd=None))]
271fn native_register_process(
272 pid: u32,
273 kind: &str,
274 command: &str,
275 cwd: Option<String>,
276) -> PyResult<()> {
277 register_active_process(pid, kind, command, cwd, unix_now_seconds());
278 Ok(())
279}
280
281#[pyfunction]
282fn untrack_process_pid(pid: u32) -> PyResult<()> {
283 unregister_active_process(pid);
284 Ok(())
285}
286
287#[pyfunction]
288fn native_unregister_process(pid: u32) -> PyResult<()> {
289 unregister_active_process(pid);
290 Ok(())
291}
292
293#[pyfunction]
294fn list_tracked_processes() -> PyResult<Vec<TrackedProcessEntry>> {
295 let snapshot: Vec<ActiveProcessRecord> = {
296 let registry = active_process_registry()
297 .lock()
298 .expect("active process registry mutex poisoned");
299 registry.values().cloned().collect()
300 };
301 let mut entries: Vec<_> = snapshot
302 .into_iter()
303 .map(|entry| {
304 (
305 entry.pid,
306 process_created_at(entry.pid).unwrap_or(entry.started_at),
307 entry.kind,
308 entry.command,
309 entry.cwd,
310 )
311 })
312 .collect();
313 entries.sort_by(|left, right| {
314 left.1
315 .partial_cmp(&right.1)
316 .unwrap_or(std::cmp::Ordering::Equal)
317 .then_with(|| left.0.cmp(&right.0))
318 });
319 Ok(entries)
320}
321
322fn kill_process_tree_impl(pid: u32, timeout_seconds: f64) {
323 let mut system = System::new();
324 system.refresh_processes();
325 let pid = system_pid(pid);
326 let Some(_) = system.process(pid) else {
327 return;
328 };
329
330 let mut kill_order = descendant_pids(&system, pid);
331 kill_order.reverse();
332 kill_order.push(pid);
333
334 for target in &kill_order {
335 if let Some(process) = system.process(*target) {
336 if !process.kill_with(Signal::Kill).unwrap_or(false) {
337 process.kill();
338 }
339 }
340 }
341
342 let deadline = Instant::now()
343 .checked_add(Duration::from_secs_f64(timeout_seconds.max(0.0)))
344 .unwrap_or_else(Instant::now);
345 loop {
346 system.refresh_processes();
347 if kill_order
348 .iter()
349 .all(|target| system.process(*target).is_none())
350 {
351 break;
352 }
353 if Instant::now() >= deadline {
354 break;
355 }
356 thread::sleep(Duration::from_millis(25));
357 }
358}
359
360#[pyfunction]
376fn native_get_process_tree_info(pid: u32) -> String {
377 let mut system = System::new();
378 system.refresh_processes();
379 let pid = system_pid(pid);
380 let Some(process) = system.process(pid) else {
381 return format!("Could not get process info for PID {}", pid.as_u32());
382 };
383
384 let mut info = vec![
385 format!("Process {} ({})", pid.as_u32(), process.name()),
386 format!("Status: {:?}", process.status()),
387 ];
388 let children = descendant_pids(&system, pid);
389 if !children.is_empty() {
390 info.push("Child processes:".to_string());
391 for child_pid in children {
392 if let Some(child) = system.process(child_pid) {
393 info.push(format!(" Child {} ({})", child_pid.as_u32(), child.name()));
394 }
395 }
396 }
397 info.join("\n")
398}
399
400#[pyfunction]
401#[pyo3(signature = (pid, timeout_seconds=3.0))]
402fn native_kill_process_tree(pid: u32, timeout_seconds: f64) {
403 kill_process_tree_impl(pid, timeout_seconds);
404}
405
406#[pyfunction]
407fn native_process_created_at(pid: u32) -> Option<f64> {
408 process_created_at(pid)
409}
410
411#[pyfunction]
412#[pyo3(signature = (pid, created_at, tolerance_seconds=1.0))]
413fn native_is_same_process(pid: u32, created_at: f64, tolerance_seconds: f64) -> bool {
414 same_process_identity(pid, created_at, tolerance_seconds)
415}
416
417#[pyfunction]
418#[pyo3(signature = (tolerance_seconds=1.0, kill_timeout_seconds=3.0))]
419fn native_cleanup_tracked_processes(
420 tolerance_seconds: f64,
421 kill_timeout_seconds: f64,
422) -> PyResult<Vec<TrackedProcessEntry>> {
423 let entries = list_tracked_processes()?;
424
425 let mut killed = Vec::new();
426 for entry in entries {
427 let pid = entry.0;
428 if !same_process_identity(pid, entry.1, tolerance_seconds) {
429 unregister_active_process(pid);
430 continue;
431 }
432 kill_process_tree_impl(pid, kill_timeout_seconds);
433 unregister_active_process(pid);
434 killed.push(entry);
435 }
436 Ok(killed)
437}
438
439#[pyfunction]
440fn native_list_active_processes() -> Vec<ActiveProcessEntry> {
441 let registry = active_process_registry()
442 .lock()
443 .expect("active process registry mutex poisoned");
444 let mut items: Vec<_> = registry
445 .values()
446 .map(|entry| {
447 (
448 entry.pid,
449 entry.kind.clone(),
450 entry.command.clone(),
451 entry.cwd.clone(),
452 entry.started_at,
453 )
454 })
455 .collect();
456 items.sort_by(|left, right| {
457 left.4
458 .partial_cmp(&right.4)
459 .unwrap_or(std::cmp::Ordering::Equal)
460 .then_with(|| left.0.cmp(&right.0))
461 });
462 items
463}
464
465#[pyfunction]
466#[inline(never)]
467fn native_apply_process_nice(pid: u32, nice: i32) -> PyResult<()> {
468 public_symbols::rp_native_apply_process_nice_public(pid, nice)
469}
470
471fn native_apply_process_nice_impl(pid: u32, nice: i32) -> PyResult<()> {
472 running_process_core::rp_rust_debug_scope!("running_process_py::native_apply_process_nice");
473 #[cfg(windows)]
474 {
475 public_symbols::rp_windows_apply_process_priority_public(pid, nice)
476 }
477
478 #[cfg(unix)]
479 {
480 unix_set_priority(pid, nice).map_err(to_py_err)
481 }
482}
483
484#[cfg(windows)]
485fn windows_apply_process_priority_impl(pid: u32, nice: i32) -> PyResult<()> {
486 running_process_core::rp_rust_debug_scope!(
487 "running_process_py::windows_apply_process_priority"
488 );
489 use winapi::um::handleapi::CloseHandle;
490 use winapi::um::processthreadsapi::{OpenProcess, SetPriorityClass};
491 use winapi::um::winbase::{
492 ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, HIGH_PRIORITY_CLASS,
493 IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS,
494 };
495 use winapi::um::winnt::{PROCESS_QUERY_INFORMATION, PROCESS_SET_INFORMATION};
496
497 let priority_class = if nice >= 15 {
498 IDLE_PRIORITY_CLASS
499 } else if nice >= 1 {
500 BELOW_NORMAL_PRIORITY_CLASS
501 } else if nice <= -15 {
502 HIGH_PRIORITY_CLASS
503 } else if nice <= -1 {
504 ABOVE_NORMAL_PRIORITY_CLASS
505 } else {
506 NORMAL_PRIORITY_CLASS
507 };
508
509 let handle =
510 unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_INFORMATION, 0, pid) };
511 if handle.is_null() {
512 return Err(to_py_err(std::io::Error::last_os_error()));
513 }
514 let result = unsafe { SetPriorityClass(handle, priority_class) };
515 let close_result = unsafe { CloseHandle(handle) };
516 if close_result == 0 {
517 return Err(to_py_err(std::io::Error::last_os_error()));
518 }
519 if result == 0 {
520 return Err(to_py_err(std::io::Error::last_os_error()));
521 }
522 Ok(())
523}
524
525#[cfg(windows)]
526fn windows_generate_console_ctrl_break_impl(pid: u32, creationflags: Option<u32>) -> PyResult<()> {
527 running_process_core::rp_rust_debug_scope!(
528 "running_process_py::windows_generate_console_ctrl_break"
529 );
530 use winapi::um::wincon::{GenerateConsoleCtrlEvent, CTRL_BREAK_EVENT};
531
532 let new_process_group =
533 creationflags.unwrap_or(0) & winapi::um::winbase::CREATE_NEW_PROCESS_GROUP;
534 if new_process_group == 0 {
535 return Err(PyRuntimeError::new_err(
536 "send_interrupt on Windows requires CREATE_NEW_PROCESS_GROUP",
537 ));
538 }
539 let result = unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) };
540 if result == 0 {
541 return Err(to_py_err(std::io::Error::last_os_error()));
542 }
543 Ok(())
544}
545
546#[pyfunction]
547fn native_windows_terminal_input_bytes(py: Python<'_>, data: &[u8]) -> Py<PyAny> {
548 #[cfg(windows)]
549 let payload = core_pty::windows_terminal_input_payload(data);
550 #[cfg(not(windows))]
551 let payload = data.to_vec();
552 PyBytes::new(py, &payload).into_any().unbind()
553}
554
555#[pyfunction]
556fn native_dump_rust_debug_traces() -> String {
557 render_rust_debug_traces()
558}
559
560#[pyfunction]
561fn native_test_capture_rust_debug_trace() -> String {
562 #[inline(never)]
563 fn inner() -> String {
564 running_process_core::rp_rust_debug_scope!(
565 "running_process_py::native_test_capture_rust_debug_trace::inner"
566 );
567 render_rust_debug_traces()
568 }
569
570 #[inline(never)]
571 fn outer() -> String {
572 running_process_core::rp_rust_debug_scope!(
573 "running_process_py::native_test_capture_rust_debug_trace::outer"
574 );
575 inner()
576 }
577
578 outer()
579}
580
581#[cfg(windows)]
582#[no_mangle]
583#[inline(never)]
584pub fn running_process_py_debug_hang_inner(release_path: &std::path::Path) -> PyResult<()> {
585 running_process_core::rp_rust_debug_scope!("running_process_py::debug_hang_inner");
586 while !release_path.exists() {
587 std::hint::spin_loop();
588 }
589 Ok(())
590}
591
592#[cfg(windows)]
593#[no_mangle]
594#[inline(never)]
595pub fn running_process_py_debug_hang_outer(
596 ready_path: &std::path::Path,
597 release_path: &std::path::Path,
598) -> PyResult<()> {
599 running_process_core::rp_rust_debug_scope!("running_process_py::debug_hang_outer");
600 fs::write(ready_path, b"ready").map_err(to_py_err)?;
601 running_process_py_debug_hang_inner(release_path)
602}
603
604#[pyfunction]
605#[cfg(windows)]
606#[inline(never)]
607fn native_test_hang_in_rust(ready_path: String, release_path: String) -> PyResult<()> {
608 running_process_core::rp_rust_debug_scope!("running_process_py::native_test_hang_in_rust");
609 running_process_py_debug_hang_outer(
610 std::path::Path::new(&ready_path),
611 std::path::Path::new(&release_path),
612 )
613}
614
615#[pymethods]
616impl NativeProcessMetrics {
617 #[new]
618 fn new(pid: u32) -> Self {
619 let pid = system_pid(pid);
620 let mut system = System::new();
621 system.refresh_process_specifics(
622 pid,
623 ProcessRefreshKind::new()
624 .with_cpu()
625 .with_disk_usage()
626 .with_memory()
627 .with_exe(UpdateKind::Never),
628 );
629 Self {
630 pid,
631 system: Mutex::new(system),
632 }
633 }
634
635 fn prime(&self) {
636 let mut system = self.system.lock().expect("process metrics mutex poisoned");
637 system.refresh_process_specifics(
638 self.pid,
639 ProcessRefreshKind::new()
640 .with_cpu()
641 .with_disk_usage()
642 .with_memory()
643 .with_exe(UpdateKind::Never),
644 );
645 }
646
647 fn sample(&self) -> (bool, f32, u64, u64) {
648 let mut system = self.system.lock().expect("process metrics mutex poisoned");
649 system.refresh_process_specifics(
650 self.pid,
651 ProcessRefreshKind::new()
652 .with_cpu()
653 .with_disk_usage()
654 .with_memory()
655 .with_exe(UpdateKind::Never),
656 );
657 let Some(process) = system.process(self.pid) else {
658 return (false, 0.0, 0, 0);
659 };
660 let disk = process.disk_usage();
661 (
662 true,
663 process.cpu_usage(),
664 disk.total_read_bytes
665 .saturating_add(disk.total_written_bytes),
666 0,
667 )
668 }
669}
670
671#[pyclass]
674struct NativeProcessMetrics {
675 pid: Pid,
676 system: Mutex<System>,
677}
678
679#[pyclass]
680struct NativePtyProcess {
681 inner: CoreNativePtyProcess,
682}
683
684impl NativePtyProcess {
685 fn pty_err_to_py(err: PtyError) -> PyErr {
686 match err {
687 PtyError::Timeout => PyTimeoutError::new_err(err.to_string()),
688 _ => PyRuntimeError::new_err(err.to_string()),
689 }
690 }
691
692 fn start_terminal_input_relay_py(&self) -> PyResult<()> {
693 self.inner
694 .start_terminal_input_relay_impl()
695 .map_err(Self::pty_err_to_py)
696 }
697}
698
699fn parse_command(command: &Bound<'_, PyAny>, shell: bool) -> PyResult<CommandSpec> {
702 if let Ok(command) = command.extract::<String>() {
703 if !shell {
704 return Err(PyValueError::new_err(
705 "String commands require shell=True. Use shell=True or provide command as list[str].",
706 ));
707 }
708 return Ok(CommandSpec::Shell(command));
709 }
710
711 if let Ok(command) = command.downcast::<PyList>() {
712 let argv = command.extract::<Vec<String>>()?;
713 if argv.is_empty() {
714 return Err(PyValueError::new_err("command cannot be empty"));
715 }
716 if shell {
717 return Ok(CommandSpec::Shell(argv.join(" ")));
718 }
719 return Ok(CommandSpec::Argv(argv));
720 }
721
722 Err(PyValueError::new_err(
723 "command must be either a string or a list[str]",
724 ))
725}
726
727fn stream_kind(name: &str) -> PyResult<StreamKind> {
728 match name {
729 "stdout" => Ok(StreamKind::Stdout),
730 "stderr" => Ok(StreamKind::Stderr),
731 _ => Err(PyValueError::new_err("stream must be 'stdout' or 'stderr'")),
732 }
733}
734
735fn stdin_mode(name: &str) -> PyResult<StdinMode> {
736 match name {
737 "inherit" => Ok(StdinMode::Inherit),
738 "piped" => Ok(StdinMode::Piped),
739 "null" => Ok(StdinMode::Null),
740 _ => Err(PyValueError::new_err(
741 "stdin_mode must be 'inherit', 'piped', or 'null'",
742 )),
743 }
744}
745
746fn stderr_mode(name: &str) -> PyResult<StderrMode> {
747 match name {
748 "stdout" => Ok(StderrMode::Stdout),
749 "pipe" => Ok(StderrMode::Pipe),
750 _ => Err(PyValueError::new_err(
751 "stderr_mode must be 'stdout' or 'pipe'",
752 )),
753 }
754}
755
756#[pyclass]
757struct NativeRunningProcess {
758 inner: NativeProcess,
759 text: bool,
760 encoding: Option<String>,
761 errors: Option<String>,
762 #[cfg(windows)]
763 creationflags: Option<u32>,
764 #[cfg(unix)]
765 create_process_group: bool,
766}
767
768enum NativeProcessBackend {
769 Running(NativeRunningProcess),
770 Pty(NativePtyProcess),
771}
772
773#[pyclass(name = "NativeProcess")]
774struct PyNativeProcess {
775 backend: NativeProcessBackend,
776}
777
778#[pyclass]
779#[derive(Clone)]
780struct NativeSignalBool {
781 value: Arc<AtomicBool>,
782 write_lock: Arc<Mutex<()>>,
783}
784
785#[pyclass]
790struct NativeIdleDetector {
791 core: Arc<IdleDetectorCore>,
792}
793
794struct PtyBufferState {
795 chunks: VecDeque<Vec<u8>>,
796 history: Vec<u8>,
797 history_bytes: usize,
798 closed: bool,
799}
800
801#[pyclass]
802struct NativePtyBuffer {
803 text: bool,
804 encoding: String,
805 errors: String,
806 state: Mutex<PtyBufferState>,
807 condvar: Condvar,
808}
809
810#[pyclass]
819#[derive(Clone)]
820struct NativeTerminalInputEvent {
821 data: Vec<u8>,
822 submit: bool,
823 shift: bool,
824 ctrl: bool,
825 alt: bool,
826 virtual_key_code: u16,
827 repeat_count: u16,
828}
829
830#[pyclass]
831struct NativeTerminalInput {
832 inner: TerminalInputCore,
833}
834
835#[pymethods]
836impl NativeRunningProcess {
837 #[new]
838 #[allow(clippy::too_many_arguments)]
839 #[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))]
840 fn new(
841 command: &Bound<'_, PyAny>,
842 cwd: Option<String>,
843 shell: bool,
844 capture: bool,
845 env: Option<Bound<'_, PyDict>>,
846 creationflags: Option<u32>,
847 text: bool,
848 encoding: Option<String>,
849 errors: Option<String>,
850 stdin_mode_name: &str,
851 stderr_mode_name: &str,
852 nice: Option<i32>,
853 create_process_group: bool,
854 ) -> PyResult<Self> {
855 let parsed = parse_command(command, shell)?;
856 let env_pairs = env
857 .map(|mapping| {
858 mapping
859 .iter()
860 .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
861 .collect::<PyResult<Vec<(String, String)>>>()
862 })
863 .transpose()?;
864
865 Ok(Self {
866 inner: NativeProcess::new(ProcessConfig {
867 command: parsed,
868 cwd: cwd.map(PathBuf::from),
869 env: env_pairs,
870 capture,
871 stderr_mode: stderr_mode(stderr_mode_name)?,
872 creationflags,
873 create_process_group,
874 stdin_mode: stdin_mode(stdin_mode_name)?,
875 nice,
876 containment: None,
877 }),
878 text,
879 encoding,
880 errors,
881 #[cfg(windows)]
882 creationflags,
883 #[cfg(unix)]
884 create_process_group,
885 })
886 }
887
888 #[inline(never)]
889 fn start(&self) -> PyResult<()> {
890 public_symbols::rp_native_running_process_start_public(self)
891 }
892
893 fn poll(&self) -> PyResult<Option<i32>> {
894 self.inner.poll().map_err(to_py_err)
895 }
896
897 #[pyo3(signature = (timeout=None))]
898 #[inline(never)]
899 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
900 public_symbols::rp_native_running_process_wait_public(self, py, timeout)
901 }
902
903 #[inline(never)]
904 fn kill(&self) -> PyResult<()> {
905 public_symbols::rp_native_running_process_kill_public(self)
906 }
907
908 #[inline(never)]
909 fn terminate(&self) -> PyResult<()> {
910 public_symbols::rp_native_running_process_terminate_public(self)
911 }
912
913 #[inline(never)]
914 fn close(&self, py: Python<'_>) -> PyResult<()> {
915 public_symbols::rp_native_running_process_close_public(self, py)
916 }
917
918 fn terminate_group(&self) -> PyResult<()> {
919 #[cfg(unix)]
920 {
921 let pid = self
922 .inner
923 .pid()
924 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
925 if self.create_process_group {
926 unix_signal_process_group(pid as i32, UnixSignal::Terminate).map_err(to_py_err)?;
927 return Ok(());
928 }
929 }
930 self.inner.terminate().map_err(to_py_err)
931 }
932
933 fn write_stdin(&self, data: &[u8]) -> PyResult<()> {
934 self.inner.write_stdin(data).map_err(to_py_err)
935 }
936
937 #[getter]
938 fn pid(&self) -> Option<u32> {
939 self.inner.pid()
940 }
941
942 #[getter]
943 fn returncode(&self) -> Option<i32> {
944 self.inner.returncode()
945 }
946
947 #[inline(never)]
948 fn send_interrupt(&self) -> PyResult<()> {
949 public_symbols::rp_native_running_process_send_interrupt_public(self)
950 }
951
952 fn kill_group(&self) -> PyResult<()> {
953 #[cfg(unix)]
954 {
955 let pid = self
956 .inner
957 .pid()
958 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
959 if self.create_process_group {
960 unix_signal_process_group(pid as i32, UnixSignal::Kill).map_err(to_py_err)?;
961 return Ok(());
962 }
963 }
964 self.inner.kill().map_err(to_py_err)
965 }
966
967 fn has_pending_combined(&self) -> bool {
968 self.inner.has_pending_combined()
969 }
970
971 fn has_pending_stream(&self, stream: &str) -> PyResult<bool> {
972 Ok(self.inner.has_pending_stream(stream_kind(stream)?))
973 }
974
975 fn drain_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
976 self.inner
977 .drain_combined()
978 .into_iter()
979 .map(|event| {
980 Ok((
981 event.stream.as_str().to_string(),
982 self.decode_line(py, &event.line)?,
983 ))
984 })
985 .collect()
986 }
987
988 fn drain_stream(&self, py: Python<'_>, stream: &str) -> PyResult<Vec<Py<PyAny>>> {
989 self.inner
990 .drain_stream(stream_kind(stream)?)
991 .into_iter()
992 .map(|line| self.decode_line(py, &line))
993 .collect()
994 }
995
996 #[pyo3(signature = (timeout=None))]
997 fn take_combined_line(
998 &self,
999 py: Python<'_>,
1000 timeout: Option<f64>,
1001 ) -> PyResult<(String, Option<String>, Option<Py<PyAny>>)> {
1002 match self
1003 .inner
1004 .read_combined(timeout.map(Duration::from_secs_f64))
1005 {
1006 ReadStatus::Line(StreamEvent { stream, line }) => Ok((
1007 "line".into(),
1008 Some(stream.as_str().into()),
1009 Some(self.decode_line(py, &line)?),
1010 )),
1011 ReadStatus::Timeout => Ok(("timeout".into(), None, None)),
1012 ReadStatus::Eof => Ok(("eof".into(), None, None)),
1013 }
1014 }
1015
1016 #[pyo3(signature = (stream, timeout=None))]
1017 fn take_stream_line(
1018 &self,
1019 py: Python<'_>,
1020 stream: &str,
1021 timeout: Option<f64>,
1022 ) -> PyResult<(String, Option<Py<PyAny>>)> {
1023 match self
1024 .inner
1025 .read_stream(stream_kind(stream)?, timeout.map(Duration::from_secs_f64))
1026 {
1027 ReadStatus::Line(line) => Ok(("line".into(), Some(self.decode_line(py, &line)?))),
1028 ReadStatus::Timeout => Ok(("timeout".into(), None)),
1029 ReadStatus::Eof => Ok(("eof".into(), None)),
1030 }
1031 }
1032
1033 fn captured_stdout(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1034 self.inner
1035 .captured_stdout()
1036 .into_iter()
1037 .map(|line| self.decode_line(py, &line))
1038 .collect()
1039 }
1040
1041 fn captured_stderr(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1042 self.inner
1043 .captured_stderr()
1044 .into_iter()
1045 .map(|line| self.decode_line(py, &line))
1046 .collect()
1047 }
1048
1049 fn captured_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
1050 self.inner
1051 .captured_combined()
1052 .into_iter()
1053 .map(|event| {
1054 Ok((
1055 event.stream.as_str().to_string(),
1056 self.decode_line(py, &event.line)?,
1057 ))
1058 })
1059 .collect()
1060 }
1061
1062 fn captured_stream_bytes(&self, stream: &str) -> PyResult<usize> {
1063 Ok(self.inner.captured_stream_bytes(stream_kind(stream)?))
1064 }
1065
1066 fn captured_combined_bytes(&self) -> usize {
1067 self.inner.captured_combined_bytes()
1068 }
1069
1070 fn clear_captured_stream(&self, stream: &str) -> PyResult<usize> {
1071 Ok(self.inner.clear_captured_stream(stream_kind(stream)?))
1072 }
1073
1074 fn clear_captured_combined(&self) -> usize {
1075 self.inner.clear_captured_combined()
1076 }
1077
1078 #[pyo3(signature = (stream, pattern, is_regex=false, timeout=None))]
1079 fn expect(
1080 &self,
1081 py: Python<'_>,
1082 stream: &str,
1083 pattern: &str,
1084 is_regex: bool,
1085 timeout: Option<f64>,
1086 ) -> PyResult<ExpectResult> {
1087 let stream_kind = if stream == "combined" {
1088 None
1089 } else {
1090 Some(stream_kind(stream)?)
1091 };
1092 let mut buffer = match stream_kind {
1093 Some(kind) => self.captured_stream_text(py, kind)?,
1094 None => self.captured_combined_text(py)?,
1095 };
1096 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
1097 let compiled_regex = if is_regex {
1098 Some(Regex::new(pattern).map_err(to_py_err)?)
1099 } else {
1100 None
1101 };
1102
1103 loop {
1104 if let Some((matched, start, end, groups)) =
1105 self.find_expect_match(&buffer, pattern, compiled_regex.as_ref())?
1106 {
1107 return Ok((
1108 "match".to_string(),
1109 buffer,
1110 Some(matched),
1111 Some(start),
1112 Some(end),
1113 groups,
1114 ));
1115 }
1116
1117 let wait_timeout = deadline.map(|limit| {
1118 let now = Instant::now();
1119 if now >= limit {
1120 Duration::from_secs(0)
1121 } else {
1122 limit
1123 .saturating_duration_since(now)
1124 .min(Duration::from_millis(100))
1125 }
1126 });
1127 if deadline.is_some_and(|limit| Instant::now() >= limit) {
1128 return Ok(("timeout".to_string(), buffer, None, None, None, Vec::new()));
1129 }
1130
1131 match self.read_status_text(stream_kind, wait_timeout)? {
1132 ReadStatus::Line(line) => {
1133 let decoded = self.decode_line_to_string(py, &line)?;
1134 buffer.push_str(&decoded);
1135 buffer.push('\n');
1136 }
1137 ReadStatus::Timeout => {
1138 continue;
1140 }
1141 ReadStatus::Eof => {
1142 return Ok(("eof".to_string(), buffer, None, None, None, Vec::new()));
1143 }
1144 }
1145 }
1146 }
1147
1148 #[staticmethod]
1149 fn is_pty_available() -> bool {
1150 false
1151 }
1152}
1153
1154#[pymethods]
1155impl PyNativeProcess {
1156 #[new]
1157 #[allow(clippy::too_many_arguments)]
1158 #[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))]
1159 fn new(
1160 command: &Bound<'_, PyAny>,
1161 cwd: Option<String>,
1162 shell: bool,
1163 capture: bool,
1164 env: Option<Bound<'_, PyDict>>,
1165 creationflags: Option<u32>,
1166 text: bool,
1167 encoding: Option<String>,
1168 errors: Option<String>,
1169 stdin_mode_name: &str,
1170 stderr_mode_name: &str,
1171 nice: Option<i32>,
1172 create_process_group: bool,
1173 ) -> PyResult<Self> {
1174 Ok(Self {
1175 backend: NativeProcessBackend::Running(NativeRunningProcess::new(
1176 command,
1177 cwd,
1178 shell,
1179 capture,
1180 env,
1181 creationflags,
1182 text,
1183 encoding,
1184 errors,
1185 stdin_mode_name,
1186 stderr_mode_name,
1187 nice,
1188 create_process_group,
1189 )?),
1190 })
1191 }
1192
1193 #[staticmethod]
1194 #[pyo3(signature = (argv, cwd=None, env=None, rows=24, cols=80, nice=None))]
1195 fn for_pty(
1196 argv: Vec<String>,
1197 cwd: Option<String>,
1198 env: Option<Bound<'_, PyDict>>,
1199 rows: u16,
1200 cols: u16,
1201 nice: Option<i32>,
1202 ) -> PyResult<Self> {
1203 let env_pairs = env
1204 .map(|mapping| {
1205 mapping
1206 .iter()
1207 .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
1208 .collect::<PyResult<Vec<(String, String)>>>()
1209 })
1210 .transpose()?;
1211 let inner = CoreNativePtyProcess::new(argv, cwd, env_pairs, rows, cols, nice)
1212 .map_err(NativePtyProcess::pty_err_to_py)?;
1213 Ok(Self {
1214 backend: NativeProcessBackend::Pty(NativePtyProcess { inner }),
1215 })
1216 }
1217
1218 fn start(&self) -> PyResult<()> {
1219 match &self.backend {
1220 NativeProcessBackend::Running(process) => process.start(),
1221 NativeProcessBackend::Pty(process) => process.start(),
1222 }
1223 }
1224
1225 fn poll(&self) -> PyResult<Option<i32>> {
1226 match &self.backend {
1227 NativeProcessBackend::Running(process) => process.poll(),
1228 NativeProcessBackend::Pty(process) => process.poll(),
1229 }
1230 }
1231
1232 #[pyo3(signature = (timeout=None))]
1233 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
1234 match &self.backend {
1235 NativeProcessBackend::Running(process) => process.wait(py, timeout),
1236 NativeProcessBackend::Pty(process) => py.allow_threads(|| process.wait(timeout)),
1237 }
1238 }
1239
1240 fn kill(&self, py: Python<'_>) -> PyResult<()> {
1241 match &self.backend {
1242 NativeProcessBackend::Running(process) => process.kill(),
1243 NativeProcessBackend::Pty(process) => process.kill(py),
1244 }
1245 }
1246
1247 fn terminate(&self, py: Python<'_>) -> PyResult<()> {
1248 match &self.backend {
1249 NativeProcessBackend::Running(process) => process.terminate(),
1250 NativeProcessBackend::Pty(process) => process.terminate(py),
1251 }
1252 }
1253
1254 fn terminate_group(&self) -> PyResult<()> {
1255 match &self.backend {
1256 NativeProcessBackend::Running(process) => process.terminate_group(),
1257 NativeProcessBackend::Pty(process) => process.terminate_tree(),
1258 }
1259 }
1260
1261 fn kill_group(&self) -> PyResult<()> {
1262 match &self.backend {
1263 NativeProcessBackend::Running(process) => process.kill_group(),
1264 NativeProcessBackend::Pty(process) => process.kill_tree(),
1265 }
1266 }
1267
1268 fn has_pending_combined(&self) -> PyResult<bool> {
1269 match &self.backend {
1270 NativeProcessBackend::Running(process) => Ok(process.has_pending_combined()),
1271 NativeProcessBackend::Pty(_) => Ok(false),
1272 }
1273 }
1274
1275 fn has_pending_stream(&self, stream: &str) -> PyResult<bool> {
1276 match &self.backend {
1277 NativeProcessBackend::Running(process) => process.has_pending_stream(stream),
1278 NativeProcessBackend::Pty(_) => Ok(false),
1279 }
1280 }
1281
1282 fn drain_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
1283 match &self.backend {
1284 NativeProcessBackend::Running(process) => process.drain_combined(py),
1285 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
1286 }
1287 }
1288
1289 fn drain_stream(&self, py: Python<'_>, stream: &str) -> PyResult<Vec<Py<PyAny>>> {
1290 match &self.backend {
1291 NativeProcessBackend::Running(process) => process.drain_stream(py, stream),
1292 NativeProcessBackend::Pty(_) => {
1293 let _ = stream;
1294 Ok(Vec::new())
1295 }
1296 }
1297 }
1298
1299 #[pyo3(signature = (timeout=None))]
1300 fn take_combined_line(
1301 &self,
1302 py: Python<'_>,
1303 timeout: Option<f64>,
1304 ) -> PyResult<(String, Option<String>, Option<Py<PyAny>>)> {
1305 match &self.backend {
1306 NativeProcessBackend::Running(process) => process.take_combined_line(py, timeout),
1307 NativeProcessBackend::Pty(_) => Ok(("eof".into(), None, None)),
1308 }
1309 }
1310
1311 #[pyo3(signature = (stream, timeout=None))]
1312 fn take_stream_line(
1313 &self,
1314 py: Python<'_>,
1315 stream: &str,
1316 timeout: Option<f64>,
1317 ) -> PyResult<(String, Option<Py<PyAny>>)> {
1318 match &self.backend {
1319 NativeProcessBackend::Running(process) => process.take_stream_line(py, stream, timeout),
1320 NativeProcessBackend::Pty(_) => {
1321 let _ = (py, stream, timeout);
1322 Ok(("eof".into(), None))
1323 }
1324 }
1325 }
1326
1327 fn captured_stdout(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1328 match &self.backend {
1329 NativeProcessBackend::Running(process) => process.captured_stdout(py),
1330 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
1331 }
1332 }
1333
1334 fn captured_stderr(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1335 match &self.backend {
1336 NativeProcessBackend::Running(process) => process.captured_stderr(py),
1337 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
1338 }
1339 }
1340
1341 fn captured_combined(&self, py: Python<'_>) -> PyResult<Vec<(String, Py<PyAny>)>> {
1342 match &self.backend {
1343 NativeProcessBackend::Running(process) => process.captured_combined(py),
1344 NativeProcessBackend::Pty(_) => Ok(Vec::new()),
1345 }
1346 }
1347
1348 fn captured_stream_bytes(&self, stream: &str) -> PyResult<usize> {
1349 match &self.backend {
1350 NativeProcessBackend::Running(process) => process.captured_stream_bytes(stream),
1351 NativeProcessBackend::Pty(_) => Ok(0),
1352 }
1353 }
1354
1355 fn captured_combined_bytes(&self) -> PyResult<usize> {
1356 match &self.backend {
1357 NativeProcessBackend::Running(process) => Ok(process.captured_combined_bytes()),
1358 NativeProcessBackend::Pty(_) => Ok(0),
1359 }
1360 }
1361
1362 fn clear_captured_stream(&self, stream: &str) -> PyResult<usize> {
1363 match &self.backend {
1364 NativeProcessBackend::Running(process) => process.clear_captured_stream(stream),
1365 NativeProcessBackend::Pty(_) => Ok(0),
1366 }
1367 }
1368
1369 fn clear_captured_combined(&self) -> PyResult<usize> {
1370 match &self.backend {
1371 NativeProcessBackend::Running(process) => Ok(process.clear_captured_combined()),
1372 NativeProcessBackend::Pty(_) => Ok(0),
1373 }
1374 }
1375
1376 fn write_stdin(&self, data: &[u8]) -> PyResult<()> {
1377 match &self.backend {
1378 NativeProcessBackend::Running(process) => process.write_stdin(data),
1379 NativeProcessBackend::Pty(process) => process.write(data, false),
1380 }
1381 }
1382
1383 #[pyo3(signature = (data, submit=false))]
1384 fn write(&self, data: &[u8], submit: bool) -> PyResult<()> {
1385 match &self.backend {
1386 NativeProcessBackend::Running(process) => process.write_stdin(data),
1387 NativeProcessBackend::Pty(process) => process.write(data, submit),
1388 }
1389 }
1390
1391 #[pyo3(signature = (timeout=None))]
1392 fn read_chunk(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
1393 match &self.backend {
1394 NativeProcessBackend::Pty(process) => process.read_chunk(py, timeout),
1395 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1396 "read_chunk is only available for PTY-backed NativeProcess",
1397 )),
1398 }
1399 }
1400
1401 #[pyo3(signature = (timeout=None))]
1402 fn wait_for_pty_reader_closed(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<bool> {
1403 match &self.backend {
1404 NativeProcessBackend::Pty(process) => process.wait_for_reader_closed(py, timeout),
1405 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1406 "wait_for_pty_reader_closed is only available for PTY-backed NativeProcess",
1407 )),
1408 }
1409 }
1410
1411 fn respond_to_queries(&self, data: &[u8]) -> PyResult<()> {
1412 match &self.backend {
1413 NativeProcessBackend::Pty(process) => process.respond_to_queries(data),
1414 NativeProcessBackend::Running(_) => Ok(()),
1415 }
1416 }
1417
1418 fn resize(&self, rows: u16, cols: u16) -> PyResult<()> {
1419 match &self.backend {
1420 NativeProcessBackend::Pty(process) => process.resize(rows, cols),
1421 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1422 "resize is only available for PTY-backed NativeProcess",
1423 )),
1424 }
1425 }
1426
1427 fn send_interrupt(&self) -> PyResult<()> {
1428 match &self.backend {
1429 NativeProcessBackend::Running(process) => process.send_interrupt(),
1430 NativeProcessBackend::Pty(process) => process.send_interrupt(),
1431 }
1432 }
1433
1434 #[pyo3(signature = (stream, pattern, is_regex=false, timeout=None))]
1435 fn expect(
1436 &self,
1437 py: Python<'_>,
1438 stream: &str,
1439 pattern: &str,
1440 is_regex: bool,
1441 timeout: Option<f64>,
1442 ) -> PyResult<ExpectResult> {
1443 match &self.backend {
1444 NativeProcessBackend::Running(process) => {
1445 process.expect(py, stream, pattern, is_regex, timeout)
1446 }
1447 NativeProcessBackend::Pty(_) => Err(PyRuntimeError::new_err(
1448 "expect is only available for subprocess-backed NativeProcess",
1449 )),
1450 }
1451 }
1452
1453 fn close(&self, py: Python<'_>) -> PyResult<()> {
1454 match &self.backend {
1455 NativeProcessBackend::Running(process) => process.close(py),
1456 NativeProcessBackend::Pty(process) => process.close(py),
1457 }
1458 }
1459
1460 fn start_terminal_input_relay(&self) -> PyResult<()> {
1461 match &self.backend {
1462 NativeProcessBackend::Pty(process) => process.start_terminal_input_relay(),
1463 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1464 "terminal input relay is only available for PTY-backed NativeProcess",
1465 )),
1466 }
1467 }
1468
1469 fn stop_terminal_input_relay(&self) -> PyResult<()> {
1470 match &self.backend {
1471 NativeProcessBackend::Pty(process) => {
1472 process.stop_terminal_input_relay();
1473 Ok(())
1474 }
1475 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1476 "terminal input relay is only available for PTY-backed NativeProcess",
1477 )),
1478 }
1479 }
1480
1481 fn terminal_input_relay_active(&self) -> PyResult<bool> {
1482 match &self.backend {
1483 NativeProcessBackend::Pty(process) => Ok(process.terminal_input_relay_active()),
1484 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1485 "terminal input relay is only available for PTY-backed NativeProcess",
1486 )),
1487 }
1488 }
1489
1490 fn pty_input_bytes_total(&self) -> PyResult<usize> {
1491 match &self.backend {
1492 NativeProcessBackend::Pty(process) => Ok(process.pty_input_bytes_total()),
1493 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1494 "PTY input metrics are only available for PTY-backed NativeProcess",
1495 )),
1496 }
1497 }
1498
1499 fn pty_newline_events_total(&self) -> PyResult<usize> {
1500 match &self.backend {
1501 NativeProcessBackend::Pty(process) => Ok(process.pty_newline_events_total()),
1502 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1503 "PTY input metrics are only available for PTY-backed NativeProcess",
1504 )),
1505 }
1506 }
1507
1508 fn pty_submit_events_total(&self) -> PyResult<usize> {
1509 match &self.backend {
1510 NativeProcessBackend::Pty(process) => Ok(process.pty_submit_events_total()),
1511 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1512 "PTY input metrics are only available for PTY-backed NativeProcess",
1513 )),
1514 }
1515 }
1516
1517 #[getter]
1518 fn pid(&self) -> PyResult<Option<u32>> {
1519 match &self.backend {
1520 NativeProcessBackend::Running(process) => Ok(process.pid()),
1521 NativeProcessBackend::Pty(process) => process.pid(),
1522 }
1523 }
1524
1525 #[getter]
1526 fn returncode(&self) -> PyResult<Option<i32>> {
1527 match &self.backend {
1528 NativeProcessBackend::Running(process) => Ok(process.returncode()),
1529 NativeProcessBackend::Pty(process) => Ok(*process
1530 .inner
1531 .returncode
1532 .lock()
1533 .expect("pty returncode mutex poisoned")),
1534 }
1535 }
1536
1537 fn is_pty(&self) -> bool {
1538 matches!(self.backend, NativeProcessBackend::Pty(_))
1539 }
1540
1541 #[pyo3(signature = (timeout=None, drain_timeout=2.0))]
1543 fn wait_and_drain(
1544 &self,
1545 py: Python<'_>,
1546 timeout: Option<f64>,
1547 drain_timeout: f64,
1548 ) -> PyResult<i32> {
1549 match &self.backend {
1550 NativeProcessBackend::Pty(process) => {
1551 process.wait_and_drain(py, timeout, drain_timeout)
1552 }
1553 NativeProcessBackend::Running(_) => Err(PyRuntimeError::new_err(
1554 "wait_and_drain is only available for PTY-backed NativeProcess",
1555 )),
1556 }
1557 }
1558}
1559
1560#[pymethods]
1561impl NativePtyProcess {
1562 #[new]
1563 #[pyo3(signature = (argv, cwd=None, env=None, rows=24, cols=80, nice=None))]
1564 fn new(
1565 argv: Vec<String>,
1566 cwd: Option<String>,
1567 env: Option<Bound<'_, PyDict>>,
1568 rows: u16,
1569 cols: u16,
1570 nice: Option<i32>,
1571 ) -> PyResult<Self> {
1572 let env_pairs = env
1573 .map(|mapping| {
1574 mapping
1575 .iter()
1576 .map(|(key, value)| Ok((key.extract::<String>()?, value.extract::<String>()?)))
1577 .collect::<PyResult<Vec<(String, String)>>>()
1578 })
1579 .transpose()?;
1580 let inner = CoreNativePtyProcess::new(argv, cwd, env_pairs, rows, cols, nice)
1581 .map_err(Self::pty_err_to_py)?;
1582 Ok(Self { inner })
1583 }
1584
1585 #[inline(never)]
1586 fn start(&self) -> PyResult<()> {
1587 self.inner.start_impl().map_err(Self::pty_err_to_py)
1588 }
1589
1590 #[pyo3(signature = (timeout=None))]
1591 fn read_chunk(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
1592 let result = py.allow_threads(|| self.inner.read_chunk_impl(timeout));
1593 match result {
1594 Ok(Some(chunk)) => Ok(PyBytes::new(py, &chunk).into_any().unbind()),
1595 Ok(None) => Err(PyTimeoutError::new_err(
1596 "No pseudo-terminal output available before timeout",
1597 )),
1598 Err(e) => Err(Self::pty_err_to_py(e)),
1599 }
1600 }
1601
1602 #[pyo3(signature = (timeout=None))]
1603 fn wait_for_reader_closed(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<bool> {
1604 Ok(py.allow_threads(|| self.inner.wait_for_reader_closed_impl(timeout)))
1605 }
1606
1607 #[pyo3(signature = (data, submit=false))]
1608 fn write(&self, data: &[u8], submit: bool) -> PyResult<()> {
1609 self.inner
1610 .write_impl(data, submit)
1611 .map_err(Self::pty_err_to_py)
1612 }
1613
1614 fn respond_to_queries(&self, data: &[u8]) -> PyResult<()> {
1615 self.inner
1616 .respond_to_queries_impl(data)
1617 .map_err(Self::pty_err_to_py)
1618 }
1619
1620 #[inline(never)]
1621 fn resize(&self, rows: u16, cols: u16) -> PyResult<()> {
1622 self.inner
1623 .resize_impl(rows, cols)
1624 .map_err(Self::pty_err_to_py)
1625 }
1626
1627 #[inline(never)]
1628 fn send_interrupt(&self) -> PyResult<()> {
1629 self.inner
1630 .send_interrupt_impl()
1631 .map_err(Self::pty_err_to_py)
1632 }
1633
1634 fn poll(&self) -> PyResult<Option<i32>> {
1635 core_pty::poll_pty_process(&self.inner.handles, &self.inner.returncode).map_err(to_py_err)
1636 }
1637
1638 #[pyo3(signature = (timeout=None))]
1639 #[inline(never)]
1640 fn wait(&self, timeout: Option<f64>) -> PyResult<i32> {
1641 self.inner.wait_impl(timeout).map_err(Self::pty_err_to_py)
1642 }
1643
1644 #[inline(never)]
1645 fn terminate(&self, py: Python<'_>) -> PyResult<()> {
1646 py.allow_threads(|| self.inner.terminate_impl().map_err(Self::pty_err_to_py))
1647 }
1648
1649 #[inline(never)]
1650 fn kill(&self, py: Python<'_>) -> PyResult<()> {
1651 py.allow_threads(|| self.inner.kill_impl().map_err(Self::pty_err_to_py))
1652 }
1653
1654 #[inline(never)]
1655 fn terminate_tree(&self) -> PyResult<()> {
1656 self.inner
1657 .terminate_tree_impl()
1658 .map_err(Self::pty_err_to_py)
1659 }
1660
1661 #[inline(never)]
1662 fn kill_tree(&self) -> PyResult<()> {
1663 self.inner.kill_tree_impl().map_err(Self::pty_err_to_py)
1664 }
1665
1666 fn start_terminal_input_relay(&self) -> PyResult<()> {
1667 self.start_terminal_input_relay_py()
1668 }
1669
1670 fn stop_terminal_input_relay(&self) {
1671 self.inner.stop_terminal_input_relay_impl();
1672 }
1673
1674 fn terminal_input_relay_active(&self) -> bool {
1675 self.inner.terminal_input_relay_active()
1676 }
1677
1678 fn pty_input_bytes_total(&self) -> usize {
1679 self.inner.pty_input_bytes_total()
1680 }
1681
1682 fn pty_newline_events_total(&self) -> usize {
1683 self.inner.pty_newline_events_total()
1684 }
1685
1686 fn pty_submit_events_total(&self) -> usize {
1687 self.inner.pty_submit_events_total()
1688 }
1689
1690 fn pty_output_bytes_total(&self) -> usize {
1691 self.inner.pty_output_bytes_total()
1692 }
1693
1694 fn pty_control_churn_bytes_total(&self) -> usize {
1695 self.inner.pty_control_churn_bytes_total()
1696 }
1697
1698 #[pyo3(signature = (timeout=None, drain_timeout=2.0))]
1699 fn wait_and_drain(
1700 &self,
1701 py: Python<'_>,
1702 timeout: Option<f64>,
1703 drain_timeout: f64,
1704 ) -> PyResult<i32> {
1705 py.allow_threads(|| {
1706 self.inner
1707 .wait_and_drain_impl(timeout, drain_timeout)
1708 .map_err(Self::pty_err_to_py)
1709 })
1710 }
1711
1712 fn set_echo(&self, enabled: bool) {
1713 self.inner.set_echo(enabled);
1714 }
1715
1716 fn echo_enabled(&self) -> bool {
1717 self.inner.echo_enabled()
1718 }
1719
1720 fn attach_idle_detector(&self, detector: &NativeIdleDetector) {
1721 self.inner.attach_idle_detector(&detector.core);
1722 }
1723
1724 fn detach_idle_detector(&self) {
1725 self.inner.detach_idle_detector();
1726 }
1727
1728 #[pyo3(signature = (detector, timeout=None))]
1729 fn wait_for_idle(
1730 &self,
1731 py: Python<'_>,
1732 detector: &NativeIdleDetector,
1733 timeout: Option<f64>,
1734 ) -> PyResult<(bool, String, f64, Option<i32>)> {
1735 self.inner.attach_idle_detector(&detector.core);
1737
1738 let handles = Arc::clone(&self.inner.handles);
1740 let returncode = Arc::clone(&self.inner.returncode);
1741 let core = Arc::clone(&detector.core);
1742 let exit_watcher = thread::spawn(move || loop {
1743 match core_pty::poll_pty_process(&handles, &returncode) {
1744 Ok(Some(code)) => {
1745 let interrupted = code == -2 || code == 130;
1746 core.mark_exit(code, interrupted);
1747 return;
1748 }
1749 Ok(None) => {}
1750 Err(_) => return,
1751 }
1752 thread::sleep(Duration::from_millis(1));
1753 });
1754
1755 let result = py.allow_threads(|| detector.core.wait(timeout));
1756
1757 self.inner.detach_idle_detector();
1758 let _ = exit_watcher.join();
1759 Ok(result)
1760 }
1761
1762 #[getter]
1763 fn pid(&self) -> PyResult<Option<u32>> {
1764 self.inner.pid().map_err(Self::pty_err_to_py)
1765 }
1766
1767 fn close(&self, py: Python<'_>) -> PyResult<()> {
1768 py.allow_threads(|| self.inner.close_impl().map_err(Self::pty_err_to_py))
1769 }
1770}
1771
1772#[pymethods]
1773impl NativeSignalBool {
1774 #[new]
1775 #[pyo3(signature = (value=false))]
1776 fn new(value: bool) -> Self {
1777 Self {
1778 value: Arc::new(AtomicBool::new(value)),
1779 write_lock: Arc::new(Mutex::new(())),
1780 }
1781 }
1782
1783 #[getter]
1784 fn value(&self) -> bool {
1785 self.load_nolock()
1786 }
1787
1788 #[setter]
1789 fn set_value(&self, value: bool) {
1790 self.store_locked(value);
1791 }
1792
1793 fn load_nolock(&self) -> bool {
1794 self.value.load(Ordering::Acquire)
1795 }
1796
1797 fn store_locked(&self, value: bool) {
1798 let _guard = self.write_lock.lock().expect("signal bool mutex poisoned");
1799 self.value.store(value, Ordering::Release);
1800 }
1801
1802 fn compare_and_swap_locked(&self, current: bool, new: bool) -> bool {
1803 let _guard = self.write_lock.lock().expect("signal bool mutex poisoned");
1804 self.value
1805 .compare_exchange(current, new, Ordering::AcqRel, Ordering::Acquire)
1806 .is_ok()
1807 }
1808}
1809
1810#[pymethods]
1811impl NativePtyBuffer {
1812 #[new]
1813 #[pyo3(signature = (text=false, encoding="utf-8", errors="replace"))]
1814 fn new(text: bool, encoding: &str, errors: &str) -> Self {
1815 Self {
1816 text,
1817 encoding: encoding.to_string(),
1818 errors: errors.to_string(),
1819 state: Mutex::new(PtyBufferState {
1820 chunks: VecDeque::new(),
1821 history: Vec::new(),
1822 history_bytes: 0,
1823 closed: false,
1824 }),
1825 condvar: Condvar::new(),
1826 }
1827 }
1828
1829 fn available(&self) -> bool {
1830 !self
1831 .state
1832 .lock()
1833 .expect("pty buffer mutex poisoned")
1834 .chunks
1835 .is_empty()
1836 }
1837
1838 fn record_output(&self, data: &[u8]) {
1839 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1840 guard.history_bytes += data.len();
1841 guard.history.extend_from_slice(data);
1842 guard.chunks.push_back(data.to_vec());
1843 self.condvar.notify_all();
1844 }
1845
1846 fn close(&self) {
1847 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1848 guard.closed = true;
1849 self.condvar.notify_all();
1850 }
1851
1852 #[pyo3(signature = (timeout=None))]
1853 fn read(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
1854 enum WaitOutcome {
1858 Chunk(Vec<u8>),
1859 Closed,
1860 Timeout,
1861 }
1862
1863 let outcome = py.allow_threads(|| -> WaitOutcome {
1864 let deadline = timeout.map(|secs| Instant::now() + Duration::from_secs_f64(secs));
1865 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1866 loop {
1867 if let Some(chunk) = guard.chunks.pop_front() {
1868 return WaitOutcome::Chunk(chunk);
1869 }
1870 if guard.closed {
1871 return WaitOutcome::Closed;
1872 }
1873 match deadline {
1874 Some(deadline) => {
1875 let now = Instant::now();
1876 if now >= deadline {
1877 return WaitOutcome::Timeout;
1878 }
1879 let wait = deadline.saturating_duration_since(now);
1880 let result = self
1881 .condvar
1882 .wait_timeout(guard, wait)
1883 .expect("pty buffer mutex poisoned");
1884 guard = result.0;
1885 }
1886 None => {
1887 guard = self.condvar.wait(guard).expect("pty buffer mutex poisoned");
1888 }
1889 }
1890 }
1891 });
1892
1893 match outcome {
1894 WaitOutcome::Chunk(chunk) => self.decode_chunk(py, &chunk),
1895 WaitOutcome::Closed => Err(PyRuntimeError::new_err("Pseudo-terminal stream is closed")),
1896 WaitOutcome::Timeout => Err(PyTimeoutError::new_err(
1897 "No pseudo-terminal output available before timeout",
1898 )),
1899 }
1900 }
1901
1902 fn read_non_blocking(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
1903 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1904 if let Some(chunk) = guard.chunks.pop_front() {
1905 return self.decode_chunk(py, &chunk).map(Some);
1906 }
1907 if guard.closed {
1908 return Err(PyRuntimeError::new_err("Pseudo-terminal stream is closed"));
1909 }
1910 Ok(None)
1911 }
1912
1913 fn drain(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1914 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1915 guard
1916 .chunks
1917 .drain(..)
1918 .map(|chunk| self.decode_chunk(py, &chunk))
1919 .collect()
1920 }
1921
1922 fn output(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
1923 let guard = self.state.lock().expect("pty buffer mutex poisoned");
1924 self.decode_chunk(py, &guard.history)
1925 }
1926
1927 fn output_since(&self, py: Python<'_>, start: usize) -> PyResult<Py<PyAny>> {
1928 let guard = self.state.lock().expect("pty buffer mutex poisoned");
1929 let start = start.min(guard.history.len());
1930 self.decode_chunk(py, &guard.history[start..])
1931 }
1932
1933 fn history_bytes(&self) -> usize {
1934 self.state
1935 .lock()
1936 .expect("pty buffer mutex poisoned")
1937 .history_bytes
1938 }
1939
1940 fn clear_history(&self) -> usize {
1941 let mut guard = self.state.lock().expect("pty buffer mutex poisoned");
1942 let released = guard.history_bytes;
1943 guard.history.clear();
1944 guard.history_bytes = 0;
1945 released
1946 }
1947}
1948
1949impl NativeTerminalInput {
1950 fn event_to_py(
1951 py: Python<'_>,
1952 event: TerminalInputEventRecord,
1953 ) -> PyResult<Py<NativeTerminalInputEvent>> {
1954 Py::new(
1955 py,
1956 NativeTerminalInputEvent {
1957 data: event.data,
1958 submit: event.submit,
1959 shift: event.shift,
1960 ctrl: event.ctrl,
1961 alt: event.alt,
1962 virtual_key_code: event.virtual_key_code,
1963 repeat_count: event.repeat_count,
1964 },
1965 )
1966 }
1967
1968 fn wait_for_event(
1969 &self,
1970 py: Python<'_>,
1971 timeout: Option<f64>,
1972 ) -> PyResult<TerminalInputEventRecord> {
1973 py.allow_threads(|| {
1974 self.inner.wait_for_event(timeout).map_err(|err| match err {
1975 TerminalInputError::Closed => {
1976 PyRuntimeError::new_err("Native terminal input is closed")
1977 }
1978 TerminalInputError::Timeout => {
1979 PyTimeoutError::new_err("No terminal input available before timeout")
1980 }
1981 other => to_py_err(other),
1982 })
1983 })
1984 }
1985}
1986
1987#[pymethods]
1988impl NativeTerminalInputEvent {
1989 #[getter]
1990 fn data(&self, py: Python<'_>) -> Py<PyAny> {
1991 PyBytes::new(py, &self.data).into_any().unbind()
1992 }
1993
1994 #[getter]
1995 fn submit(&self) -> bool {
1996 self.submit
1997 }
1998
1999 #[getter]
2000 fn shift(&self) -> bool {
2001 self.shift
2002 }
2003
2004 #[getter]
2005 fn ctrl(&self) -> bool {
2006 self.ctrl
2007 }
2008
2009 #[getter]
2010 fn alt(&self) -> bool {
2011 self.alt
2012 }
2013
2014 #[getter]
2015 fn virtual_key_code(&self) -> u16 {
2016 self.virtual_key_code
2017 }
2018
2019 #[getter]
2020 fn repeat_count(&self) -> u16 {
2021 self.repeat_count
2022 }
2023
2024 fn __repr__(&self) -> String {
2025 format!(
2026 "NativeTerminalInputEvent(data={:?}, submit={}, shift={}, ctrl={}, alt={}, virtual_key_code={}, repeat_count={})",
2027 self.data,
2028 self.submit,
2029 self.shift,
2030 self.ctrl,
2031 self.alt,
2032 self.virtual_key_code,
2033 self.repeat_count,
2034 )
2035 }
2036}
2037
2038#[pymethods]
2039impl NativeTerminalInput {
2040 #[new]
2041 fn new() -> Self {
2042 Self {
2043 inner: TerminalInputCore::new(),
2044 }
2045 }
2046
2047 fn start(&self) -> PyResult<()> {
2048 #[cfg(windows)]
2049 {
2050 self.inner.start_impl().map_err(to_py_err)
2051 }
2052
2053 #[cfg(not(windows))]
2054 {
2055 Err(PyRuntimeError::new_err(
2056 "NativeTerminalInput is only available on Windows consoles",
2057 ))
2058 }
2059 }
2060
2061 fn stop(&self, py: Python<'_>) -> PyResult<()> {
2062 py.allow_threads(|| self.inner.stop_impl().map_err(to_py_err))
2063 }
2064
2065 fn close(&self, py: Python<'_>) -> PyResult<()> {
2066 py.allow_threads(|| self.inner.stop_impl().map_err(to_py_err))
2067 }
2068
2069 fn available(&self) -> bool {
2070 self.inner.available()
2071 }
2072
2073 #[getter]
2074 fn capturing(&self) -> bool {
2075 self.inner.capturing()
2076 }
2077
2078 #[getter]
2079 fn original_console_mode(&self) -> Option<u32> {
2080 self.inner.original_console_mode()
2081 }
2082
2083 #[getter]
2084 fn active_console_mode(&self) -> Option<u32> {
2085 self.inner.active_console_mode()
2086 }
2087
2088 #[pyo3(signature = (timeout=None))]
2089 fn read_event(
2090 &self,
2091 py: Python<'_>,
2092 timeout: Option<f64>,
2093 ) -> PyResult<Py<NativeTerminalInputEvent>> {
2094 let event = self.wait_for_event(py, timeout)?;
2095 Self::event_to_py(py, event)
2096 }
2097
2098 fn read_event_non_blocking(
2099 &self,
2100 py: Python<'_>,
2101 ) -> PyResult<Option<Py<NativeTerminalInputEvent>>> {
2102 if let Some(event) = self.inner.next_event() {
2103 return Self::event_to_py(py, event).map(Some);
2104 }
2105 if self
2106 .inner
2107 .state
2108 .lock()
2109 .expect("terminal input mutex poisoned")
2110 .closed
2111 {
2112 return Err(PyRuntimeError::new_err("Native terminal input is closed"));
2113 }
2114 Ok(None)
2115 }
2116
2117 #[pyo3(signature = (timeout=None))]
2118 fn read(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<Py<PyAny>> {
2119 let event = self.wait_for_event(py, timeout)?;
2120 Ok(PyBytes::new(py, &event.data).into_any().unbind())
2121 }
2122
2123 fn read_non_blocking(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
2124 if let Some(event) = self.inner.next_event() {
2125 return Ok(Some(PyBytes::new(py, &event.data).into_any().unbind()));
2126 }
2127 if self
2128 .inner
2129 .state
2130 .lock()
2131 .expect("terminal input mutex poisoned")
2132 .closed
2133 {
2134 return Err(PyRuntimeError::new_err("Native terminal input is closed"));
2135 }
2136 Ok(None)
2137 }
2138
2139 fn drain(&self, py: Python<'_>) -> Vec<Py<PyAny>> {
2140 self.inner
2141 .drain_events()
2142 .into_iter()
2143 .map(|event| PyBytes::new(py, &event.data).into_any().unbind())
2144 .collect()
2145 }
2146
2147 fn drain_events(&self, py: Python<'_>) -> PyResult<Vec<Py<NativeTerminalInputEvent>>> {
2148 self.inner
2149 .drain_events()
2150 .into_iter()
2151 .map(|event| Self::event_to_py(py, event))
2152 .collect()
2153 }
2154
2155 #[pyo3(signature = (timeout=None))]
2161 fn read_batch(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<(Py<PyAny>, bool)> {
2162 let first = self.wait_for_event(py, timeout)?;
2164
2165 let remaining = self.inner.drain_events();
2167
2168 let capacity = first.data.len() + remaining.iter().map(|e| e.data.len()).sum::<usize>();
2170 let mut merged = Vec::with_capacity(capacity);
2171 let mut submit = first.submit;
2172 merged.extend_from_slice(&first.data);
2173 for event in &remaining {
2174 merged.extend_from_slice(&event.data);
2175 submit = submit || event.submit;
2176 }
2177
2178 Ok((PyBytes::new(py, &merged).into_any().unbind(), submit))
2179 }
2180}
2181
2182impl NativeRunningProcess {
2185 fn start_impl(&self) -> PyResult<()> {
2186 running_process_core::rp_rust_debug_scope!(
2187 "running_process_py::NativeRunningProcess::start"
2188 );
2189 self.inner.start().map_err(to_py_err)
2190 }
2191
2192 fn wait_impl(&self, py: Python<'_>, timeout: Option<f64>) -> PyResult<i32> {
2193 running_process_core::rp_rust_debug_scope!(
2194 "running_process_py::NativeRunningProcess::wait"
2195 );
2196 py.allow_threads(|| {
2197 self.inner
2198 .wait(timeout.map(Duration::from_secs_f64))
2199 .map_err(process_err_to_py)
2200 })
2201 }
2202
2203 fn kill_impl(&self) -> PyResult<()> {
2204 running_process_core::rp_rust_debug_scope!(
2205 "running_process_py::NativeRunningProcess::kill"
2206 );
2207 self.inner.kill().map_err(to_py_err)
2208 }
2209
2210 fn terminate_impl(&self) -> PyResult<()> {
2211 running_process_core::rp_rust_debug_scope!(
2212 "running_process_py::NativeRunningProcess::terminate"
2213 );
2214 self.inner.terminate().map_err(to_py_err)
2215 }
2216
2217 fn close_impl(&self, py: Python<'_>) -> PyResult<()> {
2218 running_process_core::rp_rust_debug_scope!(
2219 "running_process_py::NativeRunningProcess::close"
2220 );
2221 py.allow_threads(|| self.inner.close().map_err(process_err_to_py))
2222 }
2223
2224 fn send_interrupt_impl(&self) -> PyResult<()> {
2225 running_process_core::rp_rust_debug_scope!(
2226 "running_process_py::NativeRunningProcess::send_interrupt"
2227 );
2228 let pid = self
2229 .inner
2230 .pid()
2231 .ok_or_else(|| PyRuntimeError::new_err("process is not running"))?;
2232
2233 #[cfg(windows)]
2234 {
2235 public_symbols::rp_windows_generate_console_ctrl_break_public(pid, self.creationflags)
2236 }
2237
2238 #[cfg(unix)]
2239 {
2240 if self.create_process_group {
2241 unix_signal_process_group(pid as i32, UnixSignal::Interrupt).map_err(to_py_err)?;
2242 } else {
2243 unix_signal_process(pid, UnixSignal::Interrupt).map_err(to_py_err)?;
2244 }
2245 Ok(())
2246 }
2247 }
2248
2249 fn decode_line_to_string(&self, py: Python<'_>, line: &[u8]) -> PyResult<String> {
2250 if !self.text {
2251 return Ok(String::from_utf8_lossy(line).into_owned());
2252 }
2253 let encoding = self.encoding.as_deref().unwrap_or("utf-8");
2254 let errors = self.errors.as_deref().unwrap_or("replace");
2255 if encoding == "utf-8" && errors == "replace" {
2256 return Ok(String::from_utf8_lossy(line).into_owned());
2257 }
2258 PyBytes::new(py, line)
2259 .call_method1("decode", (encoding, errors))?
2260 .extract()
2261 }
2262
2263 fn captured_stream_text(&self, py: Python<'_>, stream: StreamKind) -> PyResult<String> {
2264 let lines = match stream {
2265 StreamKind::Stdout => self.inner.captured_stdout(),
2266 StreamKind::Stderr => self.inner.captured_stderr(),
2267 };
2268 let mut text = String::new();
2269 for (index, line) in lines.iter().enumerate() {
2270 if index > 0 {
2271 text.push('\n');
2272 }
2273 text.push_str(&self.decode_line_to_string(py, line)?);
2274 }
2275 Ok(text)
2276 }
2277
2278 fn captured_combined_text(&self, py: Python<'_>) -> PyResult<String> {
2279 let lines = self.inner.captured_combined();
2280 let mut text = String::new();
2281 for (index, event) in lines.iter().enumerate() {
2282 if index > 0 {
2283 text.push('\n');
2284 }
2285 text.push_str(&self.decode_line_to_string(py, &event.line)?);
2286 }
2287 Ok(text)
2288 }
2289
2290 fn read_status_text(
2291 &self,
2292 stream: Option<StreamKind>,
2293 timeout: Option<Duration>,
2294 ) -> PyResult<ReadStatus<Vec<u8>>> {
2295 Ok(match stream {
2296 Some(kind) => self.inner.read_stream(kind, timeout),
2297 None => match self.inner.read_combined(timeout) {
2298 ReadStatus::Line(StreamEvent { line, .. }) => ReadStatus::Line(line),
2299 ReadStatus::Timeout => ReadStatus::Timeout,
2300 ReadStatus::Eof => ReadStatus::Eof,
2301 },
2302 })
2303 }
2304
2305 fn find_expect_match(
2306 &self,
2307 buffer: &str,
2308 pattern: &str,
2309 compiled_regex: Option<&Regex>,
2310 ) -> PyResult<Option<ExpectDetails>> {
2311 if compiled_regex.is_none() {
2312 let Some(start) = buffer.find(pattern) else {
2314 return Ok(None);
2315 };
2316 return Ok(Some((
2317 pattern.to_string(),
2318 start,
2319 start + pattern.len(),
2320 Vec::new(),
2321 )));
2322 }
2323
2324 let regex = compiled_regex.unwrap();
2325 let Some(captures) = regex.captures(buffer) else {
2326 return Ok(None);
2327 };
2328 let whole = captures
2329 .get(0)
2330 .ok_or_else(|| PyRuntimeError::new_err("regex capture missing group 0"))?;
2331 let groups = captures
2332 .iter()
2333 .skip(1)
2334 .map(|group| {
2335 group
2336 .map(|value| value.as_str().to_string())
2337 .unwrap_or_default()
2338 })
2339 .collect();
2340 Ok(Some((
2341 whole.as_str().to_string(),
2342 whole.start(),
2343 whole.end(),
2344 groups,
2345 )))
2346 }
2347
2348 fn decode_line(&self, py: Python<'_>, line: &[u8]) -> PyResult<Py<PyAny>> {
2349 if !self.text {
2350 return Ok(PyBytes::new(py, line).into_any().unbind());
2351 }
2352 let encoding = self.encoding.as_deref().unwrap_or("utf-8");
2353 let errors = self.errors.as_deref().unwrap_or("replace");
2354 if encoding == "utf-8" && errors == "replace" {
2355 let s = String::from_utf8_lossy(line);
2356 return Ok(PyString::new(py, &s).into_any().unbind());
2357 }
2358 Ok(PyBytes::new(py, line)
2359 .call_method1("decode", (encoding, errors))?
2360 .into_any()
2361 .unbind())
2362 }
2363}
2364
2365impl NativePtyBuffer {
2366 fn decode_chunk(&self, py: Python<'_>, line: &[u8]) -> PyResult<Py<PyAny>> {
2367 if !self.text {
2368 return Ok(PyBytes::new(py, line).into_any().unbind());
2369 }
2370 if self.encoding == "utf-8" && self.errors == "replace" {
2371 let s = String::from_utf8_lossy(line);
2372 return Ok(PyString::new(py, &s).into_any().unbind());
2373 }
2374 Ok(PyBytes::new(py, line)
2375 .call_method1("decode", (&self.encoding, &self.errors))?
2376 .into_any()
2377 .unbind())
2378 }
2379}
2380
2381#[pymethods]
2382impl NativeIdleDetector {
2383 #[new]
2384 #[allow(clippy::too_many_arguments)]
2385 #[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))]
2386 fn new(
2387 py: Python<'_>,
2388 timeout_seconds: f64,
2389 stability_window_seconds: f64,
2390 sample_interval_seconds: f64,
2391 enabled_signal: Py<NativeSignalBool>,
2392 reset_on_input: bool,
2393 reset_on_output: bool,
2394 count_control_churn_as_output: bool,
2395 initial_idle_for_seconds: f64,
2396 ) -> Self {
2397 let now = Instant::now();
2398 let initial_idle_for_seconds = initial_idle_for_seconds.max(0.0);
2399 let last_reset_at = now
2400 .checked_sub(Duration::from_secs_f64(initial_idle_for_seconds))
2401 .unwrap_or(now);
2402 let enabled = enabled_signal.borrow(py).value.clone();
2403 Self {
2404 core: Arc::new(IdleDetectorCore {
2405 timeout_seconds,
2406 stability_window_seconds,
2407 sample_interval_seconds,
2408 reset_on_input,
2409 reset_on_output,
2410 count_control_churn_as_output,
2411 enabled,
2412 state: Mutex::new(IdleMonitorState {
2413 last_reset_at,
2414 returncode: None,
2415 interrupted: false,
2416 }),
2417 condvar: Condvar::new(),
2418 }),
2419 }
2420 }
2421
2422 #[getter]
2423 fn enabled(&self) -> bool {
2424 self.core.enabled()
2425 }
2426
2427 #[setter]
2428 fn set_enabled(&self, enabled: bool) {
2429 self.core.set_enabled(enabled);
2430 }
2431
2432 fn record_input(&self, byte_count: usize) {
2433 self.core.record_input(byte_count);
2434 }
2435
2436 fn record_output(&self, data: &[u8]) {
2437 self.core.record_output(data);
2438 }
2439
2440 fn mark_exit(&self, returncode: i32, interrupted: bool) {
2441 self.core.mark_exit(returncode, interrupted);
2442 }
2443
2444 #[pyo3(signature = (timeout=None))]
2445 fn wait(&self, py: Python<'_>, timeout: Option<f64>) -> (bool, String, f64, Option<i32>) {
2446 py.allow_threads(|| self.core.wait(timeout))
2447 }
2448}
2449
2450#[cfg(test)]
2455mod tests {
2456 use super::*;
2457 #[cfg(windows)]
2458 use running_process_core::pty::terminal_input::{
2459 control_character_for_unicode, format_terminal_input_bytes, native_terminal_input_mode,
2460 native_terminal_input_trace_target, repeat_terminal_input_bytes,
2461 repeated_modified_sequence, repeated_tilde_sequence, terminal_input_modifier_parameter,
2462 translate_console_key_event, TerminalInputState,
2463 };
2464 use running_process_core::pty::{
2465 self as core_pty, NativePtyHandles, NativePtyProcess as CoreNativePtyProcess,
2466 PtyReadShared, PtyReadState,
2467 };
2468 #[cfg(windows)]
2469 use running_process_core::pty::{
2470 apply_windows_pty_priority, assign_child_to_windows_kill_on_close_job,
2471 };
2472
2473 #[cfg(windows)]
2474 use winapi::um::wincon::{
2475 ENABLE_ECHO_INPUT, ENABLE_EXTENDED_FLAGS, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
2476 ENABLE_QUICK_EDIT_MODE, ENABLE_WINDOW_INPUT,
2477 };
2478 #[cfg(windows)]
2479 use winapi::um::wincontypes::{
2480 KEY_EVENT_RECORD, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, SHIFT_PRESSED,
2481 };
2482 #[cfg(windows)]
2483 use winapi::um::winuser::{VK_RETURN, VK_TAB, VK_UP};
2484
2485 #[cfg(windows)]
2486 fn key_event(
2487 virtual_key_code: u16,
2488 unicode: u16,
2489 control_key_state: u32,
2490 repeat_count: u16,
2491 ) -> KEY_EVENT_RECORD {
2492 let mut event: KEY_EVENT_RECORD = unsafe { std::mem::zeroed() };
2493 event.bKeyDown = 1;
2494 event.wRepeatCount = repeat_count;
2495 event.wVirtualKeyCode = virtual_key_code;
2496 event.wVirtualScanCode = 0;
2497 event.dwControlKeyState = control_key_state;
2498 unsafe {
2499 *event.uChar.UnicodeChar_mut() = unicode;
2500 }
2501 event
2502 }
2503
2504 #[test]
2505 #[cfg(windows)]
2506 fn native_terminal_input_mode_disables_cooked_console_flags() {
2507 let original_mode =
2508 ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE;
2509
2510 let active_mode = native_terminal_input_mode(original_mode);
2511
2512 assert_eq!(active_mode & ENABLE_ECHO_INPUT, 0);
2513 assert_eq!(active_mode & ENABLE_LINE_INPUT, 0);
2514 assert_eq!(active_mode & ENABLE_PROCESSED_INPUT, 0);
2515 assert_eq!(active_mode & ENABLE_QUICK_EDIT_MODE, 0);
2516 assert_ne!(active_mode & ENABLE_EXTENDED_FLAGS, 0);
2517 assert_ne!(active_mode & ENABLE_WINDOW_INPUT, 0);
2518 }
2519
2520 #[test]
2521 #[cfg(windows)]
2522 fn translate_terminal_input_preserves_submit_hint_for_enter() {
2523 let event = translate_console_key_event(&key_event(VK_RETURN as u16, '\r' as u16, 0, 1))
2524 .expect("enter should translate");
2525 assert_eq!(event.data, b"\r");
2526 assert!(event.submit);
2527 }
2528
2529 #[test]
2530 #[cfg(windows)]
2531 fn translate_terminal_input_keeps_shift_enter_non_submit() {
2532 let event = translate_console_key_event(&key_event(
2533 VK_RETURN as u16,
2534 '\r' as u16,
2535 SHIFT_PRESSED,
2536 1,
2537 ))
2538 .expect("shift-enter should translate");
2539 assert_eq!(event.data, b"\x1b[13;2u");
2542 assert!(!event.submit);
2543 assert!(event.shift);
2544 }
2545
2546 #[test]
2547 #[cfg(windows)]
2548 fn translate_terminal_input_encodes_shift_tab() {
2549 let event = translate_console_key_event(&key_event(VK_TAB as u16, 0, SHIFT_PRESSED, 1))
2550 .expect("shift-tab should translate");
2551 assert_eq!(event.data, b"\x1b[Z");
2552 assert!(!event.submit);
2553 }
2554
2555 #[test]
2556 #[cfg(windows)]
2557 fn translate_terminal_input_encodes_modified_arrows() {
2558 let event = translate_console_key_event(&key_event(
2559 VK_UP as u16,
2560 0,
2561 SHIFT_PRESSED | LEFT_CTRL_PRESSED,
2562 1,
2563 ))
2564 .expect("modified arrow should translate");
2565 assert_eq!(event.data, b"\x1b[1;6A");
2566 }
2567
2568 #[test]
2569 #[cfg(windows)]
2570 fn translate_terminal_input_encodes_alt_printable_with_escape_prefix() {
2571 let event =
2572 translate_console_key_event(&key_event(b'X' as u16, 'x' as u16, LEFT_ALT_PRESSED, 1))
2573 .expect("alt printable should translate");
2574 assert_eq!(event.data, b"\x1bx");
2575 }
2576
2577 #[test]
2578 #[cfg(windows)]
2579 fn translate_terminal_input_encodes_ctrl_printable_as_control_character() {
2580 let event =
2581 translate_console_key_event(&key_event(b'C' as u16, 'c' as u16, LEFT_CTRL_PRESSED, 1))
2582 .expect("ctrl-c should translate");
2583 assert_eq!(event.data, [0x03]);
2584 }
2585
2586 #[test]
2587 #[cfg(windows)]
2588 fn translate_terminal_input_ignores_keyup_events() {
2589 let mut event = key_event(VK_RETURN as u16, '\r' as u16, 0, 1);
2590 event.bKeyDown = 0;
2591 assert!(translate_console_key_event(&event).is_none());
2592 }
2593
2594 #[test]
2597 fn control_churn_bytes_empty() {
2598 assert_eq!(core_pty::control_churn_bytes(b""), 0);
2599 }
2600
2601 #[test]
2602 fn control_churn_bytes_plain_text() {
2603 assert_eq!(core_pty::control_churn_bytes(b"hello world"), 0);
2604 }
2605
2606 #[test]
2607 fn control_churn_bytes_ansi_csi_sequence() {
2608 assert_eq!(core_pty::control_churn_bytes(b"\x1b[31mhello\x1b[0m"), 9);
2610 }
2611
2612 #[test]
2613 fn control_churn_bytes_backspace_cr_del() {
2614 assert_eq!(core_pty::control_churn_bytes(b"\x08\x0D\x7F"), 3);
2615 }
2616
2617 #[test]
2618 fn control_churn_bytes_bare_escape() {
2619 assert_eq!(core_pty::control_churn_bytes(b"\x1b"), 1);
2621 }
2622
2623 #[test]
2624 fn control_churn_bytes_mixed() {
2625 assert_eq!(core_pty::control_churn_bytes(b"ok\x1b[Jmore\x08"), 4);
2627 }
2628
2629 #[test]
2632 fn input_contains_newline_cr() {
2633 assert!(core_pty::input_contains_newline(b"hello\rworld"));
2634 }
2635
2636 #[test]
2637 fn input_contains_newline_lf() {
2638 assert!(core_pty::input_contains_newline(b"hello\nworld"));
2639 }
2640
2641 #[test]
2642 fn input_contains_newline_none() {
2643 assert!(!core_pty::input_contains_newline(b"hello world"));
2644 }
2645
2646 #[test]
2647 fn input_contains_newline_empty() {
2648 assert!(!core_pty::input_contains_newline(b""));
2649 }
2650
2651 #[test]
2654 fn ignorable_error_not_found() {
2655 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
2656 assert!(is_ignorable_process_control_error(&err));
2657 }
2658
2659 #[test]
2660 fn ignorable_error_invalid_input() {
2661 let err = std::io::Error::new(std::io::ErrorKind::InvalidInput, "bad input");
2662 assert!(is_ignorable_process_control_error(&err));
2663 }
2664
2665 #[test]
2666 fn ignorable_error_permission_denied_is_not_ignorable() {
2667 let err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
2668 assert!(!is_ignorable_process_control_error(&err));
2669 }
2670
2671 #[test]
2672 #[cfg(unix)]
2673 fn ignorable_error_esrch() {
2674 let err = std::io::Error::from_raw_os_error(libc::ESRCH);
2675 assert!(is_ignorable_process_control_error(&err));
2676 }
2677
2678 #[test]
2681 #[cfg(windows)]
2682 fn windows_terminal_input_payload_passthrough() {
2683 let result = core_pty::windows_terminal_input_payload(b"hello");
2684 assert_eq!(result, b"hello");
2685 }
2686
2687 #[test]
2688 #[cfg(windows)]
2689 fn windows_terminal_input_payload_lone_lf_becomes_cr() {
2690 let result = core_pty::windows_terminal_input_payload(b"\n");
2691 assert_eq!(result, b"\r");
2692 }
2693
2694 #[test]
2695 #[cfg(windows)]
2696 fn windows_terminal_input_payload_crlf_preserved() {
2697 let result = core_pty::windows_terminal_input_payload(b"\r\n");
2698 assert_eq!(result, b"\r\n");
2699 }
2700
2701 #[test]
2702 #[cfg(windows)]
2703 fn windows_terminal_input_payload_lone_cr_preserved() {
2704 let result = core_pty::windows_terminal_input_payload(b"\r");
2705 assert_eq!(result, b"\r");
2706 }
2707
2708 #[test]
2709 #[cfg(windows)]
2710 fn terminal_input_modifier_none() {
2711 assert!(terminal_input_modifier_parameter(false, false, false).is_none());
2712 }
2713
2714 #[test]
2715 #[cfg(windows)]
2716 fn terminal_input_modifier_shift() {
2717 assert_eq!(
2718 terminal_input_modifier_parameter(true, false, false),
2719 Some(2)
2720 );
2721 }
2722
2723 #[test]
2724 #[cfg(windows)]
2725 fn terminal_input_modifier_alt() {
2726 assert_eq!(
2727 terminal_input_modifier_parameter(false, true, false),
2728 Some(3)
2729 );
2730 }
2731
2732 #[test]
2733 #[cfg(windows)]
2734 fn terminal_input_modifier_ctrl() {
2735 assert_eq!(
2736 terminal_input_modifier_parameter(false, false, true),
2737 Some(5)
2738 );
2739 }
2740
2741 #[test]
2742 #[cfg(windows)]
2743 fn terminal_input_modifier_shift_ctrl() {
2744 assert_eq!(
2745 terminal_input_modifier_parameter(true, false, true),
2746 Some(6)
2747 );
2748 }
2749
2750 #[test]
2751 #[cfg(windows)]
2752 fn control_character_for_unicode_letters() {
2753 assert_eq!(control_character_for_unicode('A' as u16), Some(0x01));
2754 assert_eq!(control_character_for_unicode('C' as u16), Some(0x03));
2755 assert_eq!(control_character_for_unicode('Z' as u16), Some(0x1A));
2756 }
2757
2758 #[test]
2759 #[cfg(windows)]
2760 fn control_character_for_unicode_special() {
2761 assert_eq!(control_character_for_unicode('@' as u16), Some(0x00));
2762 assert_eq!(control_character_for_unicode('[' as u16), Some(0x1B));
2763 }
2764
2765 #[test]
2766 #[cfg(windows)]
2767 fn control_character_for_unicode_digit_returns_none() {
2768 assert!(control_character_for_unicode('1' as u16).is_none());
2769 }
2770
2771 #[test]
2772 #[cfg(windows)]
2773 fn format_terminal_input_bytes_empty() {
2774 assert_eq!(format_terminal_input_bytes(b""), "[]");
2775 }
2776
2777 #[test]
2778 #[cfg(windows)]
2779 fn format_terminal_input_bytes_multi() {
2780 assert_eq!(format_terminal_input_bytes(&[0x41, 0x42]), "[41 42]");
2781 }
2782
2783 #[test]
2784 #[cfg(windows)]
2785 fn repeated_tilde_sequence_no_modifier() {
2786 assert_eq!(repeated_tilde_sequence(3, None, 1), b"\x1b[3~");
2787 }
2788
2789 #[test]
2790 #[cfg(windows)]
2791 fn repeated_tilde_sequence_with_modifier() {
2792 assert_eq!(repeated_tilde_sequence(3, Some(2), 1), b"\x1b[3;2~");
2793 }
2794
2795 #[test]
2796 #[cfg(windows)]
2797 fn repeated_tilde_sequence_repeated() {
2798 let result = repeated_tilde_sequence(3, None, 3);
2799 assert_eq!(result, b"\x1b[3~\x1b[3~\x1b[3~");
2800 }
2801
2802 #[test]
2803 #[cfg(windows)]
2804 fn repeated_modified_sequence_no_modifier() {
2805 let result = repeated_modified_sequence(b"\x1b[A", None, 1);
2806 assert_eq!(result, b"\x1b[A");
2807 }
2808
2809 #[test]
2810 #[cfg(windows)]
2811 fn repeated_modified_sequence_with_modifier() {
2812 let result = repeated_modified_sequence(b"\x1b[A", Some(2), 1);
2814 assert_eq!(result, b"\x1b[1;2A");
2815 }
2816
2817 #[test]
2818 #[cfg(windows)]
2819 fn repeated_modified_sequence_repeated() {
2820 let result = repeated_modified_sequence(b"\x1b[A", None, 2);
2821 assert_eq!(result, b"\x1b[A\x1b[A");
2822 }
2823
2824 #[test]
2825 #[cfg(windows)]
2826 fn repeat_terminal_input_bytes_single() {
2827 let result = repeat_terminal_input_bytes(b"\r", 1);
2828 assert_eq!(result, b"\r");
2829 }
2830
2831 #[test]
2832 #[cfg(windows)]
2833 fn repeat_terminal_input_bytes_multiple() {
2834 let result = repeat_terminal_input_bytes(b"ab", 3);
2835 assert_eq!(result, b"ababab");
2836 }
2837
2838 #[test]
2839 #[cfg(windows)]
2840 fn repeat_terminal_input_bytes_zero_clamps_to_one() {
2841 let result = repeat_terminal_input_bytes(b"x", 0);
2842 assert_eq!(result, b"x");
2843 }
2844
2845 #[test]
2848 #[cfg(windows)]
2849 fn translate_console_key_home() {
2850 use winapi::um::winuser::VK_HOME;
2851 let event = translate_console_key_event(&key_event(VK_HOME as u16, 0, 0, 1))
2852 .expect("VK_HOME should translate");
2853 assert_eq!(event.data, b"\x1b[H");
2854 assert!(!event.submit);
2855 }
2856
2857 #[test]
2858 #[cfg(windows)]
2859 fn translate_console_key_end() {
2860 use winapi::um::winuser::VK_END;
2861 let event = translate_console_key_event(&key_event(VK_END as u16, 0, 0, 1))
2862 .expect("VK_END should translate");
2863 assert_eq!(event.data, b"\x1b[F");
2864 assert!(!event.submit);
2865 }
2866
2867 #[test]
2868 #[cfg(windows)]
2869 fn translate_console_key_insert() {
2870 use winapi::um::winuser::VK_INSERT;
2871 let event = translate_console_key_event(&key_event(VK_INSERT as u16, 0, 0, 1))
2872 .expect("VK_INSERT should translate");
2873 assert_eq!(event.data, b"\x1b[2~");
2874 assert!(!event.submit);
2875 }
2876
2877 #[test]
2878 #[cfg(windows)]
2879 fn translate_console_key_delete() {
2880 use winapi::um::winuser::VK_DELETE;
2881 let event = translate_console_key_event(&key_event(VK_DELETE as u16, 0, 0, 1))
2882 .expect("VK_DELETE should translate");
2883 assert_eq!(event.data, b"\x1b[3~");
2884 assert!(!event.submit);
2885 }
2886
2887 #[test]
2888 #[cfg(windows)]
2889 fn translate_console_key_page_up() {
2890 use winapi::um::winuser::VK_PRIOR;
2891 let event = translate_console_key_event(&key_event(VK_PRIOR as u16, 0, 0, 1))
2892 .expect("VK_PRIOR should translate");
2893 assert_eq!(event.data, b"\x1b[5~");
2894 assert!(!event.submit);
2895 }
2896
2897 #[test]
2898 #[cfg(windows)]
2899 fn translate_console_key_page_down() {
2900 use winapi::um::winuser::VK_NEXT;
2901 let event = translate_console_key_event(&key_event(VK_NEXT as u16, 0, 0, 1))
2902 .expect("VK_NEXT should translate");
2903 assert_eq!(event.data, b"\x1b[6~");
2904 assert!(!event.submit);
2905 }
2906
2907 #[test]
2908 #[cfg(windows)]
2909 fn translate_console_key_shift_home() {
2910 use winapi::um::winuser::VK_HOME;
2911 let event = translate_console_key_event(&key_event(VK_HOME as u16, 0, SHIFT_PRESSED, 1))
2912 .expect("Shift+Home should translate");
2913 assert_eq!(event.data, b"\x1b[1;2H");
2914 assert!(event.shift);
2915 }
2916
2917 #[test]
2918 #[cfg(windows)]
2919 fn translate_console_key_shift_end() {
2920 use winapi::um::winuser::VK_END;
2921 let event = translate_console_key_event(&key_event(VK_END as u16, 0, SHIFT_PRESSED, 1))
2922 .expect("Shift+End should translate");
2923 assert_eq!(event.data, b"\x1b[1;2F");
2924 assert!(event.shift);
2925 }
2926
2927 #[test]
2928 #[cfg(windows)]
2929 fn translate_console_key_ctrl_home() {
2930 use winapi::um::winuser::VK_HOME;
2931 let event =
2932 translate_console_key_event(&key_event(VK_HOME as u16, 0, LEFT_CTRL_PRESSED, 1))
2933 .expect("Ctrl+Home should translate");
2934 assert_eq!(event.data, b"\x1b[1;5H");
2935 assert!(event.ctrl);
2936 }
2937
2938 #[test]
2939 #[cfg(windows)]
2940 fn translate_console_key_shift_delete() {
2941 use winapi::um::winuser::VK_DELETE;
2942 let event = translate_console_key_event(&key_event(VK_DELETE as u16, 0, SHIFT_PRESSED, 1))
2943 .expect("Shift+Delete should translate");
2944 assert_eq!(event.data, b"\x1b[3;2~");
2945 assert!(event.shift);
2946 }
2947
2948 #[test]
2949 #[cfg(windows)]
2950 fn translate_console_key_ctrl_page_up() {
2951 use winapi::um::winuser::VK_PRIOR;
2952 let event =
2953 translate_console_key_event(&key_event(VK_PRIOR as u16, 0, LEFT_CTRL_PRESSED, 1))
2954 .expect("Ctrl+PageUp should translate");
2955 assert_eq!(event.data, b"\x1b[5;5~");
2956 assert!(event.ctrl);
2957 }
2958
2959 #[test]
2960 #[cfg(windows)]
2961 fn translate_console_key_backspace() {
2962 use winapi::um::winuser::VK_BACK;
2963 let event = translate_console_key_event(&key_event(VK_BACK as u16, 0x08, 0, 1))
2964 .expect("Backspace should translate");
2965 assert_eq!(event.data, b"\x08");
2966 }
2967
2968 #[test]
2969 #[cfg(windows)]
2970 fn translate_console_key_escape() {
2971 use winapi::um::winuser::VK_ESCAPE;
2972 let event = translate_console_key_event(&key_event(VK_ESCAPE as u16, 0x1b, 0, 1))
2973 .expect("Escape should translate");
2974 assert_eq!(event.data, b"\x1b");
2975 }
2976
2977 #[test]
2978 #[cfg(windows)]
2979 fn translate_console_key_tab() {
2980 let event = translate_console_key_event(&key_event(VK_TAB as u16, 0, 0, 1))
2981 .expect("Tab should translate");
2982 assert_eq!(event.data, b"\t");
2983 }
2984
2985 #[test]
2986 #[cfg(windows)]
2987 fn translate_console_key_plain_enter_is_submit() {
2988 let event = translate_console_key_event(&key_event(VK_RETURN as u16, '\r' as u16, 0, 1))
2989 .expect("Enter should translate");
2990 assert_eq!(event.data, b"\r");
2991 assert!(event.submit);
2992 assert!(!event.shift);
2993 }
2994
2995 #[test]
2996 #[cfg(windows)]
2997 fn translate_console_key_unicode_printable() {
2998 let event = translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, 0, 1))
3000 .expect("printable should translate");
3001 assert_eq!(event.data, b"a");
3002 }
3003
3004 #[test]
3005 #[cfg(windows)]
3006 fn translate_console_key_unicode_repeated() {
3007 let event = translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, 0, 3))
3008 .expect("repeated printable should translate");
3009 assert_eq!(event.data, b"aaa");
3010 }
3011
3012 #[test]
3013 #[cfg(windows)]
3014 fn translate_console_key_down_arrow() {
3015 use winapi::um::winuser::VK_DOWN;
3016 let event = translate_console_key_event(&key_event(VK_DOWN as u16, 0, 0, 1))
3017 .expect("Down arrow should translate");
3018 assert_eq!(event.data, b"\x1b[B");
3019 }
3020
3021 #[test]
3022 #[cfg(windows)]
3023 fn translate_console_key_right_arrow() {
3024 use winapi::um::winuser::VK_RIGHT;
3025 let event = translate_console_key_event(&key_event(VK_RIGHT as u16, 0, 0, 1))
3026 .expect("Right arrow should translate");
3027 assert_eq!(event.data, b"\x1b[C");
3028 }
3029
3030 #[test]
3031 #[cfg(windows)]
3032 fn translate_console_key_left_arrow() {
3033 use winapi::um::winuser::VK_LEFT;
3034 let event = translate_console_key_event(&key_event(VK_LEFT as u16, 0, 0, 1))
3035 .expect("Left arrow should translate");
3036 assert_eq!(event.data, b"\x1b[D");
3037 }
3038
3039 #[test]
3040 #[cfg(windows)]
3041 fn translate_console_key_unknown_vk_no_unicode_returns_none() {
3042 let result = translate_console_key_event(&key_event(0xFF, 0, 0, 1));
3044 assert!(result.is_none());
3045 }
3046
3047 #[test]
3048 #[cfg(windows)]
3049 fn translate_console_key_alt_escape_prefix() {
3050 let event =
3052 translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, LEFT_ALT_PRESSED, 1))
3053 .expect("Alt+a should translate");
3054 assert_eq!(event.data, b"\x1ba");
3055 assert!(event.alt);
3056 }
3057
3058 #[test]
3059 #[cfg(windows)]
3060 fn translate_console_key_ctrl_a() {
3061 let event =
3062 translate_console_key_event(&key_event(b'A' as u16, 'a' as u16, LEFT_CTRL_PRESSED, 1))
3063 .expect("Ctrl+A should translate");
3064 assert_eq!(event.data, [0x01]); assert!(event.ctrl);
3066 }
3067
3068 #[test]
3069 #[cfg(windows)]
3070 fn translate_console_key_ctrl_z() {
3071 let event =
3072 translate_console_key_event(&key_event(b'Z' as u16, 'z' as u16, LEFT_CTRL_PRESSED, 1))
3073 .expect("Ctrl+Z should translate");
3074 assert_eq!(event.data, [0x1A]); assert!(event.ctrl);
3076 }
3077
3078 #[test]
3081 fn signal_bool_default_false() {
3082 let sb = NativeSignalBool::new(false);
3083 assert!(!sb.load_nolock());
3084 }
3085
3086 #[test]
3087 fn signal_bool_default_true() {
3088 let sb = NativeSignalBool::new(true);
3089 assert!(sb.load_nolock());
3090 }
3091
3092 #[test]
3093 fn signal_bool_store_and_load() {
3094 let sb = NativeSignalBool::new(false);
3095 sb.store_locked(true);
3096 assert!(sb.load_nolock());
3097 sb.store_locked(false);
3098 assert!(!sb.load_nolock());
3099 }
3100
3101 #[test]
3102 fn signal_bool_compare_and_swap_success() {
3103 let sb = NativeSignalBool::new(false);
3104 assert!(sb.compare_and_swap_locked(false, true));
3105 assert!(sb.load_nolock());
3106 }
3107
3108 #[test]
3109 fn signal_bool_compare_and_swap_failure() {
3110 let sb = NativeSignalBool::new(false);
3111 assert!(!sb.compare_and_swap_locked(true, false));
3112 assert!(!sb.load_nolock());
3113 }
3114
3115 #[test]
3118 fn pty_buffer_available_empty() {
3119 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3120 assert!(!buf.available());
3121 }
3122
3123 #[test]
3124 fn pty_buffer_record_and_available() {
3125 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3126 buf.record_output(b"hello");
3127 assert!(buf.available());
3128 }
3129
3130 #[test]
3131 fn pty_buffer_history_bytes_and_clear() {
3132 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3133 buf.record_output(b"hello");
3134 buf.record_output(b"world");
3135 assert_eq!(buf.history_bytes(), 10);
3136 let released = buf.clear_history();
3137 assert_eq!(released, 10);
3138 assert_eq!(buf.history_bytes(), 0);
3139 }
3140
3141 #[test]
3142 fn pty_buffer_close() {
3143 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3144 buf.close();
3145 }
3148
3149 #[test]
3152 fn pty_buffer_drain_returns_recorded_chunks() {
3153 pyo3::prepare_freethreaded_python();
3154 pyo3::Python::with_gil(|py| {
3155 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3156 buf.record_output(b"chunk1");
3157 buf.record_output(b"chunk2");
3158 let drained = buf.drain(py).unwrap();
3159 assert_eq!(drained.len(), 2);
3160 assert!(!buf.available());
3161 });
3162 }
3163
3164 #[test]
3165 fn pty_buffer_output_returns_full_history() {
3166 pyo3::prepare_freethreaded_python();
3167 pyo3::Python::with_gil(|py| {
3168 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
3169 buf.record_output(b"hello ");
3170 buf.record_output(b"world");
3171 let output = buf.output(py).unwrap();
3172 let text: String = output.extract(py).unwrap();
3173 assert_eq!(text, "hello world");
3174 });
3175 }
3176
3177 #[test]
3178 fn pty_buffer_output_since_offset() {
3179 pyo3::prepare_freethreaded_python();
3180 pyo3::Python::with_gil(|py| {
3181 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
3182 buf.record_output(b"hello ");
3183 buf.record_output(b"world");
3184 let output = buf.output_since(py, 6).unwrap();
3185 let text: String = output.extract(py).unwrap();
3186 assert_eq!(text, "world");
3187 });
3188 }
3189
3190 #[test]
3191 fn pty_buffer_read_non_blocking_empty() {
3192 pyo3::prepare_freethreaded_python();
3193 pyo3::Python::with_gil(|py| {
3194 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3195 let result = buf.read_non_blocking(py).unwrap();
3196 assert!(result.is_none());
3197 });
3198 }
3199
3200 #[test]
3201 fn pty_buffer_read_non_blocking_with_data() {
3202 pyo3::prepare_freethreaded_python();
3203 pyo3::Python::with_gil(|py| {
3204 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3205 buf.record_output(b"data");
3206 let result = buf.read_non_blocking(py).unwrap();
3207 assert!(result.is_some());
3208 });
3209 }
3210
3211 #[test]
3212 fn pty_buffer_read_closed_returns_error() {
3213 pyo3::prepare_freethreaded_python();
3214 pyo3::Python::with_gil(|py| {
3215 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3216 buf.close();
3217 let result = buf.read_non_blocking(py);
3218 assert!(result.is_err());
3219 });
3220 }
3221
3222 #[test]
3223 fn pty_buffer_read_with_timeout() {
3224 pyo3::prepare_freethreaded_python();
3225 pyo3::Python::with_gil(|py| {
3226 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3227 let result = buf.read(py, Some(0.05));
3228 assert!(result.is_err());
3230 });
3231 }
3232
3233 #[test]
3234 fn pty_buffer_text_mode_decodes_utf8() {
3235 pyo3::prepare_freethreaded_python();
3236 pyo3::Python::with_gil(|py| {
3237 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
3238 buf.record_output("héllo".as_bytes());
3239 let result = buf.read_non_blocking(py).unwrap().unwrap();
3240 let text: String = result.extract(py).unwrap();
3241 assert_eq!(text, "héllo");
3242 });
3243 }
3244
3245 #[test]
3246 fn pty_buffer_bytes_mode_returns_bytes() {
3247 pyo3::prepare_freethreaded_python();
3248 pyo3::Python::with_gil(|py| {
3249 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3250 buf.record_output(b"\xff\xfe");
3251 let result = buf.read_non_blocking(py).unwrap().unwrap();
3252 let bytes: Vec<u8> = result.extract(py).unwrap();
3253 assert_eq!(bytes, vec![0xff, 0xfe]);
3254 });
3255 }
3256
3257 fn make_idle_detector(
3260 py: pyo3::Python<'_>,
3261 timeout_seconds: f64,
3262 enabled: bool,
3263 initial_idle_for: f64,
3264 ) -> NativeIdleDetector {
3265 let signal = pyo3::Py::new(py, NativeSignalBool::new(enabled)).unwrap();
3266 NativeIdleDetector::new(
3267 py,
3268 timeout_seconds,
3269 0.0, 0.01, signal,
3272 true, true, true, initial_idle_for,
3276 )
3277 }
3278
3279 #[test]
3280 fn idle_detector_mark_exit_returns_process_exit() {
3281 pyo3::prepare_freethreaded_python();
3282 pyo3::Python::with_gil(|py| {
3283 let det = make_idle_detector(py, 10.0, true, 0.0);
3284 det.mark_exit(42, false);
3285 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(1.0));
3286 assert!(!triggered);
3287 assert_eq!(reason, "process_exit");
3288 assert_eq!(returncode, Some(42));
3289 });
3290 }
3291
3292 #[test]
3293 fn idle_detector_mark_exit_interrupted() {
3294 pyo3::prepare_freethreaded_python();
3295 pyo3::Python::with_gil(|py| {
3296 let det = make_idle_detector(py, 10.0, true, 0.0);
3297 det.mark_exit(1, true);
3298 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(1.0));
3299 assert!(!triggered);
3300 assert_eq!(reason, "interrupt");
3301 assert_eq!(returncode, Some(1));
3302 });
3303 }
3304
3305 #[test]
3306 fn idle_detector_timeout_when_not_idle() {
3307 pyo3::prepare_freethreaded_python();
3308 pyo3::Python::with_gil(|py| {
3309 let det = make_idle_detector(py, 10.0, true, 0.0);
3310 let (triggered, reason, _idle_for, returncode) = det.wait(py, Some(0.05));
3311 assert!(!triggered);
3312 assert_eq!(reason, "timeout");
3313 assert!(returncode.is_none());
3314 });
3315 }
3316
3317 #[test]
3318 fn idle_detector_triggers_when_already_idle() {
3319 pyo3::prepare_freethreaded_python();
3320 pyo3::Python::with_gil(|py| {
3321 let det = make_idle_detector(py, 0.5, true, 1.0);
3324 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(1.0));
3325 assert!(triggered);
3326 assert_eq!(reason, "idle_timeout");
3327 });
3328 }
3329
3330 #[test]
3331 fn idle_detector_disabled_does_not_trigger() {
3332 pyo3::prepare_freethreaded_python();
3333 pyo3::Python::with_gil(|py| {
3334 let det = make_idle_detector(py, 0.01, false, 1.0);
3335 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.1));
3336 assert!(!triggered);
3337 assert_eq!(reason, "timeout");
3338 });
3339 }
3340
3341 #[test]
3342 fn idle_detector_record_input_resets_idle() {
3343 pyo3::prepare_freethreaded_python();
3344 pyo3::Python::with_gil(|py| {
3345 let det = make_idle_detector(py, 0.5, true, 1.0);
3346 det.record_input(5);
3348 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.05));
3350 assert!(!triggered);
3351 assert_eq!(reason, "timeout");
3352 });
3353 }
3354
3355 #[test]
3356 fn idle_detector_record_output_resets_idle() {
3357 pyo3::prepare_freethreaded_python();
3358 pyo3::Python::with_gil(|py| {
3359 let det = make_idle_detector(py, 0.5, true, 1.0);
3360 det.record_output(b"visible output");
3362 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.05));
3363 assert!(!triggered);
3364 assert_eq!(reason, "timeout");
3365 });
3366 }
3367
3368 #[test]
3369 fn idle_detector_control_churn_only_no_reset_when_not_counted() {
3370 pyo3::prepare_freethreaded_python();
3371 pyo3::Python::with_gil(|py| {
3372 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
3373 let det = NativeIdleDetector::new(
3374 py, 0.05, 0.0, 0.01, signal, true, true,
3375 false, 1.0, );
3378 det.record_output(b"\x1b[31m");
3380 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
3382 assert!(triggered);
3383 assert_eq!(reason, "idle_timeout");
3384 });
3385 }
3386
3387 #[test]
3390 fn process_registry_register_list_unregister() {
3391 pyo3::prepare_freethreaded_python();
3392 pyo3::Python::with_gil(|_py| {
3393 let test_pid = 99999u32;
3394 native_register_process(test_pid, "test", "test-command", None).unwrap();
3396 let list = native_list_active_processes();
3398 let found = list.iter().any(|(pid, _, _, _, _)| *pid == test_pid);
3399 assert!(found, "registered pid should appear in active list");
3400 native_unregister_process(test_pid).unwrap();
3402 let list = native_list_active_processes();
3403 let found = list.iter().any(|(pid, _, _, _, _)| *pid == test_pid);
3404 assert!(!found, "unregistered pid should not appear in active list");
3405 });
3406 }
3407
3408 #[test]
3411 fn process_metrics_sample_current_process() {
3412 let pid = std::process::id();
3413 let metrics = NativeProcessMetrics::new(pid);
3414 metrics.prime();
3415 let (exists, _cpu, _disk, _extra) = metrics.sample();
3416 assert!(exists, "current process should exist");
3417 }
3418
3419 #[test]
3420 fn process_metrics_nonexistent_process() {
3421 let metrics = NativeProcessMetrics::new(99999999);
3422 metrics.prime();
3423 let (exists, _cpu, _disk, _extra) = metrics.sample();
3424 assert!(!exists, "nonexistent pid should not exist");
3425 }
3426
3427 #[test]
3430 fn portable_exit_code_normal_exit_zero() {
3431 let status =
3432 running_process_core::pty::reexports::portable_pty::ExitStatus::with_exit_code(0);
3433 assert_eq!(core_pty::portable_exit_code(status), 0);
3434 }
3435
3436 #[test]
3437 fn portable_exit_code_normal_exit_nonzero() {
3438 let status =
3439 running_process_core::pty::reexports::portable_pty::ExitStatus::with_exit_code(42);
3440 assert_eq!(core_pty::portable_exit_code(status), 42);
3441 }
3442
3443 #[test]
3446 fn record_pty_input_metrics_basic() {
3447 let input_bytes = Arc::new(AtomicUsize::new(0));
3448 let newline_events = Arc::new(AtomicUsize::new(0));
3449 let submit_events = Arc::new(AtomicUsize::new(0));
3450
3451 core_pty::record_pty_input_metrics(
3452 &input_bytes,
3453 &newline_events,
3454 &submit_events,
3455 b"hello",
3456 false,
3457 );
3458
3459 assert_eq!(input_bytes.load(Ordering::Acquire), 5);
3460 assert_eq!(newline_events.load(Ordering::Acquire), 0);
3461 assert_eq!(submit_events.load(Ordering::Acquire), 0);
3462 }
3463
3464 #[test]
3465 fn record_pty_input_metrics_with_newline() {
3466 let input_bytes = Arc::new(AtomicUsize::new(0));
3467 let newline_events = Arc::new(AtomicUsize::new(0));
3468 let submit_events = Arc::new(AtomicUsize::new(0));
3469
3470 core_pty::record_pty_input_metrics(
3471 &input_bytes,
3472 &newline_events,
3473 &submit_events,
3474 b"hello\n",
3475 false,
3476 );
3477
3478 assert_eq!(input_bytes.load(Ordering::Acquire), 6);
3479 assert_eq!(newline_events.load(Ordering::Acquire), 1);
3480 assert_eq!(submit_events.load(Ordering::Acquire), 0);
3481 }
3482
3483 #[test]
3484 fn record_pty_input_metrics_with_submit() {
3485 let input_bytes = Arc::new(AtomicUsize::new(0));
3486 let newline_events = Arc::new(AtomicUsize::new(0));
3487 let submit_events = Arc::new(AtomicUsize::new(0));
3488
3489 core_pty::record_pty_input_metrics(
3490 &input_bytes,
3491 &newline_events,
3492 &submit_events,
3493 b"\r",
3494 true,
3495 );
3496
3497 assert_eq!(input_bytes.load(Ordering::Acquire), 1);
3498 assert_eq!(newline_events.load(Ordering::Acquire), 1);
3499 assert_eq!(submit_events.load(Ordering::Acquire), 1);
3500 }
3501
3502 #[test]
3503 fn record_pty_input_metrics_accumulates() {
3504 let input_bytes = Arc::new(AtomicUsize::new(0));
3505 let newline_events = Arc::new(AtomicUsize::new(0));
3506 let submit_events = Arc::new(AtomicUsize::new(0));
3507
3508 core_pty::record_pty_input_metrics(
3509 &input_bytes,
3510 &newline_events,
3511 &submit_events,
3512 b"ab",
3513 false,
3514 );
3515 core_pty::record_pty_input_metrics(
3516 &input_bytes,
3517 &newline_events,
3518 &submit_events,
3519 b"cd\n",
3520 true,
3521 );
3522
3523 assert_eq!(input_bytes.load(Ordering::Acquire), 5);
3524 assert_eq!(newline_events.load(Ordering::Acquire), 1);
3525 assert_eq!(submit_events.load(Ordering::Acquire), 1);
3526 }
3527
3528 #[test]
3531 fn store_pty_returncode_sets_value() {
3532 let returncode = Arc::new(Mutex::new(None));
3533 core_pty::store_pty_returncode(&returncode, 42);
3534 assert_eq!(*returncode.lock().unwrap(), Some(42));
3535 }
3536
3537 #[test]
3538 fn store_pty_returncode_overwrites() {
3539 let returncode = Arc::new(Mutex::new(Some(1)));
3540 core_pty::store_pty_returncode(&returncode, 0);
3541 assert_eq!(*returncode.lock().unwrap(), Some(0));
3542 }
3543
3544 #[test]
3547 fn write_pty_input_not_connected() {
3548 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
3549 let result = core_pty::write_pty_input(&handles, b"hello");
3550 assert!(result.is_err());
3551 let err = result.unwrap_err();
3552 assert_eq!(err.kind(), std::io::ErrorKind::NotConnected);
3553 }
3554
3555 #[test]
3558 fn poll_pty_process_no_handles_returns_stored_code() {
3559 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
3560 let returncode = Arc::new(Mutex::new(Some(42)));
3561 let result = core_pty::poll_pty_process(&handles, &returncode).unwrap();
3562 assert_eq!(result, Some(42));
3563 }
3564
3565 #[test]
3566 fn poll_pty_process_no_handles_no_code() {
3567 let handles: Arc<Mutex<Option<NativePtyHandles>>> = Arc::new(Mutex::new(None));
3568 let returncode = Arc::new(Mutex::new(None));
3569 let result = core_pty::poll_pty_process(&handles, &returncode).unwrap();
3570 assert_eq!(result, None);
3571 }
3572
3573 #[test]
3576 fn descendant_pids_returns_empty_for_unknown_pid() {
3577 let system = System::new();
3578 let pid = system_pid(99999999);
3579 let descendants = descendant_pids(&system, pid);
3580 assert!(descendants.is_empty());
3581 }
3582
3583 #[test]
3586 fn unix_now_seconds_returns_positive() {
3587 let now = unix_now_seconds();
3588 assert!(now > 0.0, "unix timestamp should be positive");
3589 }
3590
3591 #[test]
3594 fn same_process_identity_nonexistent_pid() {
3595 assert!(!same_process_identity(99999999, 0.0, 1.0));
3596 }
3597
3598 #[test]
3601 fn tracked_process_db_path_returns_ok() {
3602 with_locked_env_var("RUNNING_PROCESS_PID_DB", None, || {
3603 let path = tracked_process_db_path();
3604 assert!(path.is_ok());
3605 let path = path.unwrap();
3606 assert_eq!(
3607 path.file_name(),
3608 Some(std::ffi::OsStr::new("tracked-pids.sqlite3")),
3609 "path should use the default tracked pid database filename: {:?}",
3610 path
3611 );
3612 });
3613 }
3614
3615 #[test]
3618 fn command_builder_from_argv_single_arg() {
3619 let argv = vec!["echo".to_string()];
3620 let _cmd = core_pty::command_builder_from_argv(&argv);
3621 }
3623
3624 #[test]
3625 fn command_builder_from_argv_multi_args() {
3626 let argv = vec!["echo".to_string(), "hello".to_string(), "world".to_string()];
3627 let _cmd = core_pty::command_builder_from_argv(&argv);
3628 }
3630
3631 #[test]
3634 fn process_err_to_py_timeout() {
3635 pyo3::prepare_freethreaded_python();
3636 pyo3::Python::with_gil(|py| {
3637 let err = process_err_to_py(ProcessError::Timeout);
3638 assert!(err.is_instance_of::<pyo3::exceptions::PyTimeoutError>(py));
3639 });
3640 }
3641
3642 #[test]
3645 fn kill_process_tree_nonexistent_pid_no_panic() {
3646 kill_process_tree_impl(99999999, 0.1);
3648 }
3649
3650 #[test]
3653 fn idle_detector_record_input_zero_bytes_no_reset() {
3654 pyo3::prepare_freethreaded_python();
3655 pyo3::Python::with_gil(|py| {
3656 let det = make_idle_detector(py, 0.05, true, 1.0);
3657 det.record_input(0);
3659 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
3660 assert!(triggered);
3661 assert_eq!(reason, "idle_timeout");
3662 });
3663 }
3664
3665 #[test]
3666 fn idle_detector_record_output_empty_no_reset() {
3667 pyo3::prepare_freethreaded_python();
3668 pyo3::Python::with_gil(|py| {
3669 let det = make_idle_detector(py, 0.05, true, 1.0);
3670 det.record_output(b"");
3672 let (triggered, reason, _idle_for, _returncode) = det.wait(py, Some(0.5));
3673 assert!(triggered);
3674 assert_eq!(reason, "idle_timeout");
3675 });
3676 }
3677
3678 #[test]
3679 fn idle_detector_enabled_getter_and_setter() {
3680 pyo3::prepare_freethreaded_python();
3681 pyo3::Python::with_gil(|py| {
3682 let det = make_idle_detector(py, 1.0, true, 0.0);
3683 assert!(det.enabled());
3684 det.set_enabled(false);
3685 assert!(!det.enabled());
3686 det.set_enabled(true);
3687 assert!(det.enabled());
3688 });
3689 }
3690
3691 #[test]
3694 fn pty_buffer_multiple_record_and_drain() {
3695 pyo3::prepare_freethreaded_python();
3696 pyo3::Python::with_gil(|py| {
3697 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3698 buf.record_output(b"a");
3699 buf.record_output(b"b");
3700 buf.record_output(b"c");
3701 let drained = buf.drain(py).unwrap();
3702 assert_eq!(drained.len(), 3);
3703 assert!(!buf.available());
3704 assert_eq!(buf.history_bytes(), 3);
3706 });
3707 }
3708
3709 #[test]
3710 fn pty_buffer_output_since_beyond_length() {
3711 pyo3::prepare_freethreaded_python();
3712 pyo3::Python::with_gil(|py| {
3713 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
3714 buf.record_output(b"hi");
3715 let output = buf.output_since(py, 999).unwrap();
3716 let text: String = output.extract(py).unwrap();
3717 assert_eq!(text, "");
3718 });
3719 }
3720
3721 #[test]
3722 fn pty_buffer_clear_history_returns_correct_bytes() {
3723 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
3724 buf.record_output(b"hello");
3725 buf.record_output(b"world");
3726 assert_eq!(buf.history_bytes(), 10);
3727 let released = buf.clear_history();
3728 assert_eq!(released, 10);
3729 assert_eq!(buf.history_bytes(), 0);
3730 buf.record_output(b"new");
3732 assert_eq!(buf.history_bytes(), 3);
3733 }
3734
3735 #[test]
3738 fn signal_bool_concurrent_access() {
3739 let sb = NativeSignalBool::new(false);
3740 let sb_clone = sb.clone();
3741
3742 let handle = std::thread::spawn(move || {
3743 sb_clone.store_locked(true);
3744 });
3745 handle.join().unwrap();
3746 assert!(sb.load_nolock());
3747 }
3748
3749 #[test]
3752 fn control_churn_bytes_escape_then_non_bracket() {
3753 assert_eq!(core_pty::control_churn_bytes(b"\x1bO"), 1);
3755 }
3756
3757 #[test]
3758 fn control_churn_bytes_incomplete_csi() {
3759 assert_eq!(core_pty::control_churn_bytes(b"\x1b[123"), 5);
3761 }
3762
3763 #[test]
3764 fn control_churn_bytes_multiple_sequences() {
3765 assert_eq!(core_pty::control_churn_bytes(b"\x1b[H\x1b[2J"), 7);
3767 }
3768
3769 #[cfg(windows)]
3772 mod windows_payload_tests {
3773 use super::*;
3774
3775 #[test]
3776 fn windows_terminal_input_payload_mixed_line_endings() {
3777 let result = core_pty::windows_terminal_input_payload(b"a\nb\r\nc\rd");
3778 assert_eq!(result, b"a\rb\r\nc\rd");
3779 }
3780
3781 #[test]
3782 fn windows_terminal_input_payload_consecutive_lf() {
3783 let result = core_pty::windows_terminal_input_payload(b"\n\n");
3784 assert_eq!(result, b"\r\r");
3785 }
3786
3787 #[test]
3788 fn windows_terminal_input_payload_empty() {
3789 let result = core_pty::windows_terminal_input_payload(b"");
3790 assert!(result.is_empty());
3791 }
3792
3793 #[test]
3794 fn windows_terminal_input_payload_no_line_endings() {
3795 let result = core_pty::windows_terminal_input_payload(b"hello world");
3796 assert_eq!(result, b"hello world");
3797 }
3798
3799 #[test]
3800 fn format_terminal_input_bytes_single() {
3801 assert_eq!(format_terminal_input_bytes(&[0x0D]), "[0d]");
3802 }
3803
3804 #[test]
3805 fn native_terminal_input_mode_preserves_other_flags() {
3806 let custom_flag = 0x0100; let result = native_terminal_input_mode(custom_flag);
3809 assert_ne!(result & custom_flag, 0);
3811 }
3812 }
3813
3814 #[test]
3817 fn process_registry_register_with_cwd() {
3818 pyo3::prepare_freethreaded_python();
3819 pyo3::Python::with_gil(|_py| {
3820 let test_pid = 99998u32;
3821 native_register_process(test_pid, "test", "test-cmd", Some("/tmp/test".to_string()))
3822 .unwrap();
3823 let list = native_list_active_processes();
3824 let entry = list.iter().find(|(pid, _, _, _, _)| *pid == test_pid);
3825 assert!(entry.is_some());
3826 let (_, kind, cmd, cwd, _) = entry.unwrap();
3827 assert_eq!(kind, "test");
3828 assert_eq!(cmd, "test-cmd");
3829 assert_eq!(cwd.as_deref(), Some("/tmp/test"));
3830 native_unregister_process(test_pid).unwrap();
3831 });
3832 }
3833
3834 #[test]
3835 fn process_registry_double_register_overwrites() {
3836 pyo3::prepare_freethreaded_python();
3837 pyo3::Python::with_gil(|_py| {
3838 let test_pid = 99997u32;
3839 native_register_process(test_pid, "first", "cmd1", None).unwrap();
3840 native_register_process(test_pid, "second", "cmd2", None).unwrap();
3841 let list = native_list_active_processes();
3842 let entries: Vec<_> = list
3843 .iter()
3844 .filter(|(pid, _, _, _, _)| *pid == test_pid)
3845 .collect();
3846 assert_eq!(entries.len(), 1);
3847 assert_eq!(entries[0].1, "second");
3848 native_unregister_process(test_pid).unwrap();
3849 });
3850 }
3851
3852 #[test]
3853 fn process_registry_unregister_nonexistent_no_error() {
3854 pyo3::prepare_freethreaded_python();
3855 pyo3::Python::with_gil(|_py| {
3856 let result = native_unregister_process(99996);
3858 assert!(result.is_ok());
3859 });
3860 }
3861
3862 #[test]
3865 fn list_tracked_processes_returns_ok() {
3866 pyo3::prepare_freethreaded_python();
3867 pyo3::Python::with_gil(|_py| {
3868 let result = list_tracked_processes();
3869 assert!(result.is_ok());
3870 });
3871 }
3872
3873 #[test]
3880 fn non_ignorable_error_connection_refused() {
3881 let err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
3882 assert!(!is_ignorable_process_control_error(&err));
3883 }
3884
3885 #[test]
3888 fn to_py_err_creates_runtime_error() {
3889 pyo3::prepare_freethreaded_python();
3890 let err = to_py_err("test error message");
3891 assert!(err.to_string().contains("test error message"));
3892 }
3893
3894 #[test]
3897 fn process_err_to_py_timeout_is_timeout_error() {
3898 pyo3::prepare_freethreaded_python();
3899 let err = process_err_to_py(running_process_core::ProcessError::Timeout);
3900 pyo3::Python::with_gil(|py| {
3901 assert!(err.is_instance_of::<pyo3::exceptions::PyTimeoutError>(py));
3902 });
3903 }
3904
3905 #[test]
3906 fn process_err_to_py_not_running_is_runtime_error() {
3907 pyo3::prepare_freethreaded_python();
3908 let err = process_err_to_py(running_process_core::ProcessError::NotRunning);
3909 pyo3::Python::with_gil(|py| {
3910 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
3911 });
3912 }
3913
3914 #[test]
3917 fn input_contains_newline_with_cr() {
3918 assert!(core_pty::input_contains_newline(b"hello\rworld"));
3919 }
3920
3921 #[test]
3922 fn input_contains_newline_with_lf() {
3923 assert!(core_pty::input_contains_newline(b"hello\nworld"));
3924 }
3925
3926 #[test]
3927 fn input_contains_newline_with_crlf() {
3928 assert!(core_pty::input_contains_newline(b"hello\r\nworld"));
3929 }
3930
3931 #[test]
3932 fn input_contains_newline_without_newline() {
3933 assert!(!core_pty::input_contains_newline(b"hello world"));
3934 }
3935
3936 #[test]
3939 fn control_churn_bytes_backspace() {
3940 assert_eq!(core_pty::control_churn_bytes(b"\x08"), 1);
3941 }
3942
3943 #[test]
3944 fn control_churn_bytes_carriage_return() {
3945 assert_eq!(core_pty::control_churn_bytes(b"\x0D"), 1);
3946 }
3947
3948 #[test]
3949 fn control_churn_bytes_delete_char() {
3950 assert_eq!(core_pty::control_churn_bytes(b"\x7F"), 1);
3951 }
3952
3953 #[test]
3954 fn control_churn_bytes_mixed_with_text() {
3955 assert_eq!(core_pty::control_churn_bytes(b"hello\x0D\x1b[H"), 4);
3956 }
3957
3958 #[test]
3959 fn control_churn_bytes_plain_text_no_churn() {
3960 assert_eq!(core_pty::control_churn_bytes(b"hello world"), 0);
3961 }
3962
3963 #[test]
3966 fn system_pid_converts_u32() {
3967 let pid = system_pid(12345);
3968 assert_eq!(pid.as_u32(), 12345);
3969 }
3970
3971 #[test]
3974 fn unix_now_seconds_is_recent() {
3975 let now = unix_now_seconds();
3976 assert!(now > 1_577_836_800.0);
3977 }
3978
3979 #[test]
3982 fn idle_detector_wait_idle_timeout_with_initial_idle() {
3983 pyo3::prepare_freethreaded_python();
3984 pyo3::Python::with_gil(|py| {
3985 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
3986 let detector =
3987 NativeIdleDetector::new(py, 0.01, 0.01, 0.001, signal, true, true, true, 100.0);
3988 let (idle, reason, _, code) = detector.wait(py, Some(1.0));
3989 assert!(idle);
3990 assert_eq!(reason, "idle_timeout");
3991 assert!(code.is_none());
3992 });
3993 }
3994
3995 #[test]
3996 fn idle_detector_record_output_only_control_churn_with_flag() {
3997 pyo3::prepare_freethreaded_python();
3998 pyo3::Python::with_gil(|py| {
3999 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4000 let detector =
4001 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
4002 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4003 std::thread::sleep(std::time::Duration::from_millis(10));
4004 detector.record_output(b"\x1b[H");
4005 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4006 assert!(state_after > state_before);
4007 });
4008 }
4009
4010 #[test]
4011 fn idle_detector_record_output_only_control_churn_without_flag() {
4012 pyo3::prepare_freethreaded_python();
4013 pyo3::Python::with_gil(|py| {
4014 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4015 let detector =
4016 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, false, 5.0);
4017 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4018 std::thread::sleep(std::time::Duration::from_millis(10));
4019 detector.record_output(b"\x1b[H");
4020 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4021 assert_eq!(state_before, state_after);
4022 });
4023 }
4024
4025 #[test]
4026 fn idle_detector_record_output_not_enabled() {
4027 pyo3::prepare_freethreaded_python();
4028 pyo3::Python::with_gil(|py| {
4029 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4030 let detector =
4031 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, false, true, 5.0);
4032 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4033 std::thread::sleep(std::time::Duration::from_millis(10));
4034 detector.record_output(b"visible");
4035 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4036 assert_eq!(state_before, state_after);
4037 });
4038 }
4039
4040 #[test]
4041 fn idle_detector_record_input_not_enabled() {
4042 pyo3::prepare_freethreaded_python();
4043 pyo3::Python::with_gil(|py| {
4044 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4045 let detector =
4046 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, false, true, true, 5.0);
4047 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4048 std::thread::sleep(std::time::Duration::from_millis(10));
4049 detector.record_input(100);
4050 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4051 assert_eq!(state_before, state_after);
4052 });
4053 }
4054
4055 #[test]
4056 fn idle_detector_record_input_nonzero_bytes_resets() {
4057 pyo3::prepare_freethreaded_python();
4058 pyo3::Python::with_gil(|py| {
4059 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4060 let detector =
4061 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
4062 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4063 std::thread::sleep(std::time::Duration::from_millis(10));
4064 detector.record_input(100);
4065 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4066 assert!(state_after > state_before);
4067 });
4068 }
4069
4070 #[test]
4071 fn idle_detector_record_output_visible_resets() {
4072 pyo3::prepare_freethreaded_python();
4073 pyo3::Python::with_gil(|py| {
4074 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4075 let detector =
4076 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 5.0);
4077 let state_before = detector.core.state.lock().unwrap().last_reset_at;
4078 std::thread::sleep(std::time::Duration::from_millis(10));
4079 detector.record_output(b"visible output");
4080 let state_after = detector.core.state.lock().unwrap().last_reset_at;
4081 assert!(state_after > state_before);
4082 });
4083 }
4084
4085 #[test]
4086 fn idle_detector_mark_exit_sets_returncode() {
4087 pyo3::prepare_freethreaded_python();
4088 pyo3::Python::with_gil(|py| {
4089 let signal = pyo3::Py::new(py, NativeSignalBool::new(true)).unwrap();
4090 let detector =
4091 NativeIdleDetector::new(py, 1.0, 0.5, 0.1, signal, true, true, true, 0.0);
4092 detector.mark_exit(42, false);
4093 let state = detector.core.state.lock().unwrap();
4094 assert_eq!(state.returncode, Some(42));
4095 assert!(!state.interrupted);
4096 });
4097 }
4098
4099 #[test]
4102 fn find_expect_match_literal_found() {
4103 pyo3::prepare_freethreaded_python();
4104 pyo3::Python::with_gil(|py| {
4105 let process = make_test_running_process(py);
4106 let result = process
4107 .find_expect_match("hello world", "world", None)
4108 .unwrap();
4109 assert!(result.is_some());
4110 let (matched, start, end, groups) = result.unwrap();
4111 assert_eq!(matched, "world");
4112 assert_eq!(start, 6);
4113 assert_eq!(end, 11);
4114 assert!(groups.is_empty());
4115 });
4116 }
4117
4118 #[test]
4119 fn find_expect_match_literal_not_found() {
4120 pyo3::prepare_freethreaded_python();
4121 pyo3::Python::with_gil(|py| {
4122 let process = make_test_running_process(py);
4123 let result = process
4124 .find_expect_match("hello world", "missing", None)
4125 .unwrap();
4126 assert!(result.is_none());
4127 });
4128 }
4129
4130 #[test]
4131 fn find_expect_match_regex_found() {
4132 pyo3::prepare_freethreaded_python();
4133 pyo3::Python::with_gil(|py| {
4134 let process = make_test_running_process(py);
4135 let re = Regex::new(r"\d+").unwrap();
4136 let result = process
4137 .find_expect_match("hello 123 world", r"\d+", Some(&re))
4138 .unwrap();
4139 assert!(result.is_some());
4140 let (matched, start, end, _) = result.unwrap();
4141 assert_eq!(matched, "123");
4142 assert_eq!(start, 6);
4143 assert_eq!(end, 9);
4144 });
4145 }
4146
4147 #[test]
4148 fn find_expect_match_regex_with_groups() {
4149 pyo3::prepare_freethreaded_python();
4150 pyo3::Python::with_gil(|py| {
4151 let process = make_test_running_process(py);
4152 let re = Regex::new(r"(\d+) (\w+)").unwrap();
4153 let result = process
4154 .find_expect_match("hello 123 world", r"(\d+) (\w+)", Some(&re))
4155 .unwrap();
4156 assert!(result.is_some());
4157 let (_, _, _, groups) = result.unwrap();
4158 assert_eq!(groups.len(), 2);
4159 assert_eq!(groups[0], "123");
4160 assert_eq!(groups[1], "world");
4161 });
4162 }
4163
4164 #[test]
4165 fn find_expect_match_regex_not_found() {
4166 pyo3::prepare_freethreaded_python();
4167 pyo3::Python::with_gil(|py| {
4168 let process = make_test_running_process(py);
4169 let re = Regex::new(r"\d+").unwrap();
4170 let result = process
4171 .find_expect_match("hello world", r"\d+", Some(&re))
4172 .unwrap();
4173 assert!(result.is_none());
4174 });
4175 }
4176
4177 #[test]
4178 #[allow(clippy::invalid_regex)]
4179 fn find_expect_match_invalid_regex_errors() {
4180 pyo3::prepare_freethreaded_python();
4181 pyo3::Python::with_gil(|_py| {
4182 let result = Regex::new(r"[invalid");
4183 assert!(result.is_err());
4184 });
4185 }
4186
4187 fn make_test_running_process(py: Python<'_>) -> NativeRunningProcess {
4188 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
4189 NativeRunningProcess::new(
4190 cmd.as_any(),
4191 None,
4192 false,
4193 true,
4194 None,
4195 None,
4196 true,
4197 None,
4198 None,
4199 "inherit",
4200 "stdout",
4201 None,
4202 false,
4203 )
4204 .unwrap()
4205 }
4206
4207 #[test]
4210 fn parse_command_string_with_shell() {
4211 pyo3::prepare_freethreaded_python();
4212 pyo3::Python::with_gil(|py| {
4213 let cmd = pyo3::types::PyString::new(py, "echo hello");
4214 let result = parse_command(cmd.as_any(), true).unwrap();
4215 assert!(matches!(result, CommandSpec::Shell(ref s) if s == "echo hello"));
4216 });
4217 }
4218
4219 #[test]
4220 fn parse_command_string_without_shell_errors() {
4221 pyo3::prepare_freethreaded_python();
4222 pyo3::Python::with_gil(|py| {
4223 let cmd = pyo3::types::PyString::new(py, "echo hello");
4224 let result = parse_command(cmd.as_any(), false);
4225 assert!(result.is_err());
4226 });
4227 }
4228
4229 #[test]
4230 fn parse_command_list_without_shell() {
4231 pyo3::prepare_freethreaded_python();
4232 pyo3::Python::with_gil(|py| {
4233 let cmd = pyo3::types::PyList::new(py, ["echo", "hello"]).unwrap();
4234 let result = parse_command(cmd.as_any(), false).unwrap();
4235 assert!(matches!(result, CommandSpec::Argv(ref v) if v.len() == 2));
4236 });
4237 }
4238
4239 #[test]
4240 fn parse_command_list_with_shell_joins() {
4241 pyo3::prepare_freethreaded_python();
4242 pyo3::Python::with_gil(|py| {
4243 let cmd = pyo3::types::PyList::new(py, ["echo", "hello"]).unwrap();
4244 let result = parse_command(cmd.as_any(), true).unwrap();
4245 assert!(matches!(result, CommandSpec::Shell(ref s) if s == "echo hello"));
4246 });
4247 }
4248
4249 #[test]
4250 fn parse_command_empty_list_errors() {
4251 pyo3::prepare_freethreaded_python();
4252 pyo3::Python::with_gil(|py| {
4253 let cmd = pyo3::types::PyList::empty(py);
4254 let result = parse_command(cmd.as_any(), false);
4255 assert!(result.is_err());
4256 });
4257 }
4258
4259 #[test]
4260 fn parse_command_invalid_type_errors() {
4261 pyo3::prepare_freethreaded_python();
4262 pyo3::Python::with_gil(|py| {
4263 let cmd = 42i32.into_pyobject(py).unwrap();
4264 let result = parse_command(cmd.as_any(), false);
4265 assert!(result.is_err());
4266 });
4267 }
4268
4269 #[test]
4272 fn stream_kind_stdout() {
4273 let result = stream_kind("stdout").unwrap();
4274 assert_eq!(result, StreamKind::Stdout);
4275 }
4276
4277 #[test]
4278 fn stream_kind_stderr() {
4279 let result = stream_kind("stderr").unwrap();
4280 assert_eq!(result, StreamKind::Stderr);
4281 }
4282
4283 #[test]
4284 fn stream_kind_invalid() {
4285 let result = stream_kind("invalid");
4286 assert!(result.is_err());
4287 }
4288
4289 #[test]
4292 fn stdin_mode_inherit() {
4293 assert_eq!(stdin_mode("inherit").unwrap(), StdinMode::Inherit);
4294 }
4295
4296 #[test]
4297 fn stdin_mode_piped() {
4298 assert_eq!(stdin_mode("piped").unwrap(), StdinMode::Piped);
4299 }
4300
4301 #[test]
4302 fn stdin_mode_null() {
4303 assert_eq!(stdin_mode("null").unwrap(), StdinMode::Null);
4304 }
4305
4306 #[test]
4307 fn stdin_mode_invalid() {
4308 assert!(stdin_mode("invalid").is_err());
4309 }
4310
4311 #[test]
4314 fn stderr_mode_stdout() {
4315 assert_eq!(stderr_mode("stdout").unwrap(), StderrMode::Stdout);
4316 }
4317
4318 #[test]
4319 fn stderr_mode_pipe() {
4320 assert_eq!(stderr_mode("pipe").unwrap(), StderrMode::Pipe);
4321 }
4322
4323 #[test]
4324 fn stderr_mode_invalid() {
4325 assert!(stderr_mode("invalid").is_err());
4326 }
4327
4328 #[cfg(windows)]
4331 mod windows_additional_tests {
4332 use super::*;
4333 use winapi::um::winuser::VK_F1;
4334
4335 #[test]
4338 fn control_char_at_sign() {
4339 assert_eq!(control_character_for_unicode('@' as u16), Some(0x00));
4340 }
4341
4342 #[test]
4343 fn control_char_space() {
4344 assert_eq!(control_character_for_unicode(' ' as u16), Some(0x00));
4345 }
4346
4347 #[test]
4348 fn control_char_a() {
4349 assert_eq!(control_character_for_unicode('a' as u16), Some(0x01));
4350 }
4351
4352 #[test]
4353 fn control_char_z() {
4354 assert_eq!(control_character_for_unicode('z' as u16), Some(0x1A));
4355 }
4356
4357 #[test]
4358 fn control_char_bracket() {
4359 assert_eq!(control_character_for_unicode('[' as u16), Some(0x1B));
4360 }
4361
4362 #[test]
4363 fn control_char_backslash() {
4364 assert_eq!(control_character_for_unicode('\\' as u16), Some(0x1C));
4365 }
4366
4367 #[test]
4368 fn control_char_close_bracket() {
4369 assert_eq!(control_character_for_unicode(']' as u16), Some(0x1D));
4370 }
4371
4372 #[test]
4373 fn control_char_caret() {
4374 assert_eq!(control_character_for_unicode('^' as u16), Some(0x1E));
4375 }
4376
4377 #[test]
4378 fn control_char_underscore() {
4379 assert_eq!(control_character_for_unicode('_' as u16), Some(0x1F));
4380 }
4381
4382 #[test]
4383 fn control_char_digit_returns_none() {
4384 assert_eq!(control_character_for_unicode('0' as u16), None);
4385 }
4386
4387 #[test]
4388 fn control_char_exclamation_returns_none() {
4389 assert_eq!(control_character_for_unicode('!' as u16), None);
4390 }
4391
4392 #[test]
4395 fn modifier_param_no_modifiers_returns_none() {
4396 assert_eq!(terminal_input_modifier_parameter(false, false, false), None);
4397 }
4398
4399 #[test]
4400 fn modifier_param_shift_only() {
4401 assert_eq!(
4402 terminal_input_modifier_parameter(true, false, false),
4403 Some(2)
4404 );
4405 }
4406
4407 #[test]
4408 fn modifier_param_alt_only() {
4409 assert_eq!(
4410 terminal_input_modifier_parameter(false, true, false),
4411 Some(3)
4412 );
4413 }
4414
4415 #[test]
4416 fn modifier_param_ctrl_only() {
4417 assert_eq!(
4418 terminal_input_modifier_parameter(false, false, true),
4419 Some(5)
4420 );
4421 }
4422
4423 #[test]
4424 fn modifier_param_shift_ctrl() {
4425 assert_eq!(
4426 terminal_input_modifier_parameter(true, false, true),
4427 Some(6)
4428 );
4429 }
4430
4431 #[test]
4432 fn modifier_param_shift_alt() {
4433 assert_eq!(
4434 terminal_input_modifier_parameter(true, true, false),
4435 Some(4)
4436 );
4437 }
4438
4439 #[test]
4440 fn modifier_param_all_modifiers() {
4441 assert_eq!(terminal_input_modifier_parameter(true, true, true), Some(8));
4442 }
4443
4444 #[test]
4447 fn tilde_sequence_no_modifier() {
4448 let result = repeated_tilde_sequence(3, None, 1);
4449 assert_eq!(result, b"\x1b[3~");
4450 }
4451
4452 #[test]
4453 fn tilde_sequence_with_modifier() {
4454 let result = repeated_tilde_sequence(3, Some(2), 1);
4455 assert_eq!(result, b"\x1b[3;2~");
4456 }
4457
4458 #[test]
4459 fn tilde_sequence_repeated() {
4460 let result = repeated_tilde_sequence(3, None, 3);
4461 assert_eq!(result, b"\x1b[3~\x1b[3~\x1b[3~");
4462 }
4463
4464 #[test]
4467 fn modified_sequence_no_modifier() {
4468 let result = repeated_modified_sequence(b"\x1b[A", None, 1);
4469 assert_eq!(result, b"\x1b[A");
4470 }
4471
4472 #[test]
4473 fn modified_sequence_with_modifier() {
4474 let result = repeated_modified_sequence(b"\x1b[A", Some(2), 1);
4475 assert_eq!(result, b"\x1b[1;2A");
4476 }
4477
4478 #[test]
4479 fn modified_sequence_repeated_with_modifier() {
4480 let result = repeated_modified_sequence(b"\x1b[A", Some(5), 2);
4481 assert_eq!(result, b"\x1b[1;5A\x1b[1;5A");
4482 }
4483
4484 #[test]
4487 fn format_bytes_empty() {
4488 assert_eq!(format_terminal_input_bytes(&[]), "[]");
4489 }
4490
4491 #[test]
4492 fn format_bytes_multiple() {
4493 assert_eq!(
4494 format_terminal_input_bytes(&[0x1B, 0x5B, 0x41]),
4495 "[1b 5b 41]"
4496 );
4497 }
4498
4499 #[test]
4502 fn trace_target_empty_env_returns_none() {
4503 with_locked_env_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV, None, || {
4504 assert!(native_terminal_input_trace_target().is_none());
4505 });
4506 }
4507
4508 #[test]
4509 fn trace_target_whitespace_env_returns_none() {
4510 with_locked_env_var(NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV, Some(" "), || {
4511 assert!(native_terminal_input_trace_target().is_none());
4512 });
4513 }
4514
4515 #[test]
4516 fn trace_target_valid_env_returns_value() {
4517 with_locked_env_var(
4518 NATIVE_TERMINAL_INPUT_TRACE_PATH_ENV,
4519 Some("/tmp/trace.log"),
4520 || {
4521 let result = native_terminal_input_trace_target();
4522 assert_eq!(result, Some("/tmp/trace.log".to_string()));
4523 },
4524 );
4525 }
4526
4527 #[test]
4530 fn translate_key_up_event_returns_none() {
4531 let mut event: KEY_EVENT_RECORD = unsafe { std::mem::zeroed() };
4532 event.bKeyDown = 0;
4533 event.wVirtualKeyCode = VK_RETURN as u16;
4534 let result = translate_console_key_event(&event);
4535 assert!(result.is_none());
4536 }
4537
4538 #[test]
4541 fn translate_f1_key_returns_none() {
4542 let event = key_event(VK_F1 as u16, 0, 0, 1);
4543 let result = translate_console_key_event(&event);
4544 assert!(result.is_none());
4545 }
4546
4547 #[test]
4550 fn translate_alt_a_has_escape_prefix() {
4551 let event = key_event('a' as u16, 'a' as u16, LEFT_ALT_PRESSED, 1);
4552 let result = translate_console_key_event(&event).unwrap();
4553 assert!(result.data.starts_with(b"\x1b"));
4554 assert!(result.alt);
4555 }
4556
4557 #[test]
4560 fn translate_ctrl_c_produces_etx() {
4561 let event = key_event('C' as u16, 'c' as u16, LEFT_CTRL_PRESSED, 1);
4562 let result = translate_console_key_event(&event).unwrap();
4563 assert_eq!(result.data, &[0x03]);
4564 assert!(result.ctrl);
4565 }
4566 }
4567
4568 #[test]
4571 fn terminal_input_new_starts_closed() {
4572 let input = NativeTerminalInput::new();
4573 assert!(!input.capturing());
4574 let state = input.inner.state.lock().unwrap();
4575 assert!(state.closed);
4576 assert!(state.events.is_empty());
4577 }
4578
4579 #[test]
4580 fn terminal_input_available_false_when_empty() {
4581 let input = NativeTerminalInput::new();
4582 assert!(!input.available());
4583 }
4584
4585 #[test]
4586 fn terminal_input_next_event_none_when_empty() {
4587 let input = NativeTerminalInput::new();
4588 assert!(input.inner.next_event().is_none());
4589 }
4590
4591 #[test]
4592 fn terminal_input_inject_and_consume_event() {
4593 let input = NativeTerminalInput::new();
4594 {
4595 let mut state = input.inner.state.lock().unwrap();
4596 state.events.push_back(TerminalInputEventRecord {
4597 data: b"test".to_vec(),
4598 submit: false,
4599 shift: false,
4600 ctrl: false,
4601 alt: false,
4602 virtual_key_code: 0,
4603 repeat_count: 1,
4604 });
4605 }
4606 assert!(input.available());
4607 let event = input.inner.next_event().unwrap();
4608 assert_eq!(event.data, b"test");
4609 assert!(!input.available());
4610 }
4611
4612 #[test]
4613 #[cfg(not(windows))]
4614 fn terminal_input_start_errors_on_non_windows() {
4615 pyo3::prepare_freethreaded_python();
4616 let input = NativeTerminalInput::new();
4617 let result = input.start();
4618 assert!(result.is_err());
4619 }
4620
4621 #[test]
4624 fn terminal_input_event_repr() {
4625 let event = NativeTerminalInputEvent {
4626 data: vec![0x0D],
4627 submit: true,
4628 shift: false,
4629 ctrl: false,
4630 alt: false,
4631 virtual_key_code: 13,
4632 repeat_count: 1,
4633 };
4634 let repr = event.__repr__();
4635 assert!(repr.contains("submit=true"));
4636 assert!(repr.contains("virtual_key_code=13"));
4637 }
4638
4639 #[test]
4642 fn tracked_process_db_path_with_env() {
4643 pyo3::prepare_freethreaded_python();
4644 with_locked_env_var("RUNNING_PROCESS_PID_DB", Some("/custom/path/db.sqlite3"), || {
4645 let result = tracked_process_db_path().unwrap();
4646 assert_eq!(result, std::path::PathBuf::from("/custom/path/db.sqlite3"));
4647 });
4648 }
4649
4650 #[test]
4651 fn tracked_process_db_path_empty_env_falls_back() {
4652 pyo3::prepare_freethreaded_python();
4653 with_locked_env_var("RUNNING_PROCESS_PID_DB", Some(" "), || {
4654 let result = tracked_process_db_path().unwrap();
4655 assert_eq!(
4656 result.file_name(),
4657 Some(std::ffi::OsStr::new("tracked-pids.sqlite3"))
4658 );
4659 });
4660 }
4661
4662 #[test]
4670 fn process_metrics_sample_nonexistent_pid() {
4671 pyo3::prepare_freethreaded_python();
4672 let metrics = NativeProcessMetrics::new(999999);
4673 let (alive, cpu, io, _) = metrics.sample();
4674 assert!(!alive);
4675 assert_eq!(cpu, 0.0);
4676 assert_eq!(io, 0);
4677 }
4678
4679 #[test]
4680 fn process_metrics_prime_no_panic() {
4681 pyo3::prepare_freethreaded_python();
4682 let metrics = NativeProcessMetrics::new(999999);
4683 metrics.prime();
4684 }
4685
4686 #[test]
4689 fn active_process_record_clone() {
4690 let record = ActiveProcessRecord {
4691 pid: 1234,
4692 kind: "test".to_string(),
4693 command: "echo".to_string(),
4694 cwd: Some("/tmp".to_string()),
4695 started_at: 1000.0,
4696 };
4697 let cloned = record.clone();
4698 assert_eq!(cloned.pid, 1234);
4699 assert_eq!(cloned.kind, "test");
4700 assert_eq!(cloned.command, "echo");
4701 assert_eq!(cloned.cwd, Some("/tmp".to_string()));
4702 }
4703
4704 #[test]
4707 fn pty_process_empty_argv_errors() {
4708 pyo3::prepare_freethreaded_python();
4709 pyo3::Python::with_gil(|_py| {
4710 let result = CoreNativePtyProcess::new(vec![], None, None, 24, 80, None);
4711 assert!(result.is_err());
4712 });
4713 }
4714
4715 #[test]
4718 #[cfg(not(windows))]
4719 fn pty_process_start_already_started_errors() {
4720 pyo3::prepare_freethreaded_python();
4721 pyo3::Python::with_gil(|_py| {
4722 let argv = vec![
4723 "python".to_string(),
4724 "-c".to_string(),
4725 "import time; time.sleep(0.1)".to_string(),
4726 ];
4727 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4728 process.start_impl().unwrap();
4729 let result = process.start_impl();
4730 assert!(result.is_err());
4731 let _ = process.close_impl();
4732 });
4733 }
4734
4735 #[test]
4738 fn pty_buffer_new_defaults() {
4739 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4740 assert!(!buf.available());
4741 assert_eq!(buf.history_bytes(), 0);
4742 }
4743
4744 #[test]
4745 fn pty_buffer_record_output_makes_available() {
4746 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4747 buf.record_output(b"hello");
4748 assert!(buf.available());
4749 }
4750
4751 #[test]
4752 fn pty_buffer_history_bytes_accumulates() {
4753 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4754 buf.record_output(b"hello");
4755 assert_eq!(buf.history_bytes(), 5);
4756 buf.record_output(b" world");
4757 assert_eq!(buf.history_bytes(), 11);
4758 }
4759
4760 #[test]
4761 fn pty_buffer_clear_history_resets_to_zero() {
4762 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4763 buf.record_output(b"data");
4764 let released = buf.clear_history();
4765 assert_eq!(released, 4);
4766 assert_eq!(buf.history_bytes(), 0);
4767 }
4768
4769 #[test]
4770 fn pty_buffer_close_sets_closed_flag() {
4771 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4772 buf.close();
4773 let state = buf.state.lock().unwrap();
4774 assert!(state.closed);
4775 }
4776
4777 #[test]
4778 fn pty_buffer_record_multiple_chunks_all_available() {
4779 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
4780 buf.record_output(b"a");
4781 buf.record_output(b"bb");
4782 buf.record_output(b"ccc");
4783 assert_eq!(buf.history_bytes(), 6);
4784 let state = buf.state.lock().unwrap();
4785 assert_eq!(state.chunks.len(), 3);
4786 }
4787
4788 #[test]
4791 fn pty_process_pid_none_before_start() {
4792 pyo3::prepare_freethreaded_python();
4793 pyo3::Python::with_gil(|_py| {
4794 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4795 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4796 assert!(process.pid().unwrap().is_none());
4797 });
4798 }
4799
4800 #[test]
4801 #[cfg(not(windows))]
4802 fn pty_process_lifecycle_start_wait_close() {
4803 pyo3::prepare_freethreaded_python();
4804 pyo3::Python::with_gil(|_py| {
4805 let argv = vec![
4806 "python".to_string(),
4807 "-c".to_string(),
4808 "print('hello')".to_string(),
4809 ];
4810 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4811 process.start_impl().unwrap();
4812 assert!(process.pid().unwrap().is_some());
4813 let code = process.wait_impl(Some(10.0)).unwrap();
4814 assert_eq!(code, 0);
4815 let _ = process.close_impl();
4816 });
4817 }
4818
4819 #[test]
4820 #[cfg(not(windows))]
4821 fn pty_process_poll_none_while_running() {
4822 pyo3::prepare_freethreaded_python();
4823 pyo3::Python::with_gil(|_py| {
4824 let argv = vec![
4825 "python".to_string(),
4826 "-c".to_string(),
4827 "import time; time.sleep(5)".to_string(),
4828 ];
4829 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4830 process.start_impl().unwrap();
4831 assert!(
4832 core_pty::poll_pty_process(&process.handles, &process.returncode)
4833 .unwrap()
4834 .is_none()
4835 );
4836 let _ = process.close_impl();
4837 });
4838 }
4839
4840 #[test]
4841 #[cfg(not(windows))]
4842 fn pty_process_nonzero_exit_code() {
4843 pyo3::prepare_freethreaded_python();
4844 pyo3::Python::with_gil(|_py| {
4845 let argv = vec![
4846 "python".to_string(),
4847 "-c".to_string(),
4848 "import sys; sys.exit(42)".to_string(),
4849 ];
4850 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4851 process.start_impl().unwrap();
4852 let code = process.wait_impl(Some(10.0)).unwrap();
4853 assert_eq!(code, 42);
4854 let _ = process.close_impl();
4855 });
4856 }
4857
4858 #[test]
4859 fn pty_process_write_before_start_errors() {
4860 pyo3::prepare_freethreaded_python();
4861 pyo3::Python::with_gil(|_py| {
4862 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4863 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4864 assert!(process.write_impl(b"test", false).is_err());
4865 });
4866 }
4867
4868 #[test]
4869 #[cfg(not(windows))]
4870 fn pty_process_input_metrics_tracked() {
4871 pyo3::prepare_freethreaded_python();
4872 pyo3::Python::with_gil(|_py| {
4873 let argv = vec![
4874 "python".to_string(),
4875 "-c".to_string(),
4876 "import time; time.sleep(2)".to_string(),
4877 ];
4878 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4879 process.start_impl().unwrap();
4880 assert_eq!(process.pty_input_bytes_total(), 0);
4881 let _ = process.write_impl(b"hello\n", false);
4882 assert_eq!(process.pty_input_bytes_total(), 6);
4883 assert_eq!(process.pty_newline_events_total(), 1);
4884 let _ = process.write_impl(b"x", true);
4885 assert_eq!(process.pty_submit_events_total(), 1);
4886 let _ = process.close_impl();
4887 });
4888 }
4889
4890 #[test]
4891 #[cfg(not(windows))]
4892 fn pty_process_resize_while_running() {
4893 pyo3::prepare_freethreaded_python();
4894 pyo3::Python::with_gil(|_py| {
4895 let argv = vec![
4896 "python".to_string(),
4897 "-c".to_string(),
4898 "import time; time.sleep(2)".to_string(),
4899 ];
4900 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4901 process.start_impl().unwrap();
4902 assert!(process.resize_impl(40, 120).is_ok());
4903 let _ = process.close_impl();
4904 });
4905 }
4906
4907 #[test]
4908 #[cfg(not(windows))]
4909 fn pty_process_kill_running_process() {
4910 pyo3::prepare_freethreaded_python();
4911 pyo3::Python::with_gil(|_py| {
4912 let argv = vec![
4913 "python".to_string(),
4914 "-c".to_string(),
4915 "import time; time.sleep(0.1)".to_string(),
4916 ];
4917 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4918 process.start_impl().unwrap();
4919 assert!(process.kill_impl().is_ok());
4920 });
4921 }
4922
4923 #[test]
4924 #[cfg(not(windows))]
4925 fn pty_process_terminate_running_process() {
4926 pyo3::prepare_freethreaded_python();
4927 pyo3::Python::with_gil(|_py| {
4928 let argv = vec![
4929 "python".to_string(),
4930 "-c".to_string(),
4931 "import time; time.sleep(0.1)".to_string(),
4932 ];
4933 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4934 process.start_impl().unwrap();
4935 assert!(process.terminate_impl().is_ok());
4936 let _ = process.close_impl();
4937 });
4938 }
4939
4940 #[test]
4941 #[cfg(not(windows))]
4942 fn pty_process_close_already_closed_is_noop() {
4943 pyo3::prepare_freethreaded_python();
4944 pyo3::Python::with_gil(|_py| {
4945 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4946 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4947 process.start_impl().unwrap();
4948 let _ = process.wait_impl(Some(10.0));
4949 let _ = process.close_impl();
4950 assert!(process.close_impl().is_ok());
4951 });
4952 }
4953
4954 #[test]
4955 #[cfg(not(windows))]
4956 fn pty_process_wait_timeout_errors() {
4957 pyo3::prepare_freethreaded_python();
4958 pyo3::Python::with_gil(|_py| {
4959 let argv = vec![
4960 "python".to_string(),
4961 "-c".to_string(),
4962 "import time; time.sleep(10)".to_string(),
4963 ];
4964 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4965 process.start_impl().unwrap();
4966 assert!(process.wait_impl(Some(0.1)).is_err());
4967 let _ = process.close_impl();
4968 });
4969 }
4970
4971 #[test]
4972 fn pty_process_send_interrupt_before_start_errors() {
4973 pyo3::prepare_freethreaded_python();
4974 pyo3::Python::with_gil(|_py| {
4975 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4976 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4977 assert!(process.send_interrupt_impl().is_err());
4978 });
4979 }
4980
4981 #[test]
4982 fn pty_process_terminate_before_start_errors() {
4983 pyo3::prepare_freethreaded_python();
4984 pyo3::Python::with_gil(|_py| {
4985 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4986 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4987 assert!(process.terminate_impl().is_err());
4988 });
4989 }
4990
4991 #[test]
4992 fn pty_process_kill_before_start_errors() {
4993 pyo3::prepare_freethreaded_python();
4994 pyo3::Python::with_gil(|_py| {
4995 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
4996 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
4997 assert!(process.kill_impl().is_err());
4998 });
4999 }
5000
5001 #[test]
5004 fn kill_process_tree_nonexistent_pid_is_noop() {
5005 kill_process_tree_impl(999999, 0.5);
5006 }
5007
5008 #[test]
5009 fn get_process_tree_info_current_pid() {
5010 let pid = std::process::id();
5011 let info = native_get_process_tree_info(pid);
5012 assert!(info.contains(&format!("{}", pid)));
5013 }
5014
5015 #[test]
5016 fn get_process_tree_info_nonexistent_pid() {
5017 let info = native_get_process_tree_info(999999);
5018 assert!(info.contains("Could not get process info"));
5019 }
5020
5021 #[test]
5022 fn register_and_list_active_processes() {
5023 let fake_pid = 777777u32;
5024 register_active_process(
5025 fake_pid,
5026 "test",
5027 "echo hello",
5028 Some("/tmp".to_string()),
5029 1000.0,
5030 );
5031 let items = native_list_active_processes();
5032 assert!(items.iter().any(|e| e.0 == fake_pid));
5033 unregister_active_process(fake_pid);
5034 let items = native_list_active_processes();
5035 assert!(!items.iter().any(|e| e.0 == fake_pid));
5036 }
5037
5038 #[test]
5039 fn process_created_at_current_process_returns_some() {
5040 let created = process_created_at(std::process::id());
5041 assert!(created.is_some());
5042 assert!(created.unwrap() > 0.0);
5043 }
5044
5045 #[test]
5046 fn process_created_at_nonexistent_returns_none() {
5047 assert!(process_created_at(999999).is_none());
5048 }
5049
5050 #[test]
5051 fn same_process_identity_current_process_matches() {
5052 let pid = std::process::id();
5053 let created = process_created_at(pid).unwrap();
5054 assert!(same_process_identity(pid, created, 2.0));
5055 }
5056
5057 #[test]
5058 fn same_process_identity_wrong_time_no_match() {
5059 assert!(!same_process_identity(std::process::id(), 0.0, 1.0));
5060 }
5061
5062 #[test]
5063 #[cfg(windows)]
5064 fn windows_apply_process_priority_current_pid_ok() {
5065 pyo3::prepare_freethreaded_python();
5066 assert!(windows_apply_process_priority_impl(std::process::id(), 0).is_ok());
5067 }
5068
5069 #[test]
5070 #[cfg(windows)]
5071 fn windows_apply_process_priority_nonexistent_errors() {
5072 pyo3::prepare_freethreaded_python();
5073 assert!(windows_apply_process_priority_impl(999999, 0).is_err());
5074 }
5075
5076 #[test]
5077 fn signal_bool_new_default_false() {
5078 assert!(!NativeSignalBool::new(false).load_nolock());
5079 }
5080
5081 #[test]
5082 fn signal_bool_new_true() {
5083 assert!(NativeSignalBool::new(true).load_nolock());
5084 }
5085
5086 #[test]
5087 fn signal_bool_store_locked_changes_value() {
5088 let sb = NativeSignalBool::new(false);
5089 sb.store_locked(true);
5090 assert!(sb.load_nolock());
5091 }
5092
5093 #[test]
5094 fn signal_bool_compare_and_swap_success_iter3() {
5095 let sb = NativeSignalBool::new(false);
5096 assert!(sb.compare_and_swap_locked(false, true));
5097 assert!(sb.load_nolock());
5098 }
5099
5100 #[test]
5101 fn idle_monitor_state_initial_values() {
5102 let state = IdleMonitorState {
5103 last_reset_at: Instant::now(),
5104 returncode: None,
5105 interrupted: false,
5106 };
5107 assert!(state.returncode.is_none());
5108 assert!(!state.interrupted);
5109 }
5110
5111 #[test]
5112 #[cfg(windows)]
5113 fn terminal_input_wait_returns_event_immediately() {
5114 let state = Arc::new(Mutex::new(TerminalInputState {
5115 events: {
5116 let mut q = VecDeque::new();
5117 q.push_back(TerminalInputEventRecord {
5118 data: b"x".to_vec(),
5119 submit: false,
5120 shift: false,
5121 ctrl: false,
5122 alt: false,
5123 virtual_key_code: 0,
5124 repeat_count: 1,
5125 });
5126 q
5127 },
5128 closed: false,
5129 }));
5130 let condvar = Arc::new(Condvar::new());
5131 match wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(100))) {
5132 TerminalInputWaitOutcome::Event(e) => assert_eq!(e.data, b"x"),
5133 _ => panic!("expected Event"),
5134 }
5135 }
5136
5137 #[test]
5138 #[cfg(windows)]
5139 fn terminal_input_wait_returns_closed() {
5140 let state = Arc::new(Mutex::new(TerminalInputState {
5141 events: VecDeque::new(),
5142 closed: true,
5143 }));
5144 let condvar = Arc::new(Condvar::new());
5145 assert!(matches!(
5146 wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(100))),
5147 TerminalInputWaitOutcome::Closed
5148 ));
5149 }
5150
5151 #[test]
5152 #[cfg(windows)]
5153 fn terminal_input_wait_returns_timeout() {
5154 let state = Arc::new(Mutex::new(TerminalInputState {
5155 events: VecDeque::new(),
5156 closed: false,
5157 }));
5158 let condvar = Arc::new(Condvar::new());
5159 assert!(matches!(
5160 wait_for_terminal_input_event(&state, &condvar, Some(Duration::from_millis(50))),
5161 TerminalInputWaitOutcome::Timeout
5162 ));
5163 }
5164
5165 #[test]
5166 fn native_running_process_is_pty_available_false() {
5167 assert!(!NativeRunningProcess::is_pty_available());
5168 }
5169
5170 #[test]
5171 #[cfg(not(windows))]
5172 fn posix_input_payload_passthrough() {
5173 let data = b"hello\n";
5176 assert_eq!(data.to_vec(), b"hello\n");
5177 }
5178
5179 #[test]
5192 #[cfg(windows)]
5193 fn pty_process_start_and_close_windows() {
5194 pyo3::prepare_freethreaded_python();
5195 pyo3::Python::with_gil(|_py| {
5196 let argv = vec![
5197 "python".to_string(),
5198 "-c".to_string(),
5199 "print('hello')".to_string(),
5200 ];
5201 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5202 process.start_impl().unwrap();
5203 assert!(process.pid().unwrap().is_some());
5204 assert!(process.close_impl().is_ok());
5206 });
5207 }
5208
5209 #[test]
5210 #[cfg(windows)]
5211 fn pty_process_poll_none_while_running_windows() {
5212 pyo3::prepare_freethreaded_python();
5213 pyo3::Python::with_gil(|_py| {
5214 let argv = vec![
5215 "python".to_string(),
5216 "-c".to_string(),
5217 "import time; time.sleep(5)".to_string(),
5218 ];
5219 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5220 process.start_impl().unwrap();
5221 assert!(
5222 core_pty::poll_pty_process(&process.handles, &process.returncode)
5223 .unwrap()
5224 .is_none()
5225 );
5226 let _ = process.close_impl();
5227 });
5228 }
5229
5230 #[test]
5231 #[cfg(windows)]
5232 fn pty_process_kill_running_process_windows() {
5233 pyo3::prepare_freethreaded_python();
5234 pyo3::Python::with_gil(|_py| {
5235 let argv = vec![
5236 "python".to_string(),
5237 "-c".to_string(),
5238 "import time; time.sleep(0.1)".to_string(),
5239 ];
5240 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5241 process.start_impl().unwrap();
5242 assert!(process.kill_impl().is_ok());
5243 });
5244 }
5245
5246 #[test]
5247 #[cfg(windows)]
5248 fn pty_process_terminate_running_process_windows() {
5249 pyo3::prepare_freethreaded_python();
5250 pyo3::Python::with_gil(|_py| {
5251 let argv = vec![
5252 "python".to_string(),
5253 "-c".to_string(),
5254 "import time; time.sleep(0.1)".to_string(),
5255 ];
5256 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5257 process.start_impl().unwrap();
5258 assert!(process.terminate_impl().is_ok());
5260 });
5261 }
5262
5263 #[test]
5264 #[cfg(windows)]
5265 fn pty_process_close_not_started_is_ok_windows() {
5266 pyo3::prepare_freethreaded_python();
5267 pyo3::Python::with_gil(|_py| {
5268 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5269 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5270 assert!(process.close_impl().is_ok());
5272 });
5273 }
5274
5275 #[test]
5276 #[cfg(windows)]
5277 fn pty_process_start_already_started_errors_windows() {
5278 pyo3::prepare_freethreaded_python();
5279 pyo3::Python::with_gil(|_py| {
5280 let argv = vec![
5281 "python".to_string(),
5282 "-c".to_string(),
5283 "import time; time.sleep(0.1)".to_string(),
5284 ];
5285 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5286 process.start_impl().unwrap();
5287 let result = process.start_impl();
5288 assert!(result.is_err());
5289 let _ = process.close_impl();
5290 });
5291 }
5292
5293 #[test]
5294 #[cfg(windows)]
5295 fn pty_process_resize_while_running_windows() {
5296 pyo3::prepare_freethreaded_python();
5297 pyo3::Python::with_gil(|_py| {
5298 let argv = vec![
5299 "python".to_string(),
5300 "-c".to_string(),
5301 "import time; time.sleep(2)".to_string(),
5302 ];
5303 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5304 process.start_impl().unwrap();
5305 assert!(process.resize_impl(40, 120).is_ok());
5306 let _ = process.close_impl();
5307 });
5308 }
5309
5310 #[test]
5311 #[cfg(windows)]
5312 fn pty_process_write_windows() {
5313 pyo3::prepare_freethreaded_python();
5314 pyo3::Python::with_gil(|_py| {
5315 let argv = vec![
5316 "python".to_string(),
5317 "-c".to_string(),
5318 "import time; time.sleep(2)".to_string(),
5319 ];
5320 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5321 process.start_impl().unwrap();
5322 let _ = process.write_impl(b"hello\n", false);
5323 assert!(process.pty_input_bytes_total() >= 6);
5324 assert!(process.pty_newline_events_total() >= 1);
5325 let _ = process.close_impl();
5326 });
5327 }
5328
5329 #[test]
5330 #[cfg(windows)]
5331 fn pty_process_input_metrics_tracked_windows() {
5332 pyo3::prepare_freethreaded_python();
5333 pyo3::Python::with_gil(|_py| {
5334 let argv = vec![
5335 "python".to_string(),
5336 "-c".to_string(),
5337 "import time; time.sleep(2)".to_string(),
5338 ];
5339 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5340 process.start_impl().unwrap();
5341 assert_eq!(process.pty_input_bytes_total(), 0);
5342 let _ = process.write_impl(b"hello\n", false);
5343 assert_eq!(process.pty_input_bytes_total(), 6);
5344 assert_eq!(process.pty_newline_events_total(), 1);
5345 let _ = process.write_impl(b"x", true);
5346 assert_eq!(process.pty_submit_events_total(), 1);
5347 let _ = process.close_impl();
5348 });
5349 }
5350
5351 #[test]
5352 #[cfg(windows)]
5353 fn pty_process_send_interrupt_windows() {
5354 pyo3::prepare_freethreaded_python();
5355 pyo3::Python::with_gil(|_py| {
5356 let argv = vec![
5357 "python".to_string(),
5358 "-c".to_string(),
5359 "import time; time.sleep(0.1)".to_string(),
5360 ];
5361 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5362 process.start_impl().unwrap();
5363 assert!(process.send_interrupt_impl().is_ok());
5365 let _ = process.close_impl();
5366 });
5367 }
5368
5369 #[test]
5370 #[cfg(windows)]
5371 fn pty_process_with_cwd_windows() {
5372 pyo3::prepare_freethreaded_python();
5373 pyo3::Python::with_gil(|_py| {
5374 let tmp = std::env::temp_dir();
5375 let cwd = tmp.to_str().unwrap().to_string();
5376 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5377 let process = CoreNativePtyProcess::new(argv, Some(cwd), None, 24, 80, None).unwrap();
5378 process.start_impl().unwrap();
5379 assert!(process.close_impl().is_ok());
5380 });
5381 }
5382
5383 #[test]
5384 #[cfg(windows)]
5385 fn pty_process_with_env_windows() {
5386 pyo3::prepare_freethreaded_python();
5387 pyo3::Python::with_gil(|_py| {
5388 let mut env_pairs = Vec::new();
5389 if let Ok(path) = std::env::var("PATH") {
5390 env_pairs.push(("PATH".to_string(), path));
5391 }
5392 if let Ok(root) = std::env::var("SystemRoot") {
5393 env_pairs.push(("SystemRoot".to_string(), root));
5394 }
5395 env_pairs.push(("RP_TEST_PTY".to_string(), "test_value".to_string()));
5396 let argv = vec![
5397 "python".to_string(),
5398 "-c".to_string(),
5399 "import os; print(os.environ.get('RP_TEST_PTY', 'MISSING'))".to_string(),
5400 ];
5401 let process =
5402 CoreNativePtyProcess::new(argv, None, Some(env_pairs), 24, 80, None).unwrap();
5403 process.start_impl().unwrap();
5404 assert!(process.close_impl().is_ok());
5405 });
5406 }
5407
5408 #[test]
5411 #[cfg(windows)]
5412 fn pty_process_terminal_input_relay_not_active_initially() {
5413 pyo3::prepare_freethreaded_python();
5414 pyo3::Python::with_gil(|_py| {
5415 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5416 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5417 assert!(!process.terminal_input_relay_active.load(Ordering::Acquire));
5418 });
5419 }
5420
5421 #[test]
5422 #[cfg(windows)]
5423 fn pty_process_stop_terminal_input_relay_noop_when_not_started() {
5424 pyo3::prepare_freethreaded_python();
5425 pyo3::Python::with_gil(|_py| {
5426 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5427 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5428 process.stop_terminal_input_relay_impl(); });
5430 }
5431
5432 #[test]
5435 #[cfg(windows)]
5436 fn assign_child_to_job_null_handle_errors() {
5437 pyo3::prepare_freethreaded_python();
5438 let result = assign_child_to_windows_kill_on_close_job(None);
5439 assert!(result.is_err());
5440 }
5441
5442 #[test]
5443 #[cfg(windows)]
5444 fn apply_windows_pty_priority_none_handle_ok() {
5445 pyo3::prepare_freethreaded_python();
5446 assert!(apply_windows_pty_priority(None, Some(5)).is_ok());
5448 assert!(apply_windows_pty_priority(None, None).is_ok());
5449 }
5450
5451 #[test]
5452 #[cfg(windows)]
5453 fn apply_windows_pty_priority_zero_nice_noop() {
5454 pyo3::prepare_freethreaded_python();
5455 use std::os::windows::io::AsRawHandle;
5457 let current = std::process::Command::new("cmd")
5458 .args(["/C", "echo"])
5459 .stdout(std::process::Stdio::null())
5460 .spawn()
5461 .unwrap();
5462 let handle = current.as_raw_handle();
5463 assert!(apply_windows_pty_priority(Some(handle), Some(0)).is_ok());
5464 assert!(apply_windows_pty_priority(Some(handle), None).is_ok());
5465 }
5466
5467 #[test]
5470 fn running_process_start_wait_lifecycle() {
5471 pyo3::prepare_freethreaded_python();
5472 pyo3::Python::with_gil(|py| {
5473 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('hello')"]).unwrap();
5474 let process = NativeRunningProcess::new(
5475 cmd.as_any(),
5476 None,
5477 false,
5478 true,
5479 None,
5480 None,
5481 true,
5482 None,
5483 None,
5484 "inherit",
5485 "stdout",
5486 None,
5487 false,
5488 )
5489 .unwrap();
5490 process.start_impl().unwrap();
5491 assert!(process.inner.pid().is_some());
5492 let code = process.wait_impl(py, Some(10.0)).unwrap();
5493 assert_eq!(code, 0);
5494 });
5495 }
5496
5497 #[test]
5498 fn running_process_kill_running() {
5499 pyo3::prepare_freethreaded_python();
5500 pyo3::Python::with_gil(|py| {
5501 let cmd =
5502 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
5503 .unwrap();
5504 let process = NativeRunningProcess::new(
5505 cmd.as_any(),
5506 None,
5507 false,
5508 false,
5509 None,
5510 None,
5511 true,
5512 None,
5513 None,
5514 "inherit",
5515 "stdout",
5516 None,
5517 false,
5518 )
5519 .unwrap();
5520 process.start_impl().unwrap();
5521 assert!(process.kill_impl().is_ok());
5522 });
5523 }
5524
5525 #[test]
5526 fn running_process_terminate_running() {
5527 pyo3::prepare_freethreaded_python();
5528 pyo3::Python::with_gil(|py| {
5529 let cmd =
5530 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
5531 .unwrap();
5532 let process = NativeRunningProcess::new(
5533 cmd.as_any(),
5534 None,
5535 false,
5536 false,
5537 None,
5538 None,
5539 true,
5540 None,
5541 None,
5542 "inherit",
5543 "stdout",
5544 None,
5545 false,
5546 )
5547 .unwrap();
5548 process.start_impl().unwrap();
5549 assert!(process.terminate_impl().is_ok());
5550 });
5551 }
5552
5553 #[test]
5554 fn running_process_close_finished() {
5555 pyo3::prepare_freethreaded_python();
5556 pyo3::Python::with_gil(|py| {
5557 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "pass"]).unwrap();
5558 let process = NativeRunningProcess::new(
5559 cmd.as_any(),
5560 None,
5561 false,
5562 false,
5563 None,
5564 None,
5565 true,
5566 None,
5567 None,
5568 "inherit",
5569 "stdout",
5570 None,
5571 false,
5572 )
5573 .unwrap();
5574 process.start_impl().unwrap();
5575 let _ = process.wait_impl(py, Some(10.0));
5576 assert!(process.close_impl(py).is_ok());
5577 });
5578 }
5579
5580 #[test]
5581 fn running_process_close_running() {
5582 pyo3::prepare_freethreaded_python();
5583 pyo3::Python::with_gil(|py| {
5584 let cmd =
5585 pyo3::types::PyList::new(py, ["python", "-c", "import time; time.sleep(0.1)"])
5586 .unwrap();
5587 let process = NativeRunningProcess::new(
5588 cmd.as_any(),
5589 None,
5590 false,
5591 false,
5592 None,
5593 None,
5594 true,
5595 None,
5596 None,
5597 "inherit",
5598 "stdout",
5599 None,
5600 false,
5601 )
5602 .unwrap();
5603 process.start_impl().unwrap();
5604 assert!(process.close_impl(py).is_ok());
5605 });
5606 }
5607
5608 #[test]
5611 fn running_process_decode_line_text_mode() {
5612 pyo3::prepare_freethreaded_python();
5613 pyo3::Python::with_gil(|py| {
5614 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5615 let process = NativeRunningProcess::new(
5616 cmd.as_any(),
5617 None,
5618 false,
5619 true,
5620 None,
5621 None,
5622 true, None,
5624 None,
5625 "inherit",
5626 "stdout",
5627 None,
5628 false,
5629 )
5630 .unwrap();
5631 let result = process.decode_line_to_string(py, b"hello world").unwrap();
5632 assert_eq!(result, "hello world");
5633 });
5634 }
5635
5636 #[test]
5637 fn running_process_decode_line_binary_mode() {
5638 pyo3::prepare_freethreaded_python();
5639 pyo3::Python::with_gil(|py| {
5640 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5641 let process = NativeRunningProcess::new(
5642 cmd.as_any(),
5643 None,
5644 false,
5645 true,
5646 None,
5647 None,
5648 false, None,
5650 None,
5651 "inherit",
5652 "stdout",
5653 None,
5654 false,
5655 )
5656 .unwrap();
5657 let result = process.decode_line_to_string(py, b"\xff\xfe").unwrap();
5658 assert!(!result.is_empty());
5660 });
5661 }
5662
5663 #[test]
5664 fn running_process_decode_line_custom_encoding() {
5665 pyo3::prepare_freethreaded_python();
5666 pyo3::Python::with_gil(|py| {
5667 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5668 let process = NativeRunningProcess::new(
5669 cmd.as_any(),
5670 None,
5671 false,
5672 true,
5673 None,
5674 None,
5675 true,
5676 Some("ascii".to_string()),
5677 Some("replace".to_string()),
5678 "inherit",
5679 "stdout",
5680 None,
5681 false,
5682 )
5683 .unwrap();
5684 let result = process.decode_line_to_string(py, b"hello").unwrap();
5685 assert_eq!(result, "hello");
5686 });
5687 }
5688
5689 #[test]
5690 fn running_process_captured_stream_text() {
5691 pyo3::prepare_freethreaded_python();
5692 pyo3::Python::with_gil(|py| {
5693 let cmd =
5694 pyo3::types::PyList::new(py, ["python", "-c", "print('line1'); print('line2')"])
5695 .unwrap();
5696 let process = NativeRunningProcess::new(
5697 cmd.as_any(),
5698 None,
5699 false,
5700 true,
5701 None,
5702 None,
5703 true,
5704 None,
5705 None,
5706 "inherit",
5707 "stdout",
5708 None,
5709 false,
5710 )
5711 .unwrap();
5712 process.start_impl().unwrap();
5713 let _ = process.wait_impl(py, Some(10.0));
5714 let text = process
5715 .captured_stream_text(py, StreamKind::Stdout)
5716 .unwrap();
5717 assert!(text.contains("line1"));
5718 assert!(text.contains("line2"));
5719 });
5720 }
5721
5722 #[test]
5723 fn running_process_captured_combined_text() {
5724 pyo3::prepare_freethreaded_python();
5725 pyo3::Python::with_gil(|py| {
5726 let cmd = pyo3::types::PyList::new(
5727 py,
5728 [
5729 "python",
5730 "-c",
5731 "import sys; print('out'); print('err', file=sys.stderr)",
5732 ],
5733 )
5734 .unwrap();
5735 let process = NativeRunningProcess::new(
5736 cmd.as_any(),
5737 None,
5738 false,
5739 true,
5740 None,
5741 None,
5742 true,
5743 None,
5744 None,
5745 "inherit",
5746 "pipe",
5747 None,
5748 false,
5749 )
5750 .unwrap();
5751 process.start_impl().unwrap();
5752 let _ = process.wait_impl(py, Some(10.0));
5753 let text = process.captured_combined_text(py).unwrap();
5754 assert!(text.contains("out"));
5755 assert!(text.contains("err"));
5756 });
5757 }
5758
5759 #[test]
5760 fn running_process_read_status_text_stream() {
5761 pyo3::prepare_freethreaded_python();
5762 pyo3::Python::with_gil(|py| {
5763 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('data')"]).unwrap();
5764 let process = NativeRunningProcess::new(
5765 cmd.as_any(),
5766 None,
5767 false,
5768 true,
5769 None,
5770 None,
5771 true,
5772 None,
5773 None,
5774 "inherit",
5775 "stdout",
5776 None,
5777 false,
5778 )
5779 .unwrap();
5780 process.start_impl().unwrap();
5781 let _ = process.wait_impl(py, Some(10.0));
5782 std::thread::sleep(Duration::from_millis(50));
5783 let status = process
5785 .read_status_text(Some(StreamKind::Stdout), Some(Duration::from_millis(100)));
5786 assert!(status.is_ok());
5787 });
5788 }
5789
5790 #[test]
5791 fn running_process_read_status_text_combined() {
5792 pyo3::prepare_freethreaded_python();
5793 pyo3::Python::with_gil(|py| {
5794 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "print('data')"]).unwrap();
5795 let process = NativeRunningProcess::new(
5796 cmd.as_any(),
5797 None,
5798 false,
5799 true,
5800 None,
5801 None,
5802 true,
5803 None,
5804 None,
5805 "inherit",
5806 "stdout",
5807 None,
5808 false,
5809 )
5810 .unwrap();
5811 process.start_impl().unwrap();
5812 let _ = process.wait_impl(py, Some(10.0));
5813 std::thread::sleep(Duration::from_millis(50));
5814 let status = process.read_status_text(None, Some(Duration::from_millis(100)));
5816 assert!(status.is_ok());
5817 });
5818 }
5819
5820 #[test]
5821 fn running_process_decode_line_returns_bytes_in_binary_mode() {
5822 pyo3::prepare_freethreaded_python();
5823 pyo3::Python::with_gil(|py| {
5824 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5825 let process = NativeRunningProcess::new(
5826 cmd.as_any(),
5827 None,
5828 false,
5829 true,
5830 None,
5831 None,
5832 false, None,
5834 None,
5835 "inherit",
5836 "stdout",
5837 None,
5838 false,
5839 )
5840 .unwrap();
5841 let result = process.decode_line(py, b"hello").unwrap();
5842 let bytes: Vec<u8> = result.extract(py).unwrap();
5844 assert_eq!(bytes, b"hello");
5845 });
5846 }
5847
5848 #[test]
5849 fn running_process_decode_line_returns_string_in_text_mode() {
5850 pyo3::prepare_freethreaded_python();
5851 pyo3::Python::with_gil(|py| {
5852 let cmd = pyo3::types::PyList::new(py, ["echo", "test"]).unwrap();
5853 let process = NativeRunningProcess::new(
5854 cmd.as_any(),
5855 None,
5856 false,
5857 true,
5858 None,
5859 None,
5860 true, None,
5862 None,
5863 "inherit",
5864 "stdout",
5865 None,
5866 false,
5867 )
5868 .unwrap();
5869 let result = process.decode_line(py, b"hello").unwrap();
5870 let text: String = result.extract(py).unwrap();
5871 assert_eq!(text, "hello");
5872 });
5873 }
5874
5875 #[test]
5878 fn pty_buffer_decode_chunk_text_mode() {
5879 pyo3::prepare_freethreaded_python();
5880 pyo3::Python::with_gil(|py| {
5881 let buf = NativePtyBuffer::new(true, "utf-8", "replace");
5882 let result = buf.decode_chunk(py, b"hello").unwrap();
5883 let text: String = result.extract(py).unwrap();
5884 assert_eq!(text, "hello");
5885 });
5886 }
5887
5888 #[test]
5889 fn pty_buffer_decode_chunk_binary_mode() {
5890 pyo3::prepare_freethreaded_python();
5891 pyo3::Python::with_gil(|py| {
5892 let buf = NativePtyBuffer::new(false, "utf-8", "replace");
5893 let result = buf.decode_chunk(py, b"\xff\xfe").unwrap();
5894 let bytes: Vec<u8> = result.extract(py).unwrap();
5895 assert_eq!(bytes, vec![0xff, 0xfe]);
5896 });
5897 }
5898
5899 #[test]
5902 fn pty_process_mark_reader_closed() {
5903 pyo3::prepare_freethreaded_python();
5904 pyo3::Python::with_gil(|_py| {
5905 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5906 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5907 assert!(!process.reader.state.lock().unwrap().closed);
5909 process.mark_reader_closed();
5910 assert!(process.reader.state.lock().unwrap().closed);
5911 });
5912 }
5913
5914 #[test]
5915 fn pty_process_store_returncode_sets_value() {
5916 pyo3::prepare_freethreaded_python();
5917 pyo3::Python::with_gil(|_py| {
5918 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5919 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5920 assert!(process.returncode.lock().unwrap().is_none());
5921 process.store_returncode(42);
5922 assert_eq!(*process.returncode.lock().unwrap(), Some(42));
5923 });
5924 }
5925
5926 #[test]
5927 fn pty_process_record_input_metrics_tracks_data() {
5928 pyo3::prepare_freethreaded_python();
5929 pyo3::Python::with_gil(|_py| {
5930 let argv = vec!["python".to_string(), "-c".to_string(), "pass".to_string()];
5931 let process = CoreNativePtyProcess::new(argv, None, None, 24, 80, None).unwrap();
5932 assert_eq!(process.pty_input_bytes_total(), 0);
5933 process.record_input_metrics(b"hello\n", false);
5934 assert_eq!(process.pty_input_bytes_total(), 6);
5935 assert_eq!(process.pty_newline_events_total(), 1);
5936 assert_eq!(process.pty_submit_events_total(), 0);
5937 process.record_input_metrics(b"\r", true);
5938 assert_eq!(process.pty_submit_events_total(), 1);
5939 });
5940 }
5941
5942 #[test]
5945 fn process_err_to_py_already_started_is_runtime_error() {
5946 pyo3::prepare_freethreaded_python();
5947 let err = process_err_to_py(running_process_core::ProcessError::AlreadyStarted);
5948 pyo3::Python::with_gil(|py| {
5949 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
5950 });
5951 }
5952
5953 #[test]
5954 fn process_err_to_py_stdin_unavailable_is_runtime_error() {
5955 pyo3::prepare_freethreaded_python();
5956 let err = process_err_to_py(running_process_core::ProcessError::StdinUnavailable);
5957 pyo3::Python::with_gil(|py| {
5958 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
5959 });
5960 }
5961
5962 #[test]
5963 fn process_err_to_py_spawn_is_runtime_error() {
5964 pyo3::prepare_freethreaded_python();
5965 let err = process_err_to_py(running_process_core::ProcessError::Spawn(
5966 std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
5967 ));
5968 pyo3::Python::with_gil(|py| {
5969 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
5970 });
5971 }
5972
5973 #[test]
5974 fn process_err_to_py_io_is_runtime_error() {
5975 pyo3::prepare_freethreaded_python();
5976 let err = process_err_to_py(running_process_core::ProcessError::Io(std::io::Error::new(
5977 std::io::ErrorKind::BrokenPipe,
5978 "broken pipe",
5979 )));
5980 pyo3::Python::with_gil(|py| {
5981 assert!(err.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
5982 });
5983 }
5984
5985 #[test]
5988 fn running_process_piped_stdin() {
5989 pyo3::prepare_freethreaded_python();
5990 pyo3::Python::with_gil(|py| {
5991 let cmd = pyo3::types::PyList::new(
5992 py,
5993 [
5994 "python",
5995 "-c",
5996 "import sys; data=sys.stdin.buffer.read(); sys.stdout.buffer.write(data[::-1])",
5997 ],
5998 )
5999 .unwrap();
6000 let process = NativeRunningProcess::new(
6001 cmd.as_any(),
6002 None,
6003 false,
6004 true,
6005 None,
6006 None,
6007 true,
6008 None,
6009 None,
6010 "piped",
6011 "stdout",
6012 None,
6013 false,
6014 )
6015 .unwrap();
6016 process.start_impl().unwrap();
6017 process.inner.write_stdin(b"abc").unwrap();
6018 let code = process.wait_impl(py, Some(10.0)).unwrap();
6019 assert_eq!(code, 0);
6020 });
6021 }
6022
6023 #[test]
6026 fn running_process_shell_mode() {
6027 pyo3::prepare_freethreaded_python();
6028 pyo3::Python::with_gil(|py| {
6029 let cmd = pyo3::types::PyString::new(py, "echo shell-mode-test");
6030 let process = NativeRunningProcess::new(
6031 cmd.as_any(),
6032 None,
6033 true, true,
6035 None,
6036 None,
6037 true,
6038 None,
6039 None,
6040 "inherit",
6041 "stdout",
6042 None,
6043 false,
6044 )
6045 .unwrap();
6046 process.start_impl().unwrap();
6047 let code = process.wait_impl(py, Some(10.0)).unwrap();
6048 assert_eq!(code, 0);
6049 });
6050 }
6051
6052 #[test]
6055 fn running_process_send_interrupt_before_start_errors() {
6056 pyo3::prepare_freethreaded_python();
6057 pyo3::Python::with_gil(|py| {
6058 let cmd = pyo3::types::PyList::new(py, ["python", "-c", "pass"]).unwrap();
6059 let process = NativeRunningProcess::new(
6060 cmd.as_any(),
6061 None,
6062 false,
6063 false,
6064 None,
6065 None,
6066 true,
6067 None,
6068 None,
6069 "inherit",
6070 "stdout",
6071 None,
6072 false,
6073 )
6074 .unwrap();
6075 assert!(process.send_interrupt_impl().is_err());
6076 });
6077 }
6078
6079 #[test]
6082 fn terminal_input_inject_multiple_events() {
6083 let input = NativeTerminalInput::new();
6084 {
6085 let mut state = input.inner.state.lock().unwrap();
6086 for i in 0..5 {
6087 state.events.push_back(TerminalInputEventRecord {
6088 data: vec![b'a' + i],
6089 submit: false,
6090 shift: false,
6091 ctrl: false,
6092 alt: false,
6093 virtual_key_code: 0,
6094 repeat_count: 1,
6095 });
6096 }
6097 }
6098 assert!(input.available());
6099 let mut count = 0;
6100 while input.inner.next_event().is_some() {
6101 count += 1;
6102 }
6103 assert_eq!(count, 5);
6104 assert!(!input.available());
6105 }
6106
6107 #[test]
6108 fn terminal_input_capturing_false_initially() {
6109 let input = NativeTerminalInput::new();
6110 assert!(!input.capturing());
6111 }
6112
6113 #[test]
6116 fn terminal_input_event_fields() {
6117 let event = NativeTerminalInputEvent {
6118 data: vec![0x1B, 0x5B, 0x41],
6119 submit: false,
6120 shift: true,
6121 ctrl: true,
6122 alt: false,
6123 virtual_key_code: 38,
6124 repeat_count: 2,
6125 };
6126 assert_eq!(event.data, vec![0x1B, 0x5B, 0x41]);
6127 assert!(!event.submit);
6128 assert!(event.shift);
6129 assert!(event.ctrl);
6130 assert!(!event.alt);
6131 assert_eq!(event.virtual_key_code, 38);
6132 assert_eq!(event.repeat_count, 2);
6133 let repr = event.__repr__();
6135 assert!(repr.contains("shift=true"));
6136 assert!(repr.contains("ctrl=true"));
6137 assert!(repr.contains("alt=false"));
6138 }
6139
6140 #[test]
6143 fn spawn_pty_reader_reads_data_and_closes() {
6144 let shared = Arc::new(PtyReadShared {
6145 state: Mutex::new(PtyReadState {
6146 chunks: VecDeque::new(),
6147 closed: false,
6148 }),
6149 condvar: Condvar::new(),
6150 });
6151
6152 let data = b"hello from reader\n";
6153 let reader: Box<dyn std::io::Read + Send> = Box::new(std::io::Cursor::new(data.to_vec()));
6154 let echo = Arc::new(AtomicBool::new(false));
6155 let idle = Arc::new(Mutex::new(None));
6156 let out_bytes = Arc::new(AtomicUsize::new(0));
6157 let churn_bytes = Arc::new(AtomicUsize::new(0));
6158 core_pty::spawn_pty_reader(
6159 reader,
6160 Arc::clone(&shared),
6161 echo,
6162 idle,
6163 out_bytes,
6164 churn_bytes,
6165 );
6166
6167 let deadline = Instant::now() + Duration::from_secs(5);
6169 loop {
6170 let state = shared.state.lock().unwrap();
6171 if state.closed {
6172 break;
6173 }
6174 drop(state);
6175 assert!(Instant::now() < deadline, "reader thread did not close");
6176 std::thread::sleep(Duration::from_millis(10));
6177 }
6178
6179 let state = shared.state.lock().unwrap();
6180 assert!(state.closed);
6181 assert!(!state.chunks.is_empty());
6182 }
6183
6184 #[test]
6185 fn spawn_pty_reader_empty_input_closes() {
6186 let shared = Arc::new(PtyReadShared {
6187 state: Mutex::new(PtyReadState {
6188 chunks: VecDeque::new(),
6189 closed: false,
6190 }),
6191 condvar: Condvar::new(),
6192 });
6193
6194 let reader: Box<dyn std::io::Read + Send> = Box::new(std::io::Cursor::new(Vec::new()));
6195 let echo = Arc::new(AtomicBool::new(false));
6196 let idle = Arc::new(Mutex::new(None));
6197 let out_bytes = Arc::new(AtomicUsize::new(0));
6198 let churn_bytes = Arc::new(AtomicUsize::new(0));
6199 core_pty::spawn_pty_reader(
6200 reader,
6201 Arc::clone(&shared),
6202 echo,
6203 idle,
6204 out_bytes,
6205 churn_bytes,
6206 );
6207
6208 let deadline = Instant::now() + Duration::from_secs(5);
6209 loop {
6210 let state = shared.state.lock().unwrap();
6211 if state.closed {
6212 break;
6213 }
6214 drop(state);
6215 assert!(Instant::now() < deadline, "reader thread did not close");
6216 std::thread::sleep(Duration::from_millis(10));
6217 }
6218
6219 let state = shared.state.lock().unwrap();
6220 assert!(state.closed);
6221 assert!(state.chunks.is_empty());
6222 }
6223
6224 #[test]
6227 #[cfg(windows)]
6228 fn windows_generate_console_ctrl_break_nonexistent_pid() {
6229 pyo3::prepare_freethreaded_python();
6230 let result = windows_generate_console_ctrl_break_impl(999999, None);
6232 assert!(result.is_err());
6233 }
6234
6235 #[test]
6238 fn running_process_with_env() {
6239 pyo3::prepare_freethreaded_python();
6240 pyo3::Python::with_gil(|py| {
6241 let env = pyo3::types::PyDict::new(py);
6242 if let Ok(path) = std::env::var("PATH") {
6243 env.set_item("PATH", &path).unwrap();
6244 }
6245 #[cfg(windows)]
6246 if let Ok(root) = std::env::var("SystemRoot") {
6247 env.set_item("SystemRoot", &root).unwrap();
6248 }
6249 env.set_item("RP_TEST_VAR", "test_value").unwrap();
6250
6251 let cmd = pyo3::types::PyList::new(
6252 py,
6253 [
6254 "python",
6255 "-c",
6256 "import os; print(os.environ.get('RP_TEST_VAR', 'MISSING'))",
6257 ],
6258 )
6259 .unwrap();
6260 let process = NativeRunningProcess::new(
6261 cmd.as_any(),
6262 None,
6263 false,
6264 true,
6265 Some(env),
6266 None,
6267 true,
6268 None,
6269 None,
6270 "inherit",
6271 "stdout",
6272 None,
6273 false,
6274 )
6275 .unwrap();
6276 process.start_impl().unwrap();
6277 let code = process.wait_impl(py, Some(10.0)).unwrap();
6278 assert_eq!(code, 0);
6279 let text = process
6280 .captured_stream_text(py, StreamKind::Stdout)
6281 .unwrap();
6282 assert!(text.contains("test_value"));
6283 });
6284 }
6285
6286 #[test]
6289 #[cfg(windows)]
6290 fn windows_pty_input_payload_via_module() {
6291 assert_eq!(core_pty::windows_terminal_input_payload(b"hello"), b"hello");
6292 assert_eq!(core_pty::windows_terminal_input_payload(b"\n"), b"\r");
6293 }
6294}
6295
6296#[pyclass]
6300#[derive(Clone, Copy)]
6301struct PyContainment {
6302 inner: Containment,
6303}
6304
6305#[pymethods]
6306impl PyContainment {
6307 #[staticmethod]
6309 fn contained() -> Self {
6310 Self {
6311 inner: Containment::Contained,
6312 }
6313 }
6314
6315 #[staticmethod]
6317 fn detached() -> Self {
6318 Self {
6319 inner: Containment::Detached,
6320 }
6321 }
6322
6323 fn __repr__(&self) -> String {
6324 match self.inner {
6325 Containment::Contained => "Containment.Contained".to_string(),
6326 Containment::Detached => "Containment.Detached".to_string(),
6327 }
6328 }
6329}
6330
6331#[pyclass(name = "ContainedProcessGroup")]
6333struct PyContainedProcessGroup {
6334 inner: Option<ContainedProcessGroup>,
6335 children: Vec<ContainedChild>,
6336}
6337
6338#[pymethods]
6339impl PyContainedProcessGroup {
6340 #[new]
6341 #[pyo3(signature = (originator=None))]
6342 fn new(originator: Option<String>) -> PyResult<Self> {
6343 let group = match originator {
6344 Some(ref orig) => ContainedProcessGroup::with_originator(orig).map_err(to_py_err)?,
6345 None => ContainedProcessGroup::new().map_err(to_py_err)?,
6346 };
6347 Ok(Self {
6348 inner: Some(group),
6349 children: Vec::new(),
6350 })
6351 }
6352
6353 #[getter]
6354 fn originator(&self) -> Option<String> {
6355 self.inner.as_ref()?.originator().map(String::from)
6356 }
6357
6358 #[getter]
6359 fn originator_value(&self) -> Option<String> {
6360 self.inner.as_ref()?.originator_value()
6361 }
6362
6363 fn spawn(&mut self, argv: Vec<String>) -> PyResult<u32> {
6365 let group = self
6366 .inner
6367 .as_ref()
6368 .ok_or_else(|| PyRuntimeError::new_err("group already closed"))?;
6369 if argv.is_empty() {
6370 return Err(PyValueError::new_err("argv must not be empty"));
6371 }
6372 let mut cmd = std::process::Command::new(&argv[0]);
6373 if argv.len() > 1 {
6374 cmd.args(&argv[1..]);
6375 }
6376 let contained = group.spawn(&mut cmd).map_err(to_py_err)?;
6377 let pid = contained.child.id();
6378 self.children.push(contained);
6379 Ok(pid)
6380 }
6381
6382 fn spawn_detached(&mut self, argv: Vec<String>) -> PyResult<u32> {
6384 let group = self
6385 .inner
6386 .as_ref()
6387 .ok_or_else(|| PyRuntimeError::new_err("group already closed"))?;
6388 if argv.is_empty() {
6389 return Err(PyValueError::new_err("argv must not be empty"));
6390 }
6391 let mut cmd = std::process::Command::new(&argv[0]);
6392 if argv.len() > 1 {
6393 cmd.args(&argv[1..]);
6394 }
6395 let contained = group.spawn_detached(&mut cmd).map_err(to_py_err)?;
6396 let pid = contained.child.id();
6397 self.children.push(contained);
6398 Ok(pid)
6399 }
6400
6401 fn close(&mut self) {
6403 self.inner.take();
6404 }
6405
6406 fn __enter__(slf: Py<Self>) -> Py<Self> {
6408 slf
6409 }
6410
6411 #[pyo3(signature = (_exc_type=None, _exc_val=None, _exc_tb=None))]
6413 fn __exit__(
6414 &mut self,
6415 _exc_type: Option<&Bound<'_, PyAny>>,
6416 _exc_val: Option<&Bound<'_, PyAny>>,
6417 _exc_tb: Option<&Bound<'_, PyAny>>,
6418 ) {
6419 self.close();
6420 }
6421}
6422
6423#[pyclass(name = "OriginatorProcessInfo")]
6426#[derive(Clone)]
6427struct PyOriginatorProcessInfo {
6428 #[pyo3(get)]
6429 pid: u32,
6430 #[pyo3(get)]
6431 name: String,
6432 #[pyo3(get)]
6433 command: String,
6434 #[pyo3(get)]
6435 originator: String,
6436 #[pyo3(get)]
6437 parent_pid: u32,
6438 #[pyo3(get)]
6439 parent_alive: bool,
6440}
6441
6442#[pymethods]
6443impl PyOriginatorProcessInfo {
6444 fn __repr__(&self) -> String {
6445 format!(
6446 "OriginatorProcessInfo(pid={}, name={:?}, originator={:?}, parent_pid={}, parent_alive={})",
6447 self.pid, self.name, self.originator, self.parent_pid, self.parent_alive
6448 )
6449 }
6450}
6451
6452impl From<OriginatorProcessInfo> for PyOriginatorProcessInfo {
6453 fn from(info: OriginatorProcessInfo) -> Self {
6454 Self {
6455 pid: info.pid,
6456 name: info.name,
6457 command: info.command,
6458 originator: info.originator,
6459 parent_pid: info.parent_pid,
6460 parent_alive: info.parent_alive,
6461 }
6462 }
6463}
6464
6465#[pyfunction]
6468fn py_find_processes_by_originator(tool: &str) -> Vec<PyOriginatorProcessInfo> {
6469 find_processes_by_originator(tool)
6470 .into_iter()
6471 .map(PyOriginatorProcessInfo::from)
6472 .collect()
6473}
6474
6475#[pyfunction]
6480fn monitor_console_windows(py: Python<'_>, duration_secs: f64) -> PyResult<PyObject> {
6481 let duration = Duration::from_secs_f64(duration_secs);
6482 let infos = running_process_core::monitor_console_windows(duration);
6483 let list = PyList::empty(py);
6484 for info in infos {
6485 let dict = PyDict::new(py);
6486 dict.set_item("pid", info.pid)?;
6487 dict.set_item("title", &info.title)?;
6488 dict.set_item("hwnd", info.hwnd)?;
6489 list.append(dict)?;
6490 }
6491 Ok(list.into())
6492}
6493
6494#[pymodule]
6495fn _native(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
6496 module.add_class::<PyNativeProcess>()?;
6497 module.add_class::<NativeRunningProcess>()?;
6498 module.add_class::<PyContainedProcessGroup>()?;
6499 module.add_class::<PyContainment>()?;
6500 module.add_class::<PyOriginatorProcessInfo>()?;
6501 module.add_function(wrap_pyfunction!(py_find_processes_by_originator, module)?)?;
6502 module.add_class::<NativePtyProcess>()?;
6503 module.add_class::<NativeProcessMetrics>()?;
6504 module.add_class::<NativeSignalBool>()?;
6505 module.add_class::<NativeIdleDetector>()?;
6506 module.add_class::<NativePtyBuffer>()?;
6507 module.add_class::<NativeTerminalInput>()?;
6508 module.add_class::<NativeTerminalInputEvent>()?;
6509 module.add_function(wrap_pyfunction!(tracked_pid_db_path_py, module)?)?;
6510 module.add_function(wrap_pyfunction!(track_process_pid, module)?)?;
6511 module.add_function(wrap_pyfunction!(untrack_process_pid, module)?)?;
6512 module.add_function(wrap_pyfunction!(native_register_process, module)?)?;
6513 module.add_function(wrap_pyfunction!(native_unregister_process, module)?)?;
6514 module.add_function(wrap_pyfunction!(list_tracked_processes, module)?)?;
6515 module.add_function(wrap_pyfunction!(native_list_active_processes, module)?)?;
6516 module.add_function(wrap_pyfunction!(native_get_process_tree_info, module)?)?;
6517 module.add_function(wrap_pyfunction!(native_kill_process_tree, module)?)?;
6518 module.add_function(wrap_pyfunction!(native_process_created_at, module)?)?;
6519 module.add_function(wrap_pyfunction!(native_is_same_process, module)?)?;
6520 module.add_function(wrap_pyfunction!(native_cleanup_tracked_processes, module)?)?;
6521 module.add_function(wrap_pyfunction!(native_apply_process_nice, module)?)?;
6522 module.add_function(wrap_pyfunction!(
6523 native_windows_terminal_input_bytes,
6524 module
6525 )?)?;
6526 module.add_function(wrap_pyfunction!(native_dump_rust_debug_traces, module)?)?;
6527 module.add_function(wrap_pyfunction!(
6528 native_test_capture_rust_debug_trace,
6529 module
6530 )?)?;
6531 #[cfg(windows)]
6532 module.add_function(wrap_pyfunction!(native_test_hang_in_rust, module)?)?;
6533 module.add_function(wrap_pyfunction!(monitor_console_windows, module)?)?;
6534 module.add("VERSION", PyString::new(_py, env!("CARGO_PKG_VERSION")))?;
6535 Ok(())
6536}