#include "conptyprocess.h" #include #include #include #include #include #include #include #define READ_INTERVAL_MSEC 500 HRESULT ConPtyProcess::createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut, qint16 cols, qint16 rows) { HRESULT hr{ E_UNEXPECTED }; HANDLE hPipePTYIn{ INVALID_HANDLE_VALUE }; HANDLE hPipePTYOut{ INVALID_HANDLE_VALUE }; // Create the pipes to which the ConPTY will connect if (CreatePipe(&hPipePTYIn, phPipeOut, NULL, 0) && CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0)) { // Create the Pseudo Console of the required size, attached to the PTY-end of the pipes hr = m_winContext.createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, 0, phPC); // Note: We can close the handles to the PTY-end of the pipes here // because the handles are dup'ed into the ConHost and will be released // when the ConPTY is destroyed. if (INVALID_HANDLE_VALUE != hPipePTYOut) CloseHandle(hPipePTYOut); if (INVALID_HANDLE_VALUE != hPipePTYIn) CloseHandle(hPipePTYIn); } return hr; } // Initializes the specified startup info struct with the required properties and // updates its thread attribute list with the specified ConPTY handle HRESULT ConPtyProcess::initializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC) { HRESULT hr{ E_UNEXPECTED }; if (pStartupInfo) { size_t attrListSize{}; pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX); // Get the size of the thread attribute list. InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize); // Allocate a thread attribute list of the correct size pStartupInfo->lpAttributeList = reinterpret_cast(malloc(attrListSize)); // Initialize thread attribute list if (pStartupInfo->lpAttributeList && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize)) { // Set Pseudo Console attribute hr = UpdateProcThreadAttribute( pStartupInfo->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hPC, sizeof(HPCON), NULL, NULL) ? S_OK : HRESULT_FROM_WIN32(GetLastError()); } else { hr = HRESULT_FROM_WIN32(GetLastError()); } } return hr; } ConPtyProcess::ConPtyProcess() : IPtyProcess() , m_ptyHandler { INVALID_HANDLE_VALUE } , m_hPipeIn { INVALID_HANDLE_VALUE } , m_hPipeOut { INVALID_HANDLE_VALUE } , m_readThread(nullptr) { } ConPtyProcess::~ConPtyProcess() { kill(); } bool ConPtyProcess::startProcess(const QString &shellPath, QStringList environment, qint16 cols, qint16 rows) { if (!isAvailable()) { m_lastError = m_winContext.lastError(); return false; } //already running if (m_ptyHandler != INVALID_HANDLE_VALUE) return false; QFileInfo fi(shellPath); if (fi.isRelative() || !QFile::exists(shellPath)) { //todo add auto-find executable in PATH env var m_lastError = QString("ConPty Error: shell file path must be absolute"); return false; } m_shellPath = shellPath; m_size = QPair(cols, rows); //env std::stringstream envBlock; foreach (QString line, environment) { envBlock << line.toStdString() << '\0'; } envBlock << '\0'; std::string env = envBlock.str(); auto envV = vectorFromString(env); LPSTR envArg = envV.empty() ? nullptr : envV.data(); LPSTR cmdArg = new char[m_shellPath.toStdString().length() + 1]; std::strcpy(cmdArg, m_shellPath.toStdString().c_str()); //qDebug() << "m_shellPath" << m_shellPath << cmdArg << m_shellPath.toStdString().c_str(); HRESULT hr{ E_UNEXPECTED }; // Create the Pseudo Console and pipes to it hr = createPseudoConsoleAndPipes(&m_ptyHandler, &m_hPipeIn, &m_hPipeOut, cols, rows); if (S_OK != hr) { m_lastError = QString("ConPty Error: CreatePseudoConsoleAndPipes fail"); return false; } // Initialize the necessary startup info struct STARTUPINFOEX startupInfo{}; if (S_OK != initializeStartupInfoAttachedToPseudoConsole(&startupInfo, m_ptyHandler)) { m_lastError = QString("ConPty Error: InitializeStartupInfoAttachedToPseudoConsole fail"); return false; } // Launch ping to emit some text back via the pipe PROCESS_INFORMATION piClient{}; hr = CreateProcess( NULL, // No module name - use Command Line cmdArg, // Command Line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Inherit handles EXTENDED_STARTUPINFO_PRESENT, // Creation flags envArg, //NULL, // Use parent's environment block NULL, // Use parent's starting directory &startupInfo.StartupInfo, // Pointer to STARTUPINFO &piClient) // Pointer to PROCESS_INFORMATION ? S_OK : GetLastError(); if (S_OK != hr) { m_lastError = QString("ConPty Error: Cannot create process -> %1").arg(hr); return false; } m_pid = piClient.dwProcessId; //this code runned in separate thread m_readThread = QThread::create([this, &piClient, &startupInfo]() { forever { //buffers const DWORD BUFF_SIZE{ 512 }; char szBuffer[BUFF_SIZE]{}; //DWORD dwBytesWritten{}; DWORD dwBytesRead{}; BOOL fRead{ FALSE }; // Read from the pipe fRead = ReadFile(m_hPipeIn, szBuffer, BUFF_SIZE, &dwBytesRead, NULL); { QMutexLocker locker(&m_bufferMutex); m_buffer.m_readBuffer.append(szBuffer, dwBytesRead); m_buffer.emitReadyRead(); } if (QThread::currentThread()->isInterruptionRequested()) break; QCoreApplication::processEvents(); } // Now safe to clean-up client app's process-info & thread CloseHandle(piClient.hThread); CloseHandle(piClient.hProcess); // Cleanup attribute list DeleteProcThreadAttributeList(startupInfo.lpAttributeList); //free(startupInfo.lpAttributeList); }); //start read thread m_readThread->start(); return true; } bool ConPtyProcess::resize(qint16 cols, qint16 rows) { if (m_ptyHandler == nullptr) { return false; } bool res = SUCCEEDED(m_winContext.resizePseudoConsole(m_ptyHandler, {cols, rows})); if (res) { m_size = QPair(cols, rows); } return res; return true; } bool ConPtyProcess::kill() { bool exitCode = false; if ( m_ptyHandler != INVALID_HANDLE_VALUE ) { m_readThread->requestInterruption(); QThread::msleep(200); m_readThread->quit(); m_readThread->deleteLater(); m_readThread = nullptr; // Close ConPTY - this will terminate client process if running m_winContext.closePseudoConsole(m_ptyHandler); // Clean-up the pipes if (INVALID_HANDLE_VALUE != m_hPipeOut) CloseHandle(m_hPipeOut); if (INVALID_HANDLE_VALUE != m_hPipeIn) CloseHandle(m_hPipeIn); m_pid = 0; m_ptyHandler = INVALID_HANDLE_VALUE; m_hPipeIn = INVALID_HANDLE_VALUE; m_hPipeOut = INVALID_HANDLE_VALUE; exitCode = true; } return exitCode; } IPtyProcess::PtyType ConPtyProcess::type() { return PtyType::ConPty; } #ifdef PTYQT_DEBUG QString ConPtyProcess::dumpDebugInfo() { return QString("PID: %1, Type: %2, Cols: %3, Rows: %4") .arg(m_pid).arg(type()) .arg(m_size.first).arg(m_size.second); } #endif QIODevice *ConPtyProcess::notifier() { return &m_buffer; } QByteArray ConPtyProcess::readAll() { QMutexLocker locker(&m_bufferMutex); return m_buffer.m_readBuffer; } qint64 ConPtyProcess::write(const QByteArray &byteArray) { DWORD dwBytesWritten{}; WriteFile(m_hPipeOut, byteArray.data(), byteArray.size(), &dwBytesWritten, NULL); return dwBytesWritten; } bool ConPtyProcess::isAvailable() { #ifdef TOO_OLD_WINSDK return false; //very importnant! ConPty can be built, but it doesn't work if built with old sdk and Win10 < 1903 #endif qint32 buildNumber = QSysInfo::kernelVersion().split(".").last().toInt(); if (buildNumber < CONPTY_MINIMAL_WINDOWS_VERSION) return false; return m_winContext.init(); }