如何用 ConPTY API 创建自定义终端宿主:MiniTerm、EchoCon 与 GUIConsole 示例解析
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
如果你在 Windows 上想自己实现一个终端宿主——无论是控制台程序、C++ 工具还是 WPF 图形应用——核心任务都是同一件事:调用 ConPTY(Pseudo Console)API 创建一个伪控制台,把一个子进程挂接到它上面,再通过输入/输出管道读写 VT100 文本。terminal 仓库的samples/ConPTY/目录下提供了三个可直接编译运行的示例,覆盖了三种典型宿主形态:C++ 最小示例 EchoCon、C# 控制台宿主 MiniTerm、WPF 图形宿主 GUIConsole。本文按“先讲三个示例共用的 API 流程,再逐个拆解每个示例”的顺序展开,帮助你在动手前把调用链和各文件职责看清楚。
三个示例的运行环境要求来自各自文档,需要注意它们给出的版本不完全相同:
- EchoCon:Windows 10 Insider build 17733 或更高,以及最新的 Windows 10 Insider SDK(见 EchoCon/readme.md)。
- MiniTerm:文档说明在 Windows 10 Pro Build 17744 与 Windows_InsiderPreview_SDK_en-us_17749 上测试通过(见 MiniTerm/README.md);其 Program.cs 代码注释中给出的要求是“截至 2018 年 9 月,需要安装了 Redstone 5 的 Windows Insider Program 的 Windows 10,以及 Windows Insider Preview SDK”。
三个示例共用的 ConPTY 创建流程
无论 C++ 还是 C#,EchoCon 与 MiniTerm 实现的都是同一条 API 调用链。对照 EchoCon.cpp 和 MiniTerm 的 PseudoConsole.cs、ProcessFactory.cs,流程如下:
- 创建两条匿名管道:分别作为伪控制台的输入和输出端,各调用一次
CreatePipe。 - 宿主控制台开启 VT 处理:通过
GetConsoleMode读取当前模式,再用SetConsoleMode加上ENABLE_VIRTUAL_TERMINAL_PROCESSING标志,这样宿主窗口才能解释从输出管道读到的 VT 序列。MiniTerm 在 Terminal.cs 中还额外加上了DISABLE_NEWLINE_AUTO_RETURN。 - 创建伪控制台:调用
CreatePseudoConsole(COORD size, 输入端句柄, 输出端句柄, 0, out hPC),其中COORD是字符宽高(X为宽、Y为高)。 - 准备子进程的启动信息:先用
InitializeProcThreadAttributeList计算并初始化包含 1 个属性的线程属性列表,再用UpdateProcThreadAttribute把PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE属性设为上一步拿到的hPC。MiniTerm 的 PInvoke 定义 PseudoConsoleApi.cs 中给出了该属性常量值0x00020016,以及CreatePseudoConsole、ResizePseudoConsole、ClosePseudoConsole、CreatePipe四个 kernel32 函数签名。 - 创建进程:
CreateProcess时必须使用EXTENDED_STARTUPINFO_PRESENT创建标志并传入配置好的STARTUPINFOEX,子进程(如cmd.exe、ping.exe、powershell.exe)就会附着到伪控制台。 - 读写与清理:从输出管道读取 VT100 文本写到宿主窗口,把用户输入写入输入管道;结束时调用
ClosePseudoConsole,EchoCon 的代码注释指出这会终止仍在运行的客户端进程。
EchoCon:最小的 C++ 验证路径
EchoCon 是理解 API 顺序最快的入口,它做的事情(见 EchoCon/readme.md):创建输入/输出管道、调用CreatePseudoConsole()创建 ConPTY 实例、启动一个连接 ConPTY 的ping.exe,并运行一个线程监听ping.exe的输出、把收到的文本写到 Console。
对照 EchoCon.cpp 的main(),有几个实现细节值得注意:
- 伪控制台的尺寸来自宿主控制台:
GetConsoleScreenBufferInfo取得窗口信息后,用srWindow.Right - srWindow.Left + 1和srWindow.Bottom - srWindow.Top + 1计算宽高。 - 管道创建后,ConPTY 端的句柄(
hPipePTYIn/hPipePTYOut)可以立即关闭,因为句柄已被 dup 进 ConHost,会在 ConPTY 销毁时释放。 - 监听线程
PipeListener以 512 字节缓冲ReadFile读管道,然后用WriteFile写宿主控制台,而不是printf/puts——代码注释明确说明这是为了防止部分读取的 VT 序列破坏输出。 ping启动后等待最多 10 秒(WaitForSingleObject(piClient.hThread, 10 * 1000)),之后额外Sleep(500)让监听线程追平最后一段输出,再做资源清理。
验证方式:文档说明成功构建后运行 EchoCon 应清屏并显示命令的结果,readme 给出的示例输出为(文档示例,不是每次必然完全一致的固定输出):
Pinging Rincewind [::1] with 32 bytes of data: Reply from ::1: time<1ms Reply from ::1: time<1ms Reply from ::1: time<1ms Reply from ::1: time<1ms Ping statistics for ::1: Packets: Sent = 4, Received = 4, Lost = 0 (0% loss), Approximate round trip times in milli-seconds: Minimum = 0ms, Maximum = 0ms, Average = 0ms注意 readme 的文字描述提到 “display the results of the echo command”,而代码实际执行的是ping localhost,以上示例块以 readme 原文给出的 ping 输出为准。
编译入口是 EchoCon.sln。
MiniTerm:C# 交互式终端宿主
MiniTerm/README.md 对它的定位是:使用微软新版 PTY API 的实验性终端,用 C# 编写,基于原生代码示例改写;“Demonstrates the basic API calls required, but not intended for real-world usage”(演示所需的基础 API 调用,但不面向真实世界使用)。
Program.cs 的入口只做一件事:new Terminal().Run("cmd.exe"),即在一个普通控制台窗口里跑出一个受自己管理的 cmd。核心逻辑都在 Terminal.cs 的Run(string command)中:
- 用两个
PseudoConsolePipe(封装 PseudoConsolePipe.cs)分别承载输入和输出; PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, Console.WindowWidth, Console.WindowHeight)以宿主控制台窗口尺寸创建伪控制台,失败时抛出带错误码的InvalidOperationException;ProcessFactory.Start完成上文第 4、5 步的属性列表配置与CreateProcess;- 两个
Task分别负责:把输出管道内容拷贝到标准输出(CopyPipeToOutput),以及逐字符读取Console.ReadKey写入输入管道(CopyInputToPipe); - Ctrl+C 会被拦截并转发为
\x3字符写进管道,注释说明目的是“不要让它杀死终端,而应发给终端内的进程”; - 通过
SetConsoleCtrlHandler注册CTRL_CLOSE_EVENT回调,在窗口被强行关闭时释放进程、伪控制台和管道的资源。
文件布局上,Native/目录放 PInvoke 签名(ConsoleApi.cs、ProcessApi.cs、PseudoConsoleApi.cs),Processes/目录放进程封装,编译入口是 MiniTerm.sln。
GUIConsole:WPF 图形宿主骨架
GUIConsole 演示的是把 ConPTY 封装成库、再挂到 WPF 窗口上的形态(见 GUIConsole/README.md),由两个工程组成:
GUIConsole.ConPTY:.NET Standard 2.0 类库,负责创建控制台并启用伪控制台行为。公开交互面集中在 Terminal.cs,暴露两样东西:ConsoleOutStream(连接到伪控制台输出管道的FileStream,输出 VT100)和WriteToPseudoConsole(string input)(把字符串经输入管道写入伪控制台,接受 VT100)。Start方法的默认参数是宽 80、高 30 字符;GUIConsole.WPF:面向 .NET 4.6.1 的 WPF 应用,创建一个充当控制台的Window,文档说明它会保留底层控制台可见。
MainWindow.xaml.cs 展示了宿主侧的完整接线:窗口加载时Task.Run(() => _terminal.Start("powershell.exe"))启动伪控制台;订阅OutputReady事件后,用Task.Factory.StartNew(..., TaskCreationOptions.LongRunning)起一个长驻线程从ConsoleOutStream逐字符读取,并把结果追加到 TextBlock(注释说明真实场景中“应该在这里解析和分词收到的 VT100 文本,然后再渲染”,当前只是输出原始 VT100);按键事件里则调用_terminal.WriteToPseudoConsole(e.Key.ToString())把按键转成文本发回。
编译入口是 GUIConsole.sln。验证方式上,代码在OutputReady触发后把窗口标题设置为 “GUIConsole - powershell.exe”,并逐字符累积输出到TerminalHistoryBlock。
限制与适用条件
- 三个示例都面向 ConPTY 的早期版本,版本要求以各自文档为准:EchoCon 要求 Windows 10 Insider build 17733 或更高;MiniTerm 的测试环境为 Build 17744 加 Insider Preview SDK 17749,二者给出的具体数字不一致,按你要编译的示例对应的文档执行即可。
- MiniTerm 的文档明确其定位是演示基础 API 调用,不面向真实世界使用;GUIConsole 的 README 也把它描述为 “the skeleton of a custom WPF console”,即骨架示例,渲染层目前是原始 VT100 直出。
- 文档指出的共性前提:宿主必须处理输出管道中的 VT100 文本,否则无法正确显示内容;关闭 ConPTY 会终止挂在上面的子进程,清理顺序(先等子进程结束、再关 ConPTY 和管道)请参考 EchoCon 的
main()收尾段。 - 进一步学习的资料在三个 README 中均以微软 ConPTY 介绍博文与 MSDN “Creating a pseudo console session” 文档作为延伸阅读列出,可按名称自行检索。
【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考