#![cfg(windows)]
use std::ffi::CString;
use std::io;
use std::os::windows::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::OnceLock;
use windows_sys::Win32::Foundation::{HANDLE, HMODULE};
use windows_sys::Win32::System::Console::{COORD, HPCON};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress, LoadLibraryExW};
#[cfg(feature = "client")]
use super::conpty_acquire;
use super::win_version;
const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: u32 = 0x0200;
type Hresult = i32;
pub(super) type PfnCreatePseudoConsole =
unsafe extern "system" fn(COORD, HANDLE, HANDLE, u32, *mut HPCON) -> Hresult;
pub(super) type PfnResizePseudoConsole = unsafe extern "system" fn(HPCON, COORD) -> Hresult;
pub(super) type PfnClosePseudoConsole = unsafe extern "system" fn(HPCON);
pub(super) struct ConPtyApi {
pub(super) create: PfnCreatePseudoConsole,
pub(super) resize: PfnResizePseudoConsole,
pub(super) close: PfnClosePseudoConsole,
}
unsafe impl Send for ConPtyApi {}
unsafe impl Sync for ConPtyApi {}
#[derive(Debug, Clone)]
pub(super) enum ConPtySource {
Kernel32,
#[allow(dead_code)] Sidecar(PathBuf),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConPtyBackendKind {
Kernel32,
Sidecar,
}
pub fn current_backend_kind() -> ConPtyBackendKind {
match get().1 {
ConPtySource::Kernel32 => ConPtyBackendKind::Kernel32,
ConPtySource::Sidecar(_) => ConPtyBackendKind::Sidecar,
}
}
static API: OnceLock<(ConPtyApi, ConPtySource)> = OnceLock::new();
pub(super) fn get() -> &'static (ConPtyApi, ConPtySource) {
API.get_or_init(|| {
let force_system = std::env::var_os("RUNNING_PROCESS_USE_SYSTEM_CONPTY").is_some();
let diagnostics = std::env::var_os("RUNNING_PROCESS_CONPTY_DIAGNOSTICS").is_some();
let resolved = resolve_production(force_system);
if diagnostics {
let build = win_version::build_number();
match &resolved.1 {
ConPtySource::Kernel32 => {
eprintln!("running-process: ConPTY backend = kernel32 (Windows build {build})")
}
ConPtySource::Sidecar(path) => eprintln!(
"running-process: ConPTY backend = sidecar (Windows build {build}, path {})",
path.display()
),
}
}
resolved
})
}
fn resolve_production(force_system: bool) -> (ConPtyApi, ConPtySource) {
if force_system || win_version::is_win11_or_newer() {
return (
load_kernel32().unwrap_or_else(|e| catastrophe(e)),
ConPtySource::Kernel32,
);
}
if let Some(exe_dir) = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(Path::to_path_buf))
{
let dll = exe_dir.join("conpty.dll");
if dll.is_file() {
match try_load_sidecar(&dll) {
Ok(api) => return (api, ConPtySource::Sidecar(dll)),
Err(e) => eprintln!(
"running-process: pre-staged conpty.dll at {} unloadable ({e}); trying cache",
dll.display()
),
}
}
}
#[cfg(feature = "client")]
match conpty_acquire::ensure_cached_sidecar() {
Ok(cache_dir) => {
let dll = cache_dir.join("conpty.dll");
match try_load_sidecar(&dll) {
Ok(api) => return (api, ConPtySource::Sidecar(dll)),
Err(e) => eprintln!(
"running-process: cached conpty.dll at {} unloadable ({e}); using kernel32",
dll.display()
),
}
}
Err(e) => eprintln!(
"running-process: ConPTY sidecar auto-acquire unavailable ({e}); using kernel32"
),
}
(
load_kernel32().unwrap_or_else(|e| catastrophe(e)),
ConPtySource::Kernel32,
)
}
fn catastrophe(e: io::Error) -> ! {
eprintln!("running-process: ConPTY API resolution failed catastrophically: {e}");
panic!("ConPTY API unavailable: {e}");
}
#[cfg(test)]
pub(super) fn for_test_resolution(
force_sidecar_from: Option<&Path>,
force_system: bool,
) -> io::Result<(ConPtyApi, ConPtySource)> {
resolve(force_sidecar_from, force_system)
}
#[cfg(test)]
fn resolve(
sidecar_dir: Option<&Path>,
force_system: bool,
) -> io::Result<(ConPtyApi, ConPtySource)> {
if !force_system {
if let Some(dir) = sidecar_dir {
let dll = dir.join("conpty.dll");
match try_load_sidecar(&dll) {
Ok(api) => return Ok((api, ConPtySource::Sidecar(dll))),
Err(e) => {
eprintln!(
"running-process: conpty.dll sidecar at {} unavailable ({e}); using kernel32",
dll.display()
);
}
}
}
}
let api = load_kernel32()?;
Ok((api, ConPtySource::Kernel32))
}
fn try_load_sidecar(path: &Path) -> io::Result<ConPtyApi> {
if !path.is_file() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{} does not exist", path.display()),
));
}
let mut wide: Vec<u16> = path.as_os_str().encode_wide().collect();
wide.push(0);
let module = unsafe {
LoadLibraryExW(
wide.as_ptr(),
ptr::null_mut(),
LOAD_LIBRARY_SEARCH_APPLICATION_DIR,
)
};
if module.is_null() {
return Err(io::Error::last_os_error());
}
populate_from_module(module)
}
fn load_kernel32() -> io::Result<ConPtyApi> {
let name: Vec<u16> = "kernel32.dll\0".encode_utf16().collect();
let module: HMODULE = unsafe { GetModuleHandleW(name.as_ptr()) };
if module.is_null() {
return Err(io::Error::last_os_error());
}
populate_from_module(module)
}
fn populate_from_module(module: HMODULE) -> io::Result<ConPtyApi> {
unsafe {
let create: PfnCreatePseudoConsole =
std::mem::transmute(resolve_symbol(module, "CreatePseudoConsole")?);
let resize: PfnResizePseudoConsole =
std::mem::transmute(resolve_symbol(module, "ResizePseudoConsole")?);
let close: PfnClosePseudoConsole =
std::mem::transmute(resolve_symbol(module, "ClosePseudoConsole")?);
Ok(ConPtyApi {
create,
resize,
close,
})
}
}
fn resolve_symbol(module: HMODULE, name: &str) -> io::Result<unsafe extern "system" fn() -> isize> {
let cstr = CString::new(name).map_err(|e| io::Error::other(format!("invalid symbol: {e}")))?;
let proc = unsafe { GetProcAddress(module, cstr.as_ptr() as *const u8) };
match proc {
Some(p) => Ok(p),
None => Err(io::Error::new(
io::ErrorKind::NotFound,
format!("symbol {name} not exported by module"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn force_system_picks_kernel32() {
let (_api, source) = for_test_resolution(None, true).expect("kernel32 must resolve");
assert!(matches!(source, ConPtySource::Kernel32));
}
#[test]
fn missing_sidecar_falls_back_to_kernel32() {
let empty = tempfile::tempdir().expect("tempdir");
let (_api, source) =
for_test_resolution(Some(empty.path()), false).expect("fallback must succeed");
assert!(matches!(source, ConPtySource::Kernel32));
}
#[test]
fn fake_sidecar_dll_is_not_loaded() {
let dir = tempfile::tempdir().expect("tempdir");
let fake = dir.path().join("conpty.dll");
{
let mut f = std::fs::File::create(&fake).expect("create fake dll");
f.write_all(b"not a real PE file").expect("write");
}
let (_api, source) =
for_test_resolution(Some(dir.path()), false).expect("fallback must succeed");
assert!(
matches!(source, ConPtySource::Kernel32),
"fake conpty.dll must not satisfy the sidecar branch"
);
}
#[test]
fn win11_picks_kernel32_when_no_sidecar_dir() {
if !win_version::is_win11_or_newer() {
return;
}
let (_api, source) =
for_test_resolution(None, false).expect("kernel32 must resolve on Win11");
assert!(matches!(source, ConPtySource::Kernel32));
}
#[test]
fn current_backend_kind_returns_a_value() {
let kind = current_backend_kind();
assert!(matches!(
kind,
ConPtyBackendKind::Kernel32 | ConPtyBackendKind::Sidecar
));
}
}