use std::io;
use std::iter;
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::ptr;
use windows_sys::core::{BOOL, HRESULT};
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Foundation::{FreeLibrary, HMODULE};
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use windows_sys::Win32::System::Console::COORD;
use windows_sys::Win32::System::Console::HPCON;
use windows_sys::Win32::System::Diagnostics::Debug::{
GetThreadErrorMode, SetThreadErrorMode, SEM_FAILCRITICALERRORS,
};
use windows_sys::Win32::System::LibraryLoader::{
GetProcAddress, LoadLibraryExW, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, LOAD_LIBRARY_SEARCH_SYSTEM32,
};
const CONPTY_EXPORT_PREFIX: &str = "Conpty";
pub(super) const CREATE_PSEUDO_CONSOLE: &str = "CreatePseudoConsole";
const RESIZE_PSEUDO_CONSOLE: &str = "ResizePseudoConsole";
const CLOSE_PSEUDO_CONSOLE: &str = "ClosePseudoConsole";
const RELEASE_PSEUDO_CONSOLE: &str = "ReleasePseudoConsole";
const CLEAR_PSEUDO_CONSOLE: &str = "ClearPseudoConsole";
pub(super) const fn restricted_search_flags() -> u32 {
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32
}
const CLEAR_ABI_IS_CALLABLE: bool = !cfg!(target_arch = "x86");
#[cfg(any(feature = "blocking", feature = "tokio", test))]
type CreatePseudoConsoleFn =
unsafe extern "system" fn(COORD, HANDLE, HANDLE, u32, *mut HPCON) -> HRESULT;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
type ResizePseudoConsoleFn = unsafe extern "system" fn(HPCON, COORD) -> HRESULT;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
type ClosePseudoConsoleFn = unsafe extern "system" fn(HPCON);
type ReleasePseudoConsoleFn = unsafe extern "system" fn(HPCON) -> HRESULT;
type ClearPseudoConsoleFn = unsafe extern "system" fn(HPCON, BOOL) -> HRESULT;
pub(super) type ProcAddress = unsafe extern "system" fn() -> isize;
pub(super) unsafe fn resolve_export(module: HMODULE, name: &str) -> Option<ProcAddress> {
fn c_name(prefix: &str, name: &str) -> Vec<u8> {
prefix
.bytes()
.chain(name.bytes())
.chain(iter::once(0))
.collect()
}
let prefixed = c_name(CONPTY_EXPORT_PREFIX, name);
let address = unsafe { GetProcAddress(module, prefixed.as_ptr()) };
if address.is_some() {
return address;
}
let bare = c_name("", name);
unsafe { GetProcAddress(module, bare.as_ptr()) }
}
#[derive(Debug)]
pub(super) struct ModuleGuard {
pub(super) module: HMODULE,
}
struct ThreadErrorModeGuard {
previous: u32,
}
impl ThreadErrorModeGuard {
fn suppress_critical_error_dialogs() -> io::Result<Self> {
let previous = unsafe { GetThreadErrorMode() };
let mut reported_previous = 0;
let changed = unsafe {
SetThreadErrorMode(previous | SEM_FAILCRITICALERRORS, &mut reported_previous)
};
if changed == 0 {
return Err(io::Error::last_os_error());
}
debug_assert_eq!(reported_previous, previous);
Ok(Self { previous })
}
}
impl Drop for ThreadErrorModeGuard {
fn drop(&mut self) {
let _ = unsafe { SetThreadErrorMode(self.previous, ptr::null_mut()) };
}
}
unsafe impl Send for ModuleGuard {}
unsafe impl Sync for ModuleGuard {}
impl Drop for ModuleGuard {
fn drop(&mut self) {
let _ = unsafe { FreeLibrary(self.module) };
}
}
#[derive(Debug)]
pub(super) struct ConptyApi {
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) create: CreatePseudoConsoleFn,
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) resize: ResizePseudoConsoleFn,
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) close: ClosePseudoConsoleFn,
pub(super) release: Option<ReleasePseudoConsoleFn>,
pub(super) clear: Option<ClearPseudoConsoleFn>,
}
impl ConptyApi {
pub(super) unsafe fn from_module(module: HMODULE) -> Result<Self, &'static str> {
#[cfg(any(feature = "blocking", feature = "tokio", test))]
let create = {
let create = unsafe { resolve_export(module, CREATE_PSEUDO_CONSOLE) }
.ok_or(CREATE_PSEUDO_CONSOLE)?;
unsafe { mem::transmute::<ProcAddress, CreatePseudoConsoleFn>(create) }
};
#[cfg(not(any(feature = "blocking", feature = "tokio", test)))]
if unsafe { resolve_export(module, CREATE_PSEUDO_CONSOLE) }.is_none() {
return Err(CREATE_PSEUDO_CONSOLE);
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
let resize = {
let resize = unsafe { resolve_export(module, RESIZE_PSEUDO_CONSOLE) }
.ok_or(RESIZE_PSEUDO_CONSOLE)?;
unsafe { mem::transmute::<ProcAddress, ResizePseudoConsoleFn>(resize) }
};
#[cfg(not(any(feature = "blocking", feature = "tokio", test)))]
if unsafe { resolve_export(module, RESIZE_PSEUDO_CONSOLE) }.is_none() {
return Err(RESIZE_PSEUDO_CONSOLE);
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
let close = {
let close = unsafe { resolve_export(module, CLOSE_PSEUDO_CONSOLE) }
.ok_or(CLOSE_PSEUDO_CONSOLE)?;
unsafe { mem::transmute::<ProcAddress, ClosePseudoConsoleFn>(close) }
};
#[cfg(not(any(feature = "blocking", feature = "tokio", test)))]
if unsafe { resolve_export(module, CLOSE_PSEUDO_CONSOLE) }.is_none() {
return Err(CLOSE_PSEUDO_CONSOLE);
}
let release = unsafe { resolve_export(module, RELEASE_PSEUDO_CONSOLE) };
let release = release.map(|release| {
unsafe { mem::transmute::<ProcAddress, ReleasePseudoConsoleFn>(release) }
});
let clear = if CLEAR_ABI_IS_CALLABLE {
let clear = unsafe { resolve_export(module, CLEAR_PSEUDO_CONSOLE) };
clear.map(|clear| {
unsafe { mem::transmute::<ProcAddress, ClearPseudoConsoleFn>(clear) }
})
} else {
None
};
Ok(Self {
#[cfg(any(feature = "blocking", feature = "tokio", test))]
create,
#[cfg(any(feature = "blocking", feature = "tokio", test))]
resize,
#[cfg(any(feature = "blocking", feature = "tokio", test))]
close,
release,
clear,
})
}
#[cfg(test)]
pub(super) fn without_release(&self) -> Self {
Self {
create: self.create,
resize: self.resize,
close: self.close,
release: None,
clear: self.clear,
}
}
#[cfg(test)]
pub(super) fn with_close(&self, close: unsafe extern "system" fn(HPCON)) -> Self {
Self {
create: self.create,
resize: self.resize,
close,
release: self.release,
clear: self.clear,
}
}
}
pub(super) fn load_module(dll: &Path) -> io::Result<ModuleGuard> {
let path = wide_path(dll)?;
let _error_mode = ThreadErrorModeGuard::suppress_critical_error_dialogs()?;
let module =
unsafe { LoadLibraryExW(path.as_ptr(), ptr::null_mut(), restricted_search_flags()) };
if module.is_null() {
return Err(io::Error::last_os_error());
}
Ok(ModuleGuard { module })
}
pub(super) fn wide_path(path: &Path) -> io::Result<Vec<u16>> {
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if wide.contains(&0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"path contains an interior NUL",
));
}
wide.push(0);
Ok(wide)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn thread_error_mode_guard_preserves_existing_dialog_suppression() {
let original = unsafe { GetThreadErrorMode() };
let preexisting = original | SEM_FAILCRITICALERRORS;
let mut ignored_previous = 0;
assert_ne!(
unsafe { SetThreadErrorMode(preexisting, &mut ignored_previous) },
0
);
let restore_original = ThreadErrorModeGuard { previous: original };
let guard = ThreadErrorModeGuard::suppress_critical_error_dialogs()
.expect("setting a valid thread error mode must succeed");
assert_ne!(unsafe { GetThreadErrorMode() } & SEM_FAILCRITICALERRORS, 0);
drop(guard);
assert_eq!(unsafe { GetThreadErrorMode() }, preexisting);
drop(restore_original);
assert_eq!(unsafe { GetThreadErrorMode() }, original);
}
}