A cross-platform pseudoterminal (PTY) library for .NET that enables spawning and interacting with terminal processes on Windows, Linux, and macOS.
- 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.PtyOr 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.