diff options
| -rw-r--r-- | docs/next/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | src/platform/windows.rs | 58 |
2 files changed, 54 insertions, 5 deletions
diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index b4119752..64cd717d 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -27,6 +27,7 @@ - Alt-Shift letter chords are now preserved instead of being collapsed into plain uppercase input. (#1088) - Antigravity background-task waits are now detected even when the UI does not show a `/tasks` hint. (#755) - `herdr --remote` now prints clean remote attach failures and SSH authentication guidance instead of Rust Debug-formatted I/O errors when SSH authentication is denied. (#1034) +- Windows foreground-process detection now ignores cyclic process-parent snapshots instead of growing memory until the server aborts. (#1083) - `herdr server stop` now stops Windows named-pipe servers instead of failing with `named pipes do not support I/O timeouts`. (#1113) - `herdr server stop` now waits until both server sockets are unreachable before returning, avoiding an immediate first-start failure when restarting right after replacing the binary. - macOS `herdr --remote` clients now bridge Finder-dropped image files to the remote pane instead of forwarding the local file path as typed text. (#828) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 6ac05767..49a68111 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1,5 +1,5 @@ use std::{ - collections::{HashMap, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, ffi::c_void, mem::{size_of, MaybeUninit}, path::PathBuf, @@ -223,11 +223,15 @@ fn process_is_ancestor( parent_by_pid: &HashMap<u32, u32>, ) -> bool { let mut current = descendant_pid; - while let Some(parent) = parent_by_pid.get(¤t).copied() { + let mut visited = HashSet::new(); + while visited.insert(current) { + let Some(parent) = parent_by_pid.get(¤t).copied() else { + return false; + }; if parent == ancestor_pid { return true; } - if parent == 0 || parent == current { + if parent == 0 { return false; } current = parent; @@ -244,13 +248,23 @@ fn descendant_entries(root_pid: u32, entries: &[WindowsProcessEntry]) -> Vec<&Wi let mut output = Vec::new(); let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + visited.insert(root_pid); if let Some(root_children) = children.get(&root_pid) { - queue.extend(root_children.iter().copied()); + for entry in root_children.iter().copied() { + if visited.insert(entry.pid) { + queue.push_back(entry); + } + } } while let Some(entry) = queue.pop_front() { output.push(entry); if let Some(next) = children.get(&entry.pid) { - queue.extend(next.iter().copied()); + for child in next.iter().copied() { + if visited.insert(child.pid) { + queue.push_back(child); + } + } } } output @@ -897,6 +911,40 @@ mod tests { } #[test] + fn windows_process_tree_ignores_pid_reuse_cycles() { + let entries = vec![ + test_entry(10, 30, "powershell.exe", &["powershell.exe"]), + test_entry(20, 10, "codex.exe", &["codex.exe"]), + test_entry(30, 20, "node.exe", &["node.exe"]), + ]; + + let descendants = super::descendant_entries(10, &entries); + + assert_eq!( + descendants + .iter() + .map(|entry| entry.pid) + .collect::<Vec<_>>(), + vec![20, 30] + ); + } + + #[test] + fn windows_process_tree_returns_shell_when_candidate_parent_chain_cycles() { + let entries = vec![ + test_entry(10, 40, "powershell.exe", &["powershell.exe"]), + test_entry(20, 10, "codex.exe", &["codex.exe"]), + test_entry(30, 10, "codex.exe", &["codex.exe"]), + test_entry(40, 10, "node.exe", &["node.exe"]), + ]; + + let job = super::select_pane_foreground_job(10, &entries).unwrap(); + + assert_eq!(job.process_group_id, 10); + assert_eq!(job.processes[0].name, "powershell.exe"); + } + + #[test] fn scrollback_editor_argv_uses_editor_env_and_appends_path() { let path = std::path::Path::new(r"C:\Users\User\AppData\Local\Temp\herdr scrollback.txt"); let argv = super::scrollback_editor_argv_with_env( |
