diff --git a/.github/actions/spelling/allow/allow.txt b/.github/actions/spelling/allow/allow.txt index 0fddd6495a..57925b284c 100644 --- a/.github/actions/spelling/allow/allow.txt +++ b/.github/actions/spelling/allow/allow.txt @@ -1,5 +1,6 @@ AABBGGRR BBX +CLSCTX COLRv DWIDTH ENDCHAR @@ -7,18 +8,30 @@ ENDFONT ENDPROPERTIES FONTBOUNDINGBOX FreeBSD +HKCR +IClass +ITerminal LASTEXITCODE +MULTIPLEUSE +NOAGGREGATION +NOINTERFACE OPENCONSOLE OpenBSD +REFIID +REGCLS SEMA STARTCHAR STARTFONT STARTPROPERTIES +STDMETHODCALLTYPE SWIDTH +Unknwn Unpremultiply UpperCamelCase XBase +XCount YBase +YCount aea appleclang bdf @@ -38,6 +51,9 @@ nupkg nushell openxr ppem +ppv +psz +riid tablegen vswhere zypper diff --git a/cmake/presets/os-windows.json b/cmake/presets/os-windows.json index c4a3d4ad14..1174e4ca25 100644 --- a/cmake/presets/os-windows.json +++ b/cmake/presets/os-windows.json @@ -83,6 +83,42 @@ "value": "x64", "strategy": "external" } + }, + { + "name": "windows-msvc-ninja-debug", + "inherits": [ + "windows-common", + "debug" + ], + "displayName": "Windows (MSVC+Ninja) Debug", + "description": "Using MSVC compiler with Ninja generator", + "generator": "Ninja", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + } + }, + { + "name": "windows-msvc-ninja-release", + "inherits": [ + "windows-common", + "release" + ], + "displayName": "Windows (MSVC+Ninja) Release", + "description": "Using MSVC compiler with Ninja generator", + "generator": "Ninja", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + } } ], "buildPresets": [ @@ -109,6 +145,18 @@ "displayName": "x64 (ClangCL) RelWithDebInfo", "configurePreset": "clangcl-release", "configuration": "RelWithDebInfo" + }, + { + "name": "windows-msvc-ninja-debug", + "displayName": "x64 (MSVC+Ninja) Debug", + "configurePreset": "windows-msvc-ninja-debug", + "configuration": "Debug" + }, + { + "name": "windows-msvc-ninja-release", + "displayName": "x64 (MSVC+Ninja) RelWithDebInfo", + "configurePreset": "windows-msvc-ninja-release", + "configuration": "RelWithDebInfo" } ], "testPresets": [ @@ -149,6 +197,20 @@ "Debug" ], "configurePreset": "msvc-debug" + }, + { + "name": "windows-msvc-ninja-debug", + "configurations": [ + "Debug" + ], + "configurePreset": "windows-msvc-ninja-debug" + }, + { + "name": "windows-msvc-ninja-release", + "configurations": [ + "RelWithDebInfo" + ], + "configurePreset": "windows-msvc-ninja-release" } ] } diff --git a/metainfo.xml b/metainfo.xml index 21249b71ee..beff0be973 100644 --- a/metainfo.xml +++ b/metainfo.xml @@ -163,6 +163,7 @@
  • Adds VT sequences for double width / double height lines, `DECDHL`, `DECDWL`, `DECSWL` (#137)
  • Adds VT sequence extension OSC 133 shell integration (#793)
  • Adds support for rendering COLRv1 fonts (e.g. new Google Noto Color Emoji)
  • +
  • Adds Contour Terminal to the list of possible default terminals on Windows 11 (#605)
  • Drop Qt5 support
  • diff --git a/patch_openconsole.py b/patch_openconsole.py new file mode 100644 index 0000000000..bc7453cd91 --- /dev/null +++ b/patch_openconsole.py @@ -0,0 +1,94 @@ + +import sys +import struct + +def guid_to_bytes(guid_str): + # UUID format: 2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69 + parts = guid_str.replace('-', '').replace('{', '').replace('}', '') + d1 = int(parts[0:8], 16) + d2 = int(parts[8:12], 16) + d3 = int(parts[12:16], 16) + d4s = parts[16:] + d4 = [int(d4s[i:i+2], 16) for i in range(0, 16, 2)] + + # Struct: DWORD, WORD, WORD, BYTE[8] + # Little endian for first 3 + return struct.pack(' 0: + data = data.replace(org_bytes, new_bytes) + print("Replaced raw GUID bytes.") + + # 2. Patch UTF-16LE String "{GUID}" + org_wstr = f"{{{org_clsid_str}}}".encode('utf-16le') + new_wstr = f"{{{new_clsid_str}}}".encode('utf-16le') + + count_w = data.count(org_wstr) + print(f"Found {count_w} instances of UTF-16LE string.") + if count_w > 0: + data = data.replace(org_wstr, new_wstr) + print("Replaced UTF-16LE strings.") + + # 2b. Patch UTF-16LE String "GUID" (no braces) - Just in case + org_wstr_nb = f"{org_clsid_str}".encode('utf-16le') + new_wstr_nb = f"{new_clsid_str}".encode('utf-16le') + + count_w_nb = data.count(org_wstr_nb) + print(f"Found {count_w_nb} instances of UTF-16LE string (no braces).") + if count_w_nb > 0: + # Avoid double replacing if brace version covered it? + # replace() handles it, but safer to do brace version first (more specific). + # Check if any left + if data.count(org_wstr_nb) > 0: + data = data.replace(org_wstr_nb, new_wstr_nb) + print("Replaced UTF-16LE strings (no braces).") + + # 3. Patch ASCII String "{GUID}" + org_astr = f"{{{org_clsid_str}}}".encode('ascii') + new_astr = f"{{{new_clsid_str}}}".encode('ascii') + + count_a = data.count(org_astr) + print(f"Found {count_a} instances of ASCII string.") + if count_a > 0: + data = data.replace(org_astr, new_astr) + print("Replaced ASCII strings.") + + # 3b. Patch ASCII String "GUID" (no braces) + org_astr_nb = f"{org_clsid_str}".encode('ascii') + new_astr_nb = f"{new_clsid_str}".encode('ascii') + + count_a_nb = data.count(org_astr_nb) + print(f"Found {count_a_nb} instances of ASCII string (no braces).") + if count_a_nb > 0: + data = data.replace(org_astr_nb, new_astr_nb) + print("Replaced ASCII strings (no braces).") + + print(f"Writing {output_path}...") + with open(output_path, 'wb') as f: + f.write(data) + print("Done.") + +if __name__ == "__main__": + if len(sys.argv) < 3: + print("Usage: patch_openconsole.py ") + sys.exit(1) + + patch_file(sys.argv[1], sys.argv[2]) diff --git a/src/contour/CMakeLists.txt b/src/contour/CMakeLists.txt index d522b33656..9a36d84d0c 100644 --- a/src/contour/CMakeLists.txt +++ b/src/contour/CMakeLists.txt @@ -11,6 +11,12 @@ endif() option(CONTOUR_PERF_STATS "Enables debug printing some performance stats." OFF) option(CONTOUR_WAYLAND "Enables Wayland specific code paths." ON) +if(WIN32) + option(CONTOUR_SIGN_CODE_CERTIFICATE "Path to the certificate file (.pfx) to sign the executable with using signtool. If empty, a self-signed certificate will be generated." "") + option(CONTOUR_SIGN_CODE_PASSWORD "Password for the certificate file." "") + set(CONTOUR_SIGN_CODE_PUBLISHER "CN=Contour Terminal Self-Signed" CACHE STRING "Publisher DN for the AppxManifest and Self-Signed Certificate") +endif() + NumberToHex(${PROJECT_VERSION_MAJOR} HEX_MAJOR) NumberToHex(${PROJECT_VERSION_MINOR} HEX_MINOR) NumberToHex(${PROJECT_VERSION_PATCH} HEX_PATCH) @@ -72,7 +78,29 @@ if(CONTOUR_FRONTEND_GUI) endif() if(WIN32) - list(APPEND _source_files contour.rc) + list(APPEND _source_files + contour.rc + windows/TerminalHandoff.cpp + windows/TerminalHandoff.h + windows/ITerminalHandoff.h + windows/AppxManifest.xml + windows/register_package.ps1 + ) +endif() + +# Install the AppxManifest and registration script to the bin directory +if(WIN32) + if(DEFINED ENV{GITHUB_RUN_NUMBER}) + set(CONTOUR_VERSION_TWEAK "$ENV{GITHUB_RUN_NUMBER}") + else() + set(CONTOUR_VERSION_TWEAK "0") + endif() + + + configure_file(windows/AppxManifest.xml AppxManifest.xml @ONLY) + set(AppxManifest "${CMAKE_CURRENT_BINARY_DIR}/AppxManifest.xml") + install(FILES "${AppxManifest}" DESTINATION bin) + install(FILES "windows/register_package.ps1" DESTINATION bin) endif() set(_qt_resources resources.qrc) @@ -89,7 +117,7 @@ source_group(Headers FILES ${_header_files}) source_group(Resources FILES ${_qt_resources}) source_group(QML FILES ${_qml_files}) -add_executable(contour) +add_executable(contour WIN32) target_sources(contour PRIVATE ${_source_files} ${_header_files} ${_qt_resources} ${_qml_files}) set_target_properties(contour PROPERTIES AUTOMOC ON) @@ -130,6 +158,168 @@ if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug") endif() # }}} +if (WIN32) + # Set up the certificate + if(NOT CONTOUR_SIGN_CODE_CERTIFICATE) + set(CONTOUR_SIGN_CODE_CERTIFICATE "${CMAKE_CURRENT_BINARY_DIR}/contour_dev.pfx") + add_custom_command( + OUTPUT "${CONTOUR_SIGN_CODE_CERTIFICATE}" + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -Command + "$cert = New-SelfSignedCertificate -Type Custom -Subject '${CONTOUR_SIGN_CODE_PUBLISHER}' -KeyUsage DigitalSignature -FriendlyName 'Contour Code Signing' -CertStoreLocation 'Cert:\\CurrentUser\\My' -TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}') ; $pwd = ConvertTo-SecureString -String '${CONTOUR_SIGN_CODE_PASSWORD}' -Force -AsPlainText ; Export-PfxCertificate -Cert $cert -FilePath '${CONTOUR_SIGN_CODE_CERTIFICATE}' -Password $pwd" + COMMENT "Generating self-signed code signing certificate: ${CONTOUR_SIGN_CODE_CERTIFICATE}" + VERBATIM + ) + add_custom_target(generate_pfx DEPENDS "${CONTOUR_SIGN_CODE_CERTIFICATE}") + add_dependencies(contour generate_pfx) + endif() + + find_program(SIGNTOOL_EXECUTABLE signtool PATHS + "C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x64" + "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x64" + "C:/Program Files (x86)/Windows Kits/10/bin/x64" + ) + + if(NOT SIGNTOOL_EXECUTABLE) + message(WARNING "signtool not found. Code signing will be disabled.") + endif() + + add_custom_command(TARGET contour POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory $/Assets + COMMAND ${CMAKE_COMMAND} -E copy + "${AppxManifest}" + "${CMAKE_CURRENT_SOURCE_DIR}/windows/register_package.ps1" + $ + # Copy and rename assets to match the AppxManifest expectations + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/res/images/contour-logo-512.png" $/Assets/StoreLogo.png + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/res/images/contour-logo-128.png" $/Assets/Square150x150Logo.png + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/res/images/contour-logo-64.png" $/Assets/Square44x44Logo.png + COMMENT "Copying AppxManifest.xml, registration scripts, and Assets to output directory" + ) + + if(SIGNTOOL_EXECUTABLE) + add_custom_command(TARGET contour POST_BUILD + COMMAND "${SIGNTOOL_EXECUTABLE}" sign /f "${CONTOUR_SIGN_CODE_CERTIFICATE}" /p "${CONTOUR_SIGN_CODE_PASSWORD}" /fd SHA256 /v "$" + COMMENT "Signing contour.exe and AppxManifest.xml with ${CONTOUR_SIGN_CODE_CERTIFICATE}" + VERBATIM + ) + endif() + + add_custom_target(install_certificate + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -Command + "$pwd = ConvertTo-SecureString -String '${CONTOUR_SIGN_CODE_PASSWORD}' -Force -AsPlainText; Import-PfxCertificate -FilePath '${CONTOUR_SIGN_CODE_CERTIFICATE}' -CertStoreLocation 'Cert:\\LocalMachine\\TrustedPeople' -Password $pwd" + COMMENT "Installing code signing certificate to TrustedPeople store (requires Admin)" + VERBATIM + ) + + add_custom_target(register_package + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -Command + "Add-AppxPackage -Register '$/AppxManifest.xml' -ForceApplicationShutdown" + COMMENT "Registering Contour Appx Package" + VERBATIM + ) + + add_custom_target(unregister_package + COMMAND powershell -NoProfile -ExecutionPolicy Bypass -Command + "Get-AppxPackage -Name 'Contour.Terminal' | Remove-AppxPackage -ErrorAction SilentlyContinue" + COMMENT "Unregistering Contour Appx Package" + VERBATIM + ) + + if(SIGNTOOL_EXECUTABLE AND CONTOUR_SIGN_CODE_CERTIFICATE) + install(CODE " + message(STATUS \"Signing installed executable: \${CMAKE_INSTALL_PREFIX}/bin/contour.exe\") + execute_process( + COMMAND \"${SIGNTOOL_EXECUTABLE}\" sign /f \"${CONTOUR_SIGN_CODE_CERTIFICATE}\" /p \"${CONTOUR_SIGN_CODE_PASSWORD}\" /fd SHA256 /v \"\${CMAKE_INSTALL_PREFIX}/bin/contour.exe\" + RESULT_VARIABLE _sign_result + OUTPUT_VARIABLE _sign_output + ERROR_VARIABLE _sign_output + ) + if(NOT _sign_result EQUAL 0) + message(WARNING \"Failed to sign installed executable: \${_sign_output}\") + else() + message(STATUS \"\${_sign_output}\") + endif() + ") + endif() + + # --- Patch OpenConsole.exe with Contour's CLSID --- + # vtpty downloads OpenConsole.exe from Windows Terminal releases. + # We need to patch it to use Contour's CLSID for console handoff registration. + find_program(PYTHON_EXECUTABLE python REQUIRED) + + # Reference the OpenConsole.exe that vtpty downloads + set(OPENCONSOLE_SOURCE "${CMAKE_BINARY_DIR}/_deps/OpenConsole-1.24.3504.0/build/native/runtimes/x64/OpenConsole.exe") + set(CONTOUR_OPENCONSOLE "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/OpenConsole.exe") + + add_custom_command( + OUTPUT "${CONTOUR_OPENCONSOLE}" + COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/../../patch_openconsole.py" + "${OPENCONSOLE_SOURCE}" "${CONTOUR_OPENCONSOLE}" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/../../patch_openconsole.py" vtpty + COMMENT "Patching OpenConsole.exe with Contour's CLSID..." + VERBATIM + ) + + add_custom_target(PatchOpenConsole ALL DEPENDS "${CONTOUR_OPENCONSOLE}") + add_dependencies(contour PatchOpenConsole) + + install(FILES "${CONTOUR_OPENCONSOLE}" DESTINATION bin) + + # --- ContourProxy DLL for COM Marshaling --- + # Find MIDL compiler + find_program(MIDL_COMPILER midl REQUIRED) # Should be available in VS command prompt + + set(PROXY_IDL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/windows") + set(PROXY_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/proxy_gen") + file(MAKE_DIRECTORY "${PROXY_BINARY_DIR}") + + # Helper function to compile IDL + function(add_midl_target IDL_FILE BASE_NAME) + add_custom_command( + OUTPUT + "${PROXY_BINARY_DIR}/${BASE_NAME}_p.c" + "${PROXY_BINARY_DIR}/${BASE_NAME}_i.c" + "${PROXY_BINARY_DIR}/${BASE_NAME}.h" + # dlldata.c is handled manually/separately, but standard midl invocation might touch it if /dlldata is used. + # We use individual compilation so we don't use /dlldata here. + COMMAND "${MIDL_COMPILER}" /env x64 /Oicf /h "${PROXY_BINARY_DIR}/${BASE_NAME}.h" /iid "${PROXY_BINARY_DIR}/${BASE_NAME}_i.c" /proxy "${PROXY_BINARY_DIR}/${BASE_NAME}_p.c" "${PROXY_IDL_DIR}/${IDL_FILE}" + DEPENDS "${PROXY_IDL_DIR}/${IDL_FILE}" + WORKING_DIRECTORY "${PROXY_BINARY_DIR}" + COMMENT "Compiling ${IDL_FILE} with MIDL" + VERBATIM + ) + endfunction() + + add_midl_target("ITerminalHandoff.idl" "ITerminalHandoff") + add_midl_target("IConsoleHandoff.idl" "IConsoleHandoff") + + # The manual dlldata.c + set(PROXY_SOURCES + "${PROXY_IDL_DIR}/ContourProxy_dlldata.c" + "${PROXY_BINARY_DIR}/ITerminalHandoff_p.c" + "${PROXY_BINARY_DIR}/ITerminalHandoff_i.c" + "${PROXY_BINARY_DIR}/IConsoleHandoff_p.c" + "${PROXY_BINARY_DIR}/IConsoleHandoff_i.c" + "${PROXY_IDL_DIR}/contour_proxy.def" # We will create this + ) + + add_library(ContourProxy SHARED ${PROXY_SOURCES}) + target_include_directories(ContourProxy PRIVATE "${PROXY_BINARY_DIR}") + target_link_libraries(ContourProxy PRIVATE rpcrt4 kernel32 user32 advapi32 ole32 oleaut32 uuid) + target_compile_definitions(ContourProxy PRIVATE + REGISTER_PROXY_DLL + _WIN32_WINNT=0x0A00 # Win10+ + WIN32_LEAN_AND_MEAN + ) + + # Use a .def file to export DllGetClassObject etc. + # Creating it on the fly if not exists? Or write it now. + # We will write it in next step, but reference it here. + + install(TARGETS ContourProxy DESTINATION bin) + +endif() + # {{{ platform specific target set_target_properties if(WIN32) if (NOT ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")) @@ -282,6 +472,9 @@ if(WIN32) set(CPACK_PACKAGE_INSTALL_DIRECTORY "Contour Terminal Emulator ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") set(CPACK_WIX_PATCH_FILE "${CMAKE_CURRENT_SOURCE_DIR}/wix_patch.xml") set(CPACK_WIX_PROPERTY_WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT "Launch Contour") + + # Allow CPack to sign the uninstaller/MSI if we configured signing? + # CPack WIX Generator has its own signing hooks, but for now we focus on the EXE built by CMake. endif() if(APPLE) set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/res/images/contour-logo.icns") diff --git a/src/contour/ContourGuiApp.cpp b/src/contour/ContourGuiApp.cpp index 4bd69e5903..96e7136592 100644 --- a/src/contour/ContourGuiApp.cpp +++ b/src/contour/ContourGuiApp.cpp @@ -46,6 +46,83 @@ using namespace std::string_literals; namespace fs = std::filesystem; +#if defined(_WIN32) + #include + + #include + + #include + + #include + +static DWORD g_comRegistrationId = 0; + +void SimpleFileLogger(std::string const& msg); // Forward declaration + +static void RegisterCOMServer() +{ + SimpleFileLogger("Registering COM Server..."); + static TerminalHandoffFactory factory; + HRESULT hr = CoRegisterClassObject(CLSID_ContourTerminalHandoff, + &factory, + CLSCTX_LOCAL_SERVER, + REGCLS_MULTIPLEUSE, + &g_comRegistrationId); + if (FAILED(hr)) + { + SimpleFileLogger(std::format("Failed to register COM Class Object: {:x}", (unsigned) hr)); + std::cerr << "Failed to register COM Class Object: " << hr << std::endl; + } + else + { + SimpleFileLogger("COM Server registered successfully."); + } +} + +static void UnregisterCOMServer() +{ + if (g_comRegistrationId != 0) + { + SimpleFileLogger("Revoking COM Class Object."); + CoRevokeClassObject(g_comRegistrationId); + g_comRegistrationId = 0; + } +} + +// Global handler called from TerminalHandoff COM object +// Defined outside namespace to match forward declaration in TerminalHandoff.cpp +void ContourHandleHandoff(HANDLE hInput, + HANDLE hOutput, + HANDLE hSignal, + HANDLE hReference, + HANDLE hServer, + HANDLE hClient, + std::wstring const& title) +{ + SimpleFileLogger("ContourHandleHandoff invoked."); + auto* app = contour::ContourGuiApp::instance(); + if (app) + { + SimpleFileLogger("App instance found, dispatching newWindowWithHandoff."); + // Dispatch to main thread + QMetaObject::invokeMethod( + app, [app, hInput, hOutput, hSignal, hReference, hServer, hClient, title]() { + app->newWindowWithHandoff(hInput, hOutput, hSignal, hReference, hServer, hClient, title); + }); + } + else + { + SimpleFileLogger("App instance NOT found during handoff."); + CloseHandle(hInput); + CloseHandle(hOutput); + CloseHandle(hSignal); + CloseHandle(hReference); + CloseHandle(hServer); + CloseHandle(hClient); + } +} +#endif + namespace CLI = crispy::cli; namespace contour @@ -63,6 +140,20 @@ int ContourGuiApp::run(int argc, char const* argv[]) _argc = argc; _argv = argv; +#if defined(_WIN32) + // Initialize COM for this thread (required before any COM operations) + HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (FAILED(hr)) + { + SimpleFileLogger(std::format("CoInitializeEx failed: {:x}", (unsigned) hr)); + return -1; + } + + // Register COM server IMMEDIATELY for -Embedding mode + // This must happen before Qt initialization so OpenConsole can call ITerminalHandoff3 + RegisterCOMServer(); +#endif + return ContourApp::run(argc, argv); } @@ -135,6 +226,11 @@ crispy::cli::command ContourGuiApp::parameterDefinition() const CLI::option { "display", CLI::value { ""s }, "Sets the X11 display to connect to.", "DISPLAY_ID" }, #endif + CLI::option { + CLI::option_name { "embedding" }, + CLI::value { false }, + "COM Embedding flag (internal use only).", + }, CLI::option { CLI::option_name { 'e', "execute" }, CLI::value { ""s }, @@ -436,7 +532,6 @@ int ContourGuiApp::terminalGuiAction() sessionsManager().updateColorPreference(_colorPreference); }); #endif - // Enforce OpenGL over any other. As much as I'd love to provide other backends, too. // We currently only support OpenGL. // If anyone feels happy about it, I'd love to at least provide Vulkan. ;-) @@ -465,7 +560,24 @@ int ContourGuiApp::terminalGuiAction() // printf("\r%s %s %s\r", TBC, HTS, HTS); // Spawn initial window. - newWindow(); + bool isEmbedding = false; +#if defined(_WIN32) + for (int i = 0; i < _argc; ++i) + { + std::string_view arg = _argv[i]; + if (arg == "--embedding" || arg == "-embedding" || arg == "/embedding" || arg == "-Embedding" + || arg == "/Embedding") + { + isEmbedding = true; + break; + } + } +#endif + + if (!isEmbedding) + newWindow(); + else + QApplication::setQuitOnLastWindowClosed(false); if (auto const& bell = config().profile().bell.value().sound; bell == "off") { @@ -482,6 +594,10 @@ int ContourGuiApp::terminalGuiAction() auto rv = QApplication::exec(); +#if defined(_WIN32) + UnregisterCOMServer(); +#endif + if (_exitStatus.has_value()) { #if defined(VTPTY_LIBSSH2) @@ -512,6 +628,58 @@ int ContourGuiApp::terminalGuiAction() return rv; } +void ContourGuiApp::newWindowWithHandoff(void* hInput, + void* hOutput, + void* hSignal, + void* hReference, + void* hServer, + void* hClient, + std::wstring const& title) +{ +#if defined(_WIN32) + SimpleFileLogger( + std::format("newWindowWithHandoff called. in:{:p} out:{:p} sig:{:p} ref:{:p} srv:{:p} cli:{:p}", + hInput, + hOutput, + hSignal, + hReference, + hServer, + hClient)); + + HANDLE input = (hInput == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hInput); + HANDLE output = (hOutput == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hOutput); + HANDLE signal = (hSignal == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hSignal); + HANDLE reference = (hReference == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hReference); + HANDLE server = (hServer == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hServer); + HANDLE client = (hClient == INVALID_HANDLE_VALUE) ? nullptr : static_cast(hClient); + + // Allow creation explicitly + _sessionManager.allowCreation(); + + SimpleFileLogger("Creating HandoffPty..."); + // Create HandoffPty + auto pty = std::make_unique(input, output, signal, reference, server, client, title); + + SimpleFileLogger("Invoking createSessionWithPty..."); + // Create session (this will activate it and create window/tab via session manager) + _sessionManager.createSessionWithPty(std::move(pty)); + newWindow(); + SimpleFileLogger("createSessionWithPty returned. Window created."); +#else + (void) hInput; + (void) hOutput; + (void) hSignal; + (void) hReference; + (void) hServer; + (void) hClient; + (void) title; +#endif +} + +// ... (Existing implementations) + +// ... (Around line 350, inside terminalGuiAction) + void ContourGuiApp::setupQCoreApplication() { auto const* profile = _config.profile(profileName()); diff --git a/src/contour/ContourGuiApp.h b/src/contour/ContourGuiApp.h index 164a9100de..668ec727a3 100644 --- a/src/contour/ContourGuiApp.h +++ b/src/contour/ContourGuiApp.h @@ -40,6 +40,13 @@ class ContourGuiApp: public QObject, public ContourApp [[nodiscard]] crispy::cli::command parameterDefinition() const override; void newWindow(); + void newWindowWithHandoff(void* hInput, + void* hOutput, + void* hSignal, + void* hReference, + void* hServer, + void* hClient, + std::wstring const& title); static void showNotification(std::string_view title, std::string_view content); [[nodiscard]] std::string profileName() const; diff --git a/src/contour/TerminalSessionManager.cpp b/src/contour/TerminalSessionManager.cpp index 83dda4b82d..4432a652af 100644 --- a/src/contour/TerminalSessionManager.cpp +++ b/src/contour/TerminalSessionManager.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #include using namespace std::string_literals; @@ -237,9 +239,36 @@ void TerminalSessionManager::FocusOnDisplay(display::TerminalDisplay* display) TerminalSession* TerminalSessionManager::createSession() { + if (auto* session = _displayStates[nullptr].currentSession; session) + { + managerLog()("createSession: returning pending session {}({})", session->id(), (void*) session); + return activateSession(session, true); + } return activateSession(createSessionInBackground(), true /*force resize on before display-attach*/); } +TerminalSession* TerminalSessionManager::createSessionWithPty(std::unique_ptr pty) +{ + if (!_activeDisplay) + { + managerLog()("No active display found. something went wrong."); + } + + auto* session = new TerminalSession(this, std::move(pty), _app); + managerLog()("Create new handoff session with ID {}({}) at index {}", + session->id(), + (void*) session, + _sessions.size()); + + _sessions.insert(_sessions.end(), session); + + connect(session, &TerminalSession::sessionClosed, [this, session]() { removeSession(*session); }); + + QQmlEngine::setObjectOwnership(session, QQmlEngine::CppOwnership); + + return activateSession(session, true); +} + void TerminalSessionManager::switchToPreviousTab() { managerLog()("switch to previous tab (current: {}, previous: {})", diff --git a/src/contour/TerminalSessionManager.h b/src/contour/TerminalSessionManager.h index 80b5ecec28..8d229af444 100644 --- a/src/contour/TerminalSessionManager.h +++ b/src/contour/TerminalSessionManager.h @@ -30,6 +30,7 @@ class TerminalSessionManager: public QAbstractListModel contour::TerminalSession* createSessionInBackground(); Q_INVOKABLE contour::TerminalSession* createSession(); + contour::TerminalSession* createSessionWithPty(std::unique_ptr pty); void switchToPreviousTab(); void switchToTabLeft(); diff --git a/src/contour/main.cpp b/src/contour/main.cpp index ad7dbcd915..fb676e76ad 100644 --- a/src/contour/main.cpp +++ b/src/contour/main.cpp @@ -129,14 +129,93 @@ void qtCustomMessageOutput(QtMsgType type, const QMessageLogContext& context, co abort(); } } + } // namespace +#if defined(_WIN32) +int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) +{ + UNREFERENCED_PARAMETER(hInstance); + UNREFERENCED_PARAMETER(hPrevInstance); + UNREFERENCED_PARAMETER(lpCmdLine); + UNREFERENCED_PARAMETER(nCmdShow); + + int argc; + LPWSTR* argvW = CommandLineToArgvW(GetCommandLineW(), &argc); + if (!argvW) + return -1; + + std::vector args; + args.reserve(argc); + for (int i = 0; i < argc; ++i) + { + int size_needed = WideCharToMultiByte(CP_UTF8, 0, argvW[i], -1, NULL, 0, NULL, NULL); + std::string str(size_needed ? size_needed - 1 : 0, 0); + WideCharToMultiByte(CP_UTF8, 0, argvW[i], -1, &str[0], size_needed, NULL, NULL); + args.push_back(std::move(str)); + } + LocalFree(argvW); + + std::vector argvC; + argvC.reserve(args.size()); + for (const auto& arg: args) + argvC.push_back(arg.c_str()); + + // Forward to main + extern int main(int argc, char const* argv[]); + return main(static_cast(argvC.size()), argvC.data()); +} +#endif + +void SimpleFileLogger(std::string const& msg) +{ +#if defined(_WIN32) + char tempPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tempPath)) + { + std::string path = std::string(tempPath) + "contour_debug.txt"; + FILE* f = fopen(path.c_str(), "a"); + if (f) + { + fprintf(f, "[%u] %s\n", GetCurrentProcessId(), msg.c_str()); + fclose(f); + } + } +#endif +} + int main(int argc, char const* argv[]) { + // #if defined(_WIN32) + // MessageBoxA(nullptr, "Contour Main Reached!", "Debug", MB_OK); + // #endif + #if defined(_WIN32) tryAttachConsole(); #endif + // Normalize arguments: Windows passes -Embedding, but our CLI might expect --embedding + std::vector args; + args.reserve(argc); + for (int i = 0; i < argc; ++i) + { + std::string arg = argv[i]; + if (arg == "-Embedding" || arg == "/Embedding") + args.emplace_back("--embedding"); + else + args.emplace_back(arg); + } + + // Create new argv array + std::vector newArgv; + newArgv.reserve(args.size()); + for (auto const& s: args) + newArgv.emplace_back(s.c_str()); + + SimpleFileLogger("Contour started."); + for (int i = 0; i < newArgv.size(); ++i) + SimpleFileLogger(std::format("Arg {}: {}", i, newArgv[i])); + qInstallMessageHandler(qtCustomMessageOutput); #if defined(CONTOUR_FRONTEND_GUI) @@ -145,5 +224,5 @@ int main(int argc, char const* argv[]) contour::ContourApp app; #endif - return app.run(argc, argv); + return app.run(static_cast(newArgv.size()), newArgv.data()); } diff --git a/src/contour/windows/AppxManifest.xml b/src/contour/windows/AppxManifest.xml new file mode 100644 index 0000000000..4cc6024fc0 --- /dev/null +++ b/src/contour/windows/AppxManifest.xml @@ -0,0 +1,90 @@ + + + + + + + Contour Terminal + @CONTOUR_SIGN_CODE_PUBLISHER@ + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + {B178D323-E77D-4C67-AF21-AE2B81F269F0} + + + + + + + + + {F00DCAFE-0000-0000-0000-000000000001} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/contour/windows/ContourProxy_dlldata.c b/src/contour/windows/ContourProxy_dlldata.c new file mode 100644 index 0000000000..1f4f150ecc --- /dev/null +++ b/src/contour/windows/ContourProxy_dlldata.c @@ -0,0 +1,22 @@ + +#define PROXY_DELEGATION +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + EXTERN_PROXY_FILE(ITerminalHandoff) + + PROXYFILE_LIST_START + /* list of proxy files */ + REFERENCE_PROXY_FILE(ITerminalHandoff), + /* End of list */ + PROXYFILE_LIST_END + + DLLDATA_ROUTINES(aProxyFileList, GET_DLL_CLSID) + +#ifdef __cplusplus +} /*extern "C" */ +#endif diff --git a/src/contour/windows/IConsoleHandoff.idl b/src/contour/windows/IConsoleHandoff.idl new file mode 100644 index 0000000000..a36a728109 --- /dev/null +++ b/src/contour/windows/IConsoleHandoff.idl @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "unknwn.idl"; + +typedef struct _CONSOLE_PORTABLE_ATTACH_MSG +{ + DWORD IdLowPart; + LONG IdHighPart; + ULONG64 Process; + ULONG64 Object; + ULONG Function; + ULONG InputSize; + ULONG OutputSize; +} CONSOLE_PORTABLE_ATTACH_MSG; + +typedef CONSOLE_PORTABLE_ATTACH_MSG* PCONSOLE_PORTABLE_ATTACH_MSG; +typedef const CONSOLE_PORTABLE_ATTACH_MSG* PCCONSOLE_PORTABLE_ATTACH_MSG; + +[ + object, + uuid(E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4) +] interface IConsoleHandoff : IUnknown +{ + HRESULT EstablishHandoff([in, system_handle(sh_file)] HANDLE server, + [in, system_handle(sh_event)] HANDLE inputEvent, + [in, ref] PCCONSOLE_PORTABLE_ATTACH_MSG msg, + [in, system_handle(sh_pipe)] HANDLE signalPipe, + [in, system_handle(sh_process)] HANDLE inboxProcess, + [out, system_handle(sh_process)] HANDLE* process); +}; + +[ + object, + uuid(746E6BC0-AB05-4E38-AB14-71E86763141F) +] interface IDefaultTerminalMarker : IUnknown +{ +}; diff --git a/src/contour/windows/ITerminalHandoff.h b/src/contour/windows/ITerminalHandoff.h new file mode 100644 index 0000000000..c25730b8b9 --- /dev/null +++ b/src/contour/windows/ITerminalHandoff.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include + +struct TERMINAL_STARTUP_INFO +{ + BSTR pszTitle; + BSTR pszIconPath; + LONG iconIndex; + DWORD dwX; + DWORD dwY; + DWORD dwXSize; + DWORD dwYSize; + DWORD dwXCountChars; + DWORD dwYCountChars; + DWORD dwFillAttribute; + DWORD dwFlags; + WORD wShowWindow; +}; + +// Interface ITerminalHandoff3 +// {6F23DA90-15C5-4203-9DB0-64E73F1B1B00} +static const IID IID_ITerminalHandoff3 = { + 0x6F23DA90, 0x15C5, 0x4203, { 0x9D, 0xB0, 0x64, 0xE7, 0x3F, 0x1B, 0x1B, 0x00 } +}; + +struct ITerminalHandoff3: public IUnknown +{ + virtual HRESULT STDMETHODCALLTYPE EstablishPtyHandoff( + /* [out] */ HANDLE* in, + /* [out] */ HANDLE* out, + /* [in] */ HANDLE signal, + /* [in] */ HANDLE reference, + /* [in] */ HANDLE server, + /* [in] */ HANDLE client, + /* [in] */ const TERMINAL_STARTUP_INFO* startupInfo) = 0; +}; diff --git a/src/contour/windows/ITerminalHandoff.idl b/src/contour/windows/ITerminalHandoff.idl new file mode 100644 index 0000000000..83ab617870 --- /dev/null +++ b/src/contour/windows/ITerminalHandoff.idl @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "unknwn.idl"; + +typedef struct _TERMINAL_STARTUP_INFO +{ + // In STARTUPINFO + BSTR pszTitle; + + // Also wanted + BSTR pszIconPath; + LONG iconIndex; + + // The rest of STARTUPINFO + DWORD dwX; + DWORD dwY; + DWORD dwXSize; + DWORD dwYSize; + DWORD dwXCountChars; + DWORD dwYCountChars; + DWORD dwFillAttribute; + DWORD dwFlags; + WORD wShowWindow; +} TERMINAL_STARTUP_INFO; + +// LOAD BEARING! +// +// There is only ever one OpenConsoleProxy.dll loaded by COM for _ALL_ terminal +// instances, across Dev, Preview, Stable, whatever. So we have to keep all old +// versions of interfaces in the file here, even if the old version is no longer +// in use. + +// This was the original prototype. The reasons for changes to it are explained below. +[ + object, + uuid(59D55CCE-FC8A-48B4-ACE8-0A9286C6557F) +] interface ITerminalHandoff : IUnknown +{ + // DEPRECATED! + HRESULT EstablishPtyHandoff([in, system_handle(sh_pipe)] HANDLE in, + [in, system_handle(sh_pipe)] HANDLE out, + [in, system_handle(sh_pipe)] HANDLE signal, + [in, system_handle(sh_file)] HANDLE ref, + [in, system_handle(sh_process)] HANDLE server, + [in, system_handle(sh_process)] HANDLE client); +}; + +// We didn't consider the need for TERMINAL_STARTUP_INFO, because Windows Terminal never had support for launching +// .lnk files in the first place. They aren't executables after all, but rather a shell thing. +// Its need quickly became apparent right after releasing ITerminalHandoff, which is why it was very short-lived. +[ + object, + uuid(AA6B364F-4A50-4176-9002-0AE755E7B5EF) +] interface ITerminalHandoff2 : IUnknown +{ + HRESULT EstablishPtyHandoff([in, system_handle(sh_pipe)] HANDLE in, + [in, system_handle(sh_pipe)] HANDLE out, + [in, system_handle(sh_pipe)] HANDLE signal, + [in, system_handle(sh_file)] HANDLE ref, + [in, system_handle(sh_process)] HANDLE server, + [in, system_handle(sh_process)] HANDLE client, + [in] TERMINAL_STARTUP_INFO startupInfo); +}; + +// Quite a while later, we realized that passing the in/out handles as [in] instead of [out] has always been flawed. +// It prevents the terminal from making choices over the pipe buffer size and whether to use overlapped IO or not. +// The other handles remain [in] parameters because they have always been created internally by ConPTY. +// Additionally, passing TERMINAL_STARTUP_INFO by-value was unusual under COM as structs are usually given by-ref. +[ + object, + uuid(6F23DA90-15C5-4203-9DB0-64E73F1B1B00) +] interface ITerminalHandoff3 : IUnknown +{ + HRESULT EstablishPtyHandoff([out, system_handle(sh_pipe)] HANDLE* in, + [out, system_handle(sh_pipe)] HANDLE* out, + [in, system_handle(sh_pipe)] HANDLE signal, + [in, system_handle(sh_file)] HANDLE reference, + [in, system_handle(sh_process)] HANDLE server, + [in, system_handle(sh_process)] HANDLE client, + [in] const TERMINAL_STARTUP_INFO* startupInfo); +}; diff --git a/src/contour/windows/TerminalHandoff.cpp b/src/contour/windows/TerminalHandoff.cpp new file mode 100644 index 0000000000..84c4217aa3 --- /dev/null +++ b/src/contour/windows/TerminalHandoff.cpp @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include +#include +#include +#include + +extern void SimpleFileLogger(std::string const& msg); + +// Forward declaration of handler in the main application +// Forward declaration of the global handler in ContourGuiApp.cpp +void ContourHandleHandoff(HANDLE hInput, + HANDLE hOutput, + HANDLE hSignal, + HANDLE hReference, + HANDLE hServer, + HANDLE hClient, + std::wstring const& title); + +static HANDLE duplicateHandle(HANDLE h) +{ + HANDLE dup = INVALID_HANDLE_VALUE; + if (h != INVALID_HANDLE_VALUE) + { + if (!DuplicateHandle( + GetCurrentProcess(), h, GetCurrentProcess(), &dup, 0, FALSE, DUPLICATE_SAME_ACCESS)) + { + SimpleFileLogger(std::format( + "duplicateHandle: Failed to duplicate handle {:p}. Error: {}", h, GetLastError())); + return INVALID_HANDLE_VALUE; + } + } + return dup; +} + +// --- TerminalHandoff --- + +TerminalHandoff::TerminalHandoff() = default; + +extern void SimpleFileLogger(std::string const& msg); + +static std::string GuidToString(REFIID iid) +{ + LPOLESTR str = nullptr; + if (StringFromIID(iid, &str) == S_OK) + { + std::wstring w(str); + CoTaskMemFree(str); + std::string s(w.begin(), w.end()); + return s; + } + return "{???}"; +} + +HRESULT STDMETHODCALLTYPE TerminalHandoff::QueryInterface(REFIID riid, void** ppvObject) +{ + SimpleFileLogger(std::format("TerminalHandoff::QueryInterface request: {}", GuidToString(riid))); + if (!ppvObject) + return E_POINTER; + if (IsEqualIID(riid, IID_IUnknown)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + + // ITerminalHandoff (V1) {59D55CCE-FC8A-48B4-ACE8-0A9286C6557F} + static const IID IID_ITerminalHandoffV1_Real = { + 0x59D55CCE, 0xFC8A, 0x48B4, { 0xAC, 0xE8, 0x0A, 0x92, 0x86, 0xC6, 0x55, 0x7F } + }; + if (IsEqualIID(riid, IID_ITerminalHandoffV1_Real)) + { + SimpleFileLogger("TerminalHandoff::QueryInterface: Accepting ITerminalHandoff (V1)"); + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + + // ITerminalHandoff3 (V3) {6F23DA90-15C5-4203-9DB0-64E73F1B1B00} + // Also matches ITerminalHandoff3 interface layout (HANDLE* for in/out) + static const IID IID_ITerminalHandoff3_Real = { + 0x6F23DA90, 0x15C5, 0x4203, { 0x9D, 0xB0, 0x64, 0xE7, 0x3F, 0x1B, 0x1B, 0x00 } + }; + + if (IsEqualIID(riid, IID_ITerminalHandoff3) || IsEqualIID(riid, IID_ITerminalHandoff3_Real)) + { + SimpleFileLogger("TerminalHandoff::QueryInterface: Accepting ITerminalHandoff3 (V3)"); + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + + // Explicitly Log and Reject IConsoleHandoff {E686C757...} + static const IID IID_IConsoleHandoff = { + 0xE686C757, 0x9A35, 0x4A1C, { 0xB3, 0xCE, 0x0B, 0xCC, 0x8B, 0x5C, 0x69, 0xF4 } + }; + if (IsEqualIID(riid, IID_IConsoleHandoff)) + { + SimpleFileLogger( + "TerminalHandoff::QueryInterface: Rejecting IConsoleHandoff (E686C...) - Wrong Interface!"); + *ppvObject = nullptr; + return E_NOINTERFACE; + } + + if (IsEqualIID(riid, IID_IMarshal)) + { + // Standard marshalling request, log less verbosely + // SimpleFileLogger("TerminalHandoff::QueryInterface: IID_IMarshal"); + } + else + { + SimpleFileLogger("TerminalHandoff::QueryInterface: E_NOINTERFACE"); + } + *ppvObject = nullptr; + return E_NOINTERFACE; +} + +ULONG STDMETHODCALLTYPE TerminalHandoff::AddRef() +{ + return ++_refCount; +} + +ULONG STDMETHODCALLTYPE TerminalHandoff::Release() +{ + ULONG count = --_refCount; + if (count == 0) + delete this; + return count; +} + +HRESULT STDMETHODCALLTYPE TerminalHandoff::EstablishPtyHandoff(HANDLE* in, + HANDLE* out, + HANDLE signal, + HANDLE reference, + HANDLE server, + HANDLE client, + const TERMINAL_STARTUP_INFO* startupInfo) +{ + SimpleFileLogger("TerminalHandoff::EstablishPtyHandoff (V3) called!"); + + if (!in || !out) + return E_POINTER; + + // Create In Pipe (Server Write, Client Read) + SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE }; // Inheritable + HANDLE hInRead, hInWrite; + if (!CreatePipe(&hInRead, &hInWrite, &sa, 0)) + { + SimpleFileLogger("EstablishPtyHandoff: Failed to create IN pipe."); + return E_FAIL; + } + + // Create Out Pipe (Server Read, Client Write) + HANDLE hOutRead, hOutWrite; + if (!CreatePipe(&hOutRead, &hOutWrite, &sa, 0)) + { + SimpleFileLogger("EstablishPtyHandoff: Failed to create OUT pipe."); + CloseHandle(hInRead); + CloseHandle(hInWrite); + return E_FAIL; + } + + // Assign to [out] params. + // The COM stub will duplicate these handles for the caller and close the originals on our side. + *in = hInRead; + *out = hOutWrite; + + // We KEEP Server ends (InWrite, OutRead) for our usage. + // Pass them to Contour handoff logic. + + HANDLE hSignalDup = duplicateHandle(signal); + HANDLE hReferenceDup = duplicateHandle(reference); + HANDLE hServerDup = duplicateHandle(server); + HANDLE hClientDup = duplicateHandle(client); + + std::wstring title; + if (startupInfo && startupInfo->pszTitle) + title = startupInfo->pszTitle; + + SimpleFileLogger("TerminalHandoff: Calling ContourHandleHandoff..."); + + // NOTE: 'hInWrite' is what I write to (Client Input). 'hOutRead' is where I read from (Client Output). + ContourHandleHandoff(hInWrite, hOutRead, hSignalDup, hReferenceDup, hServerDup, hClientDup, title); + + SimpleFileLogger("TerminalHandoff: ContourHandleHandoff returned."); + + return S_OK; +} + +// ITerminalHandoffV1 implementation +HRESULT STDMETHODCALLTYPE TerminalHandoff::EstablishPtyHandoff( + HANDLE in, HANDLE out, HANDLE signal, HANDLE reference, HANDLE server, HANDLE client) +{ + SimpleFileLogger(std::format( + "TerminalHandoff::EstablishPtyHandoff (V1 - By Value) called! in: {:p}, out: {:p}", in, out)); + + // In V1, the caller provides the pipes. We just use them. + HANDLE hSignalDup = duplicateHandle(signal); + HANDLE hReferenceDup = duplicateHandle(reference); + HANDLE hServerDup = duplicateHandle(server); + HANDLE hClientDup = duplicateHandle(client); + + // We need to duplicate 'in' and 'out' as well because we might need to take ownership given they are + // passed by value? Actually, usually in COM [in] handles are borrowed. We should duplicate them to be + // safe. + HANDLE hInKey = duplicateHandle(in); + HANDLE hOutKey = duplicateHandle(out); + + SimpleFileLogger("TerminalHandoff: Calling ContourHandleHandoff (V1)..."); + ContourHandleHandoff(hInKey, hOutKey, hSignalDup, hReferenceDup, hServerDup, hClientDup, L""); + SimpleFileLogger("TerminalHandoff: ContourHandleHandoff returned."); + + return S_OK; +} + +// --- TerminalHandoffFactory --- + +HRESULT STDMETHODCALLTYPE TerminalHandoffFactory::QueryInterface(REFIID riid, void** ppvObject) +{ + SimpleFileLogger(std::format("TerminalHandoffFactory::QueryInterface request: {}", GuidToString(riid))); + if (!ppvObject) + return E_POINTER; + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IClassFactory)) + { + *ppvObject = static_cast(this); + AddRef(); + return S_OK; + } + *ppvObject = nullptr; + return E_NOINTERFACE; +} + +ULONG STDMETHODCALLTYPE TerminalHandoffFactory::AddRef() +{ + return ++_refCount; +} + +ULONG STDMETHODCALLTYPE TerminalHandoffFactory::Release() +{ + ULONG count = --_refCount; + if (count == 0) + delete this; + return count; +} + +HRESULT STDMETHODCALLTYPE TerminalHandoffFactory::CreateInstance(IUnknown* pUnkOuter, + REFIID riid, + void** ppvObject) +{ + SimpleFileLogger("TerminalHandoffFactory::CreateInstance called."); + if (pUnkOuter) + return CLASS_E_NOAGGREGATION; + if (!ppvObject) + return E_POINTER; + + TerminalHandoff* p = new TerminalHandoff(); + if (!p) + return E_OUTOFMEMORY; + + HRESULT hr = p->QueryInterface(riid, ppvObject); + p->Release(); + return hr; +} + +HRESULT STDMETHODCALLTYPE TerminalHandoffFactory::LockServer(BOOL fLock) +{ + // Simplified lock handling + return S_OK; +} diff --git a/src/contour/windows/TerminalHandoff.h b/src/contour/windows/TerminalHandoff.h new file mode 100644 index 0000000000..ae3e4c4ebd --- /dev/null +++ b/src/contour/windows/TerminalHandoff.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +// {B178D323-E77D-4C67-AF21-AE2B81F269F0} +static const CLSID CLSID_ContourTerminalHandoff = { + 0xB178D323, 0xE77D, 0x4C67, { 0xAF, 0x21, 0xAE, 0x2B, 0x81, 0xF2, 0x69, 0xF0 } +}; + +// {E686C757-9A35-4A1C-B3CE-0BCC8B5C69F4} - The Mystery IID from Logs +static const IID IID_ITerminalHandoff_Unknown = { + 0xE686C757, 0x9A35, 0x4A1C, { 0xB3, 0xCE, 0x0B, 0xCC, 0x8B, 0x5C, 0x69, 0xF4 } +}; + +// V1 Signature (By Value) +struct ITerminalHandoffV1: public IUnknown +{ + virtual HRESULT STDMETHODCALLTYPE EstablishPtyHandoff( + HANDLE in, HANDLE out, HANDLE signal, HANDLE reference, HANDLE server, HANDLE client) = 0; +}; + +class TerminalHandoff: public ITerminalHandoff3, public ITerminalHandoffV1 +{ + public: + TerminalHandoff(); + + // IUnknown methods + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + + // ITerminalHandoff3 methods (By Reference - We create pipes) + HRESULT STDMETHODCALLTYPE EstablishPtyHandoff(HANDLE* in, + HANDLE* out, + HANDLE signal, + HANDLE reference, + HANDLE server, + HANDLE client, + const TERMINAL_STARTUP_INFO* startupInfo) override; + + // ITerminalHandoffV1 methods (By Value - Caller created pipes) + HRESULT STDMETHODCALLTYPE EstablishPtyHandoff( + HANDLE in, HANDLE out, HANDLE signal, HANDLE reference, HANDLE server, HANDLE client) override; + + private: + std::atomic _refCount = 1; +}; + +class TerminalHandoffFactory: public IClassFactory +{ + public: + // IUnknown methods + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + + // IClassFactory methods + HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) override; + HRESULT STDMETHODCALLTYPE LockServer(BOOL fLock) override; + + private: + std::atomic _refCount = 1; +}; diff --git a/src/contour/windows/contour_proxy.def b/src/contour/windows/contour_proxy.def new file mode 100644 index 0000000000..7060e2b873 --- /dev/null +++ b/src/contour/windows/contour_proxy.def @@ -0,0 +1,6 @@ +LIBRARY ContourProxy +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE diff --git a/src/contour/windows/register_com.ps1 b/src/contour/windows/register_com.ps1 new file mode 100644 index 0000000000..296527d038 --- /dev/null +++ b/src/contour/windows/register_com.ps1 @@ -0,0 +1,61 @@ +# Register COM servers and App Extensions for Contour sparse package +$binPath = "D:\contour\out\build\windows-msvc-ninja-debug-user\bin" +$contourExe = Join-Path $binPath "contour.exe" +$openConsoleExe = Join-Path $binPath "OpenConsole.exe" + +# Terminal Handoff CLSID +$terminalCLSID = "{B178D323-E77D-4C67-AF21-AE2B81F269F0}" +# Console Handoff CLSID +$consoleCLSID = "{F00DCAFE-0000-0000-0000-000000000001}" + +Write-Host "Registering Contour COM servers..." + +# Register Terminal Handoff (contour.exe) +$regPath = "HKCU:\Software\Classes\CLSID\$terminalCLSID" +New-Item -Path $regPath -Force | Out-Null +New-ItemProperty -Path $regPath -Name "(Default)" -Value "Contour Terminal Handoff" -Force | Out-Null + +$localServerPath = "$regPath\LocalServer32" +New-Item -Path $localServerPath -Force | Out-Null +New-ItemProperty -Path $localServerPath -Name "(Default)" -Value "`"$contourExe`"" -Force | Out-Null + +Write-Host "Registered Terminal CLSID: $terminalCLSID" + +# Register Console Handoff (OpenConsole.exe) +$regPath2 = "HKCU:\Software\Classes\CLSID\$consoleCLSID" +New-Item -Path $regPath2 -Force | Out-Null +New-ItemProperty -Path $regPath2 -Name "(Default)" -Value "Contour Console Handoff" -Force | Out-Null + +$localServerPath2 = "$regPath2\LocalServer32" +New-Item -Path $localServerPath2 -Force | Out-Null +New-ItemProperty -Path $localServerPath2 -Name "(Default)" -Value "`"$openConsoleExe`"" -Force | Out-Null + +Write-Host "Registered Console CLSID: $consoleCLSID" + +# Register App Extensions manually since sparse packages don't auto-register them +Write-Host "Registering App Extensions..." + +# Get package family name +$package = Get-AppxPackage -Name "Contour.Terminal" +if ($package) { + $pfn = $package.PackageFamilyName + + # Register terminal.host extension + $extensionPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\App Extensions\Catalog\PackageContract\com.microsoft.windows.terminal.host\$pfn!ContourTerminalExtension" + New-Item -Path $extensionPath -Force | Out-Null + New-ItemProperty -Path $extensionPath -Name "DisplayName" -Value "Contour Terminal" -Force | Out-Null + New-ItemProperty -Path $extensionPath -Name "Clsid" -Value $terminalCLSID -Force | Out-Null + Write-Host "Registered terminal.host extension" + + # Register console.host extension + $consolePath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\App Extensions\Catalog\PackageContract\com.microsoft.windows.console.host\$pfn!ContourConsole" + New-Item -Path $consolePath -Force | Out-Null + New-ItemProperty -Path $consolePath -Name "DisplayName" -Value "Contour Console" -Force | Out-Null + New-ItemProperty -Path $consolePath -Name "Clsid" -Value $consoleCLSID -Force | Out-Null + Write-Host "Registered console.host extension" +} +else { + Write-Host "WARNING: Contour.Terminal package not found. Register the package first." -ForegroundColor Yellow +} + +Write-Host "Registration complete!" -ForegroundColor Green diff --git a/src/contour/windows/register_package.ps1 b/src/contour/windows/register_package.ps1 new file mode 100644 index 0000000000..28310ad7fc --- /dev/null +++ b/src/contour/windows/register_package.ps1 @@ -0,0 +1,26 @@ +# Register-ContourPackage.ps1 +# Helper script to register Contour as a Sparse Package +# REQUIRES: The AppxManifest.xml must be signed with a trusted certificate before running this. + +param( + [string]$ManifestPath = "$PSScriptRoot\AppxManifest.xml" +) + +if (-not (Test-Path $ManifestPath)) { + Write-Error "Manifest not found at $ManifestPath" + exit 1 +} + +Write-Host "Registering Contour Sparse Package from: $ManifestPath" -ForegroundColor Cyan + +# NOTE: Add-AppxPackage -Register requires the manifest to be signed. +# If the signature is missing or untrusted, this command will fail. +try { + Add-AppxPackage -Register $ManifestPath -DisableDevelopmentMode -ErrorAction Stop + Write-Host "Successfully registered Contour package!" -ForegroundColor Green +} +catch { + Write-Error "Failed to register package. Ensure AppxManifest.xml is signed with a trusted certificate." + Write-Error $_ + exit 1 +} diff --git a/src/contour/windows/setup_dev_env.ps1 b/src/contour/windows/setup_dev_env.ps1 new file mode 100644 index 0000000000..5140c8b82c --- /dev/null +++ b/src/contour/windows/setup_dev_env.ps1 @@ -0,0 +1,85 @@ +# basic_sign_test.ps1 +# Helper to set up a testing environment for Sparse Packages +# 1. Creates a Self-Signed Certificate +# 2. Installs it to Trusted People +# 3. Signs the Package (technically, creates a signed signature file) + +param( + [string]$Action = "Sign", # "Sign" or "Setup" + [string]$ManifestPath = "bin\AppxManifest.xml" +) + +$CertName = "ContourDevCert" +$Publisher = "CN=Christian Parpart" + +function Setup-Certificate { + Write-Host "Checking for existing certificate..." -ForegroundColor Cyan + $cert = Get-ChildItem Cert:\CurrentUser\My | Where-Object { $_.Subject -eq $Publisher } | Select-Object -First 1 + + if (-not $cert) { + Write-Host "Creating new self-signed certificate..." -ForegroundColor Yellow + $cert = New-SelfSignedCertificate -Type Custom -Subject $Publisher ` + -KeyUsage DigitalSignature ` + -FriendlyName "Contour Dev Certificate" ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}") + } + + Write-Host "Certificate thumbprint: $($cert.Thumbprint)" -ForegroundColor Green + + Write-Host "Installing certificate to Trusted People store (Requires Admin)..." -ForegroundColor Yellow + + # Export public key + $certPath = "$env:TEMP\ContourDevCert.cer" + Export-Certificate -Cert $cert -FilePath $certPath -Type CERT + + # Import to Trusted People + try { + Import-Certificate -FilePath $certPath -CertStoreLocation Cert:\LocalMachine\TrustedPeople + Write-Host "Certificate installed to Trusted People." -ForegroundColor Green + } + catch { + Write-Error "Failed to install certificate. Please run this script as Administrator." + exit 1 + } + + return $cert +} + +function Sign-Package { + param($Certificate) + + # Sparse Package signing is tricky. + # The 'proper' way is to use `signtool` on a package, but we are unpackaged. + # + # WORKAROUND for Testing: + # 1. Enable Developer Mode in Windows Settings. + # 2. Register WITHOUT signing. + + Write-Host "`n=== SIGNING GUIDE ===" -ForegroundColor Cyan + Write-Host "For Sparse Packages (Unpackaged), the easiest way to test is enabling 'Developer Mode'." + Write-Host "Settings > Privacy & security > For developers > Developer Mode (ON)" + Write-Host "--------------------------------------------------------" + Write-Host "If Developer Mode is ON, you do NOT need to sign the manifest manually." + Write-Host "Just run: .\register_package.ps1" + Write-Host "--------------------------------------------------------" + + # If the user REALLY wants to sign, they need to sign the binaries and have a catalog. + # But simply signing the manifest file doesn't work with SignTool alone. +} + +# Main execution +if (-not (Test-Path $ManifestPath)) { + # Try looking in src/contour/windows if not in bin + if (Test-Path "src\contour\windows\AppxManifest.xml") { + $ManifestPath = "src\contour\windows\AppxManifest.xml" + } +} + +try { + $cert = Setup-Certificate + Sign-Package -Certificate $cert +} +catch { + Write-Error $_ +} diff --git a/src/contour/wix_patch.xml b/src/contour/wix_patch.xml index 6fd1ea5cb9..9a79240559 100644 --- a/src/contour/wix_patch.xml +++ b/src/contour/wix_patch.xml @@ -3,6 +3,13 @@ + + + + + + + diff --git a/src/vtpty/CMakeLists.txt b/src/vtpty/CMakeLists.txt index adc76b5ff2..b891eeaa94 100644 --- a/src/vtpty/CMakeLists.txt +++ b/src/vtpty/CMakeLists.txt @@ -61,8 +61,8 @@ if(UNIX) list(APPEND vtpty_SOURCES UnixPty.cpp UnixUtils.cpp) list(APPEND vtpty_SOURCES UnixPty.h UnixUtils.h) else() - list(APPEND vtpty_SOURCES ConPty.cpp) - list(APPEND vtpty_HEADERS ConPty.h) + list(APPEND vtpty_SOURCES ConPty.cpp HandoffPty.cpp) + list(APPEND vtpty_HEADERS ConPty.h HandoffPty.h) #TODO: list(APPEND vtpty_SOURCES WinPty.cpp) endif() diff --git a/src/vtpty/HandoffPty.cpp b/src/vtpty/HandoffPty.cpp new file mode 100644 index 0000000000..635d2f5565 --- /dev/null +++ b/src/vtpty/HandoffPty.cpp @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +#include + +#include + +#include +#include +#include +#include + +using namespace std; + +namespace +{ +void SimpleFileLogger(std::string_view message) +{ + static std::mutex mtx; + std::lock_guard lock(mtx); + char tmpPath[MAX_PATH]; + if (GetTempPathA(MAX_PATH, tmpPath)) + { + std::string path = std::string(tmpPath) + "contour_debug.txt"; + std::ofstream logFile(path, std::ios::app); + if (logFile) + { + logFile << "[" << GetCurrentProcessId() << "] " << message << std::endl; + } + } +} +} // namespace + +namespace vtpty +{ + +HandoffPty::HandoffPty(HANDLE hInputWrite, + HANDLE hOutputRead, + HANDLE hSignal, + HANDLE hReference, + HANDLE hServer, + HANDLE hClient, + std::wstring const& title): + _hInputWrite(hInputWrite), + _hOutputRead(hOutputRead), + _hSignal(hSignal), + _hReference(hReference), + _hServer(hServer), + _hClient(hClient), + _title(title) +{ + SimpleFileLogger(std::format("HandoffPty ctor: in={:p} out={:p} sig={:p} ref={:p} srv={:p} cli={:p}", + _hInputWrite, + _hOutputRead, + _hSignal, + _hReference, + _hServer, + _hClient)); + _hWakeup = CreateEvent(nullptr, TRUE, FALSE, nullptr); + _hExitEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + _readOverlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + _writeOverlapped.hEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + + // Default page size + _pageSize.lines = LineCount(24); + _pageSize.columns = ColumnCount(80); +} + +HandoffPty::~HandoffPty() +{ + SimpleFileLogger("HandoffPty dtor"); + close(); + CloseHandle(_hWakeup); + CloseHandle(_hExitEvent); + CloseHandle(_readOverlapped.hEvent); + CloseHandle(_writeOverlapped.hEvent); +} + +void HandoffPty::start() +{ + SimpleFileLogger("HandoffPty::start"); + // Nothing to do +} + +PtySlave& HandoffPty::slave() noexcept +{ + return _slave; +} + +void HandoffPty::close() +{ + if (_closed) + return; + SimpleFileLogger("HandoffPty::close"); + _closed = true; + SetEvent(_hExitEvent); + + auto const closeAndInvalidate = [](HANDLE& handle) { + if (handle != INVALID_HANDLE_VALUE) + { + CloseHandle(handle); + handle = INVALID_HANDLE_VALUE; + } + }; + + closeAndInvalidate(_hInputWrite); + + if (_hOutputRead != INVALID_HANDLE_VALUE) + { + // CancelIO might be needed? + CancelIo(_hOutputRead); + closeAndInvalidate(_hOutputRead); + } + + closeAndInvalidate(_hSignal); + closeAndInvalidate(_hReference); + closeAndInvalidate(_hServer); + closeAndInvalidate(_hClient); +} + +void HandoffPty::waitForClosed() +{ + SimpleFileLogger("HandoffPty::waitForClosed (waiting)"); + WaitForSingleObject(_hExitEvent, INFINITE); + SimpleFileLogger("HandoffPty::waitForClosed (done)"); +} + +bool HandoffPty::isClosed() const noexcept +{ + return _closed; +} + +std::optional HandoffPty::read(crispy::buffer_object& storage, + std::optional timeout, + size_t size) +{ + if (_closed) + return std::nullopt; + + DWORD bytesRead = 0; + + // Reset event + ResetEvent(_readOverlapped.hEvent); + + // Ensure storage has space + // We assume storage has some capacity or we can write to it directly? + // buffer_object has .data() and .capacity()? + // But buffer_object is usually a wrapper around a resource pool. + // We can use a temporary buffer. + + if (_readBuffer.size() < size) + _readBuffer.resize(size); + + BOOL res = + ReadFile(_hOutputRead, _readBuffer.data(), static_cast(size), &bytesRead, &_readOverlapped); + if (!res) + { + DWORD err = GetLastError(); + if (err == ERROR_IO_PENDING) + { + HANDLE handles[2] = { _readOverlapped.hEvent, _hWakeup }; + DWORD timeoutMs = timeout ? static_cast(timeout->count()) : INFINITE; + + DWORD waitRes = WaitForMultipleObjects(2, handles, FALSE, timeoutMs); + if (waitRes == WAIT_OBJECT_0) + { + // Read completed + if (GetOverlappedResult(_hOutputRead, &_readOverlapped, &bytesRead, FALSE)) + { + // Success + } + else + { + // Error + std::cout << "HandoffPty::read failed (OVERLAPPED): " << GetLastError() << "\n"; + SetEvent(_hExitEvent); + if (GetLastError() == ERROR_BROKEN_PIPE) + return std::nullopt; // EOF + return std::nullopt; + } + } + else if (waitRes == WAIT_OBJECT_0 + 1) + { + // Wakeup + CancelIo(_hOutputRead); + return std::nullopt; // Or empty result? + } + else // Timeout or failed + { + CancelIo(_hOutputRead); + return std::nullopt; + } + } + else if (err == ERROR_BROKEN_PIPE) + { + SetEvent(_hExitEvent); + return std::nullopt; // EOF + } + else + { + SetEvent(_hExitEvent); + return std::nullopt; + } + } + + if (bytesRead > 0) + { + // Copy to storage + auto chunk = storage.advance(bytesRead); + std::copy(_readBuffer.begin(), _readBuffer.begin() + bytesRead, chunk.data()); + return ReadResult { std::string_view(chunk.data(), chunk.size()), false }; + } + + return std::nullopt; +} + +void HandoffPty::wakeupReader() +{ + SetEvent(_hWakeup); +} + +int HandoffPty::write(std::string_view buf) +{ + if (_closed) + return -1; + + DWORD bytesWritten = 0; + // Handle overlapped write synchronously for now (or wait) + // Or async? WriteFile usually buffers. + + // Reset event + ResetEvent(_writeOverlapped.hEvent); + + BOOL res = + WriteFile(_hInputWrite, buf.data(), static_cast(buf.size()), &bytesWritten, &_writeOverlapped); + if (!res) + { + if (GetLastError() == ERROR_IO_PENDING) + { + if (GetOverlappedResult(_hInputWrite, &_writeOverlapped, &bytesWritten, TRUE)) + { + return static_cast(bytesWritten); + } + } + std::cout << "HandoffPty::write failed: " << GetLastError() << "\n"; + SimpleFileLogger(std::format("HandoffPty::write failed: {}", GetLastError())); + return -1; + } + return static_cast(bytesWritten); +} + +PageSize HandoffPty::pageSize() const noexcept +{ + return _pageSize; +} + +void HandoffPty::resizeScreen(PageSize cells, std::optional pixels) +{ + _pageSize = cells; + // Resizing is not propagated to OpenConsoleProxy via handles. + // It's likely propagated via VT sequences or other mechanism not exposed here? + // Or maybe we don't need to propagate. +} + +} // namespace vtpty diff --git a/src/vtpty/HandoffPty.h b/src/vtpty/HandoffPty.h new file mode 100644 index 0000000000..f49b631008 --- /dev/null +++ b/src/vtpty/HandoffPty.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include + +namespace vtpty +{ + +class HandoffPty: public Pty +{ + public: + HandoffPty(HANDLE hInputWrite, + HANDLE hOutputRead, + HANDLE hSignal, + HANDLE hReference, + HANDLE hServer, + HANDLE hClient, + std::wstring const& title); + ~HandoffPty() override; + + void start() override; + PtySlave& slave() noexcept override; + void close() override; + void waitForClosed() override; + bool isClosed() const noexcept override; + std::optional read(crispy::buffer_object& storage, + std::optional timeout, + size_t size) override; + void wakeupReader() override; + int write(std::string_view buf) override; + PageSize pageSize() const noexcept override; + void resizeScreen(PageSize cells, std::optional pixels = std::nullopt) override; + + private: + private: + HANDLE _hInputWrite; // We write to this + HANDLE _hOutputRead; // We read from this + HANDLE _hSignal; + HANDLE _hReference; + HANDLE _hServer; + HANDLE _hClient; + std::wstring _title; + + HANDLE _hWakeup; // Event for canceling read + HANDLE _hExitEvent; // Event signaled when PTY is closed/EOF + bool _closed = false; + PtySlaveDummy _slave; + PageSize _pageSize; + + OVERLAPPED _readOverlapped {}; + OVERLAPPED _writeOverlapped {}; + std::vector _readBuffer; +}; + +} // namespace vtpty