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