Создание сеанса псевдоконсоли

Псевдоконсоль Windows (иногда также псевдо консоль, conpty или псевдотерминал Windows) — это механизм, предназначенный для создания внешнего узла для действий подсистемы с поддержкой символьного режима, заменяющего часть взаимодействия пользователя окна узла консоли по умолчанию.

Процесс реализации сеанса псевдоконсоли немного отличается от сеанса традиционной консоли. Сеансы традиционной консоли запускаются автоматически, когда операционная система распознает начало запуска приложения с поддержкой символьного режима. В отличие от них сеанс псевдоконсоли и каналы связи необходимо создавать, используя приложение размещения, до создания процесса с размещением дочернего приложения с поддержкой символьного режима. Дочерний процесс будет по-прежнему создаваться с помощью функции CreateProcess, но уже с дополнительными сведениями, которые позволят операционной системе настроить соответствующую среду.

Дополнительные сведения об этой системе см. в записи блога о первоначальном объявлении.

Полные примеры использования псевдоконсоли см. в каталоге примеров раздела Терминал Windows нашего репозитория GitHub.

Подготовка каналов связи

Сначала надо объединить в пару синхронные каналы связи, которые будут предоставлены в процессе создания сеанса псевдоконсоли для двухстороннего обмена данными с размещенным приложением. Система псевдоконсоли обрабатывает эти каналы, используя функции ReadFile и WriteFile с синхронным вводом-выводом. Дескрипторы файла или устройства ввода-вывода, такие как файловый поток или канал, допустимы, только если для асинхронной передачи данных не требуется структура типа OVERLAPPED.

Предупреждение

Чтобы предотвратить возникновение состояний гонки и взаимоблокировок, настоятельно рекомендуется обслуживать каждый канал связи в отдельном потоке, который поддерживает собственные состояние буфера клиента и очередь обмена сообщениями внутри приложения. Обслуживание всех действий псевдоконсоли в одном потоке может привести к взаимоблокировке, при которой один из буферов связи заполняется и переходит в режим ожидания действия, пока вы пытаетесь отправить запрос на блокировку в другой канал.

Создание псевдоконсоли

Настроив каналы связи, определите точку завершения "считывания" канала ввода и точку завершения "записи" канала вывода. Эта пара дескрипторов предоставляется для создания объекта при вызове 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;
    }

    // ...

}

Примечание.

Этот фрагмент кода неполон. Он предназначен только для демонстрации этого конкретного вызова. Временем существования функции HANDLE следует управлять соответствующим образом. Если временем существования функции HANDLE не управлять должным образом, то могут возникнуть ситуации взаимоблокировок, особенно для синхронных вызовов ввода-вывода.

По окончании вызова CreateProcess для создания клиентского приложения с поддержкой символьного режима, присоединенного к псевдоконсоли, заданные при создании дескрипторы следует убрать из этого процесса. Это снизит число ссылок для объекта базового устройства, а также позволит операциям ввода-вывода правильно обнаруживать поврежденный канал в случае закрытия сеансом псевдоконсоли копии ее дескрипторов.

Подготовка к созданию дочернего процесса

Следующим этапом является подготовка структуры 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, прервут свою работу после завершения сеанса. Если исходное дочернее приложение было оболочкой и создавало другие процессы, также будет прервана работа всех связанных присоединенных процессов в дереве.

Предупреждение

Если использовать псевдоконсоль в однопотоковом синхронном режиме, закрытие сеанса создает несколько побочных эффектов, способных привести к ситуации взаимоблокировки. Завершение сеанса псевдоконсоли может вызвать конечное обновление группы данных для hOutput, которое следует удалить из буфера канала связи. Кроме того, если при создании псевдоконсоли выбрать параметр PSEUDOCONSOLE_INHERIT_CURSOR, то при попытке закрыть псевдоконсоль, игнорируя сообщение о запросе на наследование курсора (присылаемое на hOutput и получающее ответ через hInput), может возникнуть еще одна ситуация взаимоблокировки. Каналы связи для псевдоконсоли рекомендуется обслуживать в отдельных потоках, а также очищать и обрабатывать до их ожидаемого прекращения работы, вызванного прекращением работы клиентского приложения или завершением действий по переналадке во время вызова функции ClosePseudoConsole.