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"