Partager via


Création d’une session pseudoconsole

La pseudoconsole Windows, parfois appelée pseudo-console, ConPTY ou Windows PTY, est un mécanisme conçu pour créer un hôte externe pour les activités du sous-système en mode caractère qui remplacent la partie interactivité utilisateur de la fenêtre hôte de console par défaut.

L’hébergement d’une session pseudoconsole est un peu différent d’une session de console traditionnelle. Les sessions de console traditionnelles démarrent automatiquement lorsque le système d’exploitation reconnaît qu’une application en mode caractère est sur le point d’être exécutée. En revanche, une session pseudoconsole et les canaux de communication doivent être créés par l’application d’hébergement avant de créer le processus avec l’application en mode caractère enfant à héberger. Le processus enfant sera toujours créé à l’aide de la fonction CreateProcess , mais avec des informations supplémentaires qui dirigeront le système d’exploitation pour établir l’environnement approprié.

Vous trouverez des informations supplémentaires sur ce système sur le billet de blog d’annonce initial.

Des exemples complets d’utilisation de pseudoconsole sont disponibles sur notre dépôt GitHub microsoft/terminal dans le répertoire d’exemples.

Préparation des canaux de communication

La première étape consiste à créer une paire de canaux de communication synchrones qui seront fournis lors de la création de la session pseudoconsole pour la communication bidirectionnelle avec l’application hébergée. Ces canaux sont traités par le système pseudoconsole à l’aide de ReadFile et WriteFile avec des E/S synchrones. Les handles d’appareil de fichiers ou d’E/S comme un flux de fichiers ou un canal sont acceptables tant qu’une structure SE CHEVAUCHER n’est pas nécessaire pour la communication asynchrone.

Avertissement

Pour éviter les conditions de concurrence et les blocages, nous vous recommandons vivement de traiter chacun des canaux de communication sur un thread distinct qui conserve son propre état de mémoire tampon client et file d’attente de messagerie à l’intérieur de votre application. La maintenance de toutes les activités pseudoconsole sur le même thread peut entraîner un blocage où l’une des mémoires tampons de communication est remplie et en attente de votre action pendant que vous tentez de distribuer une demande bloquante sur un autre canal.

Création de la pseudoconsole

Avec les canaux de communication qui ont été établis, identifiez la fin « lecture » du canal d’entrée et la fin « écriture » du canal de sortie. Cette paire de handles est fournie lors de l’appel de CreatePseudoConsole pour créer l’objet.

Lors de la création, une taille représentant les dimensions X et Y (en nombre de caractères) est requise. Il s’agit des dimensions qui s’appliquent à l’aire d’affichage de la fenêtre de présentation finale (terminal). Les valeurs sont utilisées pour créer une mémoire tampon en mémoire à l’intérieur du système pseudoconsole.

La taille de la mémoire tampon fournit des réponses aux applications en mode caractère client qui sondent les informations à l’aide des fonctions de console côté client telles que GetConsoleScreenBufferInfoEx et déterminent la disposition et le positionnement du texte lorsque les clients utilisent des fonctions telles que WriteConsoleOutput.

Enfin, un champ d’indicateurs est fourni lors de la création d’une pseudoconsole pour effectuer des fonctionnalités spéciales. Par défaut, définissez cette valeur sur 0 pour qu’elle n’ait aucune fonctionnalité spéciale.

À ce stade, un seul indicateur spécial est disponible pour demander l’héritage de la position du curseur à partir d’une session de console déjà attachée à l’appelant de l’API pseudoconsole. Cela est destiné à être utilisé dans des scénarios plus avancés où une application d’hébergement qui prépare une session pseudoconsole est elle-même une application en mode caractère client d’un autre environnement de console.

Un exemple d’extrait de code est fourni ci-dessous en utilisant CreatePipe pour établir une paire de canaux de communication et créer la 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;
    }

    // ...

}

Remarque

Cet extrait de code est incomplet et utilisé pour la démonstration de cet appel spécifique uniquement. Vous devrez gérer la durée de vie des HANDLEde manière appropriée. Le fait de ne pas gérer correctement la durée de vie de HANDLEpeut entraîner des scénarios d’interblocage, en particulier avec des appels d’E/S synchrones.

Une fois l’appel CreateProcess terminé pour créer l’application en mode caractère client attachée à la pseudoconsole, les handles donnés pendant la création doivent être libérés de ce processus. Cela réduit le nombre de références sur l’objet d’appareil sous-jacent et permet aux opérations d’E/S de détecter correctement un canal rompu lorsque la session pseudoconsole ferme sa copie des handles.

Préparation de la création du processus enfant

La phase suivante consiste à préparer la structure STARTUPINFOEX qui transmet les informations pseudoconsole lors du démarrage du processus enfant.

Cette structure contient la possibilité de fournir des informations de démarrage complexes, notamment des attributs pour la création de processus et de threads.

Utilisez InitializeProcThreadAttributeList de manière double-appel pour calculer d’abord le nombre d’octets requis pour contenir la liste, allouer la mémoire demandée, puis appeler à nouveau en fournissant le pointeur de mémoire opaque pour qu’il soit configuré comme liste d’attributs.

Ensuite, appelez UpdateProcThreadAttribute en passant la liste d’attributs initialisée avec l’indicateur PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, le handle pseudoconsole et la taille du handle 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;
}

Création du processus hébergé

Ensuite, appelez CreateProcess en passant la structure STARTUPINFOEX avec le chemin d’accès à l’exécutable et toutes les informations de configuration supplémentaires le cas échéant. Il est important de définir l’indicateur de EXTENDED_STARTUPINFO_PRESENT lors de l’appel pour alerter le système que la référence pseudoconsole est contenue dans les informations étendues.

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

    // ...
}

Remarque

La fermeture de la session pseudoconsole pendant le démarrage du processus hébergé et la connexion peut entraîner une boîte de dialogue d’erreur affichée par l’application cliente. La même boîte de dialogue d’erreur s’affiche si le processus hébergé reçoit un handle pseudoconsole non valide pour le démarrage. Dans le code d’initialisation du processus hébergé, les deux circonstances sont identiques. La boîte de dialogue contextuelle de l’application cliente hébergée en cas d’échec est lue 0xc0000142 avec un message localisé détaillant l’échec de l’initialisation.

Communication avec la session pseudoconsole

Une fois le processus créé, l’application d’hébergement peut utiliser la fin d’écriture du canal d’entrée pour envoyer des informations d’interaction utilisateur dans la pseudoconsole et la fin de lecture du canal de sortie pour recevoir des informations de présentation graphique à partir de la pseudo console.

Il est entièrement à l’application d’hébergement de décider comment gérer d’autres activités. L’application d’hébergement peut lancer une fenêtre dans un autre thread pour collecter l’entrée d’interaction utilisateur et la sérialiser dans la fin d’écriture du canal d’entrée pour la pseudoconsole et l’application en mode caractère hébergé. Un autre thread peut être lancé pour vider la fin de lecture du canal de sortie pour le pseudoconsole, décoder le texte et les informations de séquence de terminal virtuel , et présenter cela à l’écran.

Les threads peuvent également être utilisés pour relayer les informations des canaux pseudoconsole vers un autre canal ou appareil, y compris un réseau vers des informations distantes vers un autre processus ou ordinateur, et éviter tout transcodage local des informations.

Redimensionnement de la pseudoconsole

Tout au long du runtime, il peut y avoir une circonstance selon laquelle la taille de la mémoire tampon doit être modifiée en raison d’une interaction utilisateur ou d’une demande reçue hors bande d’un autre appareil d’affichage/d’interaction.

Cette opération peut être effectuée avec la fonction ResizePseudoConsole spécifiant à la fois la hauteur et la largeur de la mémoire tampon dans un nombre de caractères.

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

Fin de la session pseudoconsole

Pour mettre fin à la session, appelez la fonction ClosePseudoConsole avec le handle de la création de pseudoconsole d’origine. Toutes les applications en mode caractère client attachées, telles que celles de l’appel CreateProcess , sont arrêtées lorsque la session est fermée. Si l’enfant d’origine était une application de type shell qui crée d’autres processus, tous les processus attachés associés dans l’arborescence seront également arrêtés.

Avertissement

La fermeture de la session a plusieurs effets secondaires qui peuvent entraîner une condition d’interblocage si le pseudoconsole est utilisé de manière synchrone à thread unique. L’acte de fermeture de la session pseudoconsole peut émettre une mise à jour de trame finale vers hOutput laquelle doit être vidée de la mémoire tampon du canal de communication. En outre, si PSEUDOCONSOLE_INHERIT_CURSOR elle a été sélectionnée lors de la création de la pseudoconsole, la tentative de fermeture de la pseudoconsole sans répondre au message de requête d’héritage du curseur (reçu et hOutput répondu à via hInput) peut entraîner une autre condition d’interblocage. Il est recommandé que les canaux de communication pour la pseudoconsole soient pris en charge sur des threads individuels et restent vidés et traités jusqu’à ce qu’ils soient rompus de leur propre accord par la sortie de l’application cliente ou par l’achèvement des activités de destruction lors de l’appel de la fonction ClosePseudoConsole .