Compartilhar via


Como criar uma sessão de pseudoconsole

O Pseudoconsole do Windows, às vezes também chamado de pseudoconsole, ConPTY ou Windows PTY, é um mecanismo projeto para criar um host externo para atividades de subsistema de modo de caractere que substituem a parte de interatividade do usuário da janela do host do console padrão.

Hospedar uma sessão de pseudoconsole é um pouco diferente de uma sessão de console tradicional. As sessões de console tradicionais são iniciadas automaticamente quando o sistema operacional reconhece que um aplicativo de modo de caractere está prestes a ser executado. Por outro lado, uma sessão de pseudoconsole e os canais de comunicação precisam ser criados pelo aplicativo de hospedagem antes de criar o processo com o aplicativo de modo de caractere filho a ser hospedado. O processo filho ainda será criado usando a função CreateProcess, mas com algumas informações adicionais que orientarão o sistema operacional a estabelecer o ambiente apropriado.

Você pode encontrar informações básicas adicionais sobre esse sistema na postagem no blog do comunicado inicial.

Exemplos completos de uso do pseudoconsole estão disponíveis em nosso repositório GitHub microsoft/terminal no diretório de exemplos.

Como preparar os canais de comunicação

A primeira etapa é criar um par de canais de comunicação síncronos que serão fornecidos durante a criação da sessão de pseudoconsole para comunicação bidirecional com o aplicativo hospedado. Esses canais são processados pelo sistema de pseudoconsole usando ReadFile e WriteFile com E/S síncrona. Identificadores de arquivo ou dispositivo de E/S como um fluxo de arquivo ou pipe são aceitáveis desde que uma estrutura OVERLAPPED não seja necessária para a comunicação assíncrona.

Aviso

Para evitar condições de corrida e deadlocks, é altamente recomendável que cada um dos canais de comunicação seja atendido em um thread separado que mantenha o próprio estado de buffer de cliente e a fila de mensagens dentro do seu aplicativo. A manutenção de todas as atividades do pseudoconsole no mesmo thread pode resultar em um deadlock em que um dos buffers de comunicação é preenchido e está aguardando sua ação enquanto você tenta despachar uma solicitação de bloqueio em outro canal.

Como criar o pseudoconsole

Com os canais de comunicações que foram estabelecidos, identifique a extremidade de "leitura" do canal de entrada e a extremidade de "gravação" do canal de saída. Esse par de identificadores é fornecido na chamada de CreatePseudoConsole para criar o objeto.

Na criação, é necessário um tamanho que representa as dimensões X e Y (na contagem de caracteres). Essas são as dimensões que serão aplicadas à superfície de exibição para a janela de apresentação final (terminal). Os valores são usados para criar um buffer na memória dentro do sistema do pseudoconsole.

O tamanho do buffer fornece respostas aos aplicativos de modo de caractere do cliente que investigam informações usando as funções de console do lado do cliente como GetConsoleScreenBufferInfoEx e determinam o layout e o posicionamento do texto quando os clientes usam funções como WriteConsoleOutput.

Por fim, um campo de sinalizadores é fornecido na criação de um pseudoconsole para executar uma funcionalidade especial. Por padrão, defina isso como 0 para não ter nenhuma funcionalidade especial.

Neste momento, apenas um sinalizador especial está disponível para solicitar a herança da posição do cursor de uma sessão de console já anexada ao chamador da API do pseudoconsole. Isso se destina ao uso em cenários mais avançados, em que um aplicativo de hospedagem que está preparando uma sessão de pseudoconsole é também um aplicativo de modo de caractere de cliente de um outro ambiente de console.

Um snippet de exemplo é fornecido abaixo utilizando CreatePipe para estabelecer um par de canais de comunicação e criar o pseudoconsole.


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;
    }

    // ...

}

Observação

Esse snippet está incompleto e é usado somente para demonstração dessa chamada específica. Você precisará gerenciar o tempo de vida dos IDENTIFICADORes adequadamente. A falha em gerenciar o tempo de vida dos IDENTIFICADORes corretamente pode resultar em cenários de deadlock, principalmente com chamadas de E/S síncronas.

Após a conclusão da chamada CreateProcess para criar o aplicativo de modo de caractere do cliente anexado ao pseudoconsole, os identificadores fornecidos durante a criação devem ser liberados desse processo. Isso diminuirá a contagem de referência no objeto de dispositivo subjacente e permitirá que as operações de E/S detectem corretamente um canal desfeito quando a sessão de pseudoconsole fechar a cópia dele dos identificadores.

Como preparar a criação do processo filho

A próxima fase é preparar a estrutura STARTUPINFOEX que transmitirá as informações do pseudoconsole quando o processo filho for iniciado.

Essa estrutura contém a capacidade de fornecer informações de inicialização complexas, incluindo atributos para criação de threads e processos.

Use InitializeProcThreadAttributeList em um modo de chamada dupla para calcular primeiro o número de bytes necessários para armazenar a lista, alocar a memória solicitada e chamar novamente fornecendo o ponteiro de memória opaco para configurá-lo como a lista de atributos.

Em seguida, chame UpdateProcThreadAttribute passando a lista de atributos inicializados com o sinalizador PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, o identificador do pseudoconsole e o tamanho do identificador do 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;
}

Como criar o processo hospedado

Em seguida, chame CreateProcess passando a estrutura STARTUPINFOEX junto com o caminho para o executável e informações de configuração adicionais, se aplicável. É importante definir o sinalizador EXTENDED_STARTUPINFO_PRESENT quando você fizer a chamada para alertar o sistema de que a referência do pseudoconsole está contida nas informações estendidas.

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());
    }

    // ...
}

Observação

Fechar a sessão de pseudoconsole enquanto o processo hospedado ainda estiver sendo iniciado e se conectando pode resultar na exibição de uma caixa de diálogo de erro pelo aplicativo cliente. A mesma caixa de diálogo de erro será mostrada se o processo hospedado receber um identificador de pseudoconsole inválido para inicialização. Para o código de inicialização do processo hospedado, as duas circunstâncias são idênticas. A caixa de diálogo pop-up do aplicativo cliente hospedado em caso de falha lerá 0xc0000142 com uma mensagem localizada detalhando a falha na inicialização.

Como se comunicar com a sessão de pseudoconsole

Depois que o processo for criado com êxito, o aplicativo de hospedagem poderá usar a extremidade de gravação do pipe de entrada para enviar informações de interação do usuário para o pseudoconsole e a extremidade de leitura do pipe de saída para receber informações de apresentação gráfica do pseudoconsole.

Cabe totalmente ao aplicativo de hospedagem para decidir como manipular outras atividades. O aplicativo de hospedagem pode iniciar uma janela em outro thread para coletar a entrada da interação do usuário e serializá-la na extremidade de gravação do pipe de entrada para o pseudoconsole e o aplicativo de modo de caractere hospedado. Outro thread poderia ser iniciado para drenar a extremidade de leitura do pipe de saída para o pseudoconsole, decodificar o texto e as informações da sequência do terminal virtual e apresentá-los à tela.

Os threads também poderiam ser usados para retransmitir as informações dos canais do pseudoconsole para um canal ou dispositivo diferente incluindo uma rede para informações remotas para outro processo ou computador e evitando qualquer transcodificação local das informações.

Como redimensionar o pseudoconsole

No decorrer do runtime, pode haver uma circunstância pela qual o tamanho do buffer precisa ser alterado devido a uma interação do usuário ou uma solicitação recebida fora de banda de outro dispositivo de exibição/interação.

Isso pode ser feito com a função ResizePseudoConsole especificando a altura e a largura do buffer em uma contagem de caracteres.

// 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);
}

Como encerrar uma sessão de pseudoconsole

Para encerrar a sessão, chame a função ClosePseudoConsole com o identificador da criação do pseudoconsole original. Aplicativos de modo de caractere do cliente anexados, como o da chamada CreateProcess, serão encerrados quando a sessão for fechada. Se o filho original era um aplicativo do tipo shell que cria outros processos, todos os processos anexados relacionados na árvore também serão encerrados.

Aviso

Fechar a sessão tem vários efeitos colaterais que poderão resultar em uma condição de deadlock for usado de um modo síncrono de thread único. O ato de fechar a sessão de pseudoconsole pode emitir uma atualização de quadro final para hOutput que deve ser drenada do buffer do canal de comunicações. Além disso, se PSEUDOCONSOLE_INHERIT_CURSOR tiver sido selecionado durante a criação do pseudoconsole, a tentativa de fechar o pseudoconsole sem responder à mensagem de consulta de herança do cursor (recebida em hOutput e respondida via hInput) poderá resultar em outra condição de deadlock. É recomendável que os canais de comunicações para o pseudoconsole sejam atendidos em threads individuais e permaneçam drenados e processados até que sejam desfeitos do próprio acordo pelo aplicativo cliente que sai ou pela conclusão de atividades de desinstalação na chamada da função ClosePseudoConsole.