Files
SimpleRemoter/client/ConPTYManager.cpp
2026-04-19 22:55:21 +02:00

344 lines
11 KiB
C++

// ConPTYManager.cpp: Windows ConPTY terminal manager implementation
// Provides xterm.js compatible terminal for Windows 10 1809+
#include "stdafx.h"
#include "ConPTYManager.h"
#include "Common.h"
#include "../common/commands.h"
// Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK)
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)
#endif
// Static members
PFN_CreatePseudoConsole CConPTYManager::s_pfnCreatePseudoConsole = nullptr;
PFN_ResizePseudoConsole CConPTYManager::s_pfnResizePseudoConsole = nullptr;
PFN_ClosePseudoConsole CConPTYManager::s_pfnClosePseudoConsole = nullptr;
bool CConPTYManager::s_bApiLoaded = false;
bool CConPTYManager::LoadConPTYApi()
{
if (s_bApiLoaded) {
return s_pfnCreatePseudoConsole != nullptr;
}
s_bApiLoaded = true;
HMODULE hKernel = GetModuleHandleA("kernel32.dll");
if (!hKernel) return false;
s_pfnCreatePseudoConsole = (PFN_CreatePseudoConsole)GetProcAddress(hKernel, "CreatePseudoConsole");
s_pfnResizePseudoConsole = (PFN_ResizePseudoConsole)GetProcAddress(hKernel, "ResizePseudoConsole");
s_pfnClosePseudoConsole = (PFN_ClosePseudoConsole)GetProcAddress(hKernel, "ClosePseudoConsole");
return s_pfnCreatePseudoConsole && s_pfnResizePseudoConsole && s_pfnClosePseudoConsole;
}
bool CConPTYManager::IsConPTYSupported()
{
return LoadConPTYApi();
}
CConPTYManager::CConPTYManager(IOCPClient* ClientObject, int n, void* user)
: CManager(ClientObject)
, m_hPC(nullptr)
, m_hPipeIn(nullptr)
, m_hPipeOut(nullptr)
, m_hShellProcess(nullptr)
, m_hShellThread(nullptr)
, m_hReadThread(nullptr)
, m_bRunning(TRUE)
, m_cols(80)
, m_rows(24)
{
if (!LoadConPTYApi()) {
Mprintf("[ConPTY] API not available\n");
return;
}
// Initialize with default size, will be resized when server sends size
if (!InitializeConPTY(m_cols, m_rows)) {
Mprintf("[ConPTY] Failed to initialize\n");
return;
}
// Send terminal start token
BYTE bToken = TOKEN_TERMINAL_START;
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)&bToken, 1, &mask);
// Start read thread immediately, it will wait for server ready internally
m_hReadThread = __CreateThread(NULL, 0, ReadThread, (LPVOID)this, 0, NULL);
}
CConPTYManager::~CConPTYManager()
{
m_bRunning = FALSE;
// Wake up read thread if it's waiting for server ready
NotifyDialogIsOpen();
// Close pipes first to unblock ReadThread
if (m_hPipeIn) {
CloseHandle(m_hPipeIn);
m_hPipeIn = nullptr;
}
if (m_hPipeOut) {
CloseHandle(m_hPipeOut);
m_hPipeOut = nullptr;
}
// Wait for read thread with timeout
int waitCount = 0;
while (m_hReadThread && waitCount < 30) { // 3 second timeout
Sleep(100);
waitCount++;
}
// Close ConPTY
if (m_hPC && s_pfnClosePseudoConsole) {
s_pfnClosePseudoConsole(m_hPC);
m_hPC = nullptr;
}
// Terminate process if still running
if (m_hShellProcess) {
DWORD exitCode = 0;
if (GetExitCodeProcess(m_hShellProcess, &exitCode) && exitCode == STILL_ACTIVE) {
TerminateProcess(m_hShellProcess, 0);
}
CloseHandle(m_hShellProcess);
m_hShellProcess = nullptr;
}
if (m_hShellThread) {
CloseHandle(m_hShellThread);
m_hShellThread = nullptr;
}
}
bool CConPTYManager::InitializeConPTY(int cols, int rows)
{
// Create pipes
HANDLE hPipeInRead = nullptr, hPipeInWrite = nullptr;
HANDLE hPipeOutRead = nullptr, hPipeOutWrite = nullptr;
if (!CreatePipe(&hPipeInRead, &hPipeInWrite, nullptr, 0)) {
Mprintf("[ConPTY] CreatePipe(in) failed: %d\n", GetLastError());
return false;
}
if (!CreatePipe(&hPipeOutRead, &hPipeOutWrite, nullptr, 0)) {
Mprintf("[ConPTY] CreatePipe(out) failed: %d\n", GetLastError());
CloseHandle(hPipeInRead);
CloseHandle(hPipeInWrite);
return false;
}
// Create pseudo console
COORD size = { (SHORT)cols, (SHORT)rows };
HRESULT hr = s_pfnCreatePseudoConsole(size, hPipeInRead, hPipeOutWrite, 0, &m_hPC);
if (FAILED(hr)) {
Mprintf("[ConPTY] CreatePseudoConsole failed: 0x%08X\n", hr);
CloseHandle(hPipeInRead);
CloseHandle(hPipeInWrite);
CloseHandle(hPipeOutRead);
CloseHandle(hPipeOutWrite);
return false;
}
// We read from hPipeOutRead (cmd output) and write to hPipeInWrite (cmd input)
m_hPipeIn = hPipeOutRead;
m_hPipeOut = hPipeInWrite;
// Close handles passed to ConPTY (they're now owned by ConPTY)
CloseHandle(hPipeInRead);
CloseHandle(hPipeOutWrite);
// Prepare startup info with pseudo console attribute
STARTUPINFOEXW si = {};
si.StartupInfo.cb = sizeof(si);
SIZE_T attrListSize = 0;
InitializeProcThreadAttributeList(nullptr, 1, 0, &attrListSize);
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attrListSize);
if (!si.lpAttributeList) {
Mprintf("[ConPTY] HeapAlloc failed\n");
return false;
}
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attrListSize)) {
Mprintf("[ConPTY] InitializeProcThreadAttributeList failed: %d\n", GetLastError());
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
return false;
}
// PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016
if (!UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, m_hPC, sizeof(m_hPC), nullptr, nullptr)) {
Mprintf("[ConPTY] UpdateProcThreadAttribute failed: %d\n", GetLastError());
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
return false;
}
// Get cmd.exe path
WCHAR cmdPath[MAX_PATH] = {};
GetSystemDirectoryW(cmdPath, MAX_PATH);
wcscat_s(cmdPath, L"\\cmd.exe");
// Create process
PROCESS_INFORMATION pi = {};
if (!CreateProcessW(nullptr, cmdPath, nullptr, nullptr, FALSE,
EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &si.StartupInfo, &pi)) {
Mprintf("[ConPTY] CreateProcess failed: %d\n", GetLastError());
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
return false;
}
m_hShellProcess = pi.hProcess;
m_hShellThread = pi.hThread;
m_cols = cols;
m_rows = rows;
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
Mprintf("[ConPTY] Started cmd.exe (PID=%d) with %dx%d terminal\n", pi.dwProcessId, cols, rows);
return true;
}
void CConPTYManager::ResizeTerminal(int cols, int rows)
{
if (m_hPC && s_pfnResizePseudoConsole) {
COORD size = { (SHORT)cols, (SHORT)rows };
HRESULT hr = s_pfnResizePseudoConsole(m_hPC, size);
if (SUCCEEDED(hr)) {
m_cols = cols;
m_rows = rows;
Mprintf("[ConPTY] Resized to %dx%d\n", cols, rows);
} else {
Mprintf("[ConPTY] ResizePseudoConsole failed: 0x%08X\n", hr);
}
}
}
VOID CConPTYManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength == 0) return;
switch (szBuffer[0]) {
case COMMAND_NEXT:
NotifyDialogIsOpen();
break;
case CMD_TERMINAL_RESIZE:
// Resize command: [cmd:1][cols:2][rows:2]
if (ulLength >= 5) {
int cols = *(short*)(szBuffer + 1);
int rows = *(short*)(szBuffer + 3);
ResizeTerminal(cols, rows);
}
break;
default:
// User input - write to PTY
if (m_hPipeOut) {
DWORD dwWritten = 0;
WriteFile(m_hPipeOut, szBuffer, ulLength, &dwWritten, nullptr);
}
break;
}
}
DWORD WINAPI CConPTYManager::ReadThread(LPVOID lParam)
{
CConPTYManager* pThis = (CConPTYManager*)lParam;
char buffer[4096];
// Wait for server terminal ready (WebView2 initialization may take time)
// Check m_bRunning every 500ms to allow quick exit
while (pThis->m_bRunning) {
DWORD result = WaitForSingleObject(pThis->m_hEventDlgOpen, 500);
if (result == WAIT_OBJECT_0) {
break; // Server is ready
}
// WAIT_TIMEOUT: continue loop and check m_bRunning
}
if (!pThis->m_bRunning) {
Mprintf("[ConPTY] Read thread exiting before server ready\n");
SAFE_CLOSE_HANDLE(pThis->m_hReadThread);
pThis->m_hReadThread = nullptr;
return 0;
}
Mprintf("[ConPTY] Server ready, starting to read\n");
while (pThis->m_bRunning) {
// Check if process has exited
if (pThis->m_hShellProcess) {
DWORD exitCode = 0;
if (GetExitCodeProcess(pThis->m_hShellProcess, &exitCode)) {
if (exitCode != STILL_ACTIVE) {
Mprintf("[ConPTY] Process exited with code %d\n", exitCode);
break;
}
}
}
// Check if pipe handle is still valid
if (!pThis->m_hPipeIn) {
Mprintf("[ConPTY] Pipe handle is null\n");
break;
}
// Check if data is available (non-blocking)
DWORD dwAvailable = 0;
if (!PeekNamedPipe(pThis->m_hPipeIn, nullptr, 0, nullptr, &dwAvailable, nullptr)) {
DWORD err = GetLastError();
if (err == ERROR_BROKEN_PIPE || err == ERROR_INVALID_HANDLE) {
Mprintf("[ConPTY] Pipe closed (err=%d)\n", err);
break;
}
// Other error, wait and retry
Sleep(10);
continue;
}
if (dwAvailable == 0) {
// No data available, wait a bit
Sleep(10);
continue;
}
// Read available data
DWORD dwRead = 0;
DWORD toRead = min(dwAvailable, (DWORD)sizeof(buffer));
if (!ReadFile(pThis->m_hPipeIn, buffer, toRead, &dwRead, nullptr)) {
DWORD err = GetLastError();
if (err != ERROR_BROKEN_PIPE && err != ERROR_INVALID_HANDLE) {
Mprintf("[ConPTY] ReadFile failed: %d\n", err);
}
break;
}
if (dwRead > 0) {
// Send to server
pThis->m_ClientObject->Send2Server(buffer, dwRead);
}
}
// Send close notification
if (pThis->m_ClientObject) {
BYTE closeToken = TOKEN_TERMINAL_CLOSE;
pThis->m_ClientObject->Send2Server((char*)&closeToken, 1);
Mprintf("[ConPTY] Sent TOKEN_TERMINAL_CLOSE\n");
}
SAFE_CLOSE_HANDLE(pThis->m_hReadThread);
pThis->m_hReadThread = nullptr;
Mprintf("[ConPTY] Read thread exited\n");
return 0;
}