Skip to main content

conpty_oxide/
backend.rs

1// SPDX-FileCopyrightText: 2026 conpty-oxide contributors <https://github.com/P4suta/conpty-oxide/graphs/contributors>
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Dynamic loading of the `ConPTY` entry points.
6//!
7//! The pseudoconsole API (`CreatePseudoConsole` and friends) is not linked
8//! statically. It is resolved at run time with `GetProcAddress`, for two
9//! reasons:
10//!
11//! 1. **Graceful degradation.** Linking `CreatePseudoConsole` statically makes
12//!    the executable fail to start on Windows versions older than 10 1809
13//!    (build 17763) with an unhelpful loader error. Resolving it dynamically
14//!    turns that into [`crate::BackendErrorKind::Unsupported`].
15//! 2. **Capability detection.** `ReleasePseudoConsole` only exists on Windows
16//!    11 24H2 (build 26100) and later, and `ClearPseudoConsole` exists only in
17//!    the standalone `conpty.dll`. Whether they are available decides which
18//!    shutdown strategy the crate uses and which operations it can offer, and
19//!    the presence of the export is the check microsoft/terminal recommends —
20//!    *not* an OS build-number comparison, which misfires under compatibility
21//!    shims and on backported builds (microsoft/terminal#19112).
22//!
23//! The same loader serves a bundled `conpty.dll`, which is why symbol lookup
24//! goes through [`exports::resolve_export`]: that DLL exports each entry point twice,
25//! once under its canonical `Conpty`-prefixed name and once under the bare
26//! system name.
27//!
28//! # Loading a bundled `conpty.dll`
29//!
30//! [`ConPtyBackend::from_dir`] takes a directory and does four things before
31//! any code from it runs:
32//!
33//! 1. It checks that `conpty.dll` is there at all.
34//! 2. It locates the `OpenConsole.exe` the DLL will launch — next to the DLL,
35//!    or in the architecture subdirectory the DLL itself searches.
36//! 3. It compares the two files' `ProductVersion` resources. The DLL and the
37//!    console host speak a private, versioned protocol, and a bad `ConPTY`
38//!    bundle takes the client process down with a `FailFast` rather than an
39//!    error (wezterm#7774), so it is far better to refuse the bundle than to
40//!    crash later.
41//! 4. It loads the DLL by absolute path with `LoadLibraryExW` and a search
42//!    policy that never consults `PATH`, the current directory, or the
43//!    registry, so a stray `conpty.dll` cannot be planted into the process.
44//!
45//! [`ConPtyBackend::auto`] applies that to the executable's own directory and
46//! falls back to the operating system's `ConPTY`, which is what an
47//! application that merely *may* ship a bundle wants, and returns
48//! [`crate::BackendErrorKind::Unsupported`] when neither implementation is
49//! usable.
50
51mod bundle;
52mod exports;
53
54use std::fmt;
55#[cfg(any(feature = "blocking", feature = "tokio", test))]
56use std::io;
57use std::iter;
58#[cfg(any(feature = "blocking", feature = "tokio", test))]
59use std::os::windows::io::{AsRawHandle, BorrowedHandle};
60use std::path::{Path, PathBuf};
61use std::sync::{Arc, Mutex, OnceLock};
62
63#[cfg(any(feature = "blocking", feature = "tokio", test))]
64use windows_sys::core::HRESULT;
65#[cfg(any(feature = "blocking", feature = "tokio", test))]
66use windows_sys::Win32::System::Console::COORD;
67/// Official Windows SDK pseudoconsole handle and cursor-inheritance flag.
68#[cfg(any(feature = "blocking", feature = "tokio", test))]
69pub(super) use windows_sys::Win32::System::Console::{HPCON, PSEUDOCONSOLE_INHERIT_CURSOR};
70use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
71
72#[cfg(test)]
73use bundle::{
74    absolute_dir, find_console_host, machine_arch_subdir, native_arch_subdir, parse_version,
75    read_product_version, selected_native_machine, translation_count, trim_resource_string,
76    versions_are_compatible, OPEN_CONSOLE_EXE, UNKNOWN_VERSION,
77};
78use bundle::{exe_dir, log_rejected, validate, CONPTY_DLL};
79use exports::{load_module, ConptyApi, ModuleGuard};
80#[cfg(test)]
81use exports::{resolve_export, restricted_search_flags, wide_path, CREATE_PSEUDO_CONSOLE};
82
83use crate::error::BackendError;
84#[cfg(any(feature = "blocking", feature = "tokio", test))]
85use crate::size::Size;
86
87/// Which `ConPTY` implementation a [`ConPtyBackend`] is bound to.
88///
89/// Marked `#[non_exhaustive]`, like the crate's error enums: new kinds may be
90/// added in later releases, so matches on it need a wildcard arm.
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92#[non_exhaustive]
93pub(crate) enum BackendKind {
94    /// The `ConPTY` API built into the operating system (`kernel32.dll`).
95    System,
96
97    /// A standalone `conpty.dll` loaded from the given path.
98    ///
99    /// A bundled DLL ships a newer console host than the operating system
100    /// provides, which is how an application can get `ReleasePseudoConsole`
101    /// semantics on a Windows version that does not have them natively.
102    External {
103        /// Path of the loaded `conpty.dll`.
104        dll: PathBuf,
105    },
106}
107
108/// The shared, immutable state behind a [`ConPtyBackend`].
109#[derive(Debug)]
110struct BackendInner {
111    kind: BackendKind,
112
113    /// The resolved entry points.
114    ///
115    /// A public backend is always usable: construction fails before a
116    /// `BackendInner` exists when the required `ConPTY` exports are absent.
117    api: ConptyApi,
118
119    /// Pins the module `api` was resolved from.
120    ///
121    /// [`None`] for the system backend: `kernel32.dll` is mapped into every
122    /// Win32 process for its entire lifetime, so there is nothing to pin and no
123    /// reference to release.
124    module_pin: Option<Arc<ModuleGuard>>,
125}
126
127/// A loaded `ConPTY` implementation.
128///
129/// Cloning is cheap: clones share one [`Arc`], so resolving the entry points
130/// happens once per backend rather than once per pseudoconsole.
131///
132/// # Thread safety
133///
134/// `ConPtyBackend` is `Send + Sync`, and that is sound:
135///
136/// - `BackendInner` holds a private backend-kind value — a unit variant or a
137///   [`PathBuf`] — a table of bare `extern "system"` function pointers, and a
138///   module guard. Function pointers are `Send + Sync`: they are immutable code
139///   addresses, not resources. The guard states its own argument.
140/// - The backend owns no `HPCON`, no OS handle, and no interior mutability,
141///   so a shared `&ConPtyBackend` exposes nothing mutable.
142/// - The module the addresses point into stays loaded for the lifetime of the
143///   backend: the system backend targets `kernel32.dll`, which is mapped into
144///   every Win32 process and never unloaded, and an external backend owns a
145///   `LoadLibraryExW` reference that is released only when the last clone is
146///   dropped.
147/// - `Send` is not merely convenient but required. `ClosePseudoConsole` must
148///   not be called from the thread reading the conout pipe, so the shutdown
149///   path necessarily runs on a different thread from the reader and both need
150///   the backend.
151pub struct ConPtyBackend {
152    inner: Arc<BackendInner>,
153}
154
155/// A fallible initializer that caches only its first successful result.
156///
157/// [`OnceLock::get_or_init`] cannot represent retryable failure. Pairing the
158/// value cell with a short initialization mutex keeps the detector outside the
159/// permanent state while ensuring concurrent first callers do not load the
160/// same DLL more than once. A poisoned mutex is still usable here: neither a
161/// detector error nor a panic can partially initialize `value`.
162#[derive(Debug)]
163struct SuccessfulCache<T> {
164    value: OnceLock<T>,
165    initialization: Mutex<()>,
166}
167
168impl<T> SuccessfulCache<T> {
169    const fn new() -> Self {
170        Self {
171            value: OnceLock::new(),
172            initialization: Mutex::new(()),
173        }
174    }
175}
176
177impl<T: Clone> SuccessfulCache<T> {
178    fn get_or_try_init<E>(&self, detect: impl FnOnce() -> Result<T, E>) -> Result<T, E> {
179        if let Some(value) = self.value.get() {
180            return Ok(value.clone());
181        }
182
183        let _initialization = self
184            .initialization
185            .lock()
186            .unwrap_or_else(std::sync::PoisonError::into_inner);
187        if let Some(value) = self.value.get() {
188            return Ok(value.clone());
189        }
190
191        let detected = detect()?;
192        if self.value.set(detected.clone()).is_ok() {
193            return Ok(detected);
194        }
195        Ok(self.value.get().map_or(detected, Clone::clone))
196    }
197}
198
199/// Cached successful result of [`ConPtyBackend::auto`].
200///
201/// Caching matters for more than speed: it keeps a bundled `conpty.dll` loaded
202/// once per process instead of once per session.
203static AUTO_DEFAULT: SuccessfulCache<ConPtyBackend> = SuccessfulCache::new();
204
205impl ConPtyBackend {
206    /// Loads the `ConPTY` API built into the operating system.
207    ///
208    /// Resolves the entry points from the already-mapped `kernel32.dll`; no
209    /// library is loaded and no reference count is taken, because
210    /// `kernel32.dll` is present in every Win32 process for its entire
211    /// lifetime.
212    ///
213    /// # Errors
214    ///
215    /// Returns [`crate::BackendErrorKind::Unsupported`] when
216    /// `CreatePseudoConsole`,
217    /// `ResizePseudoConsole`, or `ClosePseudoConsole` is missing, i.e. on
218    /// Windows versions older than 10 1809 (build 17763).
219    pub fn system() -> Result<Self, BackendError> {
220        // `windows-sys` has no `w!` macro, so the module name is widened at
221        // run time. This happens once per backend, not per pseudoconsole.
222        let module_name: Vec<u16> = "kernel32.dll".encode_utf16().chain(iter::once(0)).collect();
223
224        // SAFETY: `module_name` is a NUL-terminated UTF-16 string that
225        // outlives the call.
226        let module = unsafe { GetModuleHandleW(module_name.as_ptr()) };
227        if module.is_null() {
228            // Unreachable in practice: kernel32.dll is mapped into every
229            // Win32 process. Report it as "no ConPTY here" rather than
230            // panicking on a hostile or exotic environment.
231            return Err(BackendError::unsupported());
232        }
233
234        // SAFETY: `module` is a live handle to kernel32.dll, which stays
235        // loaded for the lifetime of the process, and its ConPTY exports have
236        // the signatures documented on Microsoft Learn.
237        let api = match unsafe { ConptyApi::from_module(module) } {
238            Ok(api) => api,
239            Err(symbol) => {
240                log_missing_system_export(symbol);
241                return Err(BackendError::unsupported());
242            },
243        };
244
245        Ok(Self {
246            inner: Arc::new(BackendInner {
247                kind: BackendKind::System,
248                api,
249                // kernel32.dll needs no pin; see `BackendInner::module_pin`.
250                module_pin: None,
251            }),
252        })
253    }
254
255    /// Loads a bundled `conpty.dll` from `dir`, validating the bundle first.
256    ///
257    /// A bundle is `conpty.dll` plus the `OpenConsole.exe` it launches, as
258    /// shipped by the `Microsoft.Windows.Console.ConPTY` NuGet package. Both
259    /// must come from the same package: the DLL and the console host share a
260    /// private protocol with no compatibility promise across releases, and a
261    /// bad `ConPTY` bundle crashes the client process rather than degrading —
262    /// wezterm#7774 is PowerShell dying with a `0x8013_1623` `FailFast` until
263    /// the bundle was replaced. This constructor therefore refuses a pair it
264    /// cannot prove consistent; public callers cannot bypass this validation.
265    ///
266    /// Note the check's limit: it proves the pair *matches*, not that it is
267    /// current. wezterm#7774's actual configuration was a matched but outdated
268    /// pair, which this validation accepts; keeping the bundled version up to
269    /// date remains the application's responsibility.
270    ///
271    /// The console host is looked for exactly where `conpty.dll` itself will
272    /// look: next to the DLL first, then in the single subdirectory named
273    /// after the machine's *native* architecture (`x64`, `arm64`, or `x86`).
274    /// A host anywhere else — a cross-architecture subdirectory, say — does
275    /// not count, because the DLL never searches there and would silently run
276    /// every session against the operating system's inbox `conhost.exe`
277    /// instead of the file this constructor validated. Placing
278    /// `OpenConsole.exe` next to the DLL, as `scripts/fetch-conpty.ps1` does,
279    /// is the recommended layout.
280    ///
281    /// A relative `dir` is resolved against the current working directory once,
282    /// here. The DLL is then loaded by absolute path with a search policy that
283    /// excludes `PATH`, the current directory, the application directory, and
284    /// the registry, so nothing but `dir` and `System32` can satisfy the load.
285    /// A *drive-relative* `dir` (`C:dir`) is rejected as
286    /// [`crate::BackendErrorKind::DllNotFound`]: it names a path relative to that drive's
287    /// own current directory, which cannot be resolved once and pinned.
288    ///
289    /// # Errors
290    ///
291    /// - [`crate::BackendErrorKind::DllNotFound`] if `dir/conpty.dll` is missing or
292    ///   cannot be loaded (the source carries the OS error, e.g.
293    ///   `ERROR_BAD_EXE_FORMAT` for a file that is not a DLL at all).
294    /// - [`crate::BackendErrorKind::OpenConsoleMissing`] if no `OpenConsole.exe`
295    ///   accompanies the DLL.
296    /// - [`crate::BackendErrorKind::VersionMismatch`] if the two files report different
297    ///   `ProductVersion` resources, or if either version cannot be read.
298    /// - [`crate::BackendErrorKind::MissingExport`] if the DLL lacks
299    ///   `CreatePseudoConsole`, `ResizePseudoConsole`, or
300    ///   `ClosePseudoConsole`.
301    pub fn from_dir(dir: impl AsRef<Path>) -> Result<Self, BackendError> {
302        Self::load_from_dir(dir.as_ref(), true)
303    }
304
305    /// Loads a bundled `conpty.dll` from `dir` **without** checking that it
306    /// matches its `OpenConsole.exe`.
307    ///
308    /// Every other check [`Self::from_dir`] performs still runs; only the
309    /// version comparison is skipped.
310    ///
311    /// # Why this is dangerous
312    ///
313    /// `conpty.dll` and `OpenConsole.exe` communicate over a private, versioned
314    /// protocol and are shipped as a pair for that reason. Running a DLL
315    /// against a console host from a different release is not a graceful
316    /// degradation: the failure mode of a bad `ConPTY` bundle is a hard crash of
317    /// the *client* process — in wezterm#7774, PowerShell dies with a
318    /// `0x8013_1623` `FailFast` — at an arbitrary later point, far from this
319    /// call.
320    ///
321    /// Use this only when the version resources are unreadable for a reason you
322    /// control, for example a locally rebuilt `OpenConsole.exe` that carries no
323    /// version stamp, and you can guarantee the pair by other means. Prefer
324    /// [`Self::from_dir`] everywhere else.
325    ///
326    /// # Errors
327    ///
328    /// The same as [`Self::from_dir`], minus
329    /// [`crate::BackendErrorKind::VersionMismatch`].
330    #[cfg(test)]
331    pub(crate) fn from_dir_unchecked(dir: impl AsRef<Path>) -> Result<Self, BackendError> {
332        Self::load_from_dir(dir.as_ref(), false)
333    }
334
335    /// Shared implementation of [`Self::from_dir`] and
336    /// [`Self::from_dir_unchecked`].
337    fn load_from_dir(dir: &Path, verify_pair: bool) -> Result<Self, BackendError> {
338        // Discovery and validation complete before executable code is mapped.
339        let bundle = validate(dir, verify_pair)?;
340        let dir = bundle.dir;
341        let dll = bundle.dll;
342
343        let module =
344            load_module(&dll).map_err(|source| BackendError::dll_not_found(dir.clone(), source))?;
345
346        // SAFETY: the module stays pinned in the same `BackendInner` as the
347        // resolved table, and standalone ConPTY exports use the SDK signatures.
348        let api = unsafe { ConptyApi::from_module(module.module) }
349            .map_err(|symbol| BackendError::missing_export(dll.clone(), symbol))?;
350
351        Ok(Self {
352            inner: Arc::new(BackendInner {
353                kind: BackendKind::External { dll },
354                api,
355                module_pin: Some(Arc::new(module)),
356            }),
357        })
358    }
359
360    /// Returns the best backend available to this process.
361    ///
362    /// The search order is:
363    ///
364    /// 1. A bundle next to the current executable. If `conpty.dll` sits in the
365    ///    executable's directory it is loaded with [`Self::from_dir`], with all
366    ///    of its validation.
367    /// 2. The operating system's `ConPTY` ([`Self::system`]).
368    ///
369    /// A bundle that fails to load is not an error: the process still has the
370    /// system implementation, and falling back to it is what an application
371    /// that merely *may* ship a bundle wants. The rejection is recorded with
372    /// `tracing::warn!` when the `tracing` feature is enabled, so a bundle that
373    /// is silently ignored — a version-mismatched pair, say — is still
374    /// diagnosable.
375    ///
376    /// # Errors
377    ///
378    /// Returns [`crate::BackendErrorKind::Unsupported`] when neither a valid bundle nor
379    /// the system `ConPTY` implementation is available.
380    pub fn auto() -> Result<Self, BackendError> {
381        AUTO_DEFAULT.get_or_try_init(Self::detect_auto)
382    }
383
384    /// Performs one uncached automatic-detection attempt.
385    fn detect_auto() -> Result<Self, BackendError> {
386        if let Some(dir) = exe_dir() {
387            // Only attempt the load when a bundle is actually present:
388            // otherwise every ordinary program would log a warning about a
389            // `conpty.dll` it never intended to ship.
390            if dir.join(CONPTY_DLL).is_file() {
391                match Self::from_dir(&dir) {
392                    Ok(backend) => return Ok(backend),
393                    Err(err) => log_rejected(&dir, &err),
394                }
395            }
396        }
397
398        Self::system()
399    }
400
401    /// Returns which `ConPTY` implementation this backend is bound to.
402    #[must_use]
403    pub(crate) fn kind(&self) -> &BackendKind {
404        &self.inner.kind
405    }
406
407    /// Returns whether this backend exports `ReleasePseudoConsole`.
408    ///
409    /// When `true`, the crate can relinquish the `HPCON` right after spawning
410    /// and let the pseudoconsole exit on its own once every client has
411    /// disconnected; conout then reaches end-of-file naturally. When `false`,
412    /// end-of-file has to be forced by closing the pseudoconsole after the
413    /// child exits, because the console host outlives the child.
414    ///
415    /// The value depends on the operating system (`ReleasePseudoConsole`
416    /// requires Windows 11 24H2 / Server 2025, build 26100) or on the version
417    /// of a bundled `conpty.dll`.
418    #[must_use]
419    pub(crate) fn supports_release(&self) -> bool {
420        self.inner.api.release.is_some()
421    }
422
423    /// Returns whether this backend can clear the pseudoconsole's buffer.
424    ///
425    /// `ClearPseudoConsole` is not part of the public Windows SDK and
426    /// `kernel32.dll` does not export it, so this is `false` on the system
427    /// backend and `true` only for a bundled `conpty.dll` that exports
428    /// `ConptyClearPseudoConsole`.
429    ///
430    /// It is also `false` on 32-bit x86 regardless of the DLL: the export
431    /// changed arity between releases (microsoft/terminal#18976) and `__stdcall`
432    /// makes an arity mismatch corrupt the stack, so the call is not offered
433    /// where it cannot be made safely.
434    #[must_use]
435    pub fn supports_clear(&self) -> bool {
436        self.inner.api.clear.is_some()
437    }
438
439    /// Returns a clone of this backend with the `ReleasePseudoConsole` export
440    /// removed.
441    ///
442    /// Sessions on the returned backend behave exactly as on a Windows version
443    /// that predates the export (everything before Windows 11 24H2):
444    /// [`Self::supports_release`] answers `false`, releasing after spawn is
445    /// impossible, and end-of-file has to be forced by the legacy watcher.
446    ///
447    /// This works on every backend, including an external one: the stripped
448    /// clone shares the original's module pin, so the addresses it copies stay
449    /// valid for as long as it does.
450    ///
451    /// This crate-private test hook lets the unit suite exercise the legacy
452    /// shutdown path deterministically on machines whose operating system
453    /// exports `ReleasePseudoConsole`, where ordinary sessions otherwise run
454    /// only in released mode.
455    #[must_use]
456    #[cfg(test)]
457    pub(super) fn without_release(&self) -> Self {
458        Self {
459            inner: Arc::new(BackendInner {
460                kind: self.inner.kind.clone(),
461                api: self.inner.api.without_release(),
462                // Share the pin rather than re-loading: the copied addresses
463                // point into the very module the original keeps mapped.
464                module_pin: self.inner.module_pin.clone(),
465            }),
466        }
467    }
468
469    /// Replaces only the close export so lifecycle tests can observe a
470    /// detached FFI call without passing a fabricated handle to Windows.
471    #[cfg(test)]
472    pub(super) fn with_test_close(&self, close: unsafe extern "system" fn(HPCON)) -> Self {
473        Self {
474            inner: Arc::new(BackendInner {
475                kind: self.inner.kind.clone(),
476                api: self.inner.api.with_close(close),
477                module_pin: self.inner.module_pin.clone(),
478            }),
479        }
480    }
481
482    /// Returns the backend to use when the caller did not name one.
483    ///
484    /// Only successful automatic detection is cached; failures remain
485    /// retryable.
486    #[cfg(any(feature = "blocking", feature = "tokio", test))]
487    pub(super) fn resolve_default() -> Result<Self, BackendError> {
488        Self::auto()
489    }
490
491    /// Calls `CreatePseudoConsole`.
492    ///
493    /// `input_read` is the read end of the conin pipe and `output_write` the
494    /// write end of the conout pipe; both must be synchronous handles, which
495    /// anonymous pipes always are. `ConPTY` duplicates them, so the caller
496    /// should close its own copies as soon as the child has been spawned —
497    /// until then the extra references keep conout from ever reaching
498    /// end-of-file.
499    ///
500    /// The returned `HPCON` is *not* owned by any RAII type here; the caller
501    /// must eventually pass it to [`Self::close`].
502    ///
503    /// # Errors
504    ///
505    /// Returns the failing `HRESULT` mapped to an [`io::Error`]. Construction
506    /// has already proved that the backend provides this required export.
507    #[cfg(any(feature = "blocking", feature = "tokio", test))]
508    pub(super) fn create(
509        &self,
510        size: Size,
511        input_read: BorrowedHandle<'_>,
512        output_write: BorrowedHandle<'_>,
513        flags: u32,
514    ) -> io::Result<HPCON> {
515        let api = &self.inner.api;
516        let (cols, rows) = size.to_i16_pair();
517        let size = COORD { X: cols, Y: rows };
518        let mut hpc: HPCON = 0;
519
520        // SAFETY: `api.create` was resolved from a module this backend keeps
521        // mapped. Both handles are borrowed for the duration of the call, and
522        // `hpc` is a valid out-parameter.
523        let hr = unsafe {
524            (api.create)(
525                size,
526                input_read.as_raw_handle(),
527                output_write.as_raw_handle(),
528                flags,
529                &mut hpc,
530            )
531        };
532        hresult_ok(hr)?;
533
534        Ok(hpc)
535    }
536
537    /// Calls `ResizePseudoConsole`.
538    ///
539    /// # Errors
540    ///
541    /// Returns the failing `HRESULT` mapped to an [`io::Error`].
542    ///
543    /// # Safety
544    ///
545    /// `hpc` must be a live handle from [`Self::create`] on *this* backend
546    /// that has not yet been passed to [`Self::close`].
547    #[cfg(any(feature = "blocking", feature = "tokio", test))]
548    pub(super) unsafe fn resize(&self, hpc: HPCON, size: Size) -> io::Result<()> {
549        let api = &self.inner.api;
550        let (cols, rows) = size.to_i16_pair();
551        let size = COORD { X: cols, Y: rows };
552
553        // SAFETY: `hpc` is live per this function's contract, and the
554        // function pointer was resolved from a module this backend keeps
555        // mapped.
556        let hr = unsafe { (api.resize)(hpc, size) };
557        hresult_ok(hr)
558    }
559
560    /// Calls `ClosePseudoConsole`, releasing the session's resources.
561    ///
562    /// This returns no status because `ClosePseudoConsole` returns `void`.
563    ///
564    /// # Safety
565    ///
566    /// `hpc` must be a live handle from [`Self::create`] on *this* backend
567    /// that has not been closed before; the handle is invalid afterwards.
568    ///
569    /// Beyond memory safety, two liveness rules from the `ConPTY` documentation
570    /// apply, and violating them hangs the process rather than corrupting it:
571    ///
572    /// - Before Windows 11 24H2 (build 26100), this call waits until every
573    ///   client has disconnected. The caller must therefore have closed its
574    ///   conout read end first, or keep another thread draining it.
575    /// - It must never be called from the thread that reads conout, because
576    ///   that thread is exactly the one that would have to make progress for
577    ///   the call to return.
578    #[cfg(any(feature = "blocking", feature = "tokio", test))]
579    pub(super) unsafe fn close(&self, hpc: HPCON) {
580        let api = &self.inner.api;
581
582        // SAFETY: `hpc` is live and unclosed per this function's contract.
583        unsafe { (api.close)(hpc) }
584    }
585
586    /// Calls `ReleasePseudoConsole`, or returns [`None`] if the backend does
587    /// not export it (see [`Self::supports_release`]).
588    ///
589    /// Releasing hands ownership of the session to the pseudoconsole itself:
590    /// once every client has disconnected, the console host exits on its own
591    /// and conout fails with `ERROR_BROKEN_PIPE`, which the reader maps to
592    /// end-of-file. That breaks the ownership cycle in which the application
593    /// waits for the session to end while the session waits for the
594    /// application to close it.
595    ///
596    /// Releasing does **not** free the `HPCON`: [`Self::close`] must still be
597    /// called afterwards to reclaim it.
598    ///
599    /// # Errors
600    ///
601    /// Returns `Some(Err(..))` with the failing `HRESULT` mapped to an
602    /// [`io::Error`]. Microsoft documents `E_INVALIDARG` as the only expected
603    /// failure.
604    ///
605    /// # Safety
606    ///
607    /// `hpc` must be a live handle from [`Self::create`] on *this* backend
608    /// that has not yet been passed to [`Self::close`].
609    #[must_use]
610    #[cfg(any(feature = "blocking", feature = "tokio", test))]
611    pub(super) unsafe fn release(&self, hpc: HPCON) -> Option<io::Result<()>> {
612        let release = self.inner.api.release?;
613
614        // SAFETY: `hpc` is live per this function's contract, and the
615        // function pointer was resolved from a module this backend keeps
616        // mapped.
617        let hr = unsafe { release(hpc) };
618        Some(hresult_ok(hr))
619    }
620
621    /// Calls `ClearPseudoConsole`, or returns [`None`] if the backend does not
622    /// export it (see [`Self::supports_clear`]).
623    ///
624    /// Clearing discards the console host's scrollback and its visible screen,
625    /// as the "clear buffer" action of a terminal emulator does. It is a signal
626    /// to the host, not a write into conout, so it stays valid after a release
627    /// and needs no cooperation from the reader.
628    ///
629    /// `keepCursorRow` is passed as `FALSE`, which is deliberate: it is the
630    /// behaviour every version of the export has — the parameter was added in
631    /// microsoft/terminal#18976 to *opt out* of clearing the cursor's row — so
632    /// this operation means the same thing whichever `conpty.dll` is loaded.
633    ///
634    /// # Errors
635    ///
636    /// Returns `Some(Err(..))` with the failing `HRESULT` mapped to an
637    /// [`io::Error`]; `E_INVALIDARG` for a null handle, otherwise the failure
638    /// of the write to the signal pipe.
639    ///
640    /// # Safety
641    ///
642    /// `hpc` must be a live handle from [`Self::create`] on *this* backend
643    /// that has not yet been passed to [`Self::close`].
644    #[must_use]
645    #[cfg(any(feature = "blocking", feature = "tokio", test))]
646    pub(super) unsafe fn clear(&self, hpc: HPCON) -> Option<io::Result<()>> {
647        let clear = self.inner.api.clear?;
648
649        // SAFETY: `hpc` is live per this function's contract, and the
650        // function pointer was resolved from a module this backend keeps
651        // mapped. The two-argument call shape is sound on every target that
652        // resolves the export at all; see `backend::exports`.
653        let hr = unsafe { clear(hpc, 0) };
654        Some(hresult_ok(hr))
655    }
656}
657
658impl Clone for ConPtyBackend {
659    fn clone(&self) -> Self {
660        Self {
661            inner: Arc::clone(&self.inner),
662        }
663    }
664}
665
666/// Prints the backend's identity rather than raw function addresses, which
667/// are noise and vary between runs.
668impl fmt::Debug for ConPtyBackend {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        f.debug_struct("ConPtyBackend")
671            .field("kind", &self.kind())
672            .field("supports_release", &self.supports_release())
673            .field("supports_clear", &self.supports_clear())
674            .field("module_pinned", &self.inner.module_pin.is_some())
675            .finish()
676    }
677}
678
679/// Turns an `HRESULT` into a [`io::Result`], failing on `FAILED(hr)`.
680#[cfg(any(feature = "blocking", feature = "tokio", test))]
681fn hresult_ok(hr: HRESULT) -> io::Result<()> {
682    if hr >= 0 {
683        Ok(())
684    } else {
685        Err(hresult_to_io_error(hr))
686    }
687}
688
689/// Converts a failed `HRESULT` into an [`io::Error`].
690///
691/// `HRESULT_FROM_WIN32` wraps a plain Win32 error code as `0x8007_xxxx`.
692/// Unwrapping that back to the bare code is what lets [`io::Error`] classify
693/// it (`ERROR_ACCESS_DENIED` becomes [`io::ErrorKind::PermissionDenied`]
694/// rather than an unrecognised code) and format a readable message. Any other
695/// facility is passed through unchanged, which at least keeps the exact
696/// `HRESULT` visible in the error's `Display`.
697#[cfg(any(feature = "blocking", feature = "tokio", test))]
698fn hresult_to_io_error(hr: HRESULT) -> io::Error {
699    /// Mask selecting the severity and facility bits of an `HRESULT`.
700    const FACILITY_MASK: u32 = 0xFFFF_0000;
701    /// Severity `FAILED` plus `FACILITY_WIN32`, i.e. an `HRESULT_FROM_WIN32`.
702    const FAILED_FACILITY_WIN32: u32 = 0x8007_0000;
703
704    let bits = u32::from_ne_bytes(hr.to_ne_bytes());
705    if bits & FACILITY_MASK == FAILED_FACILITY_WIN32 {
706        let code = i32::try_from(bits & 0xFFFF).unwrap_or(i32::MAX);
707        io::Error::from_raw_os_error(code)
708    } else {
709        io::Error::from_raw_os_error(hr)
710    }
711}
712
713#[cfg(feature = "tracing")]
714fn log_missing_system_export(symbol: &'static str) {
715    tracing::warn!(
716        symbol,
717        "the system ConPTY backend is missing a required export"
718    );
719}
720
721#[cfg(not(feature = "tracing"))]
722const fn log_missing_system_export(_symbol: &'static str) {}
723
724#[cfg(test)]
725#[path = "backend_tests.rs"]
726mod tests;