running_process/console_detect.rs
1//! Windows console popup detection using the Win32 `EnumWindows` API.
2//!
3//! On non-Windows platforms every function is a no-op that returns an empty
4//! `Vec`.
5
6/// Metadata about a single visible window that appeared during monitoring.
7#[derive(Debug, Clone)]
8pub struct ConsoleWindowInfo {
9 /// Process id that owns the visible window.
10 pub pid: u32,
11 /// Window title captured at enumeration time.
12 pub title: String,
13 /// Native window handle represented as an integer for stable transport.
14 pub hwnd: u64,
15}
16
17// ---------------------------------------------------------------------------
18// Windows implementation
19// ---------------------------------------------------------------------------
20#[cfg(windows)]
21mod imp {
22 use super::ConsoleWindowInfo;
23 use std::collections::HashSet;
24 use std::time::{Duration, Instant};
25
26 use winapi::shared::minwindef::{BOOL, LPARAM, TRUE};
27 use winapi::shared::windef::HWND;
28 use winapi::um::winuser::{
29 EnumWindows, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
30 };
31
32 /// Enumerate all currently visible windows and return their metadata.
33 fn enumerate_visible_windows() -> Vec<ConsoleWindowInfo> {
34 let mut results: Vec<ConsoleWindowInfo> = Vec::new();
35
36 unsafe extern "system" fn enum_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
37 // Only consider visible windows.
38 if IsWindowVisible(hwnd) == 0 {
39 return TRUE; // continue enumeration
40 }
41
42 // Obtain the owning PID.
43 let mut pid: u32 = 0;
44 GetWindowThreadProcessId(hwnd, &mut pid);
45
46 // Read the window title into a wide‑char buffer.
47 let mut title_buf: [u16; 512] = [0u16; 512];
48 let len = GetWindowTextW(hwnd, title_buf.as_mut_ptr(), title_buf.len() as i32);
49 let title = if len > 0 {
50 String::from_utf16_lossy(&title_buf[..len as usize])
51 } else {
52 String::new()
53 };
54
55 let results = &mut *(lparam as *mut Vec<ConsoleWindowInfo>);
56 results.push(ConsoleWindowInfo {
57 pid,
58 title,
59 hwnd: hwnd as u64,
60 });
61
62 TRUE // continue enumeration
63 }
64
65 unsafe {
66 EnumWindows(
67 Some(enum_callback),
68 &mut results as *mut Vec<ConsoleWindowInfo> as LPARAM,
69 );
70 }
71
72 results
73 }
74
75 /// Monitor for **new** console windows that appear within `duration`.
76 ///
77 /// 1. Takes a snapshot of all currently visible window HWNDs.
78 /// 2. Polls every 50 ms for the given duration.
79 /// 3. Any HWND not present in the initial snapshot is collected.
80 pub fn monitor_console_windows(duration: Duration) -> Vec<ConsoleWindowInfo> {
81 // Initial snapshot — these windows already existed before monitoring.
82 let baseline: HashSet<u64> = enumerate_visible_windows().iter().map(|w| w.hwnd).collect();
83
84 let mut seen_new: HashSet<u64> = HashSet::new();
85 let mut new_windows: Vec<ConsoleWindowInfo> = Vec::new();
86
87 let start = Instant::now();
88 let poll_interval = Duration::from_millis(50);
89
90 while start.elapsed() < duration {
91 // #199: intentional — Win32 exposes no "new console window"
92 // event; enumerating top-level windows is the only way to
93 // detect new ones. 50ms poll trades responsiveness for CPU.
94 std::thread::sleep(poll_interval);
95
96 for info in enumerate_visible_windows() {
97 if !baseline.contains(&info.hwnd) && seen_new.insert(info.hwnd) {
98 new_windows.push(info);
99 }
100 }
101 }
102
103 new_windows
104 }
105}
106
107#[cfg(windows)]
108pub use imp::monitor_console_windows;
109
110// ---------------------------------------------------------------------------
111// Non-Windows stub
112// ---------------------------------------------------------------------------
113#[cfg(not(windows))]
114/// Monitor for new console windows during `duration`.
115///
116/// Non-Windows platforms do not expose Win32 console popups, so this returns an
117/// empty list.
118pub fn monitor_console_windows(_duration: std::time::Duration) -> Vec<ConsoleWindowInfo> {
119 Vec::new()
120}