mod bundle;
mod exports;
use std::fmt;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use std::io;
use std::iter;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use std::os::windows::io::{AsRawHandle, BorrowedHandle};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use windows_sys::core::HRESULT;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use windows_sys::Win32::System::Console::COORD;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) use windows_sys::Win32::System::Console::{HPCON, PSEUDOCONSOLE_INHERIT_CURSOR};
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
#[cfg(test)]
use bundle::{
absolute_dir, find_console_host, machine_arch_subdir, native_arch_subdir, parse_version,
read_product_version, selected_native_machine, translation_count, trim_resource_string,
versions_are_compatible, OPEN_CONSOLE_EXE, UNKNOWN_VERSION,
};
use bundle::{exe_dir, log_rejected, validate, CONPTY_DLL};
use exports::{load_module, ConptyApi, ModuleGuard};
#[cfg(test)]
use exports::{resolve_export, restricted_search_flags, wide_path, CREATE_PSEUDO_CONSOLE};
use crate::error::BackendError;
#[cfg(any(feature = "blocking", feature = "tokio", test))]
use crate::size::Size;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub(crate) enum BackendKind {
System,
External {
dll: PathBuf,
},
}
#[derive(Debug)]
struct BackendInner {
kind: BackendKind,
api: ConptyApi,
module_pin: Option<Arc<ModuleGuard>>,
}
pub struct ConPtyBackend {
inner: Arc<BackendInner>,
}
#[derive(Debug)]
struct SuccessfulCache<T> {
value: OnceLock<T>,
initialization: Mutex<()>,
}
impl<T> SuccessfulCache<T> {
const fn new() -> Self {
Self {
value: OnceLock::new(),
initialization: Mutex::new(()),
}
}
}
impl<T: Clone> SuccessfulCache<T> {
fn get_or_try_init<E>(&self, detect: impl FnOnce() -> Result<T, E>) -> Result<T, E> {
if let Some(value) = self.value.get() {
return Ok(value.clone());
}
let _initialization = self
.initialization
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(value) = self.value.get() {
return Ok(value.clone());
}
let detected = detect()?;
if self.value.set(detected.clone()).is_ok() {
return Ok(detected);
}
Ok(self.value.get().map_or(detected, Clone::clone))
}
}
static AUTO_DEFAULT: SuccessfulCache<ConPtyBackend> = SuccessfulCache::new();
impl ConPtyBackend {
pub fn system() -> Result<Self, BackendError> {
let module_name: Vec<u16> = "kernel32.dll".encode_utf16().chain(iter::once(0)).collect();
let module = unsafe { GetModuleHandleW(module_name.as_ptr()) };
if module.is_null() {
return Err(BackendError::unsupported());
}
let api = match unsafe { ConptyApi::from_module(module) } {
Ok(api) => api,
Err(symbol) => {
log_missing_system_export(symbol);
return Err(BackendError::unsupported());
},
};
Ok(Self {
inner: Arc::new(BackendInner {
kind: BackendKind::System,
api,
module_pin: None,
}),
})
}
pub fn from_dir(dir: impl AsRef<Path>) -> Result<Self, BackendError> {
Self::load_from_dir(dir.as_ref(), true)
}
#[cfg(test)]
pub(crate) fn from_dir_unchecked(dir: impl AsRef<Path>) -> Result<Self, BackendError> {
Self::load_from_dir(dir.as_ref(), false)
}
fn load_from_dir(dir: &Path, verify_pair: bool) -> Result<Self, BackendError> {
let bundle = validate(dir, verify_pair)?;
let dir = bundle.dir;
let dll = bundle.dll;
let module =
load_module(&dll).map_err(|source| BackendError::dll_not_found(dir.clone(), source))?;
let api = unsafe { ConptyApi::from_module(module.module) }
.map_err(|symbol| BackendError::missing_export(dll.clone(), symbol))?;
Ok(Self {
inner: Arc::new(BackendInner {
kind: BackendKind::External { dll },
api,
module_pin: Some(Arc::new(module)),
}),
})
}
pub fn auto() -> Result<Self, BackendError> {
AUTO_DEFAULT.get_or_try_init(Self::detect_auto)
}
fn detect_auto() -> Result<Self, BackendError> {
if let Some(dir) = exe_dir() {
if dir.join(CONPTY_DLL).is_file() {
match Self::from_dir(&dir) {
Ok(backend) => return Ok(backend),
Err(err) => log_rejected(&dir, &err),
}
}
}
Self::system()
}
#[must_use]
pub(crate) fn kind(&self) -> &BackendKind {
&self.inner.kind
}
#[must_use]
pub(crate) fn supports_release(&self) -> bool {
self.inner.api.release.is_some()
}
#[must_use]
pub fn supports_clear(&self) -> bool {
self.inner.api.clear.is_some()
}
#[must_use]
#[cfg(test)]
pub(super) fn without_release(&self) -> Self {
Self {
inner: Arc::new(BackendInner {
kind: self.inner.kind.clone(),
api: self.inner.api.without_release(),
module_pin: self.inner.module_pin.clone(),
}),
}
}
#[cfg(test)]
pub(super) fn with_test_close(&self, close: unsafe extern "system" fn(HPCON)) -> Self {
Self {
inner: Arc::new(BackendInner {
kind: self.inner.kind.clone(),
api: self.inner.api.with_close(close),
module_pin: self.inner.module_pin.clone(),
}),
}
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) fn resolve_default() -> Result<Self, BackendError> {
Self::auto()
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) fn create(
&self,
size: Size,
input_read: BorrowedHandle<'_>,
output_write: BorrowedHandle<'_>,
flags: u32,
) -> io::Result<HPCON> {
let api = &self.inner.api;
let (cols, rows) = size.to_i16_pair();
let size = COORD { X: cols, Y: rows };
let mut hpc: HPCON = 0;
let hr = unsafe {
(api.create)(
size,
input_read.as_raw_handle(),
output_write.as_raw_handle(),
flags,
&mut hpc,
)
};
hresult_ok(hr)?;
Ok(hpc)
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) unsafe fn resize(&self, hpc: HPCON, size: Size) -> io::Result<()> {
let api = &self.inner.api;
let (cols, rows) = size.to_i16_pair();
let size = COORD { X: cols, Y: rows };
let hr = unsafe { (api.resize)(hpc, size) };
hresult_ok(hr)
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) unsafe fn close(&self, hpc: HPCON) {
let api = &self.inner.api;
unsafe { (api.close)(hpc) }
}
#[must_use]
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) unsafe fn release(&self, hpc: HPCON) -> Option<io::Result<()>> {
let release = self.inner.api.release?;
let hr = unsafe { release(hpc) };
Some(hresult_ok(hr))
}
#[must_use]
#[cfg(any(feature = "blocking", feature = "tokio", test))]
pub(super) unsafe fn clear(&self, hpc: HPCON) -> Option<io::Result<()>> {
let clear = self.inner.api.clear?;
let hr = unsafe { clear(hpc, 0) };
Some(hresult_ok(hr))
}
}
impl Clone for ConPtyBackend {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl fmt::Debug for ConPtyBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ConPtyBackend")
.field("kind", &self.kind())
.field("supports_release", &self.supports_release())
.field("supports_clear", &self.supports_clear())
.field("module_pinned", &self.inner.module_pin.is_some())
.finish()
}
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
fn hresult_ok(hr: HRESULT) -> io::Result<()> {
if hr >= 0 {
Ok(())
} else {
Err(hresult_to_io_error(hr))
}
}
#[cfg(any(feature = "blocking", feature = "tokio", test))]
fn hresult_to_io_error(hr: HRESULT) -> io::Error {
const FACILITY_MASK: u32 = 0xFFFF_0000;
const FAILED_FACILITY_WIN32: u32 = 0x8007_0000;
let bits = u32::from_ne_bytes(hr.to_ne_bytes());
if bits & FACILITY_MASK == FAILED_FACILITY_WIN32 {
let code = i32::try_from(bits & 0xFFFF).unwrap_or(i32::MAX);
io::Error::from_raw_os_error(code)
} else {
io::Error::from_raw_os_error(hr)
}
}
#[cfg(feature = "tracing")]
fn log_missing_system_export(symbol: &'static str) {
tracing::warn!(
symbol,
"the system ConPTY backend is missing a required export"
);
}
#[cfg(not(feature = "tracing"))]
const fn log_missing_system_export(_symbol: &'static str) {}
#[cfg(test)]
#[path = "backend_tests.rs"]
mod tests;