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