From f3cb406ebefc31cf3ec7d425a9af0961737b4a81 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 6 May 2023 22:48:27 +0200 Subject: [PATCH] Add CLI-wrapper for Windows Starting the console from a GUI application is not working very well - so let's just provide a 2nd executable for the CLI. It will be a simple console application that merely invokes the main application passing all standard I/O. Unfortunately this does not mean the existing hacks can be removed. Without them the wrapper still doesn't get any I/O from the GUI application. --- CMakeLists.txt | 9 +- cmake/modules/AppTarget.cmake | 16 ++++ cmake/modules/TemplateFinder.cmake | 19 ++-- cmake/modules/WindowsResources.cmake | 14 ++- cmake/templates/cli-wrapper.cpp | 109 ++++++++++++++++++++++ cmake/templates/windows-cli-wrapper.rc.in | 38 ++++++++ 6 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 cmake/templates/cli-wrapper.cpp create mode 100644 cmake/templates/windows-cli-wrapper.rc.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cedf70..0c7840b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,9 +98,16 @@ set(CMAKE_TEMPLATE_FILES cmake/templates/template.pc.in) set(SCRIPT_FILES) if (MINGW) - list(APPEND CMAKE_TEMPLATE_FILES cmake/templates/windows.rc.in) + list(APPEND CMAKE_TEMPLATE_FILES + cmake/templates/windows.rc.in + cmake/templates/windows-cli-wrapper.rc.in + ) list(APPEND SCRIPT_FILES scripts/wine.sh) endif () +if (WIN32) + list(APPEND CMAKE_TEMPLATE_FILES cmake/templates/cli-wrapper.cpp) +endif () +set(EXCLUDED_FILES cmake/templates/cli-wrapper.cpp) set(DOC_FILES README.md doc/buildvariables.md doc/testapplication.md) set(EXTRA_FILES tests/calculateoverallcoverage.awk coding-style.clang-format) diff --git a/cmake/modules/AppTarget.cmake b/cmake/modules/AppTarget.cmake index e684b3a..13f1490 100644 --- a/cmake/modules/AppTarget.cmake +++ b/cmake/modules/AppTarget.cmake @@ -110,6 +110,19 @@ if (GUI_TYPE STREQUAL "MACOSX_BUNDLE") endif () endif () +# create CLI-wrapper to be able to use CLI in Windows-termial without hacks +if (BUILD_CLI_WRAPPER) + # find source file + include(TemplateFinder) + find_template_file_full_name("cli-wrapper.cpp" CPP_UTILITIES CLI_WRAPPER_SRC_FILE) + + # add and configure additional executable + set(CLI_WRAPPER_TARGET_NAME "${META_TARGET_NAME}-cli") + add_executable(${CLI_WRAPPER_TARGET_NAME} ${CLI_WRAPPER_RES_FILES} ${CLI_WRAPPER_SRC_FILE}) + set_target_properties(${CLI_WRAPPER_TARGET_NAME} PROPERTIES CXX_STANDARD 17) + target_compile_definitions(${CLI_WRAPPER_TARGET_NAME} PRIVATE _CRT_SECURE_NO_WARNINGS=1) +endif () + # add install targets if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS) # add install target for binary @@ -129,6 +142,9 @@ if (NOT META_NO_INSTALL_TARGETS AND ENABLE_INSTALL_TARGETS) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}${SELECTED_LIB_SUFFIX}" COMPONENT binary) else () install(TARGETS ${META_TARGET_NAME} RUNTIME DESTINATION bin COMPONENT binary) + if (CLI_WRAPPER_TARGET_NAME) + install(TARGETS ${CLI_WRAPPER_TARGET_NAME} RUNTIME DESTINATION bin COMPONENT binary) + endif () endif () if (NOT TARGET install-binary) diff --git a/cmake/modules/TemplateFinder.cmake b/cmake/modules/TemplateFinder.cmake index e2bea17..6c80221 100644 --- a/cmake/modules/TemplateFinder.cmake +++ b/cmake/modules/TemplateFinder.cmake @@ -6,23 +6,28 @@ if (DEFINED TEMPLATE_FINDER_LOADED) endif () set(TEMPLATE_FINDER_LOADED YES) -function (find_template_file FILE_NAME PROJECT_VAR_NAME OUTPUT_VAR) - if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in") +function (find_template_file FILE_NAME_WITHOUT_EXTENSION PROJECT_VAR_NAME OUTPUT_VAR) + find_template_file_full_name("${FILE_NAME_WITHOUT_EXTENSION}.in" "${PROJECT_VAR_NAME}" "${OUTPUT_VAR}") + set(${OUTPUT_VAR} "${${OUTPUT_VAR}}" PARENT_SCOPE) +endfunction () + +function (find_template_file_full_name FILE_NAME PROJECT_VAR_NAME OUTPUT_VAR) + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}") # check own source directory set(${OUTPUT_VAR} - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in" + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/${FILE_NAME}" PARENT_SCOPE) message(STATUS "Using template for ${FILE_NAME} from own (${META_PROJECT_NAME}) source directory.") - elseif (EXISTS "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in") + elseif (EXISTS "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}") # check sources of project set(${OUTPUT_VAR} - "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}.in" + "${${PROJECT_VAR_NAME}_SOURCE_DIR}/cmake/templates/${FILE_NAME}" PARENT_SCOPE) message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} source directory.") - elseif (EXISTS "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}.in") + elseif (EXISTS "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}") # check installed version of project set(${OUTPUT_VAR} - "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}.in" + "${${PROJECT_VAR_NAME}_CONFIG_DIRS}/templates/${FILE_NAME}" PARENT_SCOPE) message(STATUS "Using template for ${FILE_NAME} from ${PROJECT_VAR_NAME} installation.") else () diff --git a/cmake/modules/WindowsResources.cmake b/cmake/modules/WindowsResources.cmake index 43bfc66..0626df4 100644 --- a/cmake/modules/WindowsResources.cmake +++ b/cmake/modules/WindowsResources.cmake @@ -19,8 +19,9 @@ endif () # find rc template, define path of output rc file include(TemplateFinder) find_template_file("windows.rc" CPP_UTILITIES RC_TEMPLATE_FILE) -set(WINDOWS_RC_FILE_CFG "${CMAKE_CURRENT_BINARY_DIR}/resources/windows.rc.configured") +find_template_file("windows-cli-wrapper.rc" CPP_UTILITIES RC_CLI_TEMPLATE_FILE) set(WINDOWS_RC_FILE "${CMAKE_CURRENT_BINARY_DIR}/resources/windows") +set(WINDOWS_CLI_RC_FILE "${CMAKE_CURRENT_BINARY_DIR}/resources/windows-cli-wrapper") # create Windows icon from png with ffmpeg if available unset(WINDOWS_ICON_RC_ENTRY) @@ -51,10 +52,21 @@ file( GENERATE OUTPUT "${WINDOWS_RC_FILE}-$.rc" INPUT "${WINDOWS_RC_FILE}-configured.rc") +if (BUILD_CLI_WRAPPER AND META_PROJECT_TYPE STREQUAL "application") + configure_file("${RC_CLI_TEMPLATE_FILE}" "${WINDOWS_CLI_RC_FILE}-configured.rc") + file( + GENERATE + OUTPUT "${WINDOWS_CLI_RC_FILE}-$.rc" + INPUT "${WINDOWS_CLI_RC_FILE}-configured.rc") +endif () # set windres as resource compiler list(APPEND RES_FILES "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc") set_property(SOURCE "${WINDOWS_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON) +if (BUILD_CLI_WRAPPER AND META_PROJECT_TYPE STREQUAL "application") + list(APPEND CLI_WRAPPER_RES_FILES "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc") + set_property(SOURCE "${WINDOWS_CLI_RC_FILE}-${CMAKE_BUILD_TYPE}.rc" PROPERTY GENERATED ON) +endif () set(CMAKE_RC_COMPILER_INIT windres) set(CMAKE_RC_COMPILE_OBJECT " -O coff -i -o ") enable_language(RC) diff --git a/cmake/templates/cli-wrapper.cpp b/cmake/templates/cli-wrapper.cpp new file mode 100644 index 0000000..aad7fe4 --- /dev/null +++ b/cmake/templates/cli-wrapper.cpp @@ -0,0 +1,109 @@ +#include + +#include +#include +#include +#include +#include + +/*! + * \brief Enables virutal terminal processing. + */ +static bool enableVirtualTerminalProcessing(DWORD nStdHandle) +{ + auto stdHandle = GetStdHandle(nStdHandle); + if (stdHandle == INVALID_HANDLE_VALUE) { + return false; + } + auto dwMode = DWORD(); + if (!GetConsoleMode(stdHandle, &dwMode)) { + return false; + } + dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + return SetConsoleMode(stdHandle, dwMode); +} + +/*! + * \brief Returns \a replacement if \a value matches \a key; otherwise returns \a value. + */ +static std::size_t replace(std::size_t value, std::size_t key, std::size_t replacement) +{ + return value == key ? replacement : value; +} + +/*! + * \brief Sets the console up and launches the "main" application. + */ +int main() +{ + // setup console + // -> enable UTF-8 as this is used by all my applications + SetConsoleCP(CP_UTF8); + SetConsoleOutputCP(CP_UTF8); + // -> ensure environment variables for hack to attach to parent's console are enabled; this is still required + // for this wrapper to receive standard I/O + SetEnvironmentVariableW(L"ENABLE_CONSOLE", L"1"); + SetEnvironmentVariableW(L"ENABLE_CP_UTF8", L"1"); + // -> unset environment variables that would lead to skipping the hack; for the wrapper the hack is even + // required when using Mintty + SetEnvironmentVariableW(L"MSYSCON", L""); + SetEnvironmentVariableW(L"TERM_PROGRAM", L""); + // -> enable support for ANSI escape codes if possible + if (enableVirtualTerminalProcessing(STD_OUTPUT_HANDLE) && enableVirtualTerminalProcessing(STD_ERROR_HANDLE)) { + SetEnvironmentVariableW(L"ENABLE_ESCAPE_CODES", L"1"); + } + + // determine the wrapper executable path + wchar_t pathBuffer[MAX_PATH]; + if (!GetModuleFileNameW(nullptr, pathBuffer, MAX_PATH)) { + std::cerr << "Unable to determine wrapper executable path: " << std::error_code(GetLastError(), std::system_category()) << '\n'; + return EXIT_FAILURE; + } + + // replace "-cli.exe" in the wrapper executable's file name with just ".exe" to make up the main executable path + const auto path = std::wstring_view(pathBuffer); + const auto filenameStart = replace(path.rfind(L'\\'), std::wstring_view::npos, 0); + const auto appendixStart = std::wstring_view(pathBuffer + filenameStart, path.size() - filenameStart).rfind(L"-cli.exe"); + if (appendixStart == std::wstring_view::npos) { + std::cerr << "Unable to determine main executable path: unexpected wrapper executable name\n"; + return EXIT_FAILURE; + } + std::wcscpy(pathBuffer + filenameStart + appendixStart, L".exe"); + + // compute startup parameters + auto commadLine = GetCommandLineW(); + auto processInformation = PROCESS_INFORMATION(); + auto startupInfo = STARTUPINFOW(); + ZeroMemory(&startupInfo, sizeof(startupInfo)); + ZeroMemory(&processInformation, sizeof(processInformation)); + startupInfo.cb = sizeof(startupInfo); + startupInfo.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + startupInfo.hStdError = GetStdHandle(STD_ERROR_HANDLE); + startupInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startupInfo.dwFlags |= STARTF_USESTDHANDLES; + + // start main executable in new group and print debug information if that's not possible + auto res = CreateProcessW( + pathBuffer, // path of main executable + commadLine, // command line arguments + nullptr, // process handle not inheritable + nullptr, // thread handle not inheritable + true, // set handle inheritance to true + CREATE_NEW_PROCESS_GROUP, // creation flags + nullptr, // use parent's environment block + nullptr, // use parent's starting directory + &startupInfo, // pointer to STARTUPINFO structure + &processInformation); // pointer to PROCESS_INFORMATION structure + if (!res) { + std::cerr << "Unable to launch main executable: " << std::error_code(GetLastError(), std::system_category()) << '\n'; + std::wcerr << L" - assumed path: " << pathBuffer << L'\n'; + std::wcerr << L" - assumed command-line: " << commadLine << L'\n'; + return EXIT_FAILURE; + } + + // wait for main executable and possible children to terminate and return exit code + auto exitCode = DWORD(); + WaitForSingleObject(processInformation.hProcess, INFINITE); + GetExitCodeProcess(processInformation.hProcess, &exitCode); + return exitCode; +} diff --git a/cmake/templates/windows-cli-wrapper.rc.in b/cmake/templates/windows-cli-wrapper.rc.in new file mode 100644 index 0000000..b9427d0 --- /dev/null +++ b/cmake/templates/windows-cli-wrapper.rc.in @@ -0,0 +1,38 @@ +# if defined(UNDER_CE) +# include +# else +# include +# endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @META_VERSION_MAJOR@,@META_VERSION_MINOR@,@META_VERSION_PATCH@,0 + PRODUCTVERSION @META_VERSION_MAJOR@,@META_VERSION_MINOR@,@META_VERSION_PATCH@,0 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE 0x0L + BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "@META_APP_AUTHOR@\0" + VALUE "FileDescription", "@META_APP_DESCRIPTION@ - CLI wrapper\0" + VALUE "FileVersion", "@META_APP_VERSION@\0" + VALUE "LegalCopyright", "by @META_APP_AUTHOR@\0" + VALUE "OriginalFilename", "$\0" + VALUE "ProductName", "@META_APP_NAME@\0" + VALUE "ProductVersion", "@META_APP_VERSION@\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 1200 + END + END +/* End of Version info */