疑似コンソール セッションの作成
Windows 疑似コンソールは、疑似コンソール、ConPTY、または Windows PTY とも呼ばれ、既定のコンソール ホスト ウィンドウのユーザー対話機能部分を置き換える文字モード サブシステム アクティビティ向けに、外部ホストを作成するために設計されたメカニズムです。
疑似コンソール セッションのホストは、従来のコンソール セッションとは少し異なります。 従来のコンソール セッションは、オペレーティング システムによって文字モード アプリケーションが実行されようとしていることが認識されると、自動的に開始されます。 これに対し、疑似コンソール セッションと通信チャネルは、ホスト対象の子文字モード アプリケーションでプロセスを作成する前に、ホスト アプリケーションによって作成される必要があります。 子プロセスは引き続き CreateProcess 関数を使用して作成されますが、適切な環境を確立するようにオペレーティング システムに指示する追加情報がいくつかあります。
このシステムに関する追加の背景情報については、最初の発表のブログ投稿を参照してください。
疑似コンソールの完全な使用例は、GitHub リポジトリ microsoft/terminal のサンプル ディレクトリにあります。
通信チャネルの準備
最初の手順は、同期通信チャネルのペアを作成することです。これは、ホストされるアプリケーションとの双方向通信用の疑似コンソール セッションの作成時に提供されます。 これらのチャネルは、ReadFile および WriteFile と同期 I/O を使用して疑似コンソール システムによって処理されます。 非同期通信に OVERLAPPED 構造体が必要でない限り、ファイル ストリームやパイプなどのファイルまたは I/O デバイスのハンドルを使用できます。
警告
競合状態とデッドロックを防ぐために、各通信チャネルは、アプリケーション内で独自のクライアント バッファー状態とメッセージング キューを保持する個別のスレッドで処理することを強くお勧めします。 同じスレッド上ですべての疑似コンソール アクティビティを処理すると、別のチャネルでブロック要求をディスパッチしようとしているときに、通信バッファーの 1 つがいっぱいになり、アクションが待機状態になるという、デッドロックが発生する可能性があります。
擬似コンソールの作成
確立された通信チャネルを使用して、入力チャネルの "読み取り" エンドと出力チャネルの "書き込み" エンドを識別します。 このハンドルのペアは、CreatePseudoConsole を呼び出してオブジェクトを作成するときに提供されます。
作成時に、X と Y の寸法 (文字数) を表すサイズが必要です。 これらは、最終的な (ターミナル) 表示ウィンドウのサーフェスに適用される寸法です。 この値は、疑似コンソール システム内にメモリ内バッファーを作成するために使用されます。
このバッファー サイズにより、クライアント文字モード アプリケーションで GetConsoleScreenBufferInfoEx などのクライアント側コンソール関数 を使用して情報をプローブできるようになります。また、クライアントに Write Console Output などの関数を使用するときのテキストのレイアウトと配置も決まります。
最後に、疑似コンソールの作成時に、特別な機能を実行するためのフラグ フィールドが用意されています。 既定では、特別な機能を使用しないように、これが 0 に設定されています。
現時点では、疑似コンソール API の呼び出し元に既に接続されているコンソール セッションからカーソル位置の継承を要求するために使用できる特別なフラグは 1 つだけです。 これは、疑似コンソール セッションを準備しているホスト アプリケーション自体が、別のコンソール環境のクライアント文字モード アプリケーションでもあるという、より高度なシナリオで使用するためです。
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;
}
// ...
}
Note
このスニペットは不完全であり、この特定の呼び出しのデモンストレーションのみを目的として使用されます。 HANDLE の有効期間を適切に管理する必要があります。 HANDLE の有効期間を正しく管理できないと、特に同期 I/O 呼び出しで、デッドロックのシナリオが発生する可能性があります。
疑似コンソールに接続されたクライアント文字モード アプリケーションを作成する CreateProcess 呼び出しが完了したら、作成時に指定されたハンドルをこのプロセスから解放する必要があります。 これにより、基になるデバイス オブジェクトの参照カウントが減り、疑似コンソール セッションによってハンドルのコピーが閉じられたときに、I/O 操作で壊れたチャネルを適切に検出できるようになります。
子プロセスの作成の準備
次のフェーズは、子プロセスの開始時に疑似コンソール情報を伝達する 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());
}
// ...
}
Note
ホスト対象プロセスがまだ起動して接続している間に疑似コンソール セッションを閉じると、クライアント アプリケーションによってエラー ダイアログが表示される可能性があります。 ホスト対象プロセスのスタートアップに無効な疑似コンソール ハンドルが指定されている場合も、同じエラー ダイアログが表示されます。 ホスト対象プロセスの初期化コードにとって、この 2 つの状況は同じです。 エラー発生時のホスト対象クライアント アプリケーションからのポップアップ ダイアログには、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 関数の呼び出し時のティアダウン アクティビティの完了によって自発的に中断されるまで、ドレインおよび処理の状態を維持することをお勧めします。