Leith.Pty 0.1.0-ci.17

Leith.Pty

Leith.Pty is a native pseudo-terminal (PTY) library for .NET.

It provides one managed API over:

  • Unix PTYs through a small native shim (forkpty, plus a posix_spawn helper on Darwin).
  • Windows ConPTY through P/Invoke.
  • Two explicit I/O execution modes: Dedicated and Scalable.

Both I/O modes expose the same PtySession API and preserve the same process-exit, output-drain, EOF, cancellation, resize, and completion semantics. The difference is how the library spends resources while waiting for PTY I/O and process lifecycle events.

See Docs/IO-MODES.md for the full execution-mode contract and Docs/STATUS.md for the current platform validation state.

Quick start

using Leith.Pty;

var startInfo = OperatingSystem.IsWindows()
    ? new PtyStartInfo
    {
        FileName = "cmd.exe",
        Arguments = ["/d", "/s", "/c", "echo hello & exit /b 42"],
        Columns = 80,
        Rows = 24,
    }
    : new PtyStartInfo
    {
        FileName = "/bin/sh",
        Arguments = ["-c", "echo hello; exit 42"],
        Columns = 80,
        Rows = 24,
    };

using var session = Pty.Start(startInfo);

var buffer = new byte[4096];

while (true)
{
    int read = await session.Output.ReadAsync(buffer, CancellationToken.None);
    if (read == 0)
        break;

    Console.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, read));
}

PtyExitStatus exit = await session.WaitForProcessExitAsync();
await session.WaitForCompletionAsync();

Output.ReadAsync(...) returns 0 only after the child has exited, the required quiet-drain window has elapsed, and buffered output has been delivered.

I/O modes

PtyIoMode controls the resource topology used by a session. It does not change the programming model or PTY semantics.

Workload Recommended mode Why
Desktop terminal emulator Dedicated Small PTY population; prioritize per-session isolation and throughput
IDE integrated terminals Dedicated Usually a small or moderate number of interactive sessions
One or a few output-heavy PTYs Dedicated Prefer the direct per-session path
Browser or web terminal service Scalable Many concurrent or mostly-idle sessions
Agent or automation host Scalable Avoid one blocked PTY-owned waiter per live session
CI or PTY test farm Scalable Session density is part of the architecture
Unsure Dedicated It is the default and the safer general-purpose choice
var dedicated = Pty.Start(new PtyStartInfo
{
    FileName = "/bin/bash",
    IoMode = PtyIoMode.Dedicated,
});

var scalable = Pty.Start(new PtyStartInfo
{
    FileName = "/bin/bash",
    IoMode = PtyIoMode.Scalable,
});

There is no Auto mode. A caller that requests Scalable either gets the scalable backend or receives PlatformNotSupportedException; Leith.Pty does not silently fall back to Dedicated.

See Docs/IO-MODES.md for platform details and the resource-scaling contract.

Windows ConPTY host

On Windows, Leith.Pty can use either the ConPTY host bundled with the library or the system ConPTY implementation.

The default is:

WindowsConsoleHost = PtyWindowsConsoleHost.Bundled

The available policies are:

Value Behavior
Bundled Prefer the bundled ConPTY host. If it cannot be used, fall back to the system host and record the reason.
BundledOnly Require the bundled host. If it cannot be used, throw PlatformNotSupportedException.
System Always use the system kernel32!CreatePseudoConsole / conhost.exe path.

The bundled Windows assets are pinned to a tested Microsoft Terminal upstream build:

Microsoft Terminal upstream build @ fda72a0
source commit: fda72a070905570cd44e022658c7b9d1ee89322a

For each Windows RID, the package keeps the matching artifact set together:

conpty.dll
OpenConsole.exe
OpenConsoleProxy.dll

The exact provenance and SHA-256 hashes are committed in src/Leith.Pty/conpty/PROVENANCE.json.

Inspecting the selected host

Every Windows session exposes the resolved host information through PtySession.ConsoleHost. The property is null on non-Windows platforms.

using var session = Pty.Start(new PtyStartInfo
{
    FileName = "cmd.exe",
    WindowsConsoleHost = PtyWindowsConsoleHost.Bundled,
});

