// SPDX-License-Identifier: MIT // Author: Diego Iastrubni #include "PtyProcess_win.h" #include #include #include // Define necessary types if building on older SDKs or mingw that might lack them // But assuming standard modern environment. class PtyProcessWin::ReaderThread : public QThread { public: ReaderThread(HANDLE hPipe, PtyProcessWin *parent) : m_hPipe(hPipe), m_parent(parent) {} void run() override { char buffer[4096]; DWORD bytesRead; while (m_running) { if (ReadFile(m_hPipe, buffer, sizeof(buffer), &bytesRead, NULL)) { if (bytesRead > 0) { QByteArray data(buffer, (int)bytesRead); QMetaObject::invokeMethod(m_parent, "onReadThreadData", Qt::QueuedConnection, Q_ARG(QByteArray, data)); } else { // EOF (bytesRead == 0) break; } } else { DWORD err = GetLastError(); if (err == ERROR_BROKEN_PIPE || err == ERROR_HANDLE_EOF) { break; } break; } } } void stop() { m_running = false; } private: HANDLE m_hPipe; PtyProcessWin *m_parent; bool m_running = true; }; PtyProcessWin::PtyProcessWin(QObject *parent) : PtyProcess(parent) { ZeroMemory(&m_pi, sizeof(PROCESS_INFORMATION)); m_hPC = INVALID_HANDLE_VALUE; m_hPipeIn = INVALID_HANDLE_VALUE; m_hPipeOut = INVALID_HANDLE_VALUE; } PtyProcessWin::~PtyProcessWin() { kill(); // kill() now closes everything properly } bool PtyProcessWin::start(const QSize &size) { if (m_program.isEmpty()) { return false; } HANDLE hPipePTYIn = INVALID_HANDLE_VALUE; HANDLE hPipePTYOut = INVALID_HANDLE_VALUE; // Create pipes if (!CreatePipe(&hPipePTYIn, &m_hPipeOut, NULL, 0)) { return false; } if (!CreatePipe(&m_hPipeIn, &hPipePTYOut, NULL, 0)) { return false; } // Create Pseudo Console COORD origin = {(SHORT)size.width(), (SHORT)size.height()}; HRESULT hr = CreatePseudoConsole(origin, hPipePTYIn, hPipePTYOut, 0, &m_hPC); // Close the sides we don't need CloseHandle(hPipePTYIn); CloseHandle(hPipePTYOut); if (FAILED(hr)) { return false; } // Prepare Startup Info STARTUPINFOEX si; ZeroMemory(&si, sizeof(STARTUPINFOEX)); si.StartupInfo.cb = sizeof(STARTUPINFOEX); SIZE_T bytesRequired = 0; InitializeProcThreadAttributeList(NULL, 1, 0, &bytesRequired); si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, bytesRequired); if (!si.lpAttributeList) { return false; } if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &bytesRequired)) { return false; } if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, m_hPC, sizeof(HPCON), NULL, NULL)) { return false; } // Command Line QString programNative = QDir::toNativeSeparators(m_program); QString cmd = programNative; if (cmd.contains(' ')) { cmd = "\"" + cmd + "\""; } for (const auto &arg : m_arguments) { cmd += " " + arg; // Simple quoting might be needed } // Working directory wchar_t *pWorkingDirectory = nullptr; std::vector workingDir; if (!m_workingDirectory.isEmpty()) { workingDir.resize(m_workingDirectory.length() + 1); m_workingDirectory.toWCharArray(workingDir.data()); workingDir[m_workingDirectory.length()] = 0; pWorkingDirectory = workingDir.data(); } // Environment std::vector envBlock; for (const auto &key : m_environment.keys()) { QString entry = key + "=" + m_environment.value(key); for (QChar c : entry) { envBlock.push_back(c.unicode()); } envBlock.push_back(0); } envBlock.push_back(0); // Create Process std::vector cmdLine(cmd.length() + 1); cmd.toWCharArray(cmdLine.data()); cmdLine[cmd.length()] = 0; BOOL success = CreateProcessW(NULL, cmdLine.data(), NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, envBlock.data(), pWorkingDirectory, &si.StartupInfo, &m_pi); // Cleanup attribute list DeleteProcThreadAttributeList(si.lpAttributeList); HeapFree(GetProcessHeap(), 0, si.lpAttributeList); if (!success) { return false; } // Start reader thread m_readerThread = new ReaderThread(m_hPipeIn, this); m_readerThread->start(); return true; } bool PtyProcessWin::start(const QString &program, const QStringList &arguments, const QSize &size) { return PtyProcess::start(program, arguments, size); } void PtyProcessWin::write(const QByteArray &data) { if (m_hPipeOut != INVALID_HANDLE_VALUE) { DWORD bytesWritten; WriteFile(m_hPipeOut, data.constData(), data.size(), &bytesWritten, NULL); } } void PtyProcessWin::resize(const QSize &size) { if (m_hPC != INVALID_HANDLE_VALUE) { COORD origin = {(SHORT)size.width(), (SHORT)size.height()}; ResizePseudoConsole(m_hPC, origin); } } void PtyProcessWin::kill() { // First, close the pseudo console — this is the key fix // It immediately unblocks any pending ReadFile on the pipes if (m_hPC != INVALID_HANDLE_VALUE) { ClosePseudoConsole(m_hPC); m_hPC = INVALID_HANDLE_VALUE; } // Stop and clean up reader thread if (m_readerThread) { m_readerThread->stop(); // Close our read handle as a safety net (though ClosePseudoConsole already unblocked) if (m_hPipeIn != INVALID_HANDLE_VALUE) { CloseHandle(m_hPipeIn); m_hPipeIn = INVALID_HANDLE_VALUE; } m_readerThread->wait(3000); // give it a reasonable timeout if (m_readerThread->isRunning()) { m_readerThread->terminate(); // last resort m_readerThread->wait(); } delete m_readerThread; m_readerThread = nullptr; } // Close write pipe if (m_hPipeOut != INVALID_HANDLE_VALUE) { CloseHandle(m_hPipeOut); m_hPipeOut = INVALID_HANDLE_VALUE; } // Terminate child process if still running if (m_pi.hProcess) { TerminateProcess(m_pi.hProcess, 1); WaitForSingleObject(m_pi.hProcess, 5000); CloseHandle(m_pi.hProcess); CloseHandle(m_pi.hThread); m_pi.hProcess = NULL; m_pi.hThread = NULL; } } bool PtyProcessWin::isRoot() const { // Could check for elevation here return false; } QString PtyProcessWin::foregroundProcessName() const { return QFileInfo(m_program).baseName(); } void PtyProcessWin::onReadThreadData(const QByteArray &data) { emit readyRead(data); }