认识Windows伪控制台(ConPTY)

文章发表于2018年8月2日

这是有关Windows命令行的第二篇文章,我们将在此讨论伪控制台Windows的新基础架构和编程接口,即Windows Pseudo Console(ConPTY):我们为什么开发它,为什么需要它,如何工作,如何使用它以及更多其他内容。

在上一篇文章“过去的严重遗产。 Windows命令行问题”,我们讨论了终端出现和Windows命令行发展的先决条件,并开始研究Windows控制台和Windows命令行基础结构的内部结构。 我们还讨论了Windows控制台的许多优点和主要缺点。

缺点之一是Windows试图“有用”,但它会干扰备用控制台和第三方控制台的开发人员,服务开发人员等。 在创建控制台或服务时,开发人员需要访问其终端/服务与命令行应用程序交换数据或提供对它们的访问的通信通道。 在* NIX世界中,这不是问题,因为* NIX提供了伪终端(PTY)基础结构,可以轻松地为控制台或服务创建通信通道。 但是在Windows上不是...

...直到现在!

从TTY到PTY


在详细介绍我们的开发之前,让我们简要地回到终端的开发。

最初是TTY


前一篇文章所述 ,在计算的早期,用户使用通过某种串行通信通道(通常通过20 mA电流环路 )连接到计算机的机电式电传打字机(TTY)来控制计算机。


肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)(站立)从事DEC PDP-11电传打字机(无电子显示的消息)的工作

终端分布


电传打字机被带电子显示屏(通常为CRT屏幕)的计算机终端所取代。 通常,终端是非常简单的设备(因此称为“哑终端”),仅包含以下任务所需的电子设备和处理能力:

  1. 接收来自键盘的文本输入。
  2. 将输入的文本缓冲在一行上(包括发送前的本地编辑)。
  3. 在串行通道上发送/接收文本(通常通过曾经广泛使用的RS-232接口 )。
  4. 在终端显示屏上显示接收到的文本。

尽管它简单(或可能要归功于它),但是终端迅速成为管理小型计算机,大型机和服务器的主要手段:大多数数据输入操作员,计算机操作员,系统管理员,科学家,研究人员,软件开发商和行业专家都在DEC终端上工作, IBM,Wyse和许多其他公司。


海军上将格蕾丝·霍珀(Grace Hopper)在他的办公室,办公桌上装有DEC VT220终端

分发软件终端


从1980年代中期开始,通用计算机取代了专用终端,逐渐开始被使用,它们变得更加实惠,流行和强大。 80年代的许多早期PC和其他计算机都有终端应用程序,这些应用程序打开了与PC的RS-232连接,并与连接另一端的任何人交换数据。

随着通用计算机变得越来越复杂,图形用户界面(GUI)和并发应用程序的全新世界出现了,包括终端应用程序。

但是有一个问题:终端应用程序如何与在同一台计算机上运行的另一个命令行应用程序进行交互? 以及如何在同一台计算机上运行的两个应用程序之间物理连接串行电缆?

伪终端外观(PTY)


在* NIX世界中,通过引入伪终端(PTY)解决了该问题。

PTY通过暴露主机和从机伪设备(“主机”和“从机”)来模拟计算机中的串行电信设备:终端应用程序连接到主机伪设备,命令行应用程序(例如,诸如cmd,PowerShell和bash之类的外壳)连接到从机伪设备。 当终端客户端将文本和/或控制命令(编码为文本)传输到主伪设备时,该文本将转换为与其关联的从设备。 来自应用程序的文本被发送到从属伪设备,然后返回至主设备,进而发送至终端。 数据总是异步发送/接收。


伪终端应用程序/ Shell

重要的是要注意,“从”伪设备模拟物理终端的行为,并将命令字符转换为POSIX信号。 例如,如果用户在终端中输入CTRL + C ,则CTRL + C的ASCII值(0x03)通过主机发送。 当在从属伪设备上接收到时,从输入流中删除值0x03,并生成SIGINT信号