PtyConsoleHostInfo? host = session.ConsoleHost;

Console.WriteLine(host?.RequestedMode);
Console.WriteLine(host?.EffectiveHost);
Console.WriteLine(host?.ConPtyDllVersion);
Console.WriteLine(host?.ExpectedHostPath);
Console.WriteLine(host?.FallbackReason);

PtyConsoleHostInfo exposes:

  • RequestedMode
  • EnvironmentOverride
  • EffectiveHost
  • ConPtyDllPath
  • ConPtyDllVersion
  • ExpectedHostPath
  • ExpectedHostVersion
  • FallbackReason

Process-wide override

The LEITH_PTY_WINDOWS_CONSOLE_HOST environment variable overrides PtyStartInfo.WindowsConsoleHost for the process:

LEITH_PTY_WINDOWS_CONSOLE_HOST=bundled
LEITH_PTY_WINDOWS_CONSOLE_HOST=bundled-only
LEITH_PTY_WINDOWS_CONSOLE_HOST=system

An unknown value is treated as a configuration error and throws InvalidOperationException.

For diagnostics, LEITH_PTY_CONPTY_DLL=<path-to-conpty.dll> can point host resolution at an explicit bundled ConPTY directory.

Lifecycle and completion

Leith.Pty deliberately separates process exit from PTY completion.

WaitForProcessExitAsync
    child process exit status is available

Output.ReadAsync(...) == 0
    transport EOF has been reached after the required drain

WaitForCompletionAsync
    process exit, transport drain, and output delivery are complete

This distinction matters when a child exits while the PTY still contains unread output.

PtySession.Output permits one active primitive reader across synchronous and asynchronous reads. Overlapping primitive reads fail with InvalidOperationException.

Public API

Type / member Purpose
PtyStartInfo Spawn configuration: executable, arguments, working directory, environment overlay, geometry, terminal name, raw mode, quiet-drain window, I/O mode, and Windows console-host policy
PtyIoMode Dedicated or Scalable; Dedicated is the default
PtyWindowsConsoleHost Windows host policy: Bundled, BundledOnly, or System
Pty.Start(PtyStartInfo) Start a PTY session without waiting for process exit
PtySession.Input / Output Duplex terminal streams
PtySession.ProcessId Child process ID
PtySession.MasterFd Unix master file descriptor; -1 on Windows
PtySession.ConsoleHost Windows ConPTY host diagnostics; null off Windows
PtySession.Resize(...) Resize the terminal window
PtySession.Kill(PtySignal) Send a Unix signal to the process group; on Windows, terminate the job object
PtySession.Stop(PtySignal) Mark cancellation and then terminate the session
PtySession.SendEof() Send terminal EOF using the platform-specific mechanism
PtySession.GetForegroundProcessName() Return the foreground process name on Unix or the root child process name on Windows
PtySession.WaitForExit(timeout) Synchronous process wait
PtySession.WaitForProcessExitAsync(...) Wait until child exit status is available
PtySession.WaitForCompletionAsync(...) Wait for exit, drain, and final output delivery
PtyExitStatus Exit code or terminating signal
PtyEnvironment.MergeForSpawn Merge the parent environment with a caller-provided overlay
PtyEnvironment.DefaultTerm Unix fallback terminal name: xterm-256color

Environment overlay

PtyStartInfo.Environment is an overlay on the parent process environment:

  • a null value leaves the inherited variable unchanged;
  • an empty string removes the variable;
  • any other string sets or replaces it.

On Unix, if TerminalName is not set and the inherited environment has no TERM, Leith.Pty uses xterm-256color.

On Windows, Leith.Pty does not synthesize TERM by default.

Geometry validation

Spawn and resize geometry is validated against the public PtyStartInfo.Min* / Max* limits. Invalid values throw ArgumentOutOfRangeException; values are not silently clamped.

Platform status

Leith.Pty contains production Dedicated and Scalable backends for its supported platform families.

