创建伪console 会话

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

托管伪控制台会话与传统控制台会话略有不同。 当作系统识别字符模式应用程序即将运行时,传统控制台会话会自动启动。 相比之下,在创建进程并托管子字符模式应用程序之前,需要由宿主应用程序创建伪会话和信道。 仍将使用 CreateProcess 函数创建子进程,但具有一些附加信息,这些信息将引导作系统建立适当的环境。

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

示例目录中的 GitHub 存储库 microsoft/terminal 上提供了使用伪console 的完整示例。

准备信道

第一步是创建一对同步通信通道,该通道将在创建伪console 会话期间提供,以便与托管应用程序进行双向通信。 这些通道由伪console 系统使用 ReadFileWriteFile同步 I/O 进行处理。 只要异步通信不需要 重叠 结构,文件或 I/O 设备句柄(如文件流或管道)即可接受。

警告

为了防止争用条件和死锁,我们强烈建议在单独的线程上为每个信道提供服务,该线程在应用程序中维护自己的客户端缓冲区状态和消息队列。 在同一线程上为所有伪控制活动提供服务可能会导致死锁,其中一个通信缓冲区已填充并等待作,同时尝试在另一个通道上调度阻塞请求。

创建伪console

建立通信通道后,标识输入通道的“读取”端和输出通道的“写入”端。 调用 CreatePseudoConsole 创建对象时提供了这对句柄。

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

缓冲区大小为客户端字符模式应用程序提供答案,这些应用程序使用 客户端控制台函数 (如 GetConsoleScreenBufferInfoEx )探测信息,并在客户端使用 WriteConsoleOutput 等函数时指示文本的布局和定位。

最后,在创建伪console 以执行特殊功能时提供标志字段。 默认情况下,将此设置为 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;
    }

    // ...

}

注释

此代码片段不完整,仅用于演示此特定调用。 需要适当地管理 HANDLE的生存期。 无法正确管理 HANDLE的生存期可能会导致死锁方案,尤其是使用同步 I/O 调用。

完成 CreateProcess 调用以创建附加到伪容器的客户端字符模式应用程序后,创建过程中提供的句柄应从此过程中释放。 这将减少基础设备对象上的引用计数,并允许 I/O作在伪console 会话关闭句柄副本时正确检测中断的通道。

准备创建子进程

下一阶段是准备 STARTUPINFOEX 结构,该结构将在启动子进程时传达伪console 信息。

此结构包含提供复杂启动信息的功能,包括进程和线程创建的属性。

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

接下来,调用 UpdateProcThreadAttribute ,并使用标志 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE、伪console 句柄和伪console 句柄的大小传递初始化的属性列表。


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 结构以及可执行文件的路径以及任何其他配置信息(如果适用)。 在调用以提醒系统伪console 引用包含在扩展信息中时,必须设置 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());
    }

    // ...
}

注释

在托管进程仍在启动和连接时关闭伪console 会话可能会导致客户端应用程序显示错误对话框。 如果为托管进程提供了用于启动的伪console 句柄无效,则会显示相同的错误对话框。 在托管进程初始化代码中,这两种情况是相同的。 失败时,托管客户端应用程序中的弹出对话框将读取 0xc0000142 本地化消息,其中详细说明了初始化失败。

与伪会话通信

成功创建进程后,宿主应用程序可以使用输入管道的写入端将用户交互信息发送到伪控制台和输出管道的读取端,以便从伪控制台接收图形呈现信息。

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

线程还可用于将伪console 通道中的信息中继到其他通道或设备,包括将网络远程信息传送到另一个进程或计算机,并避免对信息进行任何本地转码。

调整伪console 的大小

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

这可以通过 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);
}

结束伪console 会话

若要结束会话,请使用原始伪console 创建的句柄调用 ClosePseudoConsole 函数。 当会话关闭时,任何附加的客户端字符模式应用程序(如 CreateProcess 调用中的应用程序)都将终止。 如果原始子级是创建其他进程的 shell 类型应用程序,则树中的任何相关附加进程也将终止。

警告

关闭会话会产生多种副作用,如果伪锁以单线程同步方式使用,则可能会导致死锁条件。 关闭伪console 会话的行为可能会发出最终帧更新,该更新 hOutput 应从通信通道缓冲区中清空。 此外,如果在 PSEUDOCONSOLE_INHERIT_CURSOR 创建伪console 时已选中,则在不响应游标继承查询消息的情况下尝试关闭伪console(已 hOutput 接收并通过 hInput答复)可能会导致另一个死锁情况。 建议将伪console 的通信通道在单个线程上提供服务,并在客户端应用程序退出或调用 ClosePseudoConsole 函数时完成拆解活动之前保持清空和处理状态。