Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions crates/running-process-platform-internal/src/platform/terminal.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
//! Terminal, PTY, console, input, and terminal-I/O primitives.

// 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(all(test, feature = "pty"))]
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChildProcessInfo> {
Expand All @@ -192,13 +192,16 @@ pub fn find_child_processes(parent_pid: u32) -> Vec<ChildProcessInfo> {

#[cfg(feature = "pty")]
fn conhost_children_of_current_process() -> Vec<u32> {
use crate::platform::terminal::is_conpty_host_image;
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")]
#[cfg(feature = "pty")]
pub fn find_orphan_conhosts() -> Vec<OrphanConhostInfo> {
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) };
Expand All @@ -214,13 +217,13 @@ pub fn find_orphan_conhosts() -> Vec<OrphanConhostInfo> {
let name = String::from_utf8_lossy(&entry.szExeFile[..length].iter().map(|&byte| byte as u8).collect::<Vec<_>>()).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()
}

Expand Down
11 changes: 7 additions & 4 deletions crates/running-process/src/daemon/reaper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ pub fn scan_for_zombies(state: &DaemonState) -> Vec<ZombieInfo> {
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.
Expand All @@ -129,8 +129,11 @@ pub fn scan_for_orphan_conhosts() -> Vec<ZombieInfo> {
.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()
}
Expand Down
4 changes: 2 additions & 2 deletions crates/running-process/src/pty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,11 @@ pub fn find_child_processes(parent_pid: u32) -> Vec<ChildProcessInfo> {
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
Expand Down
Loading