Platform Implementation Current validation
Linux x64 (glibc) forkpty; Dedicated blocking read path; Scalable epoll + pidfd Core contract, output-read, lifecycle, and bulk-drain coverage
Linux arm64 (glibc) Same Linux backend Conformance coverage on arm64
Linux musl x64 / arm64 Same Linux backend with packaged musl native shims Conformance and package-consumer coverage
Windows x64 ConPTY + job object; Dedicated overlapped path; Scalable IOCP path; bundled ConPTY by default Core contract, dual-mode, host-selection, and bundled-host coverage
Windows arm64 Same Windows backend Conformance coverage; see platform matrix for the exact host/version used by each recorded run
macOS arm64 posix_spawn helper; Dedicated and shared-kqueue paths Full-suite and dual-mode coverage
macOS x64 Same Darwin backend Conformance coverage under Rosetta 2
FreeBSD x64 forkpty; Dedicated FIONREAD path; Scalable shared kqueue path Contract and dual-mode coverage

The detailed evidence index, including the exact hosts and limitations of each run, is Docs/baseline/PLATFORM-MATRIX.md.

Build and test

dotnet build Term.Pty.slnx -c Release
dotnet test tests/Leith.Pty.Tests/Leith.Pty.Tests.csproj -c Release
dotnet test tests/Leith.Pty.PackageSmoke/Leith.Pty.PackageSmoke.csproj -c Release

GitHub Actions (self-hosted buildx64, Linux x64) builds the same solution and publishes Leith.Pty NuGet packages to BaGetter (https://bageter.buchmiet.co.uk/v3/index.json) on main — no sibling repo checkouts. See Docs/CI.md.

Unix native assets

The Unix native shim is built by Leith.Pty.csproj. Depending on the platform, the output includes:

libleithpty_unix.so
libleithpty_unix.dylib
leithpty_spawn_helper

Package assets are placed under runtimes/<RID>/native/.

After loading the Unix shim, the managed library verifies leithpty_abi_version(). The private C-to-C# ABI is documented in Docs/NATIVE-ABI.md.

Building from source with bundled ConPTY

When building the repository from source, MSBuild uses scripts/Fetch-BundledConPty.ps1 to populate the pinned Windows artifact cache and verify its hashes against src/Leith.Pty/conpty/PROVENANCE.json.

The cache layout is:

artifacts/conpty/fda72a0/
├── win-x64/
│   ├── conpty.dll
│   ├── OpenConsole.exe
│   └── OpenConsoleProxy.dll
└── win-arm64/
    ├── conpty.dll
    ├── OpenConsole.exe
    └── OpenConsoleProxy.dll

The staged copies under src/Leith.Pty/runtimes/win-*/native/ are generated files and are not committed.

If the verified cache has already been populated for an offline build, automatic fetching can be skipped with:

dotnet build Term.Pty.slnx -c Release /p:SkipFetchBundledConPty=true

A source build needs PowerShell (pwsh or Windows PowerShell) unless the cache has already been populated.

NuGet packages contain the Windows runtime assets under the standard runtimes/win-<arch>/native/ layout, so package consumers do not need to run the fetch script.

Documentation

Document Contents
Docs/IO-MODES.md Public Dedicated / Scalable contract and mode-selection guidance
Docs/STATUS.md Current implementation and validation status
Docs/baseline/PLATFORM-MATRIX.md Platform-by-platform validation index
Docs/Leith.Term.Presentation-Integration.md Integration contract for the terminal presentation layer
Docs/NATIVE-ABI.md Private Unix C-to-C# ABI and packaging contract
Docs/TEST-CORPUS.md Test corpus and validation protocol
src/Leith.Pty/conpty/PROVENANCE.json Exact provenance and hashes for bundled Windows ConPTY assets
Docs/baseline/bundled-conpty/BC10-bundle-main-20536.md Detailed validation record for the currently pinned bundled ConPTY build

Historical implementation plans and research notes remain in Docs/ and research/, but the files above are the best entry points for current users.

License

MIT — see LICENSE.

Third-party attributions are in THIRD-PARTY-NOTICES.md.

Showing the top 20 packages that depend on Leith.Pty.

Packages Downloads
Leith.Term.Presentation.Transport.Local
Local PTY transport for Leith.Term.Presentation.
0

.NET 10.0

  • No dependencies.

Version Downloads Last updated
19.0.0 5 09/28/2026
0.1.0-ci.17 0 09/28/2026
0.1.0-ci.16 0 09/28/2026