File indexing completed on 2024-05-05 05:53:51

0001 /*
0002     SPDX-FileCopyrightText: 2019 Vitaly Petrov <v31337@gmail.com>
0003     SPDX-License-Identifier: MIT
0004 */
0005 #include "conptyprocess.h"
0006 #include <QCoreApplication>
0007 #include <QDebug>
0008 #include <QFile>
0009 #include <QFileInfo>
0010 #include <QMutexLocker>
0011 #include <QThread>
0012 #include <QTimer>
0013 #include <sstream>
0014 
0015 #define READ_INTERVAL_MSEC 500
0016 
0017 HRESULT ConPtyProcess::createPseudoConsoleAndPipes(HPCON *phPC, HANDLE *phPipeIn, HANDLE *phPipeOut, qint16 cols, qint16 rows)
0018 {
0019     HRESULT hr{E_UNEXPECTED};
0020     HANDLE hPipePTYIn{INVALID_HANDLE_VALUE};
0021     HANDLE hPipePTYOut{INVALID_HANDLE_VALUE};
0022 
0023     // Create the pipes to which the ConPTY will connect
0024     if (CreatePipe(&hPipePTYIn, phPipeOut, NULL, 0) && CreatePipe(phPipeIn, &hPipePTYOut, NULL, 0)) {
0025         // Create the Pseudo Console of the required size, attached to the PTY-end
0026         // of the pipes
0027         hr = m_winContext.createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, 0, phPC);
0028 
0029         // Note: We can close the handles to the PTY-end of the pipes here
0030         // because the handles are dup'ed into the ConHost and will be released
0031         // when the ConPTY is destroyed.
0032         if (INVALID_HANDLE_VALUE != hPipePTYOut)
0033             CloseHandle(hPipePTYOut);
0034         if (INVALID_HANDLE_VALUE != hPipePTYIn)
0035             CloseHandle(hPipePTYIn);
0036     }
0037 
0038     return hr;
0039 }
0040 
0041 // Initializes the specified startup info struct with the required properties
0042 // and updates its thread attribute list with the specified ConPTY handle
0043 HRESULT ConPtyProcess::initializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX *pStartupInfo, HPCON hPC)
0044 {
0045     HRESULT hr{E_UNEXPECTED};
0046 
0047     if (pStartupInfo) {
0048         SIZE_T attrListSize{};
0049 
0050         pStartupInfo->StartupInfo.hStdInput = m_hPipeIn;
0051         pStartupInfo->StartupInfo.hStdError = m_hPipeOut;
0052         pStartupInfo->StartupInfo.hStdOutput = m_hPipeOut;
0053         pStartupInfo->StartupInfo.dwFlags |= STARTF_USESTDHANDLES;
0054 
0055         pStartupInfo->StartupInfo.cb = sizeof(STARTUPINFOEX);
0056 
0057         // Get the size of the thread attribute list.
0058         InitializeProcThreadAttributeList(NULL, 1, 0, &attrListSize);
0059 
0060         // Allocate a thread attribute list of the correct size
0061         pStartupInfo->lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), 0, attrListSize));
0062 
0063         // Initialize thread attribute list
0064         if (pStartupInfo->lpAttributeList && InitializeProcThreadAttributeList(pStartupInfo->lpAttributeList, 1, 0, &attrListSize)) {
0065             // Set Pseudo Console attribute
0066             hr = UpdateProcThreadAttribute(pStartupInfo->lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hPC, sizeof(HPCON), NULL, NULL)
0067                 ? S_OK
0068                 : HRESULT_FROM_WIN32(GetLastError());
0069         } else {
0070             hr = HRESULT_FROM_WIN32(GetLastError());
0071         }
0072     }
0073     return hr;
0074 }
0075 
0076 ConPtyProcess::ConPtyProcess()
0077     : IPtyProcess()
0078     , m_ptyHandler{INVALID_HANDLE_VALUE}
0079     , m_hPipeIn{INVALID_HANDLE_VALUE}
0080     , m_hPipeOut{INVALID_HANDLE_VALUE}
0081     , m_readThread(nullptr)
0082 {
0083 }
0084 
0085 ConPtyProcess::~ConPtyProcess()
0086 {
0087     kill();
0088 }
0089 
0090 bool ConPtyProcess::startProcess(const QString &shellPath,
0091                                  const QStringList &arguments,
0092                                  const QString &workingDir,
0093                                  QStringList environment,
0094                                  qint16 cols,
0095                                  qint16 rows)
0096 {
0097     if (!isAvailable()) {
0098         m_lastError = m_winContext.lastError();
0099         return false;
0100     }
0101 
0102     // already running
0103     if (m_ptyHandler != INVALID_HANDLE_VALUE)
0104         return false;
0105 
0106     QFileInfo fi(shellPath);
0107     if (fi.isRelative() || !QFile::exists(shellPath)) {
0108         // todo add auto-find executable in PATH env var
0109         m_lastError = QStringLiteral("ConPty Error: shell file path must be absolute");
0110         return false;
0111     }
0112 
0113     m_shellPath = shellPath;
0114     m_size = QPair<qint16, qint16>(cols, rows);
0115 
0116     // env
0117     std::wstringstream envBlock;
0118     for (const QString &line : std::as_const(environment)) {
0119         envBlock << line.toStdWString() << '\0';
0120     }
0121     envBlock << '\0';
0122     std::wstring env = envBlock.str();
0123     auto envV = vectorFromString(env);
0124     LPWSTR envArg = envV.empty() ? nullptr : envV.data();
0125 
0126     QStringList exeAndArgs = arguments;
0127     exeAndArgs.prepend(m_shellPath);
0128     auto cmdArg = exeAndArgs.join(QLatin1String(" ")).toStdWString();
0129 
0130     HRESULT hr{E_UNEXPECTED};
0131 
0132     //  Create the Pseudo Console and pipes to it
0133     hr = createPseudoConsoleAndPipes(&m_ptyHandler, &m_hPipeIn, &m_hPipeOut, cols, rows);
0134 
0135     if (S_OK != hr) {
0136         m_lastError = QStringLiteral("ConPty Error: CreatePseudoConsoleAndPipes fail");
0137         return false;
0138     }
0139 
0140     // Initialize the necessary startup info struct
0141     if (S_OK != initializeStartupInfoAttachedToPseudoConsole(&m_shellStartupInfo, m_ptyHandler)) {
0142         m_lastError = QStringLiteral("ConPty Error: InitializeStartupInfoAttachedToPseudoConsole fail");
0143         return false;
0144     }
0145 
0146     // Launch ping to Q_EMIT some text back via the pipe
0147     hr = CreateProcess(NULL, // No module name - use Command Line
0148                        cmdArg.data(), // Command Line
0149                        NULL, // Process handle not inheritable
0150                        NULL, // Thread handle not inheritable
0151                        FALSE, // Inherit handles
0152                        EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // Creation flags
0153                        envArg, // Environment block
0154                        workingDir.toStdWString().data(), // Use parent's starting directory
0155                        &m_shellStartupInfo.StartupInfo, // Pointer to STARTUPINFO
0156                        &m_shellProcessInformation) // Pointer to PROCESS_INFORMATION
0157         ? S_OK
0158         : GetLastError();
0159 
0160     if (hr != S_OK) {
0161         m_lastError = QStringLiteral("ConPty Error: Cannot create process -> %1").arg(hr);
0162         return false;
0163     }
0164     m_pid = m_shellProcessInformation.dwProcessId;
0165 
0166     // Notify when the shell process has been terminated
0167     RegisterWaitForSingleObject(
0168         &m_shellCloseWaitHandle,
0169         m_shellProcessInformation.hProcess,
0170         [](PVOID data, BOOLEAN) {
0171             auto self = static_cast<ConPtyProcess *>(data);
0172             DWORD exitCode = 0;
0173             GetExitCodeProcess(self->m_shellProcessInformation.hProcess, &exitCode);
0174             self->m_exitCode = exitCode;
0175             // Do not respawn if the object is about to be destructed
0176             if (!self->m_aboutToDestruct)
0177                 Q_EMIT self->notifier()->aboutToClose();
0178             Q_EMIT self->exited();
0179         },
0180         this,
0181         INFINITE,
0182         WT_EXECUTEONLYONCE);
0183 
0184     // this code runned in separate thread
0185     m_readThread = QThread::create([this]() {
0186         // buffers
0187         const DWORD BUFF_SIZE{1024};
0188         char szBuffer[BUFF_SIZE]{};
0189 
0190         while (true) {
0191             DWORD dwBytesRead{};
0192 
0193             // Read from the pipe
0194             BOOL result = ReadFile(m_hPipeIn, szBuffer, BUFF_SIZE, &dwBytesRead, NULL);
0195 
0196             const bool needMoreData = !result && GetLastError() == ERROR_MORE_DATA;
0197             if (result || needMoreData) {
0198                 QMutexLocker locker(&m_bufferMutex);
0199                 m_buffer.m_readBuffer.append(szBuffer, dwBytesRead);
0200                 m_buffer.emitReadyRead();
0201             }
0202 
0203             const bool brokenPipe = !result && GetLastError() == ERROR_BROKEN_PIPE;
0204             if (QThread::currentThread()->isInterruptionRequested() || brokenPipe)
0205                 break;
0206         }
0207     });
0208 
0209     // start read thread
0210     m_readThread->start();
0211 
0212     return true;
0213 }
0214 
0215 bool ConPtyProcess::resize(qint16 cols, qint16 rows)
0216 {
0217     if (m_ptyHandler == nullptr) {
0218         return false;
0219     }
0220 
0221     bool res = SUCCEEDED(m_winContext.resizePseudoConsole(m_ptyHandler, {cols, rows}));
0222 
0223     if (res) {
0224         m_size = QPair<qint16, qint16>(cols, rows);
0225     }
0226 
0227     return res;
0228 }
0229 
0230 bool ConPtyProcess::kill()
0231 {
0232     bool exitCode = false;
0233 
0234     if (m_ptyHandler != INVALID_HANDLE_VALUE) {
0235         m_aboutToDestruct = true;
0236 
0237         // Close ConPTY - this will terminate client process if running
0238         m_winContext.closePseudoConsole(m_ptyHandler);
0239 
0240         // Clean-up the pipes
0241         if (INVALID_HANDLE_VALUE != m_hPipeOut)
0242             CloseHandle(m_hPipeOut);
0243         if (INVALID_HANDLE_VALUE != m_hPipeIn)
0244             CloseHandle(m_hPipeIn);
0245 
0246         m_readThread->requestInterruption();
0247         if (!m_readThread->wait(1000))
0248             m_readThread->terminate();
0249         m_readThread->deleteLater();
0250         m_readThread = nullptr;
0251 
0252         m_pid = 0;
0253         m_ptyHandler = INVALID_HANDLE_VALUE;
0254         m_hPipeIn = INVALID_HANDLE_VALUE;
0255         m_hPipeOut = INVALID_HANDLE_VALUE;
0256 
0257         CloseHandle(m_shellProcessInformation.hThread);
0258         CloseHandle(m_shellProcessInformation.hProcess);
0259         UnregisterWait(m_shellCloseWaitHandle);
0260 
0261         // Cleanup attribute list
0262         if (m_shellStartupInfo.lpAttributeList) {
0263             DeleteProcThreadAttributeList(m_shellStartupInfo.lpAttributeList);
0264             HeapFree(GetProcessHeap(), 0, m_shellStartupInfo.lpAttributeList);
0265         }
0266 
0267         exitCode = true;
0268     }
0269 
0270     return exitCode;
0271 }
0272 
0273 IPtyProcess::PtyType ConPtyProcess::type()
0274 {
0275     return PtyType::ConPty;
0276 }
0277 
0278 QString ConPtyProcess::dumpDebugInfo()
0279 {
0280 #ifdef PTYQT_DEBUG
0281     return QStringLiteral("PID: %1, Type: %2, Cols: %3, Rows: %4").arg(m_pid).arg(type()).arg(m_size.first).arg(m_size.second);
0282 #else
0283     return QStringLiteral("Nothing...");
0284 #endif
0285 }
0286 
0287 QIODevice *ConPtyProcess::notifier()
0288 {
0289     return &m_buffer;
0290 }
0291 
0292 QByteArray ConPtyProcess::readAll()
0293 {
0294     QByteArray result;
0295     {
0296         QMutexLocker locker(&m_bufferMutex);
0297         result.swap(m_buffer.m_readBuffer);
0298     }
0299     return result;
0300 }
0301 
0302 qint64 ConPtyProcess::write(const char *data, int size)
0303 {
0304     DWORD dwBytesWritten{};
0305     WriteFile(m_hPipeOut, data, size, &dwBytesWritten, NULL);
0306     return dwBytesWritten;
0307 }
0308 
0309 bool ConPtyProcess::isAvailable()
0310 {
0311     // #ifdef TOO_OLD_WINSDK
0312     // return false; // very importnant! ConPty can be built, but it doesn't work
0313     // if built with old sdk and Win10 < 1903
0314     // #endif
0315 
0316     qint32 buildNumber = QSysInfo::kernelVersion().split(QLatin1String(".")).last().toInt();
0317     if (buildNumber < CONPTY_MINIMAL_WINDOWS_VERSION)
0318         return false;
0319     return m_winContext.init();
0320 }
0321 
0322 void ConPtyProcess::moveToThread(QThread *targetThread)
0323 {
0324     // nothing for now...
0325 }
0326 
0327 int ConPtyProcess::processList() const
0328 {
0329     return 0;
0330 }
0331 
0332 #include "moc_conptyprocess.cpp"