aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/platform/windows.rs58
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(&current).copied() {
+ let mut visited = HashSet::new();
+ while visited.insert(current) {
+ let Some(parent) = parent_by_pid.get(&current).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(