#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
pub trait HideWindowCommandExt {
fn hide_window(&mut self) -> &mut Self;
}
#[cfg(windows)]
impl HideWindowCommandExt for std::process::Command {
fn hide_window(&mut self) -> &mut Self {
use std::os::windows::process::CommandExt;
self.creation_flags(CREATE_NO_WINDOW)
}
}
#[cfg(not(windows))]
impl HideWindowCommandExt for std::process::Command {
fn hide_window(&mut self) -> &mut Self {
self }
}
#[cfg(windows)]
pub fn spawn_server_hidden(exe: &std::path::Path, args: &[String]) -> std::io::Result<()> {
#[repr(C)]
#[allow(non_snake_case)]
struct STARTUPINFOW {
cb: u32,
lpReserved: *mut u16,
lpDesktop: *mut u16,
lpTitle: *mut u16,
dwX: u32,
dwY: u32,
dwXSize: u32,
dwYSize: u32,
dwXCountChars: u32,
dwYCountChars: u32,
dwFillAttribute: u32,
dwFlags: u32,
wShowWindow: u16,
cbReserved2: u16,
lpReserved2: *mut u8,
hStdInput: isize,
hStdOutput: isize,
hStdError: isize,
}
#[repr(C)]
#[allow(non_snake_case)]
struct PROCESS_INFORMATION {
hProcess: isize,
hThread: isize,
dwProcessId: u32,
dwThreadId: u32,
}
#[link(name = "kernel32")]
extern "system" {
fn CreateProcessW(
lpApplicationName: *const u16,
lpCommandLine: *mut u16,
lpProcessAttributes: *const std::ffi::c_void,
lpThreadAttributes: *const std::ffi::c_void,
bInheritHandles: i32,
dwCreationFlags: u32,
lpEnvironment: *const std::ffi::c_void,
lpCurrentDirectory: *const u16,
lpStartupInfo: *const STARTUPINFOW,
lpProcessInformation: *mut PROCESS_INFORMATION,
) -> i32;
fn CloseHandle(handle: isize) -> i32;
}
const STARTF_USESHOWWINDOW: u32 = 0x00000001;
const SW_HIDE: u16 = 0;
const CREATE_NEW_CONSOLE: u32 = 0x00000010;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x01000000;
let mut cmdline = format!("\"{}\"", exe.display());
for arg in args {
if arg.contains(' ') || arg.contains('"') {
cmdline.push_str(&format!(" \"{}\"", arg.replace('"', "\\\"")));
} else {
cmdline.push(' ');
cmdline.push_str(arg);
}
}
let mut cmdline_wide: Vec<u16> = cmdline.encode_utf16().chain(std::iter::once(0)).collect();
let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() };
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let base_flags = CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP;
let mut ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmdline_wide.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0, base_flags | CREATE_BREAKAWAY_FROM_JOB,
std::ptr::null(),
std::ptr::null(),
&si,
&mut pi,
)
};
if ok == 0 {
cmdline_wide = cmdline.encode_utf16().chain(std::iter::once(0)).collect();
ok = unsafe {
CreateProcessW(
std::ptr::null(),
cmdline_wide.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
0,
base_flags,
std::ptr::null(),
std::ptr::null(),
&si,
&mut pi,
)
};
}
if ok == 0 {
return Err(std::io::Error::last_os_error());
}
unsafe {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
Ok(())
}
#[cfg(windows)]
pub fn enable_virtual_terminal_processing() {
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
const CP_UTF8: u32 = 65001;
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn GetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, lpMode: *mut u32) -> i32;
fn SetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, dwMode: u32) -> i32;
fn SetConsoleOutputCP(wCodePageID: u32) -> i32;
fn SetConsoleCP(wCodePageID: u32) -> i32;
}
unsafe {
SetConsoleOutputCP(CP_UTF8);
SetConsoleCP(CP_UTF8);
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
if !handle.is_null() {
let mut mode: u32 = 0;
if GetConsoleMode(handle, &mut mode) != 0 {
SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
}
#[cfg(not(windows))]
pub fn enable_virtual_terminal_processing() {
}
#[cfg(windows)]
pub fn disable_vti_on_stdin() {
const STD_INPUT_HANDLE: u32 = (-10i32) as u32;
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
fn GetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, lpMode: *mut u32) -> i32;
fn SetConsoleMode(hConsoleHandle: *mut std::ffi::c_void, dwMode: u32) -> i32;
}
unsafe {
let handle = GetStdHandle(STD_INPUT_HANDLE);
if handle.is_null() || handle == (-1isize) as *mut std::ffi::c_void {
return;
}
let mut mode: u32 = 0;
if GetConsoleMode(handle, &mut mode) != 0 {
let had_vti = mode & ENABLE_VIRTUAL_TERMINAL_INPUT != 0;
crate::debug_log::input_log("console", &format!(
"stdin mode before: 0x{:04X} VTI={}", mode, had_vti
));
if had_vti {
let new_mode = mode & !ENABLE_VIRTUAL_TERMINAL_INPUT;
SetConsoleMode(handle, new_mode);
crate::debug_log::input_log("console", &format!(
"stdin mode after: 0x{:04X} (VTI cleared)", new_mode
));
}
}
}
}
#[cfg(not(windows))]
pub fn disable_vti_on_stdin() {
}
#[cfg(windows)]
pub fn install_console_ctrl_handler() {
type HandlerRoutine = unsafe extern "system" fn(u32) -> i32;
#[link(name = "kernel32")]
extern "system" {
fn SetConsoleCtrlHandler(handler: Option<HandlerRoutine>, add: i32) -> i32;
}
const CTRL_CLOSE_EVENT: u32 = 2;
const CTRL_LOGOFF_EVENT: u32 = 5;
const CTRL_SHUTDOWN_EVENT: u32 = 6;
unsafe extern "system" fn handler(ctrl_type: u32) -> i32 {
match ctrl_type {
CTRL_CLOSE_EVENT | CTRL_LOGOFF_EVENT | CTRL_SHUTDOWN_EVENT => 1,
_ => 0,
}
}
unsafe {
SetConsoleCtrlHandler(Some(handler), 1);
}
}
#[cfg(not(windows))]
pub fn install_console_ctrl_handler() {
}
#[cfg(windows)]
pub mod mouse_inject {
use std::ffi::c_void;
const GENERIC_READ: u32 = 0x80000000;
const GENERIC_WRITE: u32 = 0x40000000;
const FILE_SHARE_READ: u32 = 0x00000001;
const FILE_SHARE_WRITE: u32 = 0x00000002;
const OPEN_EXISTING: u32 = 3;
const INVALID_HANDLE: isize = -1;
const MOUSE_EVENT: u16 = 0x0002;
const ATTACH_PARENT_PROCESS: u32 = 0xFFFFFFFF;
pub const FROM_LEFT_1ST_BUTTON_PRESSED: u32 = 0x0001;
pub const RIGHTMOST_BUTTON_PRESSED: u32 = 0x0002;
pub const FROM_LEFT_2ND_BUTTON_PRESSED: u32 = 0x0004;
pub const MOUSE_MOVED: u32 = 0x0001;
pub const MOUSE_WHEELED: u32 = 0x0004;
use std::sync::Mutex;
use std::time::{Duration, Instant};
static LAST_DRAG_INJECT: Mutex<Option<Instant>> = Mutex::new(None);
const DRAG_THROTTLE: Duration = Duration::from_millis(16);
#[repr(C)]
#[derive(Copy, Clone)]
struct COORD {
x: i16,
y: i16,
}
#[repr(C)]
#[derive(Copy, Clone)]
struct MOUSE_EVENT_RECORD {
mouse_position: COORD,
button_state: u32,
control_key_state: u32,
event_flags: u32,
}
#[repr(C)]
struct INPUT_RECORD {
event_type: u16,
_padding: u16,
event: MOUSE_EVENT_RECORD,
}
#[link(name = "kernel32")]
extern "system" {
fn FreeConsole() -> i32;
fn AttachConsole(process_id: u32) -> i32;
fn GetConsoleWindow() -> isize;
fn CreateFileW(
file_name: *const u16,
desired_access: u32,
share_mode: u32,
security_attributes: *const c_void,
creation_disposition: u32,
flags_and_attributes: u32,
template_file: *const c_void,
) -> isize;
fn WriteConsoleInputW(
console_input: isize,
buffer: *const INPUT_RECORD,
length: u32,
events_written: *mut u32,
) -> i32;
fn CloseHandle(handle: isize) -> i32;
fn GetProcessId(process: isize) -> u32;
fn GetLastError() -> u32;
}
const ENABLE_MOUSE_INPUT: u32 = 0x0010;
const ENABLE_EXTENDED_FLAGS: u32 = 0x0080;
const ENABLE_QUICK_EDIT_MODE: u32 = 0x0040;
const ENABLE_VIRTUAL_TERMINAL_INPUT: u32 = 0x0200;
#[inline]
fn debug_log(msg: &str) {
use std::sync::atomic::{AtomicBool, Ordering};
static CHECKED: AtomicBool = AtomicBool::new(false);
static ENABLED: AtomicBool = AtomicBool::new(false);
if !CHECKED.swap(true, Ordering::Relaxed) {
let on = std::env::var("PSMUX_MOUSE_DEBUG").map_or(false, |v| v == "1" || v == "true");
ENABLED.store(on, Ordering::Relaxed);
}
if !ENABLED.load(Ordering::Relaxed) { return; }
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let path = format!("{}/.psmux/mouse_debug.log", home);
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
use std::io::Write;
let _ = writeln!(f, "[platform] {}", msg);
}
}
pub fn get_child_pid(child: &dyn portable_pty::Child) -> Option<u32> {
child.process_id()
}
pub fn query_vti_enabled(child_pid: u32) -> Option<bool> {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
debug_log(&format!("query_vti_enabled: AttachConsole({}) FAILED", child_pid));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return None;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
debug_log("query_vti_enabled: CreateFileW(CONIN$) FAILED");
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return None;
}
#[link(name = "kernel32")]
extern "system" {
fn GetConsoleMode(hConsoleHandle: *mut c_void, lpMode: *mut u32) -> i32;
}
let mut mode: u32 = 0;
let ok = GetConsoleMode(handle as *mut c_void, &mut mode);
CloseHandle(handle);
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
if ok == 0 {
debug_log("query_vti_enabled: GetConsoleMode FAILED");
return None;
}
let vti = (mode & ENABLE_VIRTUAL_TERMINAL_INPUT) != 0;
debug_log(&format!("query_vti_enabled: pid={} mode=0x{:04X} VTI={}", child_pid, mode, vti));
Some(vti)
}
}
pub fn send_mouse_event(
child_pid: u32,
col: i16,
row: i16,
button_state: u32,
event_flags: u32,
reattach: bool,
) -> bool {
if event_flags & MOUSE_MOVED != 0 {
if let Ok(mut guard) = LAST_DRAG_INJECT.lock() {
if let Some(t) = *guard {
if t.elapsed() < DRAG_THROTTLE {
return false;
}
}
*guard = Some(Instant::now());
}
}
unsafe {
let had_console = reattach && GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
let err = GetLastError();
debug_log(&format!("send_mouse_event: AttachConsole({}) FAILED err={}", child_pid, err));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
let err = GetLastError();
debug_log(&format!("send_mouse_event: CreateFileW(CONIN$) FAILED err={}", err));
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
{
#[link(name = "kernel32")]
extern "system" {
fn GetConsoleMode(hConsoleHandle: *mut c_void, lpMode: *mut u32) -> i32;
fn SetConsoleMode(hConsoleHandle: *mut c_void, dwMode: u32) -> i32;
}
let mut mode: u32 = 0;
let h = handle as *mut c_void;
if GetConsoleMode(h, &mut mode) != 0 {
let desired = (mode | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS)
& !ENABLE_QUICK_EDIT_MODE;
if desired != mode {
SetConsoleMode(h, desired);
}
}
}
let record = INPUT_RECORD {
event_type: MOUSE_EVENT,
_padding: 0,
event: MOUSE_EVENT_RECORD {
mouse_position: COORD { x: col, y: row },
button_state,
control_key_state: 0,
event_flags,
},
};
let mut written: u32 = 0;
let result = WriteConsoleInputW(handle, &record, 1, &mut written);
let write_err = GetLastError();
debug_log(&format!("send_mouse_event: pid={} ({},{}) btn=0x{:X} flags=0x{:X} => ok={} written={} err={}",
child_pid, col, row, button_state, event_flags, result, written, write_err));
CloseHandle(handle);
FreeConsole();
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
result != 0
}
}
pub fn query_mouse_input_enabled(child_pid: u32) -> Option<bool> {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
debug_log(&format!("query_mouse_input_enabled: AttachConsole({}) FAILED", child_pid));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return None;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
debug_log("query_mouse_input_enabled: CreateFileW(CONIN$) FAILED");
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return None;
}
#[link(name = "kernel32")]
extern "system" {
fn GetConsoleMode(hConsoleHandle: *mut c_void, lpMode: *mut u32) -> i32;
}
let mut mode: u32 = 0;
let ok = GetConsoleMode(handle as *mut c_void, &mut mode);
CloseHandle(handle);
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
if ok == 0 {
debug_log("query_mouse_input_enabled: GetConsoleMode FAILED");
return None;
}
let mouse_input = (mode & ENABLE_MOUSE_INPUT) != 0;
debug_log(&format!("query_mouse_input_enabled: pid={} mode=0x{:04X} ENABLE_MOUSE_INPUT={}", child_pid, mode, mouse_input));
Some(mouse_input)
}
}
pub fn send_vt_sequence(child_pid: u32, sequence: &[u8]) -> bool {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
#[link(name = "kernel32")]
extern "system" {
fn GetConsoleMode(hConsoleHandle: *mut c_void, lpMode: *mut u32) -> i32;
fn SetConsoleMode(hConsoleHandle: *mut c_void, dwMode: u32) -> i32;
}
let h = handle as *mut c_void;
let mut original_mode: u32 = 0;
let got_mode = GetConsoleMode(h, &mut original_mode) != 0;
if got_mode {
let desired = (original_mode | ENABLE_EXTENDED_FLAGS | 0x0200 )
& !ENABLE_QUICK_EDIT_MODE;
if desired != original_mode {
SetConsoleMode(h, desired);
}
}
const KEY_EVENT: u16 = 0x0001;
#[repr(C)]
#[derive(Copy, Clone)]
struct KEY_EVENT_RECORD {
key_down: i32,
repeat_count: u16,
virtual_key_code: u16,
virtual_scan_code: u16,
u_char: u16, control_key_state: u32,
}
#[repr(C)]
struct KEY_INPUT_RECORD {
event_type: u16,
_padding: u16,
event: KEY_EVENT_RECORD,
}
let mut records: Vec<KEY_INPUT_RECORD> = Vec::with_capacity(sequence.len());
for &byte in sequence {
records.push(KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 1,
repeat_count: 1,
virtual_key_code: 0,
virtual_scan_code: 0,
u_char: byte as u16,
control_key_state: 0,
},
});
}
let mut written: u32 = 0;
let result = WriteConsoleInputW(
handle,
records.as_ptr() as *const INPUT_RECORD,
records.len() as u32,
&mut written,
);
if got_mode {
SetConsoleMode(h, original_mode);
}
CloseHandle(handle);
FreeConsole();
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
result != 0
}
}
pub fn send_bracketed_paste(child_pid: u32, text: &str, bracket: bool) -> bool {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
let err = GetLastError();
debug_log(&format!("send_bracketed_paste: AttachConsole({}) FAILED err={}", child_pid, err));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
let err = GetLastError();
debug_log(&format!("send_bracketed_paste: CreateFileW(CONIN$) FAILED err={}", err));
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
const KEY_EVENT: u16 = 0x0001;
#[repr(C)]
#[derive(Copy, Clone)]
struct KEY_EVENT_RECORD {
key_down: i32,
repeat_count: u16,
virtual_key_code: u16,
virtual_scan_code: u16,
u_char: u16,
control_key_state: u32,
}
#[repr(C)]
struct KEY_INPUT_RECORD {
event_type: u16,
_padding: u16,
event: KEY_EVENT_RECORD,
}
let bracket_open: &[u8] = b"\x1b[200~";
let bracket_close: &[u8] = b"\x1b[201~";
let mut chars: Vec<u16> = Vec::new();
if bracket {
for &b in bracket_open {
chars.push(b as u16);
}
}
let mut prev_cr = false;
for c in text.chars() {
if c == '\n' {
if !prev_cr {
chars.push('\r' as u16);
}
prev_cr = false;
continue;
}
prev_cr = c == '\r';
let mut buf = [0u16; 2];
let encoded = c.encode_utf16(&mut buf);
for &unit in encoded.iter() {
chars.push(unit);
}
}
if bracket {
for &b in bracket_close {
chars.push(b as u16);
}
}
let mut records: Vec<KEY_INPUT_RECORD> = Vec::with_capacity(chars.len());
for &wch in &chars {
records.push(KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 1,
repeat_count: 1,
virtual_key_code: 0,
virtual_scan_code: 0,
u_char: wch,
control_key_state: 0,
},
});
}
const CHUNK_SIZE: usize = 2048;
let mut offset: usize = 0;
let mut last_result: i32 = 1;
while offset < records.len() {
let mut written: u32 = 0;
let remaining = (records.len() - offset).min(CHUNK_SIZE);
last_result = WriteConsoleInputW(
handle,
records[offset..].as_ptr() as *const INPUT_RECORD,
remaining as u32,
&mut written,
);
if last_result == 0 || written == 0 {
std::thread::sleep(std::time::Duration::from_millis(10));
last_result = WriteConsoleInputW(
handle,
records[offset..].as_ptr() as *const INPUT_RECORD,
remaining as u32,
&mut written,
);
if last_result == 0 || written == 0 {
break;
}
}
offset += written as usize;
if offset < records.len() && remaining >= CHUNK_SIZE {
std::thread::sleep(std::time::Duration::from_millis(5));
}
}
debug_log(&format!("send_bracketed_paste: pid={} bracket={} text_len={} records={} written={} ok={}",
child_pid, bracket, text.len(), records.len(), offset, last_result != 0));
CloseHandle(handle);
FreeConsole();
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
last_result != 0 && offset == records.len()
}
}
pub fn send_ctrl_c_event(child_pid: u32, reattach: bool) -> bool {
const CTRL_C_EVENT: u32 = 0;
const ENABLE_PROCESSED_INPUT: u32 = 0x0001;
type HandlerRoutine = unsafe extern "system" fn(u32) -> i32;
#[link(name = "kernel32")]
extern "system" {
fn SetConsoleCtrlHandler(
handler: Option<HandlerRoutine>,
add: i32,
) -> i32;
fn GenerateConsoleCtrlEvent(
ctrl_event: u32,
process_group_id: u32,
) -> i32;
fn GetConsoleMode(h: *mut c_void, mode: *mut u32) -> i32;
fn SetConsoleMode(h: *mut c_void, mode: u32) -> i32;
}
fn log(msg: &str) {
debug_log(&format!("ctrl_c: {}", msg));
}
unsafe {
let had_console = reattach && GetConsoleWindow() != 0;
FreeConsole();
log(&format!("called: pid={} reattach={} had_console={}", child_pid, reattach, had_console));
if AttachConsole(child_pid) == 0 {
let err = GetLastError();
log(&format!("AttachConsole({}) FAILED err={}", child_pid, err));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle != INVALID_HANDLE && handle != 0 {
let mut mode: u32 = 0;
if GetConsoleMode(handle as *mut c_void, &mut mode) != 0 {
log(&format!("console mode=0x{:04X} PROCESSED_INPUT={}", mode, mode & ENABLE_PROCESSED_INPUT != 0));
if mode & ENABLE_PROCESSED_INPUT == 0 {
log(&format!("re-enabling ENABLE_PROCESSED_INPUT for pid={}", child_pid));
SetConsoleMode(handle as *mut c_void, mode | ENABLE_PROCESSED_INPUT);
}
}
CloseHandle(handle);
}
SetConsoleCtrlHandler(None, 1);
let ok = GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0);
let err = GetLastError();
log(&format!("GenerateConsoleCtrlEvent => ok={} err={}", ok, err));
FreeConsole();
std::thread::sleep(std::time::Duration::from_millis(5));
SetConsoleCtrlHandler(None, 0);
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
ok != 0
}
}
pub fn send_modified_key_event(child_pid: u32, ch: char, ctrl: bool, alt: bool, shift: bool) -> bool {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
debug_log(&format!("send_modified_key_event: AttachConsole({}) FAILED", child_pid));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
debug_log(&format!("send_modified_key_event: CreateFileW(CONIN$) FAILED"));
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
const KEY_EVENT: u16 = 0x0001;
const LEFT_ALT_PRESSED: u32 = 0x0002;
const LEFT_CTRL_PRESSED: u32 = 0x0008;
const SHIFT_PRESSED: u32 = 0x0010;
#[repr(C)]
#[derive(Copy, Clone)]
struct KEY_EVENT_RECORD {
key_down: i32,
repeat_count: u16,
virtual_key_code: u16,
virtual_scan_code: u16,
u_char: u16,
control_key_state: u32,
}
#[repr(C)]
struct KEY_INPUT_RECORD {
event_type: u16,
_padding: u16,
event: KEY_EVENT_RECORD,
}
#[link(name = "user32")]
extern "system" {
fn VkKeyScanW(ch: u16) -> i16;
fn MapVirtualKeyW(code: u32, map_type: u32) -> u32;
}
let mut flags: u32 = 0;
if ctrl { flags |= LEFT_CTRL_PRESSED; }
if alt { flags |= LEFT_ALT_PRESSED; }
if shift { flags |= SHIFT_PRESSED; }
let base_char = if shift && !ctrl {
ch.to_ascii_uppercase()
} else {
ch
};
let u_char_value: u16 = if ctrl {
(base_char.to_ascii_lowercase() as u16) & 0x1F
} else {
let mut buf = [0u16; 2];
let encoded = base_char.encode_utf16(&mut buf);
encoded[0]
};
let mut buf = [0u16; 2];
let plain_wch = ch.to_ascii_lowercase().encode_utf16(&mut buf)[0];
let vk_result = VkKeyScanW(plain_wch);
let vk = if vk_result == -1 { 0u16 } else { (vk_result & 0xFF) as u16 };
let scan = MapVirtualKeyW(vk as u32, 0) as u16;
let records = [
KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 1,
repeat_count: 1,
virtual_key_code: vk,
virtual_scan_code: scan,
u_char: u_char_value,
control_key_state: flags,
},
},
KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 0,
repeat_count: 1,
virtual_key_code: vk,
virtual_scan_code: scan,
u_char: u_char_value,
control_key_state: flags,
},
},
];
let mut written: u32 = 0;
let result = WriteConsoleInputW(
handle,
records.as_ptr() as *const INPUT_RECORD,
2,
&mut written,
);
debug_log(&format!("send_modified_key_event: pid={} char='{}' ctrl={} alt={} shift={} vk=0x{:02X} scan=0x{:02X} u_char=0x{:04X} flags=0x{:04X} => ok={} written={}",
child_pid, ch, ctrl, alt, shift, vk, scan, u_char_value, flags, result != 0, written));
CloseHandle(handle);
FreeConsole();
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
result != 0 && written >= 1
}
}
pub fn send_alt_key_event(child_pid: u32, ch: char) -> bool {
send_modified_key_event(child_pid, ch, false, true, false)
}
pub fn send_modified_enter_event(child_pid: u32, ctrl: bool, alt: bool, shift: bool) -> bool {
unsafe {
let had_console = GetConsoleWindow() != 0;
FreeConsole();
if AttachConsole(child_pid) == 0 {
debug_log(&format!("send_modified_enter_event: AttachConsole({}) FAILED", child_pid));
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
let conin: [u16; 7] = [
'C' as u16, 'O' as u16, 'N' as u16,
'I' as u16, 'N' as u16, '$' as u16, 0,
];
let handle = CreateFileW(
conin.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null(),
);
if handle == INVALID_HANDLE || handle == 0 {
debug_log(&format!("send_modified_enter_event: CreateFileW(CONIN$) FAILED"));
FreeConsole();
if had_console { AttachConsole(ATTACH_PARENT_PROCESS); }
return false;
}
const KEY_EVENT: u16 = 0x0001;
const LEFT_ALT_PRESSED: u32 = 0x0002;
const LEFT_CTRL_PRESSED: u32 = 0x0008;
const SHIFT_PRESSED: u32 = 0x0010;
const VK_RETURN: u16 = 0x0D;
#[repr(C)]
#[derive(Copy, Clone)]
struct KEY_EVENT_RECORD {
key_down: i32,
repeat_count: u16,
virtual_key_code: u16,
virtual_scan_code: u16,
u_char: u16,
control_key_state: u32,
}
#[repr(C)]
struct KEY_INPUT_RECORD {
event_type: u16,
_padding: u16,
event: KEY_EVENT_RECORD,
}
#[link(name = "user32")]
extern "system" {
fn MapVirtualKeyW(code: u32, map_type: u32) -> u32;
}
let mut flags: u32 = 0;
if ctrl { flags |= LEFT_CTRL_PRESSED; }
if alt { flags |= LEFT_ALT_PRESSED; }
if shift { flags |= SHIFT_PRESSED; }
let scan = MapVirtualKeyW(VK_RETURN as u32, 0) as u16;
let records = [
KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 1,
repeat_count: 1,
virtual_key_code: VK_RETURN,
virtual_scan_code: scan,
u_char: '\r' as u16,
control_key_state: flags,
},
},
KEY_INPUT_RECORD {
event_type: KEY_EVENT,
_padding: 0,
event: KEY_EVENT_RECORD {
key_down: 0,
repeat_count: 1,
virtual_key_code: VK_RETURN,
virtual_scan_code: scan,
u_char: '\r' as u16,
control_key_state: flags,
},
},
];
let mut written: u32 = 0;
let result = WriteConsoleInputW(
handle,
records.as_ptr() as *const INPUT_RECORD,
2,
&mut written,
);
debug_log(&format!("send_modified_enter_event: pid={} ctrl={} alt={} shift={} scan=0x{:02X} flags=0x{:04X} => ok={} written={}",
child_pid, ctrl, alt, shift, scan, flags, result != 0, written));
CloseHandle(handle);
FreeConsole();
if had_console {
AttachConsole(ATTACH_PARENT_PROCESS);
}
result != 0 && written >= 1
}
}
}
#[cfg(not(windows))]
pub mod mouse_inject {
pub fn get_child_pid(_child: &dyn portable_pty::Child) -> Option<u32> { None }
pub fn send_mouse_event(_pid: u32, _col: i16, _row: i16, _btn: u32, _flags: u32, _reattach: bool) -> bool { false }
pub fn send_vt_sequence(_pid: u32, _sequence: &[u8]) -> bool { false }
pub fn query_vti_enabled(_pid: u32) -> Option<bool> { None }
pub fn send_ctrl_c_event(_pid: u32, _reattach: bool) -> bool { false }
pub fn query_mouse_input_enabled(_pid: u32) -> Option<bool> { None }
pub fn send_bracketed_paste(_pid: u32, _text: &str, _bracket: bool) -> bool { false }
pub fn send_modified_key_event(_pid: u32, _ch: char, _ctrl: bool, _alt: bool, _shift: bool) -> bool { false }
pub fn send_alt_key_event(_pid: u32, _ch: char) -> bool { false }
pub fn send_modified_enter_event(_pid: u32, _ctrl: bool, _alt: bool, _shift: bool) -> bool { false }
}
#[cfg(windows)]
pub mod process_kill {
const TH32CS_SNAPPROCESS: u32 = 0x00000002;
const PROCESS_TERMINATE: u32 = 0x0001;
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
const INVALID_HANDLE: isize = -1;
#[repr(C)]
struct PROCESSENTRY32W {
dw_size: u32,
cnt_usage: u32,
th32_process_id: u32,
th32_default_heap_id: usize,
th32_module_id: u32,
cnt_threads: u32,
th32_parent_process_id: u32,
pc_pri_class_base: i32,
dw_flags: u32,
sz_exe_file: [u16; 260],
}
#[link(name = "kernel32")]
extern "system" {
fn CreateToolhelp32Snapshot(dw_flags: u32, th32_process_id: u32) -> isize;
fn Process32FirstW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn Process32NextW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> isize;
fn TerminateProcess(h_process: isize, exit_code: u32) -> i32;
fn CloseHandle(handle: isize) -> i32;
}
fn collect_descendants(root_pid: u32) -> Vec<u32> {
let mut descendants = Vec::new();
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 { return descendants; }
let mut entries: Vec<(u32, u32)> = Vec::with_capacity(256); let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snap, &mut pe) != 0 {
entries.push((pe.th32_process_id, pe.th32_parent_process_id));
while Process32NextW(snap, &mut pe) != 0 {
entries.push((pe.th32_process_id, pe.th32_parent_process_id));
}
}
CloseHandle(snap);
let mut queue: Vec<u32> = vec![root_pid];
let mut head = 0;
while head < queue.len() {
let parent = queue[head];
head += 1;
for &(pid, ppid) in &entries {
if ppid == parent && pid != root_pid && !queue.contains(&pid) {
queue.push(pid);
descendants.push(pid);
}
}
}
}
descendants
}
fn terminate_pid(pid: u32) {
unsafe {
let h = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_INFORMATION, 0, pid);
if h != 0 && h != INVALID_HANDLE {
let _ = TerminateProcess(h, 1);
CloseHandle(h);
}
}
}
pub fn kill_process_tree(child: &mut Box<dyn portable_pty::Child>) {
let pid = super::mouse_inject::get_child_pid(child.as_ref());
if let Some(root_pid) = pid {
let mut descs = collect_descendants(root_pid);
descs.reverse();
for &dpid in &descs {
terminate_pid(dpid);
}
terminate_pid(root_pid);
}
let _ = child.kill();
}
pub fn kill_process_trees_batch(children: &mut [&mut Box<dyn portable_pty::Child>]) {
let root_pids: Vec<Option<u32>> = children.iter()
.map(|c| super::mouse_inject::get_child_pid(c.as_ref()))
.collect();
let entries = snapshot_process_table();
for (i, root_pid_opt) in root_pids.iter().enumerate() {
if let Some(root_pid) = root_pid_opt {
let mut descs = collect_descendants_from_table(&entries, *root_pid);
descs.reverse();
for &dpid in &descs {
terminate_pid(dpid);
}
terminate_pid(*root_pid);
}
let _ = children[i].kill();
}
}
fn snapshot_process_table() -> Vec<(u32, u32)> {
let mut entries: Vec<(u32, u32)> = Vec::with_capacity(256);
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 { return entries; }
let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snap, &mut pe) != 0 {
entries.push((pe.th32_process_id, pe.th32_parent_process_id));
while Process32NextW(snap, &mut pe) != 0 {
entries.push((pe.th32_process_id, pe.th32_parent_process_id));
}
}
CloseHandle(snap);
}
entries
}
fn collect_descendants_from_table(entries: &[(u32, u32)], root_pid: u32) -> Vec<u32> {
let mut descendants = Vec::new();
let mut queue: Vec<u32> = vec![root_pid];
let mut head = 0;
while head < queue.len() {
let parent = queue[head];
head += 1;
for &(pid, ppid) in entries {
if ppid == parent && pid != root_pid && !queue.contains(&pid) {
queue.push(pid);
descendants.push(pid);
}
}
}
descendants
}
}
#[cfg(not(windows))]
pub mod process_kill {
pub fn kill_process_tree(child: &mut Box<dyn portable_pty::Child>) {
let _ = child.kill();
}
pub fn kill_process_trees_batch(children: &mut [&mut Box<dyn portable_pty::Child>]) {
for child in children.iter_mut() {
let _ = child.kill();
}
}
}
#[cfg(windows)]
pub mod process_info {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
const PROCESS_VM_READ: u32 = 0x0010;
const MAX_PATH: usize = 260;
const TH32CS_SNAPPROCESS: u32 = 0x00000002;
const INVALID_HANDLE: isize = -1;
#[allow(non_snake_case)]
#[repr(C)]
struct PROCESS_BASIC_INFORMATION {
Reserved1: isize,
PebBaseAddress: isize, Reserved2: [isize; 2],
UniqueProcessId: isize,
Reserved3: isize,
}
#[allow(non_snake_case)]
#[repr(C)]
struct UNICODE_STRING {
Length: u16,
MaximumLength: u16,
Buffer: isize, }
#[repr(C)]
struct PROCESSENTRY32W {
dw_size: u32,
cnt_usage: u32,
th32_process_id: u32,
th32_default_heap_id: usize,
th32_module_id: u32,
cnt_threads: u32,
th32_parent_process_id: u32,
pc_pri_class_base: i32,
dw_flags: u32,
sz_exe_file: [u16; 260],
}
#[link(name = "kernel32")]
extern "system" {
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> isize;
fn CloseHandle(handle: isize) -> i32;
fn QueryFullProcessImageNameW(h: isize, flags: u32, name: *mut u16, size: *mut u32) -> i32;
fn ReadProcessMemory(
h_process: isize,
base_address: isize,
buffer: *mut u8,
size: usize,
bytes_read: *mut usize,
) -> i32;
fn CreateToolhelp32Snapshot(dw_flags: u32, th32_process_id: u32) -> isize;
fn Process32FirstW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn Process32NextW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
}
#[link(name = "ntdll")]
extern "system" {
fn NtQueryInformationProcess(
process_handle: isize,
process_information_class: u32,
process_information: *mut u8,
process_information_length: u32,
return_length: *mut u32,
) -> i32;
}
pub fn get_process_name(pid: u32) -> Option<String> {
unsafe {
let h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if h == 0 || h == -1 { return None; }
let mut buf = [0u16; 1024];
let mut size = buf.len() as u32;
let ok = QueryFullProcessImageNameW(h, 0, buf.as_mut_ptr(), &mut size);
CloseHandle(h);
if ok == 0 { return None; }
let full_path = OsString::from_wide(&buf[..size as usize])
.to_string_lossy()
.into_owned();
let name = std::path::Path::new(&full_path)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())?;
Some(name)
}
}
pub fn get_process_cwd(pid: u32) -> Option<String> {
unsafe {
let h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, pid);
if h == 0 || h == -1 { return None; }
let result = read_process_cwd(h);
CloseHandle(h);
result
}
}
unsafe fn read_process_cwd(h: isize) -> Option<String> {
let mut pbi: PROCESS_BASIC_INFORMATION = std::mem::zeroed();
let mut ret_len: u32 = 0;
let status = NtQueryInformationProcess(
h,
0, &mut pbi as *mut _ as *mut u8,
std::mem::size_of::<PROCESS_BASIC_INFORMATION>() as u32,
&mut ret_len,
);
if status != 0 { return None; }
let peb_addr = pbi.PebBaseAddress;
if peb_addr == 0 { return None; }
let params_ptr_offset = if std::mem::size_of::<usize>() == 8 { 0x20 } else { 0x10 };
let mut process_params_ptr: isize = 0;
let mut bytes_read: usize = 0;
let ok = ReadProcessMemory(
h,
peb_addr + params_ptr_offset,
&mut process_params_ptr as *mut isize as *mut u8,
std::mem::size_of::<isize>(),
&mut bytes_read,
);
if ok == 0 || process_params_ptr == 0 { return None; }
let cwd_offset = if std::mem::size_of::<usize>() == 8 { 0x38 } else { 0x24 };
let mut cwd_ustr: UNICODE_STRING = std::mem::zeroed();
let ok = ReadProcessMemory(
h,
process_params_ptr + cwd_offset,
&mut cwd_ustr as *mut UNICODE_STRING as *mut u8,
std::mem::size_of::<UNICODE_STRING>(),
&mut bytes_read,
);
if ok == 0 || cwd_ustr.Length == 0 || cwd_ustr.Buffer == 0 { return None; }
let char_count = (cwd_ustr.Length / 2) as usize;
let mut wchars: Vec<u16> = vec![0u16; char_count];
let ok = ReadProcessMemory(
h,
cwd_ustr.Buffer,
wchars.as_mut_ptr() as *mut u8,
cwd_ustr.Length as usize,
&mut bytes_read,
);
if ok == 0 { return None; }
let path = OsString::from_wide(&wchars)
.to_string_lossy()
.into_owned();
Some(path.trim_end_matches('\\').to_string())
}
fn autorename_log(msg: &str) {
use std::sync::atomic::{AtomicU32, Ordering};
static COUNT: AtomicU32 = AtomicU32::new(0);
let n = COUNT.fetch_add(1, Ordering::Relaxed);
if n > 100 { return; }
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let path = format!("{}/.psmux/autorename.log", home);
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
use std::io::Write;
let _ = writeln!(f, "[{}] {}", chrono::Local::now().format("%H:%M:%S%.3f"), msg);
}
}
pub fn get_foreground_process_name(pid: u32) -> Option<String> {
let result = find_foreground_child_pid(pid);
match result {
Some(target) if target != pid => {
let name = get_process_name(target);
autorename_log(&format!("pid={} fg_child={} name={:?}", pid, target, name));
if let Some(n) = name {
return Some(n);
}
}
Some(_) => {
autorename_log(&format!("pid={} fg_child=self (no children)", pid));
}
None => {
autorename_log(&format!("pid={} fg_child=None (BFS found nothing)", pid));
}
}
autorename_log(&format!("pid={} no_foreground_child", pid));
None
}
pub fn get_foreground_cwd(pid: u32) -> Option<String> {
if let Some(target) = find_foreground_child_pid(pid) {
if target != pid {
if let Some(cwd) = get_process_cwd(target) {
return Some(cwd);
}
}
}
get_process_cwd(pid)
}
fn is_system_exe(name: &str) -> bool {
matches!(name,
"conhost.exe" | "csrss.exe" | "dwm.exe" | "services.exe"
| "svchost.exe" | "wininit.exe" | "winlogon.exe"
| "openconsole.exe" | "runtimebroker.exe"
)
}
fn find_foreground_child_pid(root_pid: u32) -> Option<u32> {
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 {
autorename_log(&format!("root={} SNAPSHOT FAILED", root_pid));
return None;
}
let mut entries: Vec<(u32, u32, String)> = Vec::with_capacity(512);
let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snap, &mut pe) != 0 {
let name = exe_name_from_entry(&pe);
entries.push((pe.th32_process_id, pe.th32_parent_process_id, name));
while Process32NextW(snap, &mut pe) != 0 {
let name = exe_name_from_entry(&pe);
entries.push((pe.th32_process_id, pe.th32_parent_process_id, name));
}
}
CloseHandle(snap);
autorename_log(&format!("root={} snapshot_entries={}", root_pid, entries.len()));
let direct: Vec<_> = entries.iter()
.filter(|(_, ppid, _)| *ppid == root_pid)
.collect();
for (pid, _, name) in &direct {
autorename_log(&format!(" direct_child: pid={} name={}", pid, name));
}
let mut descendants: Vec<(u32, String, u32)> = Vec::new();
let mut queue: Vec<(u32, u32)> = vec![(root_pid, 0)]; let mut head = 0;
while head < queue.len() {
let (parent, depth) = queue[head];
head += 1;
for (pid, ppid, name) in &entries {
if *ppid == parent && *pid != root_pid
&& !descendants.iter().any(|(p, _, _)| p == pid)
{
descendants.push((*pid, name.clone(), depth + 1));
queue.push((*pid, depth + 1));
}
}
}
autorename_log(&format!("root={} descendants={}", root_pid, descendants.len()));
for (pid, name, depth) in &descendants {
autorename_log(&format!(" desc: pid={} name={} depth={}", pid, name, depth));
}
if descendants.is_empty() {
return None;
}
let desc_pids: std::collections::HashSet<u32> =
descendants.iter().map(|(p, _, _)| *p).collect();
let leaves: Vec<(u32, &str, u32)> = descendants.iter()
.filter(|(pid, _, _)| {
!entries.iter().any(|(ep, eppid, _)| *eppid == *pid && desc_pids.contains(ep))
})
.map(|(pid, name, depth)| (*pid, name.as_str(), *depth))
.collect();
let pool: Vec<(u32, &str, u32)> = if !leaves.is_empty() {
leaves
} else {
descendants.iter().map(|(p, n, d)| (*p, n.as_str(), *d)).collect()
};
let user_pool: Vec<&(u32, &str, u32)> = pool.iter()
.filter(|(_, name, _)| !is_system_exe(name))
.collect();
let selection = if !user_pool.is_empty() { user_pool } else { pool.iter().collect() };
let result = selection.iter()
.max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0)))
.map(|(pid, _, _)| *pid);
autorename_log(&format!("root={} selected={:?}", root_pid, result));
result
}
}
fn exe_name_from_entry(pe: &PROCESSENTRY32W) -> String {
let nul = pe.sz_exe_file.iter().position(|&c| c == 0).unwrap_or(pe.sz_exe_file.len());
String::from_utf16_lossy(&pe.sz_exe_file[..nul]).to_lowercase()
}
fn is_vt_bridge_exe(name: &str) -> bool {
let stem = name.strip_suffix(".exe").unwrap_or(name);
matches!(stem, "wsl" | "ssh" | "ubuntu" | "debian" | "kali"
| "fedoraremix" | "opensuse-leap" | "sles" | "arch")
|| stem.starts_with("wsl")
}
pub fn has_vt_bridge_descendant(root_pid: u32) -> bool {
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 { return false; }
let mut entries: Vec<(u32, u32, String)> = Vec::with_capacity(256);
let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
if Process32FirstW(snap, &mut pe) != 0 {
let name = exe_name_from_entry(&pe);
entries.push((pe.th32_process_id, pe.th32_parent_process_id, name));
while Process32NextW(snap, &mut pe) != 0 {
let name = exe_name_from_entry(&pe);
entries.push((pe.th32_process_id, pe.th32_parent_process_id, name));
}
}
CloseHandle(snap);
let mut queue: Vec<u32> = vec![root_pid];
let mut head = 0;
while head < queue.len() {
let parent = queue[head];
head += 1;
for (pid, ppid, name) in &entries {
if *ppid == parent && *pid != root_pid
&& !queue.contains(pid)
{
if is_vt_bridge_exe(name) {
return true;
}
queue.push(*pid);
}
}
}
false
}
}
}
#[cfg(not(windows))]
pub mod process_info {
pub fn get_process_name(_pid: u32) -> Option<String> { None }
pub fn get_process_cwd(_pid: u32) -> Option<String> { None }
pub fn get_foreground_process_name(_pid: u32) -> Option<String> { None }
pub fn get_foreground_cwd(_pid: u32) -> Option<String> { None }
pub fn has_vt_bridge_descendant(_root_pid: u32) -> bool { false }
}
#[cfg(windows)]
pub struct Utf16ConsoleWriter {
handle: *mut std::ffi::c_void,
frame_buf: Vec<u8>,
}
#[cfg(windows)]
unsafe impl Send for Utf16ConsoleWriter {}
#[cfg(windows)]
impl Utf16ConsoleWriter {
pub fn new() -> Self {
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
}
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
let handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) };
Self { handle, frame_buf: Vec::with_capacity(131072) }
}
fn write_wide(&self, s: &str) -> std::io::Result<()> {
if s.is_empty() {
return Ok(());
}
#[link(name = "kernel32")]
extern "system" {
fn WriteConsoleW(
hConsoleOutput: *mut std::ffi::c_void,
lpBuffer: *const u16,
nNumberOfCharsToWrite: u32,
lpNumberOfCharsWritten: *mut u32,
lpReserved: *mut std::ffi::c_void,
) -> i32;
}
let wide: Vec<u16> = s.encode_utf16().collect();
let mut total: u32 = 0;
let len = wide.len() as u32;
while total < len {
let mut written: u32 = 0;
let ok = unsafe {
WriteConsoleW(
self.handle,
wide.as_ptr().add(total as usize),
len - total,
&mut written,
std::ptr::null_mut(),
)
};
if ok == 0 {
return Err(std::io::Error::last_os_error());
}
if written == 0 {
break;
}
total += written;
}
Ok(())
}
}
#[cfg(windows)]
impl std::io::Write for Utf16ConsoleWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.frame_buf.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
if self.frame_buf.is_empty() {
return Ok(());
}
let (valid, remainder) = match std::str::from_utf8(&self.frame_buf) {
Ok(s) => (s.len(), 0),
Err(e) => {
let valid_end = e.valid_up_to();
let skip = e.error_len().unwrap_or(0);
(valid_end, self.frame_buf.len() - valid_end - skip)
}
};
if valid > 0 {
let s = unsafe { std::str::from_utf8_unchecked(&self.frame_buf[..valid]) };
self.write_wide(s)?;
}
if remainder > 0 {
let start = self.frame_buf.len() - remainder;
let mut i = 0;
while i < remainder {
self.frame_buf[i] = self.frame_buf[start + i];
i += 1;
}
self.frame_buf.truncate(remainder);
} else {
self.frame_buf.clear();
}
Ok(())
}
}
#[cfg(windows)]
pub type PsmuxWriter = Utf16ConsoleWriter;
#[cfg(not(windows))]
pub type PsmuxWriter = std::io::Stdout;
pub fn create_writer() -> PsmuxWriter {
#[cfg(windows)]
{ Utf16ConsoleWriter::new() }
#[cfg(not(windows))]
{ std::io::stdout() }
}
#[cfg(windows)]
pub mod caret {
use std::sync::atomic::{AtomicBool, Ordering};
static CARET_CREATED: AtomicBool = AtomicBool::new(false);
#[link(name = "kernel32")]
extern "system" {
fn GetConsoleWindow() -> isize;
fn GetCurrentConsoleFontEx(
hConsoleOutput: *mut std::ffi::c_void,
bMaximumWindow: i32,
lpConsoleCurrentFontEx: *mut CONSOLE_FONT_INFOEX,
) -> i32;
fn GetStdHandle(nStdHandle: u32) -> *mut std::ffi::c_void;
}
#[link(name = "user32")]
extern "system" {
fn CreateCaret(hWnd: isize, hBitmap: isize, nWidth: i32, nHeight: i32) -> i32;
fn SetCaretPos(x: i32, y: i32) -> i32;
fn ShowCaret(hWnd: isize) -> i32;
fn DestroyCaret() -> i32;
}
#[repr(C)]
#[allow(non_snake_case)]
struct CONSOLE_FONT_INFOEX {
cbSize: u32,
nFont: u32,
dwFontSize_X: i16,
dwFontSize_Y: i16,
FontFamily: u32,
FontWeight: u32,
FaceName: [u16; 32],
}
fn console_cell_size() -> (i32, i32) {
const STD_OUTPUT_HANDLE: u32 = (-11i32) as u32;
unsafe {
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
if handle.is_null() || handle == (-1isize) as *mut std::ffi::c_void {
return (8, 16);
}
let mut info: CONSOLE_FONT_INFOEX = std::mem::zeroed();
info.cbSize = std::mem::size_of::<CONSOLE_FONT_INFOEX>() as u32;
if GetCurrentConsoleFontEx(handle, 0, &mut info) != 0 {
let w = if info.dwFontSize_X > 0 { info.dwFontSize_X as i32 } else { 8 };
let h = if info.dwFontSize_Y > 0 { info.dwFontSize_Y as i32 } else { 16 };
(w, h)
} else {
(8, 16)
}
}
}
pub fn update(col: u16, row: u16) {
unsafe {
let hwnd = GetConsoleWindow();
if hwnd == 0 {
return;
}
if !CARET_CREATED.load(Ordering::Relaxed) {
let (cw, ch) = console_cell_size();
if CreateCaret(hwnd, 0, cw.max(1), ch.max(1)) != 0 {
CARET_CREATED.store(true, Ordering::Relaxed);
ShowCaret(hwnd);
}
}
let (cw, ch) = console_cell_size();
SetCaretPos(col as i32 * cw, row as i32 * ch);
}
}
pub fn destroy() {
if CARET_CREATED.swap(false, Ordering::Relaxed) {
unsafe { DestroyCaret(); }
}
}
}
#[cfg(not(windows))]
pub mod caret {
pub fn update(_col: u16, _row: u16) {}
pub fn destroy() {}
}
#[cfg(windows)]
pub fn augment_enter_shift(key: &mut crossterm::event::KeyEvent) {
use crossterm::event::{KeyCode, KeyModifiers};
if !matches!(key.code, KeyCode::Enter) {
return;
}
if key.modifiers.contains(KeyModifiers::SHIFT) {
return;
}
#[link(name = "user32")]
extern "system" {
fn GetAsyncKeyState(vKey: i32) -> i16;
}
const VK_SHIFT: i32 = 0x10;
const VK_CONTROL: i32 = 0x11;
const VK_MENU: i32 = 0x12;
unsafe {
let shift_down = GetAsyncKeyState(VK_SHIFT) < 0;
let ctrl_down = GetAsyncKeyState(VK_CONTROL) < 0;
let alt_down = GetAsyncKeyState(VK_MENU) < 0;
if shift_down {
key.modifiers.insert(KeyModifiers::SHIFT);
if !ctrl_down && key.modifiers.contains(KeyModifiers::CONTROL) {
key.modifiers.remove(KeyModifiers::CONTROL);
}
if !alt_down && key.modifiers.contains(KeyModifiers::ALT) {
key.modifiers.remove(KeyModifiers::ALT);
}
} else if !shift_down && !ctrl_down && !alt_down {
} else if !shift_down && alt_down {
}
}
}