diff options
Diffstat (limited to 'src/libs/3rdparty/libptyqt')
-rw-r--r-- | src/libs/3rdparty/libptyqt/.clang-format | 1 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/CMakeLists.txt | 28 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/LICENSE | 21 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/LICENSE-CONPTY | 21 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/conptyprocess.cpp | 1148 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/conptyprocess.h | 90 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/iptyprocess.h | 62 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/ptyqt.cpp | 56 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/ptyqt.h | 14 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/ptyqt.qbs | 40 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/unixptyprocess.cpp | 362 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/unixptyprocess.h | 66 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/winptyprocess.cpp | 278 | ||||
-rw-r--r-- | src/libs/3rdparty/libptyqt/winptyprocess.h | 43 |
14 files changed, 2230 insertions, 0 deletions
diff --git a/src/libs/3rdparty/libptyqt/.clang-format b/src/libs/3rdparty/libptyqt/.clang-format new file mode 100644 index 0000000000..b861ff7a95 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/.clang-format @@ -0,0 +1 @@ +{ "DisableFormat" : true }
\ No newline at end of file diff --git a/src/libs/3rdparty/libptyqt/CMakeLists.txt b/src/libs/3rdparty/libptyqt/CMakeLists.txt new file mode 100644 index 0000000000..c6e8b74573 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/CMakeLists.txt @@ -0,0 +1,28 @@ +set(SOURCES + iptyprocess.h + ptyqt.cpp ptyqt.h +) + +if (WIN32) + list(APPEND SOURCES + winptyprocess.cpp winptyprocess.h + conptyprocess.cpp conptyprocess.h + ) +else() + list(APPEND SOURCES unixptyprocess.cpp unixptyprocess.h) +endif() + +add_library(ptyqt STATIC ${SOURCES}) +target_link_libraries(ptyqt PUBLIC Qt::Core) + +if (WIN32) + target_link_libraries(ptyqt PRIVATE winpty Qt::Network) + #target_compile_definitions(ptyqt PRIVATE PTYQT_DEBUG) +endif() + +set_target_properties(ptyqt + PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR} + QT_COMPILE_OPTIONS_DISABLE_WARNINGS ON + POSITION_INDEPENDENT_CODE ON +) diff --git a/src/libs/3rdparty/libptyqt/LICENSE b/src/libs/3rdparty/libptyqt/LICENSE new file mode 100644 index 0000000000..73996c7c90 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Vitaly Petrov, v31337@gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.
\ No newline at end of file diff --git a/src/libs/3rdparty/libptyqt/LICENSE-CONPTY b/src/libs/3rdparty/libptyqt/LICENSE-CONPTY new file mode 100644 index 0000000000..8cb179cdb6 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/LICENSE-CONPTY @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/libs/3rdparty/libptyqt/conptyprocess.cpp b/src/libs/3rdparty/libptyqt/conptyprocess.cpp new file mode 100644 index 0000000000..94bb53b583 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/conptyprocess.cpp @@ -0,0 +1,1148 @@ +#include "conptyprocess.h" +#include <QFile> +#include <QFileInfo> +#include <QThread> +#include <sstream> +#include <QTimer> +#include <QMutexLocker> +#include <QCoreApplication> +#include <QWinEventNotifier> + +#include <qt_windows.h> + +#ifdef QTCREATOR_PCH_H +#ifndef IN +#define IN +#endif +#ifndef OUT +#define OUT +#endif +#endif + +#include <winternl.h> + +#define READ_INTERVAL_MSEC 500 + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 + +//////////////////////////////////////////////////////////////////////////////////////// +/// Ported from wil/result_macros.h + +#define FAILED_NTSTATUS(status) (((NTSTATUS)(status)) < 0) +#define SUCCEEDED_NTSTATUS(status) (((NTSTATUS)(status)) >= 0) + +#define RETURN_IF_NTSTATUS_FAILED(call) if(FAILED_NTSTATUS(call)) return E_FAIL; +#define RETURN_IF_WIN32_BOOL_FALSE(call) if((call) == FALSE) return E_FAIL; +#define RETURN_IF_NULL_ALLOC(call) if((call) == nullptr) return E_OUTOFMEMORY; +#define RETURN_IF_FAILED(call) {HRESULT hr = (call); if(hr != S_OK)return hr; } + +//! Set zero or more bitflags specified by `flags` in the variable `var`. +#define WI_SetAllFlags(var, flags) ((var) |= (flags)) +//! Set a single compile-time constant `flag` in the variable `var`. +#define WI_SetFlag(var, flag) WI_SetAllFlags(var,flag) + +//////////////////////////////////////////////////////////////////////////////////////// +/// Ported from wil +class unique_hmodule : public std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)> +{ +public: + unique_hmodule() : std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>(nullptr, FreeLibrary) {} + unique_hmodule(HMODULE module) : std::unique_ptr<std::remove_pointer_t<HMODULE>, decltype(&FreeLibrary)>(module, FreeLibrary) {} +}; + +class unique_handle : public std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)> +{ +public: + unique_handle() : std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)>(nullptr, CloseHandle) {} + unique_handle(HANDLE module) : std::unique_ptr<std::remove_pointer_t<HANDLE>, decltype(&CloseHandle)>(module, CloseHandle) {} + + + class AddressOf { + public: + AddressOf(unique_handle &h) : m_h(h) {} + ~AddressOf() { + m_h.reset(m_dest); + } + + operator PHANDLE() {return &m_dest;} + + HANDLE m_dest{INVALID_HANDLE_VALUE}; + unique_handle &m_h; + }; + + AddressOf addressof() { + return AddressOf(*this); + } +}; + +class unique_process_information : public PROCESS_INFORMATION +{ +public: + unique_process_information() { + hProcess = 0; + hThread = 0; + dwProcessId = 0; + dwThreadId = 0; + } + ~unique_process_information() { + if (hProcess) + { + CloseHandle(hProcess); + } + + if (hThread) + { + CloseHandle(hThread); + } + } + + PROCESS_INFORMATION* addressof() { + return this; + } +}; + +template <typename TLambda> +class on_scope_exit { +public: + TLambda m_func; + bool m_call{true}; + on_scope_exit(TLambda &&func) : m_func(std::move(func)) {} + ~on_scope_exit() {if(m_call)m_func();} + + void release() {m_call = false;} +}; + +template <typename TLambda> +[[nodiscard]] inline auto scope_exit(TLambda&& lambda) noexcept +{ + return on_scope_exit<TLambda>(std::forward<TLambda>(lambda)); +} + +//////////////////////////////////////////////////////////////////////////////////////// + + + +class WinNTControl +{ +public: + [[nodiscard]] static NTSTATUS NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions); + +private: + WinNTControl(); + + WinNTControl(WinNTControl const&) = delete; + void operator=(WinNTControl const&) = delete; + + static WinNTControl& GetInstance(); + + unique_hmodule const _NtDllDll; + + typedef NTSTATUS(NTAPI* PfnNtOpenFile)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PIO_STATUS_BLOCK, ULONG, ULONG); + PfnNtOpenFile const _NtOpenFile; +}; + +WinNTControl::WinNTControl() : + // NOTE: Use LoadLibraryExW with LOAD_LIBRARY_SEARCH_SYSTEM32 flag below to avoid unneeded directory traversal. + // This has triggered CPG boot IO warnings in the past. + _NtDllDll(/*THROW_LAST_ERROR_IF_NULL*/(LoadLibraryExW(L"ntdll.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32))), + _NtOpenFile(reinterpret_cast<PfnNtOpenFile>(/*THROW_LAST_ERROR_IF_NULL*/(GetProcAddress(_NtDllDll.get(), "NtOpenFile")))) +{ +} + +// Routine Description: +// - Provides the singleton pattern for WinNT control. Stores the single instance and returns it. +// Arguments: +// - <none> +// Return Value: +// - Reference to the single instance of NTDLL.dll wrapped methods. +WinNTControl& WinNTControl::GetInstance() +{ + static WinNTControl Instance; + return Instance; +} + +// Routine Description: +// - Provides access to the NtOpenFile method documented at: +// https://msdn.microsoft.com/en-us/library/bb432381(v=vs.85).aspx +// Arguments: +// - See definitions at MSDN +// Return Value: +// - See definitions at MSDN +[[nodiscard]] NTSTATUS WinNTControl::NtOpenFile(_Out_ PHANDLE FileHandle, + _In_ ACCESS_MASK DesiredAccess, + _In_ POBJECT_ATTRIBUTES ObjectAttributes, + _Out_ PIO_STATUS_BLOCK IoStatusBlock, + _In_ ULONG ShareAccess, + _In_ ULONG OpenOptions) +{ + return GetInstance()._NtOpenFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, ShareAccess, OpenOptions); +} + +namespace DeviceHandle { + +/*++ +Routine Description: +- This routine opens a handle to the console driver. + +Arguments: +- Handle - Receives the handle. +- DeviceName - Supplies the name to be used to open the console driver. +- DesiredAccess - Supplies the desired access mask. +- Parent - Optionally supplies the parent object. +- Inheritable - Supplies a boolean indicating if the new handle is to be made inheritable. +- OpenOptions - Supplies the open options to be passed to NtOpenFile. A common + option for clients is FILE_SYNCHRONOUS_IO_NONALERT, to make the handle + synchronous. + +Return Value: +- NTSTATUS indicating if the handle was successfully created. +--*/ +[[nodiscard]] NTSTATUS +_CreateHandle( + _Out_ PHANDLE Handle, + _In_ PCWSTR DeviceName, + _In_ ACCESS_MASK DesiredAccess, + _In_opt_ HANDLE Parent, + _In_ BOOLEAN Inheritable, + _In_ ULONG OpenOptions) + +{ + ULONG Flags = OBJ_CASE_INSENSITIVE; + + if (Inheritable) + { + WI_SetFlag(Flags, OBJ_INHERIT); + } + + UNICODE_STRING Name; +#pragma warning(suppress : 26492) // const_cast is prohibited, but we can't avoid it for filling UNICODE_STRING. + Name.Buffer = const_cast<wchar_t*>(DeviceName); + //Name.Length = gsl::narrow_cast<USHORT>((wcslen(DeviceName) * sizeof(wchar_t))); + Name.Length = static_cast<unsigned short>((wcslen(DeviceName) * sizeof(wchar_t))); + Name.MaximumLength = Name.Length + sizeof(wchar_t); + + OBJECT_ATTRIBUTES ObjectAttributes; +#pragma warning(suppress : 26477) // The QOS part of this macro in the define is 0. Can't fix that. + InitializeObjectAttributes(&ObjectAttributes, + &Name, + Flags, + Parent, + nullptr); + + IO_STATUS_BLOCK IoStatus; + return WinNTControl::NtOpenFile(Handle, + DesiredAccess, + &ObjectAttributes, + &IoStatus, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + OpenOptions); +} + +/*++ +Routine Description: +- This routine creates a handle to an input or output client of the given + server. No control io is sent to the server as this request must be coming + from the server itself. + +Arguments: +- Handle - Receives a handle to the new client. +- ServerHandle - Supplies a handle to the server to which to attach the + newly created client. +- Name - Supplies the name of the client object. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the client was successfully created. +--*/ +[[nodiscard]] NTSTATUS +CreateClientHandle( + _Out_ PHANDLE Handle, + _In_ HANDLE ServerHandle, + _In_ PCWSTR Name, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + Name, + GENERIC_WRITE | GENERIC_READ | SYNCHRONIZE, + ServerHandle, + Inheritable, + FILE_SYNCHRONOUS_IO_NONALERT); +} + +/*++ +Routine Description: +- This routine creates a new server on the driver and returns a handle to it. + +Arguments: +- Handle - Receives a handle to the new server. +- Inheritable - Supplies a flag indicating if the handle must be inheritable. + +Return Value: +- NTSTATUS indicating if the console was successfully created. +--*/ +[[nodiscard]] NTSTATUS +CreateServerHandle( + _Out_ PHANDLE Handle, + _In_ BOOLEAN Inheritable) +{ + return _CreateHandle(Handle, + L"\\Device\\ConDrv\\Server", + GENERIC_ALL, + nullptr, + Inheritable, + 0); +} + + +} // namespace DeviceHandle + +[[nodiscard]] static inline NTSTATUS CreateClientHandle(PHANDLE Handle, HANDLE ServerHandle, PCWSTR Name, BOOLEAN Inheritable) +{ + return DeviceHandle::CreateClientHandle(Handle, ServerHandle, Name, Inheritable); +} + +[[nodiscard]] static inline NTSTATUS CreateServerHandle(PHANDLE Handle, BOOLEAN Inheritable) +{ + return DeviceHandle::CreateServerHandle(Handle, Inheritable); +} + +typedef struct _PseudoConsole +{ + HANDLE hSignal; + HANDLE hPtyReference; + HANDLE hConPtyProcess; +} PseudoConsole; + +// Signals +// These are not defined publicly, but are used for controlling the conpty via +// the signal pipe. +#define PTY_SIGNAL_CLEAR_WINDOW (2u) +#define PTY_SIGNAL_RESIZE_WINDOW (8u) + +// CreatePseudoConsole Flags +// The other flag (PSEUDOCONSOLE_INHERIT_CURSOR) is actually defined in consoleapi.h in the OS repo +#ifndef PSEUDOCONSOLE_INHERIT_CURSOR +#define PSEUDOCONSOLE_INHERIT_CURSOR (0x1) +#endif +#define PSEUDOCONSOLE_RESIZE_QUIRK (0x2) +#define PSEUDOCONSOLE_WIN32_INPUT_MODE (0x4) + +static QString qSystemDirectory() +{ + static const QString result = []() -> QString { + QVarLengthArray<wchar_t, MAX_PATH> fullPath = {}; + UINT retLen = ::GetSystemDirectoryW(fullPath.data(), MAX_PATH); + if (retLen > MAX_PATH) { + fullPath.resize(retLen); + retLen = ::GetSystemDirectoryW(fullPath.data(), retLen); + } + // in some rare cases retLen might be 0 + return QString::fromWCharArray(fullPath.constData(), int(retLen)); + }(); + return result; +} + +// Function Description: +// - Returns the path to conhost.exe as a process heap string. +static QString _InboxConsoleHostPath() +{ + return QString("\\\\?\\%1\\conhost.exe").arg(qSystemDirectory()); +} + +// Function Description: +// - Returns the path to either conhost.exe or the side-by-side OpenConsole, depending on whether this +// module is building with Windows and OpenConsole could be found. +// Return Value: +// - A pointer to permanent storage containing the path to the console host. +static const wchar_t* _ConsoleHostPath() +{ + // Use the magic of magic statics to only calculate this once. + static QString consoleHostPath = _InboxConsoleHostPath(); + return reinterpret_cast<const wchar_t*>(consoleHostPath.utf16()); +} + +static bool _HandleIsValid(HANDLE h) noexcept +{ + return (h != INVALID_HANDLE_VALUE) && (h != nullptr); +} + + + +HRESULT _CreatePseudoConsole(const HANDLE hToken, + const COORD size, + const HANDLE hInput, + const HANDLE hOutput, + const DWORD dwFlags, + _Inout_ PseudoConsole* pPty) +{ + if (pPty == nullptr) + { + return E_INVALIDARG; + } + if (size.X == 0 || size.Y == 0) + { + return E_INVALIDARG; + } + + unique_handle serverHandle; + RETURN_IF_NTSTATUS_FAILED(CreateServerHandle(serverHandle.addressof(), TRUE)); + + unique_handle signalPipeConhostSide; + unique_handle signalPipeOurSide; + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + // Mark inheritable for signal handle when creating. It'll have the same value on the other side. + sa.bInheritHandle = FALSE; + sa.lpSecurityDescriptor = nullptr; + + RETURN_IF_WIN32_BOOL_FALSE(CreatePipe(signalPipeConhostSide.addressof(), signalPipeOurSide.addressof(), &sa, 0)); + RETURN_IF_WIN32_BOOL_FALSE(SetHandleInformation(signalPipeConhostSide.get(), HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)); + + // GH4061: Ensure that the path to executable in the format is escaped so C:\Program.exe cannot collide with C:\Program Files + const wchar_t* pwszFormat = L"\"%s\" --headless %s%s%s--width %hu --height %hu --signal 0x%x --server 0x%x"; + // This is plenty of space to hold the formatted string + wchar_t cmd[MAX_PATH]{}; + const BOOL bInheritCursor = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR) == PSEUDOCONSOLE_INHERIT_CURSOR; + const BOOL bResizeQuirk = (dwFlags & PSEUDOCONSOLE_RESIZE_QUIRK) == PSEUDOCONSOLE_RESIZE_QUIRK; + const BOOL bWin32InputMode = (dwFlags & PSEUDOCONSOLE_WIN32_INPUT_MODE) == PSEUDOCONSOLE_WIN32_INPUT_MODE; + swprintf_s(cmd, + MAX_PATH, + pwszFormat, + _ConsoleHostPath(), + bInheritCursor ? L"--inheritcursor " : L"", + bWin32InputMode ? L"--win32input " : L"", + bResizeQuirk ? L"--resizeQuirk " : L"", + size.X, + size.Y, + signalPipeConhostSide.get(), + serverHandle.get()); + + STARTUPINFOEXW siEx{ 0 }; + siEx.StartupInfo.cb = sizeof(STARTUPINFOEXW); + siEx.StartupInfo.hStdInput = hInput; + siEx.StartupInfo.hStdOutput = hOutput; + siEx.StartupInfo.hStdError = hOutput; + siEx.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + + // Only pass the handles we actually want the conhost to know about to it: + const size_t INHERITED_HANDLES_COUNT = 4; + HANDLE inheritedHandles[INHERITED_HANDLES_COUNT]; + inheritedHandles[0] = serverHandle.get(); + inheritedHandles[1] = hInput; + inheritedHandles[2] = hOutput; + inheritedHandles[3] = signalPipeConhostSide.get(); + + // Get the size of the attribute list. We need one attribute, the handle list. + SIZE_T listSize = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &listSize); + + // I have to use a HeapAlloc here because kernelbase can't link new[] or delete[] + PPROC_THREAD_ATTRIBUTE_LIST attrList = static_cast<PPROC_THREAD_ATTRIBUTE_LIST>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, listSize)); + RETURN_IF_NULL_ALLOC(attrList); + auto attrListDelete = scope_exit([&]() noexcept { + HeapFree(GetProcessHeap(), 0, attrList); + }); + + siEx.lpAttributeList = attrList; + RETURN_IF_WIN32_BOOL_FALSE(InitializeProcThreadAttributeList(siEx.lpAttributeList, 1, 0, &listSize)); + // Set cleanup data for ProcThreadAttributeList when successful. + auto cleanupProcThreadAttribute = scope_exit([&]() noexcept { + DeleteProcThreadAttributeList(siEx.lpAttributeList); + }); + RETURN_IF_WIN32_BOOL_FALSE(UpdateProcThreadAttribute(siEx.lpAttributeList, + 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inheritedHandles, + (INHERITED_HANDLES_COUNT * sizeof(HANDLE)), + nullptr, + nullptr)); + unique_process_information pi; + { // wow64 disabled filesystem redirection scope + if (hToken == INVALID_HANDLE_VALUE || hToken == nullptr) + { + // Call create process + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessW(_ConsoleHostPath(), + cmd, + nullptr, + nullptr, + TRUE, + EXTENDED_STARTUPINFO_PRESENT, + nullptr, + nullptr, + &siEx.StartupInfo, + pi.addressof())); + } + else + { + // Call create process + RETURN_IF_WIN32_BOOL_FALSE(CreateProcessAsUserW(hToken, + _ConsoleHostPath(), + cmd, + nullptr, + nullptr, + TRUE, + EXTENDED_STARTUPINFO_PRESENT, + nullptr, + nullptr, + &siEx.StartupInfo, + pi.addressof())); + } + } + + // Move the process handle out of the PROCESS_INFORMATION into out Pseudoconsole + pPty->hConPtyProcess = pi.hProcess; + pi.hProcess = nullptr; + + RETURN_IF_NTSTATUS_FAILED(CreateClientHandle(&pPty->hPtyReference, + serverHandle.get(), + L"\\Reference", + FALSE)); + + pPty->hSignal = signalPipeOurSide.release(); + + return S_OK; +} + +// Function Description: +// - Resizes the conpty +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// - size: The new dimensions of the conpty, in characters. +// Return Value: +// - S_OK if the call succeeded, else an appropriate HRESULT for failing to +// write the resize message to the pty. +HRESULT _ResizePseudoConsole(_In_ const PseudoConsole* const pPty, _In_ const COORD size) +{ + if (pPty == nullptr || size.X < 0 || size.Y < 0) + { + return E_INVALIDARG; + } + + unsigned short signalPacket[3]; + signalPacket[0] = PTY_SIGNAL_RESIZE_WINDOW; + signalPacket[1] = size.X; + signalPacket[2] = size.Y; + + const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - Clears the conpty +// Arguments: +// - hSignal: A signal pipe as returned by CreateConPty. +// Return Value: +// - S_OK if the call succeeded, else an appropriate HRESULT for failing to +// write the clear message to the pty. +HRESULT _ClearPseudoConsole(_In_ const PseudoConsole* const pPty) +{ + if (pPty == nullptr) + { + return E_INVALIDARG; + } + + unsigned short signalPacket[1]; + signalPacket[0] = PTY_SIGNAL_CLEAR_WINDOW; + + const BOOL fSuccess = WriteFile(pPty->hSignal, signalPacket, sizeof(signalPacket), nullptr, nullptr); + return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); +} + +// Function Description: +// - This closes each of the members of a PseudoConsole. It does not free the +// data associated with the PseudoConsole. This is helpful for testing, +// where we might stack allocate a PseudoConsole (instead of getting a +// HPCON via the API). +// Arguments: +// - pPty: A pointer to a PseudoConsole struct. +// Return Value: +// - <none> +void _ClosePseudoConsoleMembers(_In_ PseudoConsole* pPty) +{ + if (pPty != nullptr) + { + // See MSFT:19918626 + // First break the signal pipe - this will trigger conhost to tear itself down + if (_HandleIsValid(pPty->hSignal)) + { + CloseHandle(pPty->hSignal); + pPty->hSignal = nullptr; + } + // Then, wait on the conhost process before killing it. + // We do this to make sure the conhost finishes flushing any output it + // has yet to send before we hard kill it. + if (_HandleIsValid(pPty->hConPtyProcess)) + { + // If the conhost is already dead, then that's fine. Presumably + // it's finished flushing it's output already. + DWORD dwExit = 0; + // If GetExitCodeProcess failed, it's likely conhost is already dead + // If so, skip waiting regardless of whatever error + // GetExitCodeProcess returned. + // We'll just go straight to killing conhost. + if (GetExitCodeProcess(pPty->hConPtyProcess, &dwExit) && dwExit == STILL_ACTIVE) + { + WaitForSingleObject(pPty->hConPtyProcess, INFINITE); + } + + TerminateProcess(pPty->hConPtyProcess, 0); + CloseHandle(pPty->hConPtyProcess); + pPty->hConPtyProcess = nullptr; + } + // Then take care of the reference handle. + // TODO GH#1810: Closing the reference handle late leaves conhost thinking + // that we have an outstanding connected client. + if (_HandleIsValid(pPty->hPtyReference)) + { + CloseHandle(pPty->hPtyReference); + pPty->hPtyReference = nullptr; + } + } +} + +// Function Description: +// - This closes each of the members of a PseudoConsole, and HeapFree's the +// memory allocated to it. This should be used to cleanup any +// PseudoConsoles that were created with CreatePseudoConsole. +// Arguments: +// - pPty: A pointer to a PseudoConsole struct. +// Return Value: +// - <none> +VOID _ClosePseudoConsole(_In_ PseudoConsole* pPty) +{ + if (pPty != nullptr) + { + _ClosePseudoConsoleMembers(pPty); + HeapFree(GetProcessHeap(), 0, pPty); + } +} + +extern "C" HRESULT ConptyCreatePseudoConsoleAsUser(_In_ HANDLE hToken, + _In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC) +{ + if (phPC == nullptr) + { + return E_INVALIDARG; + } + *phPC = nullptr; + if ((!_HandleIsValid(hInput)) && (!_HandleIsValid(hOutput))) + { + return E_INVALIDARG; + } + + PseudoConsole* pPty = (PseudoConsole*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PseudoConsole)); + RETURN_IF_NULL_ALLOC(pPty); + auto cleanupPty = scope_exit([&]() noexcept { + _ClosePseudoConsole(pPty); + }); + + unique_handle duplicatedInput; + unique_handle duplicatedOutput; + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hInput, GetCurrentProcess(), duplicatedInput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); + RETURN_IF_WIN32_BOOL_FALSE(DuplicateHandle(GetCurrentProcess(), hOutput, GetCurrentProcess(), duplicatedOutput.addressof(), 0, TRUE, DUPLICATE_SAME_ACCESS)); + + RETURN_IF_FAILED(_CreatePseudoConsole(hToken, size, duplicatedInput.get(), duplicatedOutput.get(), dwFlags, pPty)); + + *phPC = (HPCON)pPty; + cleanupPty.release(); + + return S_OK; +} + +// These functions are defined in the console l1 apiset, which is generated from +// the consoleapi.apx file in minkernel\apiset\libs\Console. + +// Function Description: +// Creates a "Pseudo-console" (conpty) with dimensions (in characters) +// provided by the `size` parameter. The caller should provide two handles: +// - `hInput` is used for writing input to the pty, encoded as UTF-8 and VT sequences. +// - `hOutput` is used for reading the output of the pty, encoded as UTF-8 and VT sequences. +// Once the call completes, `phPty` will receive a token value to identify this +// conpty object. This value should be used in conjunction with the other +// Pseudoconsole API's. +// `dwFlags` is used to specify optional behavior to the created pseudoconsole. +// The flags can be combinations of the following values: +// INHERIT_CURSOR: This will cause the created conpty to attempt to inherit the +// cursor position of the parent terminal application. This can be useful +// for applications like `ssh`, where ssh (currently running in a terminal) +// might want to create a pseudoterminal session for an child application +// and the child inherit the cursor position of ssh. +// The created conpty will immediately emit a "Device Status Request" VT +// sequence to hOutput, that should be replied to on hInput in the format +// "\x1b[<r>;<c>R", where `<r>` is the row and `<c>` is the column of the +// cursor position. +// This requires a cooperating terminal application - if a caller does not +// reply to this message, the conpty will not process any input until it +// does. Most *nix terminals and the Windows Console (after Windows 10 +// Anniversary Update) will be able to handle such a message. + +extern "C" HRESULT WINAPI ConptyCreatePseudoConsole(_In_ COORD size, + _In_ HANDLE hInput, + _In_ HANDLE hOutput, + _In_ DWORD dwFlags, + _Out_ HPCON* phPC) +{ + return ConptyCreatePseudoConsoleAsUser(INVALID_HANDLE_VALUE, size, hInput, hOutput, dwFlags, phPC); +} + +// Function Description: +// Resizes the given conpty to the specified size, in characters. +extern "C" HRESULT WINAPI ConptyResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size) +{ + const PseudoConsole* const pPty = (PseudoConsole*)hPC; + HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; + if (SUCCEEDED(hr)) + { + hr = _ResizePseudoConsole(pPty, size); + } + return hr; +} + +// Function Description: +// - Clear the contents of the conpty buffer, leaving the cursor row at the top +// of the viewport. +// - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows +// a terminal to clear the contents of the ConPTY buffer, which is important +// if the user would like to be able to clear the terminal-side buffer. +extern "C" HRESULT WINAPI ConptyClearPseudoConsole(_In_ HPCON hPC) +{ + const PseudoConsole* const pPty = (PseudoConsole*)hPC; + HRESULT hr = pPty == nullptr ? E_INVALIDARG : S_OK; + if (SUCCEEDED(hr)) + { + hr = _ClearPseudoConsole(pPty); + } + return hr; +} + +// Function Description: +// Closes the conpty and all associated state. +// Client applications attached to the conpty will also behave as though the +// console window they were running in was closed. +// This can fail if the conhost hosting the pseudoconsole failed to be +// terminated, or if the pseudoconsole was already terminated. +extern "C" VOID WINAPI ConptyClosePseudoConsole(_In_ HPCON hPC) +{ + PseudoConsole* const pPty = (PseudoConsole*)hPC; + if (pPty != nullptr) + { + _ClosePseudoConsole(pPty); + } +} + + +//ConPTY is available only on Windows 10 released after 1903 (19H1) Windows release +class WindowsContext +{ +private: + WindowsContext() {} + +public: + typedef HRESULT (*CreatePseudoConsolePtr)( + COORD size, // ConPty Dimensions + HANDLE hInput, // ConPty Input + HANDLE hOutput, // ConPty Output + DWORD dwFlags, // ConPty Flags + HPCON* phPC); // ConPty Reference + + typedef HRESULT (*ResizePseudoConsolePtr)(HPCON hPC, COORD size); + + typedef VOID (*ClosePseudoConsolePtr)(HPCON hPC); + + static WindowsContext &instance() + { + static WindowsContext ctx; + return ctx; + } + + bool init() + { + createPseudoConsole = (CreatePseudoConsolePtr)ConptyCreatePseudoConsole; + resizePseudoConsole = (ResizePseudoConsolePtr)ConptyResizePseudoConsole; + closePseudoConsole = (ClosePseudoConsolePtr)ConptyClosePseudoConsole; + + return true; + } + + QString lastError() + { + return m_lastError; + } + +public: + //vars + CreatePseudoConsolePtr createPseudoConsole{nullptr}; + ResizePseudoConsolePtr resizePseudoConsole{nullptr}; + ClosePseudoConsolePtr closePseudoConsole{nullptr}; + +private: + QString m_lastError; +}; + +static bool checkConHostHasResizeQuirkOption() +{ + static bool hasResizeQuirk = std::invoke([](){ + QFile f(_InboxConsoleHostPath()); + if (!f.open(QIODevice::ReadOnly)) { + qWarning() << "couldn't open conhost.exe, assuming no resizeQuirk."; + return false; + } + // Conhost.exe should be around 1 MB + if (f.size() > 5 * 1024 * 1024) { + qWarning() << "conhost.exe is > 5MB, assuming no resizeQuirk."; + return false; + } + QByteArray content = f.readAll(); + QString searchString("--resizeQuirk"); + QByteArrayView v((const char*)searchString.data(), searchString.length()*2); + bool result = content.contains(v); + if (!result) + qDebug() << "No resizeQuirk option found in conhost."; + return result; + }); + + return hasResizeQuirk; +} + +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 = WindowsContext::instance().createPseudoConsole({cols, rows}, hPipePTYIn, hPipePTYOut, checkConHostHasResizeQuirkOption() ? PSEUDOCONSOLE_RESIZE_QUIRK : 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.hStdInput = m_hPipeIn; + pStartupInfo->StartupInfo.hStdError = m_hPipeOut; + pStartupInfo->StartupInfo.hStdOutput = m_hPipeOut; + pStartupInfo->StartupInfo.dwFlags |= STARTF_USESTDHANDLES; + + 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<LPPROC_THREAD_ATTRIBUTE_LIST>( + HeapAlloc(GetProcessHeap(), 0, 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; +} + +Q_DECLARE_METATYPE(HANDLE) + +ConPtyProcess::ConPtyProcess() + : IPtyProcess() + , m_ptyHandler { INVALID_HANDLE_VALUE } + , m_hPipeIn { INVALID_HANDLE_VALUE } + , m_hPipeOut { INVALID_HANDLE_VALUE } + , m_readThread(nullptr) +{ + qRegisterMetaType<HANDLE>("HANDLE"); +} + +ConPtyProcess::~ConPtyProcess() +{ + kill(); +} + +bool ConPtyProcess::startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows) +{ + if (!isAvailable()) { + m_lastError = WindowsContext::instance().lastError(); + return false; + } + + //already running + if (m_ptyHandler != INVALID_HANDLE_VALUE) + return false; + + QFileInfo fi(executable); + if (fi.isRelative() || !QFile::exists(executable)) { + //todo add auto-find executable in PATH env var + m_lastError = QString("ConPty Error: shell file path '%1' must be absolute").arg(executable); + return false; + } + + m_shellPath = executable; + m_shellPath.replace('/', '\\'); + m_size = QPair<qint16, qint16>(cols, rows); + + //env + const QString env = environment.join(QChar(QChar::Null)) + QChar(QChar::Null); + LPVOID envPtr = env.isEmpty() ? nullptr : (LPVOID) env.utf16(); + + LPCWSTR workingDirPtr = workingDir.isEmpty() ? nullptr : (LPCWSTR) workingDir.utf16(); + + QStringList exeAndArgs = arguments; + exeAndArgs.prepend(m_shellPath); + std::wstring cmdArg{(LPCWSTR) (exeAndArgs.join(QLatin1String(" ")).utf16())}; + + 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 + if (S_OK != initializeStartupInfoAttachedToPseudoConsole(&m_shellStartupInfo, m_ptyHandler)) { + m_lastError = QString("ConPty Error: InitializeStartupInfoAttachedToPseudoConsole fail"); + return false; + } + + hr = CreateProcessW(nullptr, // No module name - use Command Line + cmdArg.data(), // Command Line + nullptr, // Process handle not inheritable + nullptr, // Thread handle not inheritable + FALSE, // Inherit handles + EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, // Creation flags + envPtr, // Environment block + workingDirPtr, // Use parent's starting directory + &m_shellStartupInfo.StartupInfo, // Pointer to STARTUPINFO + &m_shellProcessInformation) // 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 = m_shellProcessInformation.dwProcessId; + + // Notify when the shell process has been terminated + m_shellCloseWaitNotifier = new QWinEventNotifier(m_shellProcessInformation.hProcess, notifier()); + QObject::connect(m_shellCloseWaitNotifier, + &QWinEventNotifier::activated, + notifier(), + [this](HANDLE hEvent) { + DWORD exitCode = 0; + GetExitCodeProcess(hEvent, &exitCode); + m_exitCode = exitCode; + // Do not respawn if the object is about to be destructed + if (!m_aboutToDestruct) + emit notifier()->aboutToClose(); + m_shellCloseWaitNotifier->setEnabled(false); + }, Qt::QueuedConnection); + + //this code runned in separate thread + m_readThread = QThread::create([this]() { + //buffers + const DWORD BUFF_SIZE{1024}; + char szBuffer[BUFF_SIZE]{}; + + forever { + DWORD dwBytesRead{}; + + // Read from the pipe + BOOL result = ReadFile(m_hPipeIn, szBuffer, BUFF_SIZE, &dwBytesRead, NULL); + + const bool needMoreData = !result && GetLastError() == ERROR_MORE_DATA; + if (result || needMoreData) { + QMutexLocker locker(&m_bufferMutex); + m_buffer.m_readBuffer.append(szBuffer, dwBytesRead); + m_buffer.emitReadyRead(); + } + + const bool brokenPipe = !result && GetLastError() == ERROR_BROKEN_PIPE; + if (QThread::currentThread()->isInterruptionRequested() || brokenPipe) + break; + } + + CancelIoEx(m_hPipeIn, nullptr); + }); + + //start read thread + m_readThread->start(); + + return true; +} + +bool ConPtyProcess::resize(qint16 cols, qint16 rows) +{ + if (m_ptyHandler == nullptr) + { + return false; + } + + bool res = SUCCEEDED(WindowsContext::instance().resizePseudoConsole(m_ptyHandler, {cols, rows})); + + if (res) + { + m_size = QPair<qint16, qint16>(cols, rows); + } + + return res; + + return true; +} + +bool ConPtyProcess::kill() +{ + bool exitCode = false; + + if (m_ptyHandler != INVALID_HANDLE_VALUE) { + m_aboutToDestruct = true; + + // Close ConPTY - this will terminate client process if running + WindowsContext::instance().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); + + if (m_readThread) { + m_readThread->requestInterruption(); + if (!m_readThread->wait(1000)) + m_readThread->terminate(); + m_readThread->deleteLater(); + m_readThread = nullptr; + } + + delete m_shellCloseWaitNotifier; + m_shellCloseWaitNotifier = nullptr; + + m_pid = 0; + m_ptyHandler = INVALID_HANDLE_VALUE; + m_hPipeIn = INVALID_HANDLE_VALUE; + m_hPipeOut = INVALID_HANDLE_VALUE; + + CloseHandle(m_shellProcessInformation.hThread); + CloseHandle(m_shellProcessInformation.hProcess); + + // Cleanup attribute list + if (m_shellStartupInfo.lpAttributeList) { + DeleteProcThreadAttributeList(m_shellStartupInfo.lpAttributeList); + HeapFree(GetProcessHeap(), 0, m_shellStartupInfo.lpAttributeList); + } + + exitCode = true; + } + + return exitCode; +} + +IPtyProcess::PtyType ConPtyProcess::type() +{ + return PtyType::ConPty; +} + +QString ConPtyProcess::dumpDebugInfo() +{ +#ifdef PTYQT_DEBUG + return QString("PID: %1, Type: %2, Cols: %3, Rows: %4") + .arg(m_pid).arg(type()) + .arg(m_size.first).arg(m_size.second); +#else + return QString("Nothing..."); +#endif +} + +QIODevice *ConPtyProcess::notifier() +{ + return &m_buffer; +} + +QByteArray ConPtyProcess::readAll() +{ + QByteArray result; + { + QMutexLocker locker(&m_bufferMutex); + result.swap(m_buffer.m_readBuffer); + } + return result; +} + +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 WindowsContext::instance().init(); +} + +void ConPtyProcess::moveToThread(QThread *targetThread) +{ + //nothing for now... +} diff --git a/src/libs/3rdparty/libptyqt/conptyprocess.h b/src/libs/3rdparty/libptyqt/conptyprocess.h new file mode 100644 index 0000000000..aaf56fe76f --- /dev/null +++ b/src/libs/3rdparty/libptyqt/conptyprocess.h @@ -0,0 +1,90 @@ +#ifndef CONPTYPROCESS_H +#define CONPTYPROCESS_H + +#include "iptyprocess.h" +#include <windows.h> +#include <process.h> +#include <stdio.h> + +#include <QIODevice> +#include <QLibrary> +#include <QMutex> +#include <QTimer> +#include <QThread> + +//Taken from the RS5 Windows SDK, but redefined here in case we're targeting <= 17733 +//Just for compile, ConPty doesn't work with Windows SDK < 17733 +#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE +#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \ + ProcThreadAttributeValue(22, FALSE, TRUE, FALSE) + +typedef VOID* HPCON; + +#define TOO_OLD_WINSDK +#endif + +class QWinEventNotifier; + +class PtyBuffer : public QIODevice +{ + friend class ConPtyProcess; + Q_OBJECT +public: + + //just empty realization, we need only 'readyRead' signal of this class + qint64 readData(char *data, qint64 maxlen) { return 0; } + qint64 writeData(const char *data, qint64 len) { return 0; } + + bool isSequential() const { return true; } + qint64 bytesAvailable() const { return m_readBuffer.size(); } + qint64 size() const { return m_readBuffer.size(); } + + void emitReadyRead() + { + emit readyRead(); + } + +private: + QByteArray m_readBuffer; +}; + +class ConPtyProcess : public IPtyProcess +{ +public: + ConPtyProcess(); + ~ConPtyProcess(); + + bool startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows); + bool resize(qint16 cols, qint16 rows); + bool kill(); + PtyType type(); + QString dumpDebugInfo(); + virtual QIODevice *notifier(); + virtual QByteArray readAll(); + virtual qint64 write(const QByteArray &byteArray); + static bool isAvailable(); + void moveToThread(QThread *targetThread); + +private: + HRESULT createPseudoConsoleAndPipes(HPCON* phPC, HANDLE* phPipeIn, HANDLE* phPipeOut, qint16 cols, qint16 rows); + HRESULT initializeStartupInfoAttachedToPseudoConsole(STARTUPINFOEX* pStartupInfo, HPCON hPC); + +private: + HPCON m_ptyHandler{INVALID_HANDLE_VALUE}; + HANDLE m_hPipeIn{INVALID_HANDLE_VALUE}, m_hPipeOut{INVALID_HANDLE_VALUE}; + + QThread *m_readThread{nullptr}; + QMutex m_bufferMutex; + PtyBuffer m_buffer; + bool m_aboutToDestruct{false}; + PROCESS_INFORMATION m_shellProcessInformation{}; + QWinEventNotifier *m_shellCloseWaitNotifier{nullptr}; + STARTUPINFOEX m_shellStartupInfo{}; +}; + +#endif // CONPTYPROCESS_H diff --git a/src/libs/3rdparty/libptyqt/iptyprocess.h b/src/libs/3rdparty/libptyqt/iptyprocess.h new file mode 100644 index 0000000000..6005b0efde --- /dev/null +++ b/src/libs/3rdparty/libptyqt/iptyprocess.h @@ -0,0 +1,62 @@ +#ifndef IPTYPROCESS_H +#define IPTYPROCESS_H + +#include <QDebug> +#include <QString> +#include <QThread> + +#define CONPTY_MINIMAL_WINDOWS_VERSION 18309 + +class IPtyProcess +{ +public: + enum PtyType { UnixPty = 0, WinPty = 1, ConPty = 2, AutoPty = 3 }; + enum PtyInputFlag { None = 0x0, InputModeHidden = 0x1, }; + Q_DECLARE_FLAGS(PtyInputFlags, PtyInputFlag) + + IPtyProcess() = default; + IPtyProcess(const IPtyProcess &) = delete; + IPtyProcess &operator=(const IPtyProcess &) = delete; + + virtual ~IPtyProcess() {} + + virtual bool startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows) + = 0; + virtual bool resize(qint16 cols, qint16 rows) = 0; + virtual bool kill() = 0; + virtual PtyType type() = 0; + virtual QString dumpDebugInfo() = 0; + virtual QIODevice *notifier() = 0; + virtual QByteArray readAll() = 0; + virtual qint64 write(const QByteArray &byteArray) = 0; + virtual void moveToThread(QThread *targetThread) = 0; + qint64 pid() { return m_pid; } + QPair<qint16, qint16> size() { return m_size; } + const QString lastError() { return m_lastError; } + int exitCode() { return m_exitCode; } + bool toggleTrace() + { + m_trace = !m_trace; + return m_trace; + } + + PtyInputFlags inputFlags() { return m_inputFlags; } + +protected: + QString m_shellPath; + QString m_lastError; + qint64 m_pid{0}; + int m_exitCode{0}; + QPair<qint16, qint16> m_size; //cols / rows + bool m_trace{false}; + PtyInputFlags m_inputFlags; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(IPtyProcess::PtyInputFlags) + +#endif // IPTYPROCESS_H diff --git a/src/libs/3rdparty/libptyqt/ptyqt.cpp b/src/libs/3rdparty/libptyqt/ptyqt.cpp new file mode 100644 index 0000000000..06fe449819 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/ptyqt.cpp @@ -0,0 +1,56 @@ +#include "ptyqt.h" +#include <utility> + +#ifdef Q_OS_WIN +#include "winptyprocess.h" +#include "conptyprocess.h" +#endif + +#ifdef Q_OS_UNIX +#include "unixptyprocess.h" +#endif + +bool PtyQt::isUsingConPTY() +{ +#ifdef Q_OS_WIN + if (ConPtyProcess::isAvailable() && qgetenv("QTC_USE_WINPTY").isEmpty()) + return true; +#endif + + return false; +} + +IPtyProcess *PtyQt::createPtyProcess(IPtyProcess::PtyType ptyType) +{ + switch (ptyType) + { +#ifdef Q_OS_WIN + case IPtyProcess::PtyType::WinPty: + return new WinPtyProcess(); + break; + case IPtyProcess::PtyType::ConPty: + return new ConPtyProcess(); + break; +#endif +#ifdef Q_OS_UNIX + case IPtyProcess::PtyType::UnixPty: + return new UnixPtyProcess(); + break; +#endif + case IPtyProcess::PtyType::AutoPty: + default: + break; + } + +#ifdef Q_OS_WIN + if (isUsingConPTY()) + return new ConPtyProcess(); + else + return new WinPtyProcess(); +#endif +#ifdef Q_OS_UNIX + return new UnixPtyProcess(); +#endif +} + + diff --git a/src/libs/3rdparty/libptyqt/ptyqt.h b/src/libs/3rdparty/libptyqt/ptyqt.h new file mode 100644 index 0000000000..85f27b33a9 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/ptyqt.h @@ -0,0 +1,14 @@ +#ifndef PTYQT_H +#define PTYQT_H + +#include "iptyprocess.h" + +class PtyQt +{ +public: + static IPtyProcess *createPtyProcess(IPtyProcess::PtyType ptyType); + + static bool isUsingConPTY(); +}; + +#endif // PTYQT_H diff --git a/src/libs/3rdparty/libptyqt/ptyqt.qbs b/src/libs/3rdparty/libptyqt/ptyqt.qbs new file mode 100644 index 0000000000..3b56dd8e46 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/ptyqt.qbs @@ -0,0 +1,40 @@ +QtcLibrary { + name: "ptyqt" + type: "staticlibrary" + + Depends { name: "Qt.core" } + Depends { name: "Qt.network"; condition: qbs.targetOS.contains("windows") } + Depends { name: "winpty"; condition: qbs.targetOS.contains("windows") } + + files: [ + "iptyprocess.h", + "ptyqt.cpp", + "ptyqt.h", + ] + + Group { + name: "ptyqt UNIX files" + condition: qbs.targetOS.contains("unix") + files: [ + "unixptyprocess.cpp", + "unixptyprocess.h", + ] + } + + Group { + name: "ptyqt Windows files" + condition: qbs.targetOS.contains("windows") + files: [ + "conptyprocess.cpp", + "conptyprocess.h", + "winptyprocess.cpp", + "winptyprocess.h", + ] + } + + Export { + Depends { name: "cpp" } + Depends { name: "winpty"; condition: qbs.targetOS.contains("windows") } + cpp.includePaths: exportingProduct.sourceDirectory + } +} diff --git a/src/libs/3rdparty/libptyqt/unixptyprocess.cpp b/src/libs/3rdparty/libptyqt/unixptyprocess.cpp new file mode 100644 index 0000000000..d76349a49e --- /dev/null +++ b/src/libs/3rdparty/libptyqt/unixptyprocess.cpp @@ -0,0 +1,362 @@ +#include "unixptyprocess.h" +#include <QStandardPaths> + +#include <errno.h> +#include <termios.h> +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_FREEBSD) +#include <utmpx.h> +#endif +#include <fcntl.h> +#include <stdlib.h> +#include <sys/ioctl.h> +#include <unistd.h> +#include <QCoreApplication> +#include <QFileInfo> + +UnixPtyProcess::UnixPtyProcess() + : IPtyProcess() + , m_readMasterNotify(0) +{ + m_shellProcess.setWorkingDirectory( + QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); +} + +UnixPtyProcess::~UnixPtyProcess() +{ + kill(); +} + +bool UnixPtyProcess::startProcess(const QString &shellPath, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows) +{ + if (!isAvailable()) { + m_lastError = QString("UnixPty Error: unavailable"); + return false; + } + + if (m_shellProcess.state() == QProcess::Running) + return false; + + QFileInfo fi(shellPath); + if (fi.isRelative() || !QFile::exists(shellPath)) { + //todo add auto-find executable in PATH env var + m_lastError = QString("UnixPty Error: shell file path must be absolute"); + return false; + } + + m_shellPath = shellPath; + m_size = QPair<qint16, qint16>(cols, rows); + + int rc = 0; + + m_shellProcess.m_handleMaster = ::posix_openpt(O_RDWR | O_NOCTTY); + + int flags = fcntl(m_shellProcess.m_handleMaster, F_GETFL, 0); + fcntl(m_shellProcess.m_handleMaster, F_SETFL, flags | O_NONBLOCK); + + if (m_shellProcess.m_handleMaster <= 0) { + m_lastError = QString("UnixPty Error: unable to open master -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + m_shellProcess.m_handleSlaveName = QLatin1String(ptsname(m_shellProcess.m_handleMaster)); + if (m_shellProcess.m_handleSlaveName.isEmpty()) { + m_lastError = QString("UnixPty Error: unable to get slave name -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + rc = grantpt(m_shellProcess.m_handleMaster); + if (rc != 0) { + m_lastError + = QString("UnixPty Error: unable to change perms for slave -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + rc = unlockpt(m_shellProcess.m_handleMaster); + if (rc != 0) { + m_lastError = QString("UnixPty Error: unable to unlock slave -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + m_shellProcess.m_handleSlave = ::open(m_shellProcess.m_handleSlaveName.toLatin1().data(), + O_RDWR | O_NOCTTY); + if (m_shellProcess.m_handleSlave < 0) { + m_lastError = QString("UnixPty Error: unable to open slave -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + rc = fcntl(m_shellProcess.m_handleMaster, F_SETFD, FD_CLOEXEC); + if (rc == -1) { + m_lastError + = QString("UnixPty Error: unable to set flags for master -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + rc = fcntl(m_shellProcess.m_handleSlave, F_SETFD, FD_CLOEXEC); + if (rc == -1) { + m_lastError + = QString("UnixPty Error: unable to set flags for slave -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + struct ::termios ttmode; + rc = tcgetattr(m_shellProcess.m_handleMaster, &ttmode); + if (rc != 0) { + m_lastError = QString("UnixPty Error: termios fail -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + ttmode.c_iflag = ICRNL | IXON | IXANY | IMAXBEL | BRKINT; +#if defined(IUTF8) + ttmode.c_iflag |= IUTF8; +#endif + + ttmode.c_oflag = OPOST | ONLCR; + ttmode.c_cflag = CREAD | CS8 | HUPCL; + ttmode.c_lflag = ICANON | ISIG | IEXTEN | ECHO | ECHOE | ECHOK | ECHOKE | ECHOCTL; + + ttmode.c_cc[VEOF] = 4; + ttmode.c_cc[VEOL] = -1; + ttmode.c_cc[VEOL2] = -1; + ttmode.c_cc[VERASE] = 0x7f; + ttmode.c_cc[VWERASE] = 23; + ttmode.c_cc[VKILL] = 21; + ttmode.c_cc[VREPRINT] = 18; + ttmode.c_cc[VINTR] = 3; + ttmode.c_cc[VQUIT] = 0x1c; + ttmode.c_cc[VSUSP] = 26; + ttmode.c_cc[VSTART] = 17; + ttmode.c_cc[VSTOP] = 19; + ttmode.c_cc[VLNEXT] = 22; + ttmode.c_cc[VDISCARD] = 15; + ttmode.c_cc[VMIN] = 1; + ttmode.c_cc[VTIME] = 0; + +#if (__APPLE__) + ttmode.c_cc[VDSUSP] = 25; + ttmode.c_cc[VSTATUS] = 20; +#endif + + cfsetispeed(&ttmode, B38400); + cfsetospeed(&ttmode, B38400); + + rc = tcsetattr(m_shellProcess.m_handleMaster, TCSANOW, &ttmode); + if (rc != 0) { + m_lastError + = QString("UnixPty Error: unabble to set associated params -> %1").arg(QLatin1String(strerror(errno))); + kill(); + return false; + } + + m_readMasterNotify = new QSocketNotifier(m_shellProcess.m_handleMaster, + QSocketNotifier::Read, + &m_shellProcess); + m_readMasterNotify->setEnabled(true); + m_readMasterNotify->moveToThread(m_shellProcess.thread()); + QObject::connect(m_readMasterNotify, &QSocketNotifier::activated, [this](int socket) { + Q_UNUSED(socket) + + const size_t maxRead = 16 * 1024; + static std::array<char, maxRead> buffer; + + int len = ::read(m_shellProcess.m_handleMaster, buffer.data(), buffer.size()); + + struct termios termAttributes; + tcgetattr(m_shellProcess.m_handleMaster, &termAttributes); + const bool isPasswordEntry = !(termAttributes.c_lflag & ECHO) && (termAttributes.c_lflag & ICANON); + m_inputFlags.setFlag(PtyInputFlag::InputModeHidden, isPasswordEntry); + + if (len > 0) { + m_shellReadBuffer.append(buffer.data(), len); + m_shellProcess.emitReadyRead(); + } + }); + + QObject::connect(&m_shellProcess, &QProcess::finished, &m_shellProcess, [this](int exitCode) { + m_exitCode = exitCode; + emit m_shellProcess.aboutToClose(); + m_readMasterNotify->disconnect(); + }); + + QProcessEnvironment env; + for (const QString &envEntry : environment) { + const int idx = envEntry.indexOf('='); + if (idx != -1) + env.insert(envEntry.left(idx), envEntry.mid(idx + 1)); + else + env.insert(envEntry, QString()); + } + + m_shellProcess.setWorkingDirectory(workingDir); + m_shellProcess.setProcessEnvironment(env); + m_shellProcess.setReadChannel(QProcess::StandardOutput); + m_shellProcess.start(m_shellPath, arguments); + if (!m_shellProcess.waitForStarted()) + return false; + + m_pid = m_shellProcess.processId(); + + resize(cols, rows); + + return true; +} + +bool UnixPtyProcess::resize(qint16 cols, qint16 rows) +{ + struct winsize winp; + winp.ws_col = cols; + winp.ws_row = rows; + winp.ws_xpixel = 0; + winp.ws_ypixel = 0; + + bool res = ((ioctl(m_shellProcess.m_handleMaster, TIOCSWINSZ, &winp) != -1) + && (ioctl(m_shellProcess.m_handleSlave, TIOCSWINSZ, &winp) != -1)); + + if (res) { + m_size = QPair<qint16, qint16>(cols, rows); + } + + return res; +} + +bool UnixPtyProcess::kill() +{ + m_shellProcess.m_handleSlaveName = QString(); + if (m_shellProcess.m_handleSlave >= 0) { + ::close(m_shellProcess.m_handleSlave); + m_shellProcess.m_handleSlave = -1; + } + if (m_shellProcess.m_handleMaster >= 0) { + ::close(m_shellProcess.m_handleMaster); + m_shellProcess.m_handleMaster = -1; + } + + if (m_shellProcess.state() == QProcess::Running) { + m_readMasterNotify->disconnect(); + m_readMasterNotify->deleteLater(); + + m_shellProcess.terminate(); + m_shellProcess.waitForFinished(1000); + + if (m_shellProcess.state() == QProcess::Running) { + QProcess::startDetached(QString("kill -9 %1").arg(pid())); + m_shellProcess.kill(); + m_shellProcess.waitForFinished(1000); + } + + return (m_shellProcess.state() == QProcess::NotRunning); + } + return false; +} + +IPtyProcess::PtyType UnixPtyProcess::type() +{ + return IPtyProcess::UnixPty; +} + +QString UnixPtyProcess::dumpDebugInfo() +{ +#ifdef PTYQT_DEBUG + return QString("PID: %1, In: %2, Out: %3, Type: %4, Cols: %5, Rows: %6, IsRunning: %7, Shell: " + "%8, SlaveName: %9") + .arg(m_pid) + .arg(m_shellProcess.m_handleMaster) + .arg(m_shellProcess.m_handleSlave) + .arg(type()) + .arg(m_size.first) + .arg(m_size.second) + .arg(m_shellProcess.state() == QProcess::Running) + .arg(m_shellPath) + .arg(m_shellProcess.m_handleSlaveName); +#else + return QString("Nothing..."); +#endif +} + +QIODevice *UnixPtyProcess::notifier() +{ + return &m_shellProcess; +} + +QByteArray UnixPtyProcess::readAll() +{ + QByteArray tmpBuffer = m_shellReadBuffer; + m_shellReadBuffer.clear(); + return tmpBuffer; +} + +qint64 UnixPtyProcess::write(const QByteArray &byteArray) +{ + return ::write(m_shellProcess.m_handleMaster, byteArray.constData(), byteArray.size()); +} + +bool UnixPtyProcess::isAvailable() +{ + //todo check something more if required + return true; +} + +void UnixPtyProcess::moveToThread(QThread *targetThread) +{ + m_shellProcess.moveToThread(targetThread); +} + +void ShellProcess::configChildProcess() +{ + dup2(m_handleSlave, STDIN_FILENO); + dup2(m_handleSlave, STDOUT_FILENO); + dup2(m_handleSlave, STDERR_FILENO); + + pid_t sid = setsid(); + ioctl(m_handleSlave, TIOCSCTTY, 0); + tcsetpgrp(m_handleSlave, sid); + +#if !defined(Q_OS_ANDROID) && !defined(Q_OS_FREEBSD) + // on Android imposible to put record to the 'utmp' file + struct utmpx utmpxInfo; + memset(&utmpxInfo, 0, sizeof(utmpxInfo)); + + strncpy(utmpxInfo.ut_user, qgetenv("USER"), sizeof(utmpxInfo.ut_user) - 1); + + QString device(m_handleSlaveName); + if (device.startsWith("/dev/")) + device = device.mid(5); + + const auto deviceAsLatin1 = device.toLatin1(); + const char *d = deviceAsLatin1.constData(); + + strncpy(utmpxInfo.ut_line, d, sizeof(utmpxInfo.ut_line) - 1); + + strncpy(utmpxInfo.ut_id, d + strlen(d) - sizeof(utmpxInfo.ut_id), sizeof(utmpxInfo.ut_id)); + + struct timeval tv; + gettimeofday(&tv, 0); + utmpxInfo.ut_tv.tv_sec = tv.tv_sec; + utmpxInfo.ut_tv.tv_usec = tv.tv_usec; + + utmpxInfo.ut_type = USER_PROCESS; + utmpxInfo.ut_pid = getpid(); + + utmpxname(_PATH_UTMPX); + setutxent(); + pututxline(&utmpxInfo); + endutxent(); + +#if !defined(Q_OS_UNIX) + updwtmpx(_PATH_UTMPX, &loginInfo); +#endif + +#endif +} diff --git a/src/libs/3rdparty/libptyqt/unixptyprocess.h b/src/libs/3rdparty/libptyqt/unixptyprocess.h new file mode 100644 index 0000000000..f7e3a2e528 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/unixptyprocess.h @@ -0,0 +1,66 @@ +#ifndef UNIXPTYPROCESS_H +#define UNIXPTYPROCESS_H + +#include "iptyprocess.h" +#include <QProcess> +#include <QSocketNotifier> + +// support for build with MUSL on Alpine Linux +#ifndef _PATH_UTMPX +#include <sys/time.h> +#define _PATH_UTMPX "/var/log/utmp" +#endif + +class ShellProcess : public QProcess +{ + friend class UnixPtyProcess; + Q_OBJECT +public: + ShellProcess() + : QProcess() + , m_handleMaster(-1) + , m_handleSlave(-1) + { + setProcessChannelMode(QProcess::SeparateChannels); + setChildProcessModifier([this]() { configChildProcess(); }); + } + + void emitReadyRead() { emit readyRead(); } + +protected: + void configChildProcess(); + +private: + int m_handleMaster, m_handleSlave; + QString m_handleSlaveName; +}; + +class UnixPtyProcess : public IPtyProcess +{ +public: + UnixPtyProcess(); + virtual ~UnixPtyProcess(); + + virtual bool startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows); + virtual bool resize(qint16 cols, qint16 rows); + virtual bool kill(); + virtual PtyType type(); + virtual QString dumpDebugInfo(); + virtual QIODevice *notifier(); + virtual QByteArray readAll(); + virtual qint64 write(const QByteArray &byteArray); + static bool isAvailable(); + void moveToThread(QThread *targetThread); + +private: + ShellProcess m_shellProcess; + QSocketNotifier *m_readMasterNotify; + QByteArray m_shellReadBuffer; +}; + +#endif // UNIXPTYPROCESS_H diff --git a/src/libs/3rdparty/libptyqt/winptyprocess.cpp b/src/libs/3rdparty/libptyqt/winptyprocess.cpp new file mode 100644 index 0000000000..7a781bc195 --- /dev/null +++ b/src/libs/3rdparty/libptyqt/winptyprocess.cpp @@ -0,0 +1,278 @@ +#include "winptyprocess.h" +#include <QFile> +#include <QFileInfo> +#include <sstream> +#include <QCoreApplication> +#include <QLocalSocket> +#include <QWinEventNotifier> + +#define DEBUG_VAR_LEGACY "WINPTYDBG" +#define DEBUG_VAR_ACTUAL "WINPTY_DEBUG" +#define SHOW_CONSOLE_VAR "WINPTY_SHOW_CONSOLE" +#define WINPTY_AGENT_NAME "winpty-agent.exe" +#define WINPTY_DLL_NAME "winpty.dll" + +QString castErrorToString(winpty_error_ptr_t error_ptr) +{ + return QString::fromStdWString(winpty_error_msg(error_ptr)); +} + +WinPtyProcess::WinPtyProcess() + : IPtyProcess() + , m_ptyHandler(nullptr) + , m_innerHandle(nullptr) + , m_inSocket(nullptr) + , m_outSocket(nullptr) +{ +#ifdef PTYQT_DEBUG + m_trace = true; +#endif +} + +WinPtyProcess::~WinPtyProcess() +{ + kill(); +} + +bool WinPtyProcess::startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows) +{ + if (!isAvailable()) + { + m_lastError = QString("WinPty Error: winpty-agent.exe or winpty.dll not found!"); + return false; + } + + //already running + if (m_ptyHandler != nullptr) + return false; + + QFileInfo fi(executable); + if (fi.isRelative() || !QFile::exists(executable)) + { + //todo add auto-find executable in PATH env var + m_lastError = QString("WinPty Error: shell file path must be absolute"); + return false; + } + + m_shellPath = executable; + m_shellPath.replace('/', '\\'); + m_size = QPair<qint16, qint16>(cols, rows); + +#ifdef PTYQT_DEBUG + if (m_trace) + { + environment.append(QString("%1=1").arg(DEBUG_VAR_LEGACY)); + environment.append(QString("%1=trace").arg(DEBUG_VAR_ACTUAL)); + environment.append(QString("%1=1").arg(SHOW_CONSOLE_VAR)); + SetEnvironmentVariableA(DEBUG_VAR_LEGACY, "1"); + SetEnvironmentVariableA(DEBUG_VAR_ACTUAL, "trace"); + SetEnvironmentVariableA(SHOW_CONSOLE_VAR, "1"); + } +#endif + + //env + std::wstringstream envBlock; + for (const QString &line : environment) + { + envBlock << line.toStdWString() << L'\0'; + } + std::wstring env = envBlock.str(); + + //create start config + winpty_error_ptr_t errorPtr = nullptr; + winpty_config_t* startConfig = winpty_config_new(0, &errorPtr); + if (startConfig == nullptr) + { + m_lastError = QString("WinPty Error: create start config -> %1").arg(castErrorToString(errorPtr)); + return false; + } + winpty_error_free(errorPtr); + + //set params + winpty_config_set_initial_size(startConfig, cols, rows); + winpty_config_set_mouse_mode(startConfig, WINPTY_MOUSE_MODE_AUTO); + //winpty_config_set_agent_timeout(); + + //start agent + m_ptyHandler = winpty_open(startConfig, &errorPtr); + winpty_config_free(startConfig); //start config is local var, free it after use + + if (m_ptyHandler == nullptr) + { + m_lastError = QString("WinPty Error: start agent -> %1").arg(castErrorToString(errorPtr)); + return false; + } + winpty_error_free(errorPtr); + + //create spawn config + winpty_spawn_config_t* spawnConfig = winpty_spawn_config_new(WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN, m_shellPath.toStdWString().c_str(), + //commandLine.toStdWString().c_str(), cwd.toStdWString().c_str(), + arguments.join(" ").toStdWString().c_str(), workingDir.toStdWString().c_str(), + env.c_str(), + &errorPtr); + + if (spawnConfig == nullptr) + { + m_lastError = QString("WinPty Error: create spawn config -> %1").arg(castErrorToString(errorPtr)); + return false; + } + winpty_error_free(errorPtr); + + //spawn the new process + BOOL spawnSuccess = winpty_spawn(m_ptyHandler, spawnConfig, &m_innerHandle, nullptr, nullptr, &errorPtr); + winpty_spawn_config_free(spawnConfig); //spawn config is local var, free it after use + if (!spawnSuccess) + { + m_lastError = QString("WinPty Error: start terminal process -> %1").arg(castErrorToString(errorPtr)); + return false; + } + winpty_error_free(errorPtr); + + m_pid = (int)GetProcessId(m_innerHandle); + + m_outSocket = new QLocalSocket(); + + // Notify when the shell process has been terminated + m_shellCloseWaitNotifier = new QWinEventNotifier(m_innerHandle, notifier()); + QObject::connect(m_shellCloseWaitNotifier, + &QWinEventNotifier::activated, + notifier(), + [this](HANDLE hEvent) { + DWORD exitCode = 0; + GetExitCodeProcess(hEvent, &exitCode); + m_exitCode = exitCode; + // Do not respawn if the object is about to be destructed + if (!m_aboutToDestruct) + emit notifier()->aboutToClose(); + m_shellCloseWaitNotifier->setEnabled(false); + }); + + //get pipe names + LPCWSTR conInPipeName = winpty_conin_name(m_ptyHandler); + m_conInName = QString::fromStdWString(std::wstring(conInPipeName)); + m_inSocket = new QLocalSocket(); + m_inSocket->connectToServer(m_conInName, QIODevice::WriteOnly); + m_inSocket->waitForConnected(); + + LPCWSTR conOutPipeName = winpty_conout_name(m_ptyHandler); + m_conOutName = QString::fromStdWString(std::wstring(conOutPipeName)); + m_outSocket->connectToServer(m_conOutName, QIODevice::ReadOnly); + m_outSocket->waitForConnected(); + + if (m_inSocket->state() != QLocalSocket::ConnectedState && m_outSocket->state() != QLocalSocket::ConnectedState) + { + m_lastError = QString("WinPty Error: Unable to connect local sockets -> %1 / %2").arg(m_inSocket->errorString()).arg(m_outSocket->errorString()); + m_inSocket->deleteLater(); + m_outSocket->deleteLater(); + m_inSocket = nullptr; + m_outSocket = nullptr; + return false; + } + + return true; +} + +bool WinPtyProcess::resize(qint16 cols, qint16 rows) +{ + if (m_ptyHandler == nullptr) + { + return false; + } + + bool res = winpty_set_size(m_ptyHandler, cols, rows, nullptr); + + if (res) + { + m_size = QPair<qint16, qint16>(cols, rows); + } + + return res; +} + +bool WinPtyProcess::kill() +{ + bool exitCode = false; + if (m_innerHandle != nullptr && m_ptyHandler != nullptr) { + m_aboutToDestruct = true; + //disconnect all signals (readyRead, ...) + m_inSocket->disconnect(); + m_outSocket->disconnect(); + + //disconnect for server + m_inSocket->disconnectFromServer(); + m_outSocket->disconnectFromServer(); + + m_inSocket->deleteLater(); + m_outSocket->deleteLater(); + + m_inSocket = nullptr; + m_outSocket = nullptr; + + winpty_free(m_ptyHandler); + exitCode = CloseHandle(m_innerHandle); + + delete m_shellCloseWaitNotifier; + m_shellCloseWaitNotifier = nullptr; + + m_ptyHandler = nullptr; + m_innerHandle = nullptr; + m_conInName = QString(); + m_conOutName = QString(); + m_pid = 0; + } + return exitCode; +} + +IPtyProcess::PtyType WinPtyProcess::type() +{ + return PtyType::WinPty; +} + +QString WinPtyProcess::dumpDebugInfo() +{ +#ifdef PTYQT_DEBUG + return QString("PID: %1, ConIn: %2, ConOut: %3, Type: %4, Cols: %5, Rows: %6, IsRunning: %7, Shell: %8") + .arg(m_pid).arg(m_conInName).arg(m_conOutName).arg(type()) + .arg(m_size.first).arg(m_size.second).arg(m_ptyHandler != nullptr) + .arg(m_shellPath); +#else + return QString("Nothing..."); +#endif +} + +QIODevice *WinPtyProcess::notifier() +{ + return m_outSocket; +} + +QByteArray WinPtyProcess::readAll() +{ + return m_outSocket->readAll(); +} + +qint64 WinPtyProcess::write(const QByteArray &byteArray) +{ + return m_inSocket->write(byteArray); +} + +bool WinPtyProcess::isAvailable() +{ +#ifdef PTYQT_BUILD_STATIC + return QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_AGENT_NAME); +#elif PTYQT_BUILD_DYNAMIC + return QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_AGENT_NAME) + && QFile::exists(QCoreApplication::applicationDirPath() + "/" + WINPTY_DLL_NAME); +#endif + return true; +} + +void WinPtyProcess::moveToThread(QThread *targetThread) +{ + m_inSocket->moveToThread(targetThread); + m_outSocket->moveToThread(targetThread); +} diff --git a/src/libs/3rdparty/libptyqt/winptyprocess.h b/src/libs/3rdparty/libptyqt/winptyprocess.h new file mode 100644 index 0000000000..ee91fbdf8d --- /dev/null +++ b/src/libs/3rdparty/libptyqt/winptyprocess.h @@ -0,0 +1,43 @@ +#ifndef WINPTYPROCESS_H +#define WINPTYPROCESS_H + +#include "iptyprocess.h" +#include "winpty.h" + +class QLocalSocket; +class QWinEventNotifier; + +class WinPtyProcess : public IPtyProcess +{ +public: + WinPtyProcess(); + ~WinPtyProcess(); + + bool startProcess(const QString &executable, + const QStringList &arguments, + const QString &workingDir, + QStringList environment, + qint16 cols, + qint16 rows); + bool resize(qint16 cols, qint16 rows); + bool kill(); + PtyType type(); + QString dumpDebugInfo(); + QIODevice *notifier(); + QByteArray readAll(); + qint64 write(const QByteArray &byteArray); + static bool isAvailable(); + void moveToThread(QThread *targetThread); + +private: + winpty_t *m_ptyHandler; + HANDLE m_innerHandle; + QString m_conInName; + QString m_conOutName; + QLocalSocket *m_inSocket; + QLocalSocket *m_outSocket; + bool m_aboutToDestruct{false}; + QWinEventNotifier* m_shellCloseWaitNotifier; +}; + +#endif // WINPTYPROCESS_H |