* NIX终端应用程序,文本面板管理器(例如,屏幕,tmux)等广泛使用这种PTY基础结构。 这些应用程序调用openpty() ,该openpty()返回PTY主从服务器的一对文件描述符(fd)。 然后,该应用程序可以派生/执行一个子命令行应用程序(例如bash),该应用程序使用其fd从站侦听并将文本返回给连接的终端。

这种机制允许终端应用程序直接与本地运行的命令行应用程序“对话”,就像终端机通过串行/网络连接与远程计算机对话一样。

什么,没有伪控制台Windows?


正如我们在上一篇文章中讨论的那样,尽管Windows控制台在概念上类似于传统的* NIX终端,但是它在几个关键方面有所不同,尤其是在最低级别上,这可能会给Windows命令行应用程序,第三方终端/控制台和服务器的开发人员带来问题应用范围:

  1. Windows没有PTY基础结构 :当用户启动命令行应用程序(例如Cmd,PowerShell,wsl,ipconfig等)时,Windows本身会将新的或现有的控制台实例“连接”到该应用程序。
  2. Windows会干扰第三方控制台和服务器应用程序 :Windows(当前)没有为终端提供一种提供通信渠道的方式,他们希望通过这些渠道与命令行应用程序进行交互。 第三方终端必须在屏幕之外创建控制台,将用户输入的数据发送到屏幕上,并通过在第三方控制台自己的显示器上重新绘制输出来废弃输出!
  3. 仅Windows具有控制台API :Windows命令行应用程序依赖Win32 Consol API,这会降低代码的可移植性,因为所有其他平台都支持text / VT,而不是API。
  4. 非标准的远程访问 :命令行应用程序对Consol API的依赖性使交互和远程访问脚本大大复杂化。

怎么办


许多很多开发人员经常要求在Windows下使用类似PTY的机制,特别是那些使用ConEmu / Cmder,Console2 / ConsoleZ,Hyper,VSCode,Visual Studio,WSL,Docker和OpenSSH工具的机制。

几天后,甚至当我开始在Console团队中工作时,甚至Ars Technica的技术编辑Peter Bright也要求我实施PTY机制:



最近又一次:



好吧,我们终于做到了: 我们为Windows创建了一个伪控制台

欢迎使用Windows伪控制台(ConPTY)


自大约四年前成立控制台团队以来,该小组一直在进行Windows控制台和命令行内部机制的大修。 同时,我们定期并认真考虑了上述问题以及许多其他相关问题。 但是直到现在,基础设施和代码还没有准备好使伪控制台的发布成为可能!

新的Windows伪控制台(ConPTY)基础结构,API和其他一些相关更改将消除/减轻整个问题, 而不会破坏与现有命令行应用程序的向后兼容性

新的Win32 ConPTY API(官方文档即将发布)现已在Windows 10的最新内部版本和相应的Windows 10 Insider Preview SDK中提供 。 它们将出现在Windows 10的下一个主要版本中(在2018年秋/冬某个时候)。

控制台/ ConHost体系结构


要了解ConPTY,您需要研究Windows控制台的体系结构,或者更确切地说是ConHost!

重要的是要理解,尽管ConHost实现了您作为Windows控制台应用程序看到和了解的所有内容,但ConHost还包含并实现了大多数Windows命令行基础结构! 从现在开始, ConHost成为真正的“控制台节点” ,支持所有命令行应用程序和/或与命令行应用程序交互的GUI应用程序!

怎么了 怎么了 什么啊 让我们仔细看看。

这是内部控制台体系结构/ ConHost的高级视图:



