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