From 26f8410887d5605f912c357f42830aa08952f810 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 26 Sep 2026 13:34:05 -0700 Subject: [PATCH 1/4] fix(win): track OpenConsole sidecar hosts alongside conhost --- .../src/platform_linux/terminal.rs | 1 + .../src/platform_macos/terminal.rs | 1 + .../src/platform_win/terminal.rs | 23 +++++++++++++++---- crates/running-process/src/daemon/reaper.rs | 11 +++++---- crates/running-process/src/pty/mod.rs | 4 ++-- 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/running-process-platform-internal/src/platform_linux/terminal.rs b/crates/running-process-platform-internal/src/platform_linux/terminal.rs index 0aa37174..713b08a2 100644 --- a/crates/running-process-platform-internal/src/platform_linux/terminal.rs +++ b/crates/running-process-platform-internal/src/platform_linux/terminal.rs @@ -225,6 +225,7 @@ pub struct OrphanConhostInfo { pub pid: u32, pub parent_pid: u32, pub parent_name: String, + pub host_name: String, } #[cfg(feature = "pty")] diff --git a/crates/running-process-platform-internal/src/platform_macos/terminal.rs b/crates/running-process-platform-internal/src/platform_macos/terminal.rs index dedb5925..466ed5b8 100644 --- a/crates/running-process-platform-internal/src/platform_macos/terminal.rs +++ b/crates/running-process-platform-internal/src/platform_macos/terminal.rs @@ -225,6 +225,7 @@ pub struct OrphanConhostInfo { pub pid: u32, pub parent_pid: u32, pub parent_name: String, + pub host_name: String, } #[cfg(feature = "pty")] diff --git a/crates/running-process-platform-internal/src/platform_win/terminal.rs b/crates/running-process-platform-internal/src/platform_win/terminal.rs index 746d9bdc..7fdb4119 100644 --- a/crates/running-process-platform-internal/src/platform_win/terminal.rs +++ b/crates/running-process-platform-internal/src/platform_win/terminal.rs @@ -165,7 +165,7 @@ pub struct ChildProcessInfo { pub pid: u32, pub name: String } #[cfg(feature = "pty")] #[derive(Debug, Clone)] -pub struct OrphanConhostInfo { pub pid: u32, pub parent_pid: u32, pub parent_name: String } +pub struct OrphanConhostInfo { pub pid: u32, pub parent_pid: u32, pub parent_name: String, pub host_name: String } #[cfg(feature = "pty")] pub fn find_child_processes(parent_pid: u32) -> Vec { @@ -193,10 +193,15 @@ pub fn find_child_processes(parent_pid: u32) -> Vec { #[cfg(feature = "pty")] fn conhost_children_of_current_process() -> Vec { find_child_processes(std::process::id()).into_iter() - .filter(|child| child.name.eq_ignore_ascii_case("conhost.exe")) + .filter(|child| is_conpty_host_image(&child.name)) .map(|child| child.pid).collect() } +#[cfg(feature = "pty")] +fn is_conpty_host_image(name: &str) -> bool { + name.eq_ignore_ascii_case("conhost.exe") || name.eq_ignore_ascii_case("OpenConsole.exe") +} + #[cfg(feature = "pty")] pub fn find_orphan_conhosts() -> Vec { use winapi::um::handleapi::CloseHandle; @@ -214,13 +219,13 @@ pub fn find_orphan_conhosts() -> Vec { let name = String::from_utf8_lossy(&entry.szExeFile[..length].iter().map(|&byte| byte as u8).collect::>()).into_owned(); all_pids.insert(entry.th32ProcessID); names.insert(entry.th32ProcessID, name.clone()); - if name.eq_ignore_ascii_case("conhost.exe") { conhosts.push((entry.th32ProcessID, entry.th32ParentProcessID)); } + if is_conpty_host_image(&name) { conhosts.push((entry.th32ProcessID, entry.th32ParentProcessID, name)); } if unsafe { Process32Next(snapshot, &mut entry) } == 0 { break; } } } unsafe { CloseHandle(snapshot) }; - conhosts.into_iter().filter(|(_, parent)| !all_pids.contains(parent)).map(|(pid, parent_pid)| OrphanConhostInfo { - pid, parent_pid, parent_name: names.get(&parent_pid).cloned().unwrap_or_default(), + conhosts.into_iter().filter(|(_, parent, _)| !all_pids.contains(parent)).map(|(pid, parent_pid, host_name)| OrphanConhostInfo { + pid, parent_pid, parent_name: names.get(&parent_pid).cloned().unwrap_or_default(), host_name, }).collect() } @@ -362,6 +367,14 @@ impl Drop for TerminalInputSession { mod tests { use super::*; + #[test] + fn recognizes_both_conpty_host_images() { + assert!(is_conpty_host_image("conhost.exe")); + assert!(is_conpty_host_image("OPENCONSOLE.EXE")); + assert!(!is_conpty_host_image("OpenConsole.exe.bak")); + assert!(!is_conpty_host_image("console.exe")); + } + #[test] fn assign_child_to_job_null_handle_errors() { assert!(prepare_unmanaged_pty_child(before_pty_spawn(), None).is_err()); diff --git a/crates/running-process/src/daemon/reaper.rs b/crates/running-process/src/daemon/reaper.rs index 033caf53..b90f63dc 100644 --- a/crates/running-process/src/daemon/reaper.rs +++ b/crates/running-process/src/daemon/reaper.rs @@ -117,9 +117,9 @@ pub fn scan_for_zombies(state: &DaemonState) -> Vec { zombies } -/// Scan for orphaned conhost.exe processes system-wide. +/// Scan for orphaned ConPTY console-host processes system-wide. /// -/// These are conhost.exe instances whose parent process has died — typically +/// These are conhost.exe or OpenConsole.exe instances whose parent has died — typically /// leftovers from ConPTY sessions that were not properly cleaned up. /// Unlike registry-based zombie scanning, this uses a Toolhelp process snapshot /// and does not require prior registration. @@ -129,8 +129,11 @@ pub fn scan_for_orphan_conhosts() -> Vec { .into_iter() .map(|c| ZombieInfo { pid: c.pid, - command: "conhost.exe".to_string(), - reason: format!("orphan conhost.exe — parent PID {} is dead", c.parent_pid), + command: c.host_name.clone(), + reason: format!( + "orphan {} — parent PID {} is dead", + c.host_name, c.parent_pid + ), }) .collect() } diff --git a/crates/running-process/src/pty/mod.rs b/crates/running-process/src/pty/mod.rs index 35f49fc3..67aafaea 100644 --- a/crates/running-process/src/pty/mod.rs +++ b/crates/running-process/src/pty/mod.rs @@ -558,11 +558,11 @@ pub fn find_child_processes(parent_pid: u32) -> Vec { pty_platform::find_child_processes(parent_pid) } -/// A conhost.exe process whose parent is no longer alive — likely an orphan +/// A ConPTY console host whose parent is no longer alive — likely an orphan /// from a dead ConPTY session. pub use pty_platform::OrphanConhostInfo; -/// Scan all conhost.exe processes on the system and return those whose parent +/// Scan conhost.exe and OpenConsole.exe processes and return those whose parent /// process is no longer alive. These are likely orphans from dead ConPTY sessions. /// /// Uses `CreateToolhelp32Snapshot` for a point-in-time snapshot — no sysinfo From b858532e37cc7f05d3bd0f12a73a918727ad8da7 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 26 Sep 2026 13:40:17 -0700 Subject: [PATCH 2/4] test(pty): prove sidecar image predicate red to green on every host --- .../src/platform/terminal.rs | 18 ++++++++++++++++++ .../src/platform_win/terminal.rs | 14 ++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/running-process-platform-internal/src/platform/terminal.rs b/crates/running-process-platform-internal/src/platform/terminal.rs index 1d770c96..ca3cb929 100644 --- a/crates/running-process-platform-internal/src/platform/terminal.rs +++ b/crates/running-process-platform-internal/src/platform/terminal.rs @@ -1,5 +1,23 @@ //! Terminal, PTY, console, input, and terminal-I/O primitives. +#[cfg(any(windows, test))] +pub(crate) fn is_conpty_host_image(name: &str) -> bool { + name.eq_ignore_ascii_case("conhost.exe") || name.eq_ignore_ascii_case("OpenConsole.exe") +} + +#[cfg(test)] +mod console_host_tests { + use super::is_conpty_host_image; + + #[test] + fn recognizes_system_and_sidecar_console_hosts() { + assert!(is_conpty_host_image("conhost.exe")); + assert!(is_conpty_host_image("OPENCONSOLE.EXE")); + assert!(!is_conpty_host_image("OpenConsole.exe.bak")); + assert!(!is_conpty_host_image("console.exe")); + } +} + use std::ffi::OsString; use std::io::{self, Read, Write}; use std::path::Path; diff --git a/crates/running-process-platform-internal/src/platform_win/terminal.rs b/crates/running-process-platform-internal/src/platform_win/terminal.rs index 7fdb4119..d1287822 100644 --- a/crates/running-process-platform-internal/src/platform_win/terminal.rs +++ b/crates/running-process-platform-internal/src/platform_win/terminal.rs @@ -192,18 +192,16 @@ pub fn find_child_processes(parent_pid: u32) -> Vec { #[cfg(feature = "pty")] fn conhost_children_of_current_process() -> Vec { + use crate::platform::terminal::is_conpty_host_image; find_child_processes(std::process::id()).into_iter() .filter(|child| is_conpty_host_image(&child.name)) .map(|child| child.pid).collect() } #[cfg(feature = "pty")] -fn is_conpty_host_image(name: &str) -> bool { - name.eq_ignore_ascii_case("conhost.exe") || name.eq_ignore_ascii_case("OpenConsole.exe") -} - #[cfg(feature = "pty")] pub fn find_orphan_conhosts() -> Vec { + use crate::platform::terminal::is_conpty_host_image; use winapi::um::handleapi::CloseHandle; use winapi::um::tlhelp32::{CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS}; let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; @@ -367,14 +365,6 @@ impl Drop for TerminalInputSession { mod tests { use super::*; - #[test] - fn recognizes_both_conpty_host_images() { - assert!(is_conpty_host_image("conhost.exe")); - assert!(is_conpty_host_image("OPENCONSOLE.EXE")); - assert!(!is_conpty_host_image("OpenConsole.exe.bak")); - assert!(!is_conpty_host_image("console.exe")); - } - #[test] fn assign_child_to_job_null_handle_errors() { assert!(prepare_unmanaged_pty_child(before_pty_spawn(), None).is_err()); From a31a235f8422e1ea449a30507b10f12396129b78 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 26 Sep 2026 13:49:31 -0700 Subject: [PATCH 3/4] fix(pty): gate console-host helper with PTY feature --- .../running-process-platform-internal/src/platform/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/running-process-platform-internal/src/platform/terminal.rs b/crates/running-process-platform-internal/src/platform/terminal.rs index ca3cb929..fd9fa783 100644 --- a/crates/running-process-platform-internal/src/platform/terminal.rs +++ b/crates/running-process-platform-internal/src/platform/terminal.rs @@ -1,6 +1,6 @@ //! Terminal, PTY, console, input, and terminal-I/O primitives. -#[cfg(any(windows, test))] +#[cfg(any(all(windows, feature = "pty"), test))] pub(crate) fn is_conpty_host_image(name: &str) -> bool { name.eq_ignore_ascii_case("conhost.exe") || name.eq_ignore_ascii_case("OpenConsole.exe") } From ded50b22e8336b14d9b0b5eb62e3efc0ade95713 Mon Sep 17 00:00:00 2001 From: Zach Vorhies Date: Sat, 26 Sep 2026 14:05:00 -0700 Subject: [PATCH 4/4] fix(pty): keep shared host predicate platform-neutral --- .../src/platform/terminal.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/running-process-platform-internal/src/platform/terminal.rs b/crates/running-process-platform-internal/src/platform/terminal.rs index fd9fa783..071daafe 100644 --- a/crates/running-process-platform-internal/src/platform/terminal.rs +++ b/crates/running-process-platform-internal/src/platform/terminal.rs @@ -1,11 +1,13 @@ //! Terminal, PTY, console, input, and terminal-I/O primitives. -#[cfg(any(all(windows, feature = "pty"), test))] +// Shared image classification is pure, but only Windows currently consumes it. +#[cfg(feature = "pty")] +#[allow(dead_code)] pub(crate) fn is_conpty_host_image(name: &str) -> bool { name.eq_ignore_ascii_case("conhost.exe") || name.eq_ignore_ascii_case("OpenConsole.exe") } -#[cfg(test)] +#[cfg(all(test, feature = "pty"))] mod console_host_tests { use super::is_conpty_host_image;