上一篇文章的体系结构相比,ConHost现在包含几个用于VT处理的附加模块,以及新的实现开放API的ConPTY模块:

  • ConPTY API :新的Win32 ConPTY API提供类似于POSIX PTY模型的机制,但在Windows折射中。
  • VT交互性 :接收UTF-8编码的输入文本,将每个显示的文本字符转换为相应的INPUT_RECORD记录,并将其保存在输入缓冲区中。 它还处理转义序列,例如0x03(CTRL + C),将其转换为KEY_EVENT_RECORDS ,从而产生适当的转义动作。
  • VT Renderer :生成移动光标并在输出缓冲区的与上一帧有所不同的区域中渲染文本和样式所必需的VT序列。

好的,但这到底是什么意思?

Windows命令行应用程序如何工作?


为了更好地了解新的ConPTY基础架构的影响,让我们看一下到目前为止Windows控制台和命令行应用程序是如何工作的。

每当用户启动命令行应用程序(例如Cmd,PowerShell或ssh)时,Windows都会创建一个新的Win32进程,该进程将应用程序的可执行二进制文件及其任何依赖项(资源或库)加载到其中。

新创建的进程通常从其父级继承stdin和stdout描述符。 如果父进程是Windows GUI进程,则缺少stdin和stdout描述符,因此Windows将部署新应用程序并将其附加到新的控制台实例。 命令行应用程序及其控制台之间的通信通过ConDrv传输。

例如,当从没有提升特权的PowerShell实例启动时,新的应用程序进程将继承stdin / stdout父级描述符,因此,将接收输入数据并将输出输出到与父级相同的控制台。

这里我们需要进行保留,因为在某些情况下,命令行应用程序是附加到控制台的实例启动的,特别是出于安全原因,但是上面的描述通常是正确的。

最终,当命令行/ shell应用程序启动时,Windows通过ConDrv将其连接到控制台实例(ConHost.exe):



ConHost如何工作?


每当运行命令行应用程序时,Windows就会将应用程序连接到新的或现有的ConHost实例。 该应用程序及其控制台实例通过内核模式控制台驱动程序(ConDrv)连接,该驱动程序发送/接收包含序列化API调用请求和/或文本数据的IOCTL消息。

从历史上看,如前一篇文章所述,如今,ConHost的工作相对简单:

  • 用户通过键盘/鼠标/笔/触摸板生成输入,并将其转换为KEY_EVENT_RECORDMOUSE_EVENT_RECORD并存储在输入缓冲区中。
  • 输入缓冲区一次清空一个记录,执行请求的输入操作,例如在屏幕上显示文本,移动光标,复制/粘贴文本等。 其中许多操作都会更改输出缓冲区的内容。 这些更改的区域由ConHost状态引擎记录。
  • 在每一帧中,控制台都会显示输出缓冲区的更改区域。

当命令行应用程序调用Windows控制台API时,API调用被序列化为IOCTL消息并通过ConDrv驱动程序发送。 然后,它将IOCTL消息传递到附加的控制台,该控制台解码并执行请求的API调用。 返回的/输出值被序列化回IOCTL消息,并通过ConDrv发送回应用程序。

ConHost:为过去做出贡献


Microsoft努力尽可能保持与现有应用程序和工具的向后兼容性。 特别是对于命令行。 实际上,Windows 10的32位版本仍然可以运行许多/大多数16位Win16应用程序和可执行文件!

如上所述,ConHost的关键角色之一是为其命令行应用程序提供服务,尤其是调用和依赖Win32控制台API的旧应用程序。 现在,ConHost提供了新的服务:

  • 无缝的类似PTY的基础架构,可与现代控制台和终端进行通信
  • 升级旧版/传统命令行应用程序
    • 接收UTF-8文本/ VT并将其转换为输入记录(就像用户输入的一样)
    • 控制台API调用托管应用程序,并相应地更新其输出缓冲区
    • 以UTF-8编码,文本/ VT显示输出缓冲区的修改区域

以下是现代控制台应用程序如何通过ConPTY ConHost与命令行应用程序通信的示例。



在这个新模型中:

  1. 控制台:
    1. 建立自己的沟通渠道
    2. 调用ConPTY API创建ConPTY,从而强制Windows运行连接到通道另一端的ConHost实例
    3. 照常创建连接到ConHost的命令行应用程序(例如PowerShell)的实例
  2. 主持人:
    1. 在输入处读取UTF-8文本/ VT并将其转换为INPUT_RECORD记录,然后将其发送到命令行应用程序
    2. 从可以修改输出缓冲区内容的命令行应用程序进行API调用
    3. 以UTF-8编码(文本/ VT)显示输出缓冲区中的更改,并将接收到的文本发送到其控制台
  3. 命令行应用程序:
    1. 它像往常一样工作,读取输入并调用控制台API,却不知道其ConPTY ConHost会将输入和输出从/转换为UTF-8!

最后一刻很重要! 当旧的命令行应用程序使用对控制台API的调用WriteConsoleOutput(...)WriteConsoleOutput(...)WriteConsoleOutput(...)指定的文本写入相应的ConHost输出缓冲区。 ConHost会定期将输出缓冲区的更改区域显示为text / VT,并通过stdout发送回控制台。

最终,即使是传统的命令行应用程序也可以从外部“说”文本/ VT, 而无需进行任何更改

使用新的ConPTY基础架构,第三方控制台现在可以直接与现代和传统的命令行应用程序进行交互,并以文本/ VT的形式交换它们。

与Windows命令行应用程序进行远程交互


上述机制在一台计算机上运行良好,但是在与远程Windows计算机或容器中的PowerShell实例进行交互时也很有用。

远程启动命令行应用程序时存在问题(即在远程计算机,服务器或容器上)。 事实是,远程计算机上的命令行应用程序与本地ConHost实例进行通信,因为IOCTL消息并非旨在通过网络传输。 如何将输入从本地控制台传输到远程计算机,以及如何从在此运行的应用程序获取输出? 此外,如果Mac和Linux机器有终端,却没有Windows兼容的控制台,该怎么办?

因此,为了远程控制Windows计算机,我们需要某种通信代理,他们可以通过网络透明地序列化数据,管理应用程序实例的生存期等。

也许像SSH一样?

幸运的是, OpenSSH最近被移植到Windows,并作为Windows 10的附加选项添加。 PowerShell Core还使用ssh作为受支持的PowerShell Core Remoting远程协议之一。 对于运行Windows PowerShell的用户, 远程Windows PowerShell Remoting仍然是可接受的选项。

让我们看看Windows的OpenSSH现在如何允许您远程控制Windows Shell和命令行应用程序:



OpenSSH当前包括一些不需要的并发症:

  1. 使用者:
    1. 启动ssh客户端,Windows照常连接控制台实例
    2. 在控制台中输入文本,该控制台将击键发送到ssh客户端
  2. ssh客户端:
    1. 读取输入为文本数据的字节
    2. 通过网络将文本数据发送到sshd侦听服务
  3. sshd服务经历几个阶段:
    1. 启动默认外壳程序(例如,Cmd),该外壳程序将强制Windows创建并连接控制台的新实例
    2. 查找并连接到Cmd实例的控制台
    3. 将控制台移出屏幕(和/或隐藏它)
    4. 将从ssh客户端接收的输入发送到屏幕外的控制台作为输入
  4. cmd实例照常工作:
    1. 从sshd服务收集输入
    2. 工作吗
    3. 调用控制台API渲染/设置文本样式,移动光标等。
  5. 附加的[屏幕外]控制台:
    1. 通过更新输出缓冲区进行API调用。
  6. Sshd服务:
    1. 报废屏幕外控制台的输出缓冲区,找到差异,将其编码为文本/ VT,然后发送回...
  7. 一个发送文本的ssh客户端...
  8. 显示文字的控制台

好玩吧 一点都不! 在这种情况下,可能会出现很多问题,尤其是在模拟和发送用户输入以及刷新屏幕外控制台的输出缓冲区的过程中。 这会导致不稳定,崩溃,数据损坏,过多的能耗等。 此外,并非所有应用程序都可以删除文本本身,还可以删除其属性,这就是为什么格式和颜色会丢失的原因!

使用现代ConHost和ConPTY进行远程工作


当然可以改善情况吗? 是的,我们当然可以-让我们进行一些体系结构更改并应用我们的新ConPTY:



该图显示电路已更改如下:

  1. 使用者:
    1. 启动ssh客户端,Windows照常连接控制台实例
    2. 在控制台中输入文本,该控制台将击键发送到ssh客户端
  2. ssh客户端:
    1. 读取输入为文本数据的字节
    2. 通过网络将文本数据发送到sshd侦听服务
  3. Sshd服务:
    1. 创建标准输入/标准输出通道
    2. 调用ConPTY API来启动ConPTY
    3. 启动连接到ConPTY另一端的Cmd实例。 Windows启动并安装一个新的ConHost实例
  4. cmd实例照常工作:
    1. 从sshd服务收集输入
    2. 工作吗
    3. 调用控制台API渲染/设置文本样式,移动光标等。
  5. 实例ConPTY ConHost:
    1. 通过更新输出缓冲区进行API调用。
    2. 将输出缓冲区的更改区域显示为UTF-8编码的文本/ VT,并通过ssh发送回控制台/终端

对于sshd服务,使用ConPTY的这种方法显然更干净,更简单。 Windows控制台API的调用完全在命令行应用程序的ConHost实例中进行,该实例将所有可见的更改都转换为文本/ VT。 无论连接到ConHost的人是谁,他都不需要知道那里的应用程序会调用控制台API,并且不会生成文本/ VT!

同意这种新的ConPTY远程处理机制可以带来优雅,一致且简单的体系结构。 新的ConHost和ConPTY基础结构与ConHost内置的强大功能,对较旧的应用程序的支持以及对调用控制台控制台API的应用程序的更改(以文本/ VT形式显示)的显示相结合,可以帮助我们将过去推向未来。

ConPTY API及其使用方法


Windows 10 Insider Preview SDK的当前版本中提供了ConPTY API。

现在,我确定您已经迫不及待想要查看一些代码;)

看一下API声明:

 // Creates a "Pseudo Console" (ConPTY). HRESULT WINAPI CreatePseudoConsole( _In_ COORD size, // ConPty Dimensions _In_ HANDLE hInput, // ConPty Input _In_ HANDLE hOutput, // ConPty Output _In_ DWORD dwFlags, // ConPty Flags _Out_ HPCON* phPC); // ConPty Reference // Resizes the given ConPTY to the specified size, in characters. HRESULT WINAPI ResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size); // Closes the ConPTY and all associated handles. Client applications attached // to the ConPTY will also terminated. VOID WINAPI ClosePseudoConsole(_In_ HPCON hPC); 

上面的API ConPTY本质上公开了三个新功能供使用:

  • CreatePseudoConsole(size, hInput, hOutput, dwFlags, phPC)
    • 使用调用者创建的通道在w列和h行中创建尺寸为pty的行:
      • size :ConPTY缓冲区的宽度和高度(以字符为单位)
      • hInput :用于以UTF-8编码将输入数据作为文本/ VT序列写入PTY
      • hOutput :以UTF-8编码从PTY以文本/ VT序列的形式读取输出
      • dwFlags :可能的值:
        • PSEUDOCONSOLE_INHERIT_CURSOR:创建的ConPTY将尝试继承父终端应用程序的光标位置
      • phPC :ConPty生成的控制台句柄
    • 返回 :成功/失败。 如果成功,则phPC将包含新ConPty的句柄

    ResizePseudoConsole(hPC, size)
    • 调整内部ConPTY缓冲区的大小以显示特定的宽度和高度

    ClosePseudoConsole (hPC)
    • ConPTY . , ConPTY, , ,

    ConPTY API


    ConPTY API ConPTY.

    : GitHub

      // Note: Most error checking removed for brevity. // ... // Initializes the specified startup info struct with the required properties and // updates its thread attribute list with the specified ConPTY handle HRESULT InitializeStartupInfoAttachedToConPTY(STARTUPINFOEX* siEx, HPCON hPC) { HRESULT hr = E_UNEXPECTED; size_t size; siEx->StartupInfo.cb = sizeof(STARTUPINFOEX); // Create the appropriately sized thread attribute list InitializeProcThreadAttributeList(NULL, 1, 0, &size); std::unique_ptr<BYTE[]> attrList = std::make_unique<BYTE[]>(size); // Set startup info's attribute list & initialize it siEx->lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>( attrList.get()); bool fSuccess = InitializeProcThreadAttributeList( siEx->lpAttributeList, 1, 0, (PSIZE_T)&size); if (fSuccess) { // Set thread attribute list's Pseudo Console to the specified ConPTY fSuccess = UpdateProcThreadAttribute( lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hPC, sizeof(HPCON), NULL, NULL); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); } else { hr = HRESULT_FROM_WIN32(GetLastError()); } return hr; } // ... HANDLE hOut, hIn; HANDLE outPipeOurSide, inPipeOurSide; HANDLE outPipePseudoConsoleSide, inPipePseudoConsoleSide; HPCON hPC = 0; // Create the in/out pipes: CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, NULL, 0); CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, NULL, 0); // Create the Pseudo Console, using the pipes CreatePseudoConsole( {80, 32}, inPipePseudoConsoleSide, outPipePseudoConsoleSide, 0, &hPC); // Prepare the StartupInfoEx structure attached to the ConPTY. STARTUPINFOEX siEx{}; InitializeStartupInfoAttachedToConPTY(&siEx, hPC); // Create the client application, using startup info containing ConPTY info wchar_t* commandline = L"c:\\windows\\system32\\cmd.exe"; PROCESS_INFORMATION piClient{}; fSuccess = CreateProcessW( nullptr, commandline, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &siEx->StartupInfo, &piClient); // ... 

    cmd.exe ConPTY, CreatePseudoConsole() . ConPTY / Cmd. ResizePseudoConsole() , — ClosePseudoConsole() .


    ConPTY :

     // Input "echo Hello, World!", press enter to have cmd process the command, // input an up arrow (to get the previous command), and enter again to execute. std::string helloWorld = "echo Hello, World!\n\x1b[A\n"; DWORD dwWritten; WriteFile(hIn, helloWorld.c_str(), (DWORD)helloWorld.length(), &dwWritten, nullptr); 


    , ConPTY:

     // Suppose some other async callback triggered us to resize. // This call will update the Terminal with the size we received. HRESULT hr = ResizePseudoConsole(hPC, {120, 30}); 


    ConPTY:

     ClosePseudoConsole(hPC); 

    : ConPTY ConHost .

    !


    ConPTY API — , , Windows … !

    ConPTY API Microsoft, Microsoft ( Windows Linux (WSL), Windows Containers, VSCode, Visual Studio .), , @ConEmuMaximus5ConEmu Windows.

    , ConPTY API.


    , : ConHost . Console API. , , .

    , VT, , — .

    , Windows, /VT UTF-8 Console API: « VT» , Console API (, 16M RGB True Color ).

    /


    / , ConPTY API: , , , , .

    VSCode ( GitHub #45693 ) , Windows.

    ConPTY API


    ConPTY API Windows 10 / 2018 .

    Windows, , , ConPTY. Win32 API, API Runtime Dynamic Linking LoadLibrary() GetProcAddress() .

    Windows ConPTY, API ConPTY. , , .

    , ?


    … ! , , ! :D

    , , , . — , Windows , .

    . Windows Console GitHub . , .

Source: https://habr.com/ru/post/zh-CN420853/


All Articles