创建伪控制台会话

Windows 伪控制台(有时也称为伪控制台、ConPTY 或 Windows PTY)是一种机制,旨在为字符模式子系统活动创建外部主机,以替换默认控制台主机窗口的用户交互部分。

托管伪控制台会话与传统控制台会话有些不同。 当操作系统识别到即将运行字符模式应用程序时,传统控制台会话会自动启动。 相反,在创建包含要托管的子字符模式应用程序的进程之前,托管应用程序需要先创建伪控制台会话和信道。 仍将使用 CreateProcess 函数创建子进程,但将包含一些其他信息,这些信息将引导操作系统建立适当的环境

初始公告博客文章中,可以找到有关此系统的其他背景信息。

有关使用伪控制台的完整示例,请访问我们的 GitHub 存储库 microsoft/terminal 的示例目录。

准备信道

第一步是创建一对同步信道,这些信道将在创建伪控制台会话期间提供,用于与托管应用程序进行双向通信。 伪控制台系统将 ReadFileWriteFile同步 I/O 一起使用来处理这些信道。 只要异步通信不需要重叠结构,就可以接受文件流或管道之类的文件或 I/O 设备句柄

警告

为避免出现争用情况和死锁,我们强烈建议在单独的线程中为每个信道提供服务,这些线程在应用程序中维护自己的客户端缓冲区状态和消息传递队列。 在同一线程上为所有伪控制台活动提供服务可能会导致死锁,即其中一个通信缓冲区已填满,并在你尝试在其他通道上分派阻塞的请求时等待你的操作。

创建伪控制台

使用已建立的信道,确定输入通道的“读取”端和输出通道的“写入”端。 在调用 CreatePseudoConsole 创建对象时,将提供此对句柄

创建时,需要表示 X 和 Y 维度的大小(以字符数为单位)。 这些维度将应用于最终(终端)展示窗口的显示图面。 这些值用于在伪控制台系统中创建内存中缓冲区。

缓冲区大小为客户端字符模式应用程序提供了答案,这些应用程序使用 GetConsoleScreenBufferInfoEx 之类的客户端控制台函数来探测信息,并决定了在客户端使用 WriteConsoleOutput 之类的函数时文本的布局和位置

最后,在创建伪控制台时提供了标志字段以执行特殊功能。 默认情况下,将此字段设置为 0,以不执行特殊功能。

目前,只有一个特殊标志可用于请求从已附加到伪控制台 API 调用方的控制台会话继承光标位置。 这适用于更高级的方案,在此方案中,准备伪控制台会话的托管应用程序本身也是另一控制台环境的客户端字符模式应用程序。

下面提供了一个示例代码片段,该示例利用 CreatePipe 来建立一对信道并创建伪控制台


HRESULT SetUpPseudoConsole(COORD size)
{
    HRESULT hr = S_OK;

    // Create communication channels

    // - Close these after CreateProcess of child application with pseudoconsole object.
    HANDLE inputReadSide, outputWriteSide;

    // - Hold onto these and use them for communication with the child through the pseudoconsole.
    HANDLE outputReadSide, inputWriteSide;

    if (!CreatePipe(&inputReadSide, &inputWriteSide, NULL, 0))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    if (!CreatePipe(&outputReadSide, &outputWriteSide, NULL, 0))
    {
        return HRESULT_FROM_WIN32(GetLastError());
    }

    HPCON hPC;
    hr = CreatePseudoConsole(size, inputReadSide, outputWriteSide, 0, &hPC);
    if (FAILED(hr))
    {
        return hr;
    }

    // ...

}

注意

此代码片段不完整,仅用于演示此特定调用。 你将需要适当管理句柄的生存期。 无法正确管理句柄的生存期会导致出现死锁情况,尤其是对于同步 I/O 调用

完成用于创建附加到伪控制台的客户端字符模式应用程序的 CreateProcess 调用后,应将创建进程中提供的句柄从该进程中释放出来。 当伪控制台会话关闭其句柄副本时,这将减少基础设备对象上的引用计数,并使 I/O 操作能够正确检测断开的通道。

准备创建子进程

下一阶段是准备 STARTUPINFOEX 结构,该结构将在启动子进程时传递伪控制台信息

该结构能够提供复杂的启动信息,包括用于创建进程和线程的属性。

以双重调用方式使用 InitializeProcThreadAttributeList 以首先计算保存列表所需的字节数,分配请求的内存,然后再次调用以提供不透明的内存指针,将其设置为属性列表

接下来,调用 UpdateProcThreadAttribute 并传递包含标志 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE、伪控制台句柄和伪控制台句柄大小的初始化属性列表


