의사 콘솔, ConPTY 또는 Windows PTY라고도 하는 Windows Pseudoconsole은 기본 콘솔 호스트 창의 사용자 대화형 부분을 대체하는 문자 모드 하위 시스템 활동에 대한 외부 호스트를 만들기 위해 설계된 메커니즘입니다.
의사console 세션을 호스팅하는 것은 기존 콘솔 세션과 약간 다릅니다. 기존 콘솔 세션은 운영 체제에서 문자 모드 애플리케이션이 실행하려고 하는 것을 인식할 때 자동으로 시작됩니다. 반면, 호스팅할 자식 문자 모드 애플리케이션을 사용하여 프로세스를 만들기 전에 호스팅 애플리케이션에서 의사console 세션 및 통신 채널을 만들어야 합니다. 자식 프로세스는 CreateProcess 함수를 사용하여 생성되지만 운영 체제가 적절한 환경을 설정하도록 지시하는 몇 가지 추가 정보가 있습니다.
이 시스템에 대한 추가 배경 정보는 초기 공지 블로그 게시물에서 확인할 수 있습니다.
Pseudoconsole 사용에 대한 전체 예제는 샘플 디렉터리의 GitHub 리포지토리 microsoft/터미널 에서 사용할 수 있습니다.
통신 채널 준비
첫 번째 단계는 호스트된 애플리케이션과의 양방향 통신을 위해 의사콘솔 세션을 만드는 동안 제공되는 동기 통신 채널 쌍을 만드는 것입니다. 이러한 채널은 동기 I/O가 있는 ReadFile 및 WriteFile을 사용하여 의사console 시스템에서 처리됩니다. 파일 스트림 또는 파이프와 같은 파일 또는 I/O 디바이스 핸들은 비동 기 통신에 OVERLAPPED 구조가 필요하지 않은 한 허용됩니다.
경고
경합 상태 및 교착 상태를 방지하려면 애플리케이션 내에서 자체 클라이언트 버퍼 상태 및 메시징 큐를 유지하는 별도의 스레드에서 각 통신 채널을 서비스하는 것이 좋습니다. 동일한 스레드에서 모든 의사console 작업을 서비스하면 다른 채널에서 차단 요청을 디스패치하는 동안 통신 버퍼 중 하나가 채워지고 작업을 기다리는 교착 상태가 발생할 수 있습니다.
Pseudoconsole 만들기
설정된 통신 채널을 사용하여 입력 채널의 "읽기" 끝과 출력 채널의 "쓰기" 끝을 식별합니다. 이 핸들 쌍은 CreatePseudoConsole 을 호출하여 개체를 만들 때 제공됩니다.
만들 때 X 및 Y 차원(문자 수)을 나타내는 크기가 필요합니다. 최종(터미널) 프레젠테이션 창의 표시 화면에 적용되는 차원입니다. 이 값은 pseudoconsole 시스템 내에 메모리 내 버퍼를 만드는 데 사용됩니다.
버퍼 크기는 GetConsoleScreenBufferInfoEx와 같은 클라이언트 쪽 콘솔 함수를 사용하여 정보를 검색하고 클라이언트가 WriteConsoleOutput과 같은 함수를 사용할 때 텍스트의 레이아웃과 위치를 지정하는 클라이언트 문자 모드 애플리케이션에 대한 답변을 제공합니다.
마지막으로 특수 기능을 수행하기 위해 의사콘솔을 만들 때 플래그 필드가 제공됩니다. 기본적으로 특수 기능이 없도록 0으로 설정합니다.
현재는 pseudoconsole API의 호출자에 이미 연결된 콘솔 세션에서 커서 위치의 상속을 요청하는 데 하나의 특수 플래그만 사용할 수 있습니다. 이는 의사 콘솔 세션을 준비하는 호스팅 애플리케이션 자체가 다른 콘솔 환경의 클라이언트 문자 모드 애플리케이션이기도 한 고급 시나리오에서 사용하기 위한 것입니다.
아래에 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;
}
// ...
}
비고
이 코드 조각은 불완전하며 이 특정 호출의 데모에만 사용됩니다. HANDLE의 수명을 적절하게 관리해야 합니다. HANDLE의 수명을 올바르게 관리하지 못하면 교착 상태 시나리오, 특히 동기 I/O 호출이 발생할 수 있습니다.
pseudoconsole에 연결된 클라이언트 문자 모드 애플리케이션을 만들기 위한 CreateProcess 호출이 완료되면 생성 중에 지정된 핸들이 이 프로세스에서 해제되어야 합니다. 이렇게 하면 기본 디바이스 개체에 대한 참조 수가 감소하고 의사 회의 세션이 핸들의 복사본을 닫을 때 I/O 작업에서 끊어진 채널을 제대로 검색할 수 있습니다.
자식 프로세스 만들기 준비
다음 단계는 자식 프로세스를 시작하는 동안 의사 창 정보를 전달하는 STARTUPINFOEX 구조를 준비하는 것입니다.
이 구조에는 프로세스 및 스레드 만들기에 대한 특성을 포함하여 복잡한 시작 정보를 제공하는 기능이 포함되어 있습니다.
InitializeProcThreadAttributeList를 두 번 호출 방식으로 사용하여 먼저 목록을 보관하는 데 필요한 바이트 수를 계산하고, 요청된 메모리를 할당한 다음, 다시 호출하여 불투명 메모리 포인터를 제공하여 특성 목록으로 설정합니다.
다음으로, 플래그 PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, pseudoconsole 핸들 및 pseudoconsole 핸들의 크기를 사용하여 초기화된 특성 목록을 전달하는 UpdateProcThreadAttribute를 호출합니다.
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;
}
호스트된 프로세스 만들기
다음으로, 실행 파일의 경로 및 해당하는 경우 추가 구성 정보와 함께 STARTUPINFOEX 구조를 전달하는 CreateProcess를 호출합니다. pseudoconsole 참조가 확장 정보에 포함되어 있음을 시스템에 알리기 위해 호출할 때 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());
}
// ...
}
비고
호스트된 프로세스가 계속 시작되고 연결 중일 때 의사console 세션을 닫으면 클라이언트 애플리케이션에서 오류 대화 상자가 표시될 수 있습니다. 호스트된 프로세스에 시작에 잘못된 의사console 핸들이 제공되면 동일한 오류 대화 상자가 표시됩니다. 호스트된 프로세스 초기화 코드에는 두 가지 상황이 동일합니다. 실패 시 호스트된 클라이언트 애플리케이션의 팝업 대화 상자는 초기화 실패를 자세히 설명하는 지역화된 메시지와 함께 읽습니다 0xc0000142 .
Pseudoconsole 세션과 통신
프로세스가 성공적으로 만들어지면 호스팅 애플리케이션은 입력 파이프의 쓰기 끝을 사용하여 사용자 상호 작용 정보를 의사 콘솔에서 그래픽 프레젠테이션 정보를 수신하도록 출력 파이프의 읽기 끝으로 보낼 수 있습니다.
추가 작업을 처리하는 방법을 결정하는 것은 호스팅 애플리케이션에 달려 있습니다. 호스팅 애플리케이션은 다른 스레드에서 창을 시작하여 사용자 상호 작용 입력을 수집하고 pseudoconsole 및 호스트된 문자 모드 애플리케이션에 대한 입력 파이프의 쓰기 끝으로 직렬화할 수 있습니다. 또 다른 스레드를 실행하여 pseudoconsole에 대한 출력 파이프의 읽기 끝을 드레이닝하고, 텍스트 및 가상 터미널 시퀀스 정보를 디코딩하고, 화면에 표시할 수 있습니다.
또한 스레드를 사용하여 의사콘솔 채널의 정보를 네트워크를 포함한 다른 채널 또는 디바이스로 다른 프로세스 또는 컴퓨터에 원격 정보로 릴레이하고 정보의 로컬 코드 변환을 방지할 수 있습니다.
Pseudoconsole 크기 조정
런타임 동안 사용자 상호 작용 또는 다른 디스플레이/상호 작용 디바이스에서 대역외로 수신된 요청으로 인해 버퍼의 크기를 변경해야 하는 상황이 있을 수 있습니다.
이 작업은 버퍼의 높이와 너비를 문자 수로 지정하는 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);
}
Pseudoconsole 세션 종료
세션을 종료하려면 원래 의사console 생성의 핸들을 사용하여 ClosePseudoConsole 함수를 호출합니다. 연결된 클라이언트 문자 모드 애플리케이션(예: CreateProcess 호출의 애플리케이션)은 세션이 닫히고 종료됩니다. 원래 자식이 다른 프로세스를 만드는 셸 형식 애플리케이션인 경우 트리의 연결된 모든 프로세스도 종료됩니다.
경고
세션을 닫으면 여러 가지 부작용이 발생하며, 이는 의사 구성을 단일 스레드 동기 방식으로 사용하는 경우 교착 상태를 초래할 수 있습니다. 의사console 세션을 닫는 작업은 통신 채널 버퍼에서 드레이닝해야 하는 hOutput 최종 프레임 업데이트를 내보냅니다. 또한 pseudoconsole을 만드는 동안 선택한 경우 PSEUDOCONSOLE_INHERIT_CURSOR 커서 상속 쿼리 메시지(수신 및 회신hInput)에 hOutput 응답하지 않고 의사콘솔을 닫으려고 하면 또 다른 교착 상태가 발생할 수 있습니다. pseudoconsole에 대한 통신 채널은 개별 스레드에서 서비스되고 클라이언트 애플리케이션이 종료되거나 ClosePseudoConsole 함수를 호출하는 해체 작업이 완료될 때까지 자체 협정을 깨뜨릴 때까지 드레이닝 및 처리되는 것이 좋습니다.