Megosztás:


Pseudoconsole-munkamenet létrehozása

A Windows Pseudoconsole( más néven pszeudokonzol, ConPTY vagy Windows PTY) egy olyan mechanizmus, amellyel külső gazdagépet hozhat létre karaktermódú alrendszertevékenységekhez, amelyek felülírják az alapértelmezett konzol gazdagépablakának felhasználói interaktivitási részét.

A pszeudokonzol-munkamenetek üzemeltetése kissé eltér a hagyományos konzolos munkamenetek megszokottól. A hagyományos konzolbeszélgetések automatikusan elindulnak, amikor az operációs rendszer felismeri, hogy egy karaktermódú alkalmazás fog futni. Ezzel szemben az üzemeltetési alkalmazásnak létre kell hoznia egy pszeudokole munkamenetet és a kommunikációs csatornákat, mielőtt létrehozna egy folyamatot a üzemeltetni kívánt gyermek karaktermódú alkalmazással. A gyermekfolyamat továbbra is a CreateProcess függvénnyel jön létre, de néhány további információval, amelyek az operációs rendszert a megfelelő környezet létrehozására irányítják.

A rendszerről további háttérinformációkat a kezdeti közlemény blogbejegyzésében talál.

A Pseudoconsole használatára vonatkozó teljes példák a Microsoft/Terminal GitHub-adattárban érhetők el a mintakönyvtárban.

A kommunikációs csatornák előkészítése

Az első lépés egy szinkron kommunikációs csatornapár létrehozása, amely a pszeudokonzol munkamenet létrehozásakor lesz elérhető a üzemeltetett alkalmazással való kétirányú kommunikációhoz. Ezeket a csatornákat a pseudoconsole rendszer dolgozza fel a ReadFile és a WriteFile szinkron I/O-val. A fájl- vagy I/O-eszközkezelők, például a fájlstreamek vagy a cső elfogadhatók, ha az aszinkron kommunikációhoz nincs szükség ÁTFEDÉSBEN lévő szerkezetre.

Figyelmeztetés

A versenyfeltételek és holtpontok elkerülése érdekében javasoljuk, hogy minden kommunikációs csatorna külön szálon legyen kiszolgálva, amely fenntartja saját ügyfélpufferállapotát és üzenetsorát az alkalmazásban. Az ugyanazon a szálon végzett összes pszeudokole-tevékenység karbantartása holtponthoz vezethet, ahol az egyik kommunikációs puffer megtelik, és várja a műveletet, miközben egy blokkoló kérést kísérel meg elküldeni egy másik csatornán.

A Pseudoconsole létrehozása

A létrehozott kommunikációs csatornákkal azonosítsa a bemeneti csatorna "olvasási" végét és a kimeneti csatorna "írási" végét. Ez a fogópontpár a CreatePseudoConsole meghívásával jön létre az objektum létrehozásához.

Létrehozáskor az X és az Y dimenziót (karakterszámban) képviselő méretre van szükség. Ezek azok a dimenziók, amelyek a végső (terminálos) bemutatóablak megjelenítési felületére vonatkoznak. Az értékek egy memóriabeli puffer létrehozására szolgálnak a pszeudokonzolrendszeren belül.

A pufferméret választ ad az ügyféloldali konzolfüggvények ( például GetConsoleScreenBufferInfoEx ) használatával információt kérő ügyfélkaraktér-módú alkalmazásokra, és diktálja a szöveg elrendezését és elhelyezését, amikor az ügyfelek olyan függvényeket használnak, mint a WriteConsoleOutput.

Végül egy jelzőmezőt ad meg egy pszeudokole létrehozásakor a speciális funkciók elvégzéséhez. Alapértelmezés szerint állítsa a 0 értéket úgy, hogy nincs speciális funkciója.

Jelenleg csak egy speciális jelölő érhető el, amely a kurzor pozíciójának öröklését kéri a pseudoconsole API hívójának már csatolt konzolmunkamenetből. Ez speciálisabb helyzetekben használható, ahol egy pszeudokonzol-munkamenetet előkészítő üzemeltetési alkalmazás maga is egy másik konzolkörnyezet ügyfélkaraktere.

Az alábbi mintarészlet a CreatePipe használatával hoz létre kommunikációs csatornákat, és hozza létre a pszeudokonzolt.


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

    // ...

}

Megjegyzés:

Ez a kódrészlet hiányos, és csak az adott hívás bemutatására szolgál. A HANDLE-kélettartamát megfelelően kell kezelnie. A HANDLEs élettartamának helytelen kezelése holtponti forgatókönyveket eredményezhet, különösen szinkron I/O-hívások esetén.

A pszeudokonzolhoz csatolt ügyfél karakter módú alkalmazás létrehozására irányuló CreateProcess-hívás befejezése után a létrehozás során megadott leírókat fel kell szabadítani ebből a folyamatból. Ez csökkenti a mögöttes eszközobjektum hivatkozási számát, és lehetővé teszi, hogy az I/O-műveletek megfelelően észleljék a hibás csatornát, amikor a pseudoconsole munkamenet bezárja a leírók másolatát.

Felkészülés a gyermekfolyamat létrehozására

A következő fázis a STARTUPINFOEX struktúra előkészítése, amely a gyermekfolyamat indításakor közvetíti a pszeudokonzoladatokat.

Ez a struktúra összetett indítási információkat tartalmaz, beleértve a folyamat- és szállétrehozáshoz szükséges attribútumokat is.

Az InitializeProcThreadAttributeList kettős hívással először kiszámíthatja a lista tárolásához szükséges bájtok számát, lefoglalhatja a kért memóriát, majd ismét meghívhatja az átlátszatlan memóriamutatót, hogy az attribútumlistaként legyen beállítva.

Ezután hívja meg az UpdateProcThreadAttribute parancsot , amely átadja az inicializált attribútumlistát a jelölő PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, a pseudoconsole leíróval és a pseudoconsole leíró méretével.


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

A üzemeltetett folyamat létrehozása

Ezután hívja meg a CreateProcess-et , amely átadja a STARTUPINFOEX struktúrát, valamint a végrehajtható elérési utat és adott esetben a további konfigurációs információkat. Fontos beállítani a EXTENDED_STARTUPINFO_PRESENT jelzőt, amikor a rendszer figyelmezteti, hogy a pseudoconsole-referenciát a kiterjesztett információk tartalmazzák.

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

    // ...
}

Megjegyzés:

Ha bezárja a pszeudokonzol munkamenetet, miközben az üzemeltetett folyamat még elindul, és a csatlakozás azt eredményezheti, hogy az ügyfélalkalmazás hibaüzenetet jelenít meg. Ugyanez a hiba párbeszédpanel jelenik meg, ha az üzemeltetett folyamat érvénytelen pszeudokole leírót kap az indításhoz. Az üzemeltetett folyamat inicializálási kódjának két körülménye megegyezik. A sikertelen üzemeltetésű ügyfélalkalmazás előugró párbeszédpanelje egy honosított üzenettel jelenik meg 0xc0000142 , amely részletezi az inicializálás sikertelenségét.

Kommunikáció a Pseudoconsole munkamenettel

A folyamat sikeres létrehozása után az üzemeltetési alkalmazás a bemeneti cső írási végénél felhasználói interakciós információkat küldhet a pszeudokonzolba és a kimeneti cső olvasási végére, hogy grafikus bemutatóadatokat fogadjon a pszeudokonzolról.

Teljesen az üzemeltetési alkalmazáson múlik, hogy hogyan kell kezelni a további tevékenységeket. Az üzemeltetési alkalmazás elindíthat egy ablakot egy másik szálon, amely összegyűjti a felhasználói interakció bemenetét, és szerializálja azt a pszeudokonzol és az üzemeltetett karaktermódú alkalmazás bemeneti csőjének írási végébe. Egy másik szál is elindítható a kimeneti cső olvasási végének leeresztéséhez a pszeudokonzolhoz, dekódolni a szöveget és a virtuális terminál szekvenciaadatait , és bemutatni azt a képernyőn.

A szálak arra is használhatók, hogy a pszeudokonzolcsatornákból származó információkat továbbítsuk egy másik csatornára vagy eszközre, beleértve a hálózatot egy másik folyamatba vagy gépre, és elkerüljük az információk helyi átkódolását.

A Pseudoconsole átméretezése

A futtatókörnyezet során előfordulhat, hogy a puffer méretét módosítani kell egy felhasználói beavatkozás vagy egy másik kijelző/interakciós eszközről sávon kívül kapott kérés miatt.

Ez a ResizePseudoConsole függvénnyel végezhető el, amely a puffer magasságát és szélességét karakterszámban határozza meg.

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

A Pseudoconsole munkamenet befejezése

A munkamenet befejezéséhez hívja meg a ClosePseudoConsole függvényt az eredeti pseudoconsole-létrehozás leírójával. Minden csatolt ügyfélkarakte-módú alkalmazás, például a CreateProcess-hívásból származó alkalmazás a munkamenet bezárásakor leáll. Ha az eredeti gyermek egy olyan rendszerhéj típusú alkalmazás volt, amely más folyamatokat hoz létre, a fa kapcsolódó csatolt folyamatai is leállnak.

Figyelmeztetés

A munkamenet bezárásának számos mellékhatása van, amelyek holtpontot eredményezhetnek, ha a pszeudokonzolt egyszálas szinkron módon használják. A pszeudokonzol munkamenet bezárásával végleges keretfrissítést hOutput bocsáthat ki, amelyhez a kommunikációs csatorna pufferéből kell üríteni. Emellett, ha PSEUDOCONSOLE_INHERIT_CURSOR a pszeudokonzol létrehozásakor lett kiválasztva, a pszeudokonzol bezárásának megkísérlése a kurzor öröklődési lekérdezési üzenetének megválaszolása nélkül (be lett fogadva hOutput és megválaszolva hInput) egy másik patthelyzetet eredményezhet. Javasoljuk, hogy a pszeudokonzol kommunikációs csatornái egyes szálakon legyenek kiszolgálva, és mindaddig ürítsék és dolgozzák fel őket, amíg az ügyfélalkalmazás kilép, vagy a ClosePseudoConsole függvény meghívásával kapcsolatos szakadási tevékenységek befejeződnek.