HRESULT PrepareStartupInformation(HPCON hpc, STARTUPINFOEX* psi)
{
    // Prepare Startup Information structure
    STARTUPINFOEX si;
    ZeroMemory(&si, sizeof(si));
    si.StartupInfo.cb = sizeof(STARTUPINFOEX);

    // Discover the size required for the list
    size_t bytesRequired;
    InitializeProcThreadAttributeList(NULL, 1, 0, &bytesRequired);

    // Allocate memory to represent the list
    si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, bytesRequired);
    if (!si.lpAttributeList)
    {
        return E_OUTOFMEMORY;
    }

    // Initialize the list memory location
    if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &bytesRequired))
    {
        HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
        return HRESULT_FROM_WIN32(GetLastError());
    }

    // Set the pseudoconsole information into the list
    if (!UpdateProcThreadAttribute(si.lpAttributeList,
                                   0,
                                   PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
                                   hpc,
                                   sizeof(hpc),
                                   NULL,
                                   NULL))
    {
        HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
        return HRESULT_FROM_WIN32(GetLastError());
    }

    *psi = si;

    return S_OK;
}

创建托管进程

接下来,调用 CreateProcess传递 STARTUPINFOEX 结构以及可执行文件的路径和任何其他配置信息(如果适用)。 请务必在调用时设置 EXTENDED_STARTUPINFO_PRESENT 标志,以警告系统扩展信息中包含伪控制台引用

HRESULT SetUpPseudoConsole(COORD size)
{
    // ...

    PCWSTR childApplication = L"C:\\windows\\system32\\cmd.exe";

    // Create mutable text string for CreateProcessW command line string.
    const size_t charsRequired = wcslen(childApplication) + 1; // +1 null terminator
    PWSTR cmdLineMutable = (PWSTR)HeapAlloc(GetProcessHeap(), 0, sizeof(wchar_t) * charsRequired);

    if (!cmdLineMutable)
    {
        return E_OUTOFMEMORY;
    }

    wcscpy_s(cmdLineMutable, charsRequired, childApplication);

    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));

    // Call CreateProcess
    if (!CreateProcessW(NULL,
                        cmdLineMutable,
                        NULL,
                        NULL,
                        FALSE,
                        EXTENDED_STARTUPINFO_PRESENT,
                        NULL,
                        NULL,
                        &siEx.StartupInfo,
                        &pi))
    {
        HeapFree(GetProcessHeap(), 0, cmdLineMutable);
        return HRESULT_FROM_WIN32(GetLastError());
    }

    // ...
}

注意

在托管进程仍处于启动和连接状态时关闭伪控制台会话,可能导致客户端应用程序显示错误对话框。 如果为托管进程提供了无效的伪控制台启动句柄,则会显示相同的错误对话框。 对于托管进程初始化代码,这两种情况是相同的。 发生故障时,来自托管客户端应用程序的弹出对话框将显示 0xc0000142 以及一条详细说明初始化失败的本地化消息。

通过伪控制台会话通信

成功创建进程后,托管应用程序可以使用输入管道的写入端将用户交互信息发送到伪控制台中,并使用输出管道的读取端从伪控制台接收图形表示信息。

完全由托管应用程序决定如何处理进一步的活动。 托管应用程序可以在另一个线程中启动一个窗口,以收集用户交互输入,并将其序列化到伪控制台和托管字符模式应用程序的输入管道的写入端。 可以启动另一个线程以清空伪控制台的输出管道的读取端,解码文本和虚拟终端序列信息,并将其显示在屏幕上。

线程也可以用于将信息从伪控制台通道传递到其他通道或设备(包括网络),以将信息远程传输到另一个进程或计算机,并避免对信息进行任何本地转码。

调整伪控制台的大小

在运行时的整个过程中,可能存在由于用户交互或从其他显示/交互设备带外接收到的请求而需要更改缓冲区大小的情况。

这可以通过 ResizePseudoConsole 函数来完成,该函数指定缓冲区的高度和宽度(以字符数为单位)

// Theoretical event handler function with theoretical
// event that has associated display properties
// on Source property.
void OnWindowResize(Event e)
{
    // Retrieve width and height dimensions of display in
    // characters using theoretical height/width functions
    // that can retrieve the properties from the display
    // attached to the event.
    COORD size;
    size.X = GetViewWidth(e.Source);
    size.Y = GetViewHeight(e.Source);

    // Call pseudoconsole API to inform buffer dimension update
    ResizePseudoConsole(m_hpc, size);
}

结束伪控制台会话

若要结束会话,请使用最初创建伪控制台时使用的句柄调用 ClosePseudoConsole 函数。 关闭会话时,将终止所有附加的客户端字符模式应用程序,例如 CreateProcess 调用中的应用程序。 如果原始子进程是创建其他进程的 shell 型应用程序,则树中所有相关的附加进程也将终止。

警告

如果以单线程同步方式使用伪控制台,则关闭会话会产生多种副作用,这些副作用可能导致死锁。 关闭伪控制台会话的操作可能会对应在信道缓冲区中清空的 hOutput 发出最终帧更新。 此外,如果在创建伪控制台时选择了 PSEUDOCONSOLE_INHERIT_CURSOR,则在不响应光标继承查询消息(在 hOutput 上收到并通过 hInput 回复)的情况下尝试关闭伪控制台可能会导致出现另一个死锁情况。 建议在单独的线程上为伪控制台的信道提供服务,并在客户端应用程序退出或调用 ClosePseudoConsole 函数时完成拆卸活动而自行中断之前,始终将其清空并进行处理