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