Compartilhar via


Criando uma sessão pseudoconsole

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

Hospedar uma sessão 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 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 direcionarão o sistema operacional para estabelecer o ambiente apropriado.

Você pode encontrar informações adicionais sobre esse sistema na postagem inicial do blog de anúncios.

Exemplos completos de como usar o Pseudoconsole estão disponíveis em nosso repositório GitHub microsoft/terminal no diretório de exemplos.

Preparando os canais de comunicação

A primeira etapa é criar um par de canais de comunicação síncrona que serão fornecidos durante a criação da sessão pseudoconsole para comunicação bidirecional com o aplicativo hospedado. Esses canais são processados pelo sistema pseudoconsole usando ReadFile e WriteFile com E/S síncrona. Identificadores de dispositivo de E/S ou arquivo, como um fluxo de arquivo ou pipe, são aceitáveis, desde que uma estrutura OVERLAPPED não seja necessária para 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 seu próprio estado de buffer de cliente e fila de mensagens dentro do aplicativo. A manutenção de todas as atividades pseudoconsole no mesmo thread pode resultar em um deadlock em que um dos buffers de comunicação está preenchido e aguardando sua ação enquanto você tenta expedir uma solicitação de bloqueio em outro canal.

Criando o Pseudoconsole

Com os canais de comunicação estabelecidos, identifique o final de "leitura" do canal de entrada e o final de "gravação" do canal de saída. Esse par de identificadores é fornecido ao chamar CreatePseudoConsole para criar o objeto.

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

O tamanho do buffer fornece respostas para aplicativos do modo de caractere do cliente que investigam informações usando as funções de console do lado do cliente , como GetConsoleScreenBufferInfoEx , e determina 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 herdância da posição do cursor de uma sessão de console já anexada ao chamador da API pseudoconsole. Isso se destina a ser usado em cenários mais avançados em que um aplicativo de hospedagem que está preparando uma sessão pseudoconsole também é um aplicativo de modo de caractere cliente de 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

Este snippet é incompleto e usado apenas para demonstração dessa chamada específica. Você precisará gerenciar o tempo de vida do HANDLEadequadamente. A falha ao gerenciar o tempo de vida do HANDLEs corretamente pode resultar em cenários de deadlock, especialmente com chamadas de E/S síncronas.

Após a conclusão da chamada CreateProcess para criar o aplicativo de modo de caractere cliente anexado ao pseudoconsole, os identificadores dados 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 quebrado quando a sessão pseudoconsole fechar sua cópia das alças.

Preparando-se para a criação do processo filho

A próxima fase é preparar a estrutura STARTUPINFOEX que transmitirá as informações pseudoconsole ao iniciar o processo filho.

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

Use InitializeProcThreadAttributeList de forma de chamada dupla para primeiro calcular o número de bytes necessários para manter a lista, alocar a memória solicitada e, em seguida, chamar novamente fornecendo o ponteiro de memória opaco para que ele seja configurado como a lista de atributos.

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

Criando o processo hospedado

Em seguida, chame CreateProcess passando a estrutura STARTUPINFOEX junto com o caminho para o executável e quaisquer informações de configuração adicionais, se aplicável. É importante definir o sinalizador de EXTENDED_STARTUPINFO_PRESENT ao chamar para alertar o sistema de que a referência 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

O fechamento da sessão pseudoconsole enquanto o processo hospedado ainda está sendo iniciado e a conexão pode resultar em uma caixa de diálogo de erro sendo mostrada pelo aplicativo cliente. A mesma caixa de diálogo de erro será mostrada se o processo hospedado receber um identificador 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 será lida 0xc0000142 com uma mensagem localizada detalhando a falha na inicialização.

Comunicando-se com a sessão Pseudoconsole

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

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

Os threads também podem ser usados para retransmitir as informações dos canais 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.

Redimensionando o Pseudoconsole

Ao longo 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 a uma solicitação recebida fora da 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);
}

Encerrando a sessão pseudoconsole

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

Aviso

O fechamento da sessão tem vários efeitos colaterais que podem resultar em uma condição de deadlock se o pseudoconsole for usado de forma síncrona de thread único. O ato de fechar a sessão pseudoconsole pode emitir uma atualização de quadro final para a hOutput qual 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 herdamento do cursor (recebida hOutput e respondeda via hInput) poderá resultar em outra condição de deadlock. É recomendável que os canais de comunicação para o pseudoconsole sejam atendidos em threads individuais e permaneçam drenados e processados até serem interrompidos por conta própria pelo aplicativo cliente sair ou pela conclusão das atividades de teardown na chamada da função ClosePseudoConsole .