A cross-platform pseudoterminal (PTY) library for .NET that enables spawning and interacting with terminal processes on Windows, Linux, and macOS.
Windows Linux macOS NuGet Version License: MIT
- Cross-Platform Support: Works on Windows (using ConPTY), Linux, and macOS
- Simple API: Easy-to-use async API for spawning terminal processes
- Full PTY Control: Read/write streams, resize terminal, handle process exit events
- Unicode Support: Full UTF-8 support including complex characters
- Native PTY Shim: Includes a native C library to avoid .NET runtime permissioning issues with
fork()on Linux/macOS - .NET Standard 2.0: Compatible with .NET Core 2.0+, .NET 5+, and .NET Framework 4.6.1+
Install via NuGet Package Manager:
dotnet add package Porta.Pty
Or via the Package Manager Console in Visual Studio:
Install-Package Porta.Ptyusing Porta.Pty; using System.Text; // Configure the terminal options var options = new PtyOptions { Name = "MyTerminal", Cols = 120, Rows = 30, Cwd = Environment.CurrentDirectory, App = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(Environment.SystemDirectory, "cmd.exe") : "/bin/bash", Environment = new Dictionary<string, string> { { "MY_VAR", "value" } } }; // Spawn the terminal process using IPtyConnection terminal = await PtyProvider.SpawnAsync(options, CancellationToken.None); // Handle process exit terminal.ProcessExited += (sender, e) => { Console.WriteLine($"Terminal exited with code: {e.ExitCode}"); }; // Write to the terminal byte[] command = Encoding.UTF8.GetBytes("echo Hello World\r"); await terminal.WriterStream.WriteAsync(command, 0, command.Length); await terminal.WriterStream.FlushAsync(); // Read from the terminal byte[] buffer = new byte[4096]; int bytesRead = await terminal.ReaderStream.ReadAsync(buffer, 0, buffer.Length); string output = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine(output); // Resize the terminal terminal.Resize(80, 24);
| Property | Type | Description |
|---|---|---|
Name |
string? |
Optional terminal name |
Cols |
int |
Initial number of columns |
Rows |
int |
Initial number of rows |
Cwd |
string |
Working directory for the process |
App |
string |
Path to the executable to spawn |
CommandLine |
string[] |
Command line arguments |
VerbatimCommandLine |
bool |
If true, arguments are not quoted |
Environment |
IDictionary<string, string> |
Environment variables (empty value removes the variable) |
| Member | Description |
|---|---|
ReaderStream |
Stream for reading output from the terminal |
WriterStream |
Stream for writing input to the terminal |
Pid |
Process ID of the terminal process |
ExitCode |
Exit code (available after process exits) |
ProcessExited |
Event fired when the process exits |
Resize(cols, rows) |
Resize the terminal dimensions |
Kill() |
Immediately terminate the process |
WaitForExit(ms) |
Wait for process exit with timeout |
flowchart TB
subgraph Entry["Entry Point"]
PtyProvider["PtyProvider<br/>(Static class)"]
end
subgraph Platform["PlatformServices"]
direction LR
WinProvider["Windows<br/>Provider"]
LinuxProvider["Linux<br/>Provider"]
MacProvider["macOS<br/>Provider"]
end
subgraph Connections["Platform Connections"]
direction LR
PseudoConsole["PseudoConsole<br/>Connection<br/>(ConPTY API)"]
LinuxPty["Unix PTY<br/>Connection<br/>(forkpty)"]
MacPty["Unix PTY<br/>Connection<br/>(forkpty)"]
end
PtyProvider -->|"SpawnAsync(PtyOptions)"| Platform
WinProvider --> PseudoConsole
LinuxProvider --> LinuxPty
MacProvider --> MacPty
PseudoConsole -->|"implements"| IPtyConnection["IPtyConnection"]
LinuxPty -->|"implements"| IPtyConnection
MacPty -->|"implements"| IPtyConnection
- Uses the Windows ConPTY (Pseudo Console) API introduced in Windows 10 1809
- Leverages
CreatePseudoConsole,ResizePseudoConsole, andClosePseudoConsolenative functions - Process isolation via Windows Job Objects for clean process termination
- Implements proper cleanup order per Microsoft documentation
- Uses POSIX PTY functions (
forkpty,openpty) via a native C shim library - Sets appropriate terminal environment variables (
TERM=xterm-256color) - Clears conflicting environment variables (TMUX, screen sessions)
On Linux and macOS, Porta.Pty includes a native C shim library (libporta_pty) that performs the forkpty() + execvp() sequence entirely in native code.
This is necessary because starting with .NET 7, the runtime enables W^X (Write XOR Execute) memory protection by default. W^X prevents memory pages from being both writable and executable simultaneously — a security hardening measure. This conflicts with fork() when running managed code in the forked child process, since the .NET runtime's JIT-compiled code and memory layout can violate the W^X invariant in the child.
By delegating the fork+exec to native C code, Porta.Pty avoids running any managed .NET code in the forked child process, completely eliminating the W^X conflict. The native shim libraries are bundled in the NuGet package for each supported platform (linux-x64, linux-arm64, osx-x64, osx-arm64) and are loaded automatically at runtime.
| Component | Description |
|---|---|
PtyProvider |
Static class providing the SpawnAsync entry point |
PtyOptions |
Configuration class for terminal spawning |
IPtyConnection |
Interface representing an active terminal connection |
IPtyProvider |
Internal interface implemented by platform-specific providers |
PlatformServices |
Platform detection and provider selection logic |
- Vanara.PInvoke.Kernel32: Windows API P/Invoke bindings
- Mono.Posix.NETStandard: POSIX API bindings for Unix platforms
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.