diff --git a/admin/CMakeLists.txt b/admin/CMakeLists.txt index 8908d6c81..b54e48ed1 100644 --- a/admin/CMakeLists.txt +++ b/admin/CMakeLists.txt @@ -1,2 +1,7 @@ -# traverse into osx subdirectory to install and patch the create-pack script -add_subdirectory(osx) +if(APPLE) + # traverse into osx subdirectory to install and patch the create-pack script + add_subdirectory(osx) +elseif(WIN32) + # MSI package scripts, helper DLL and migration tools + add_subdirectory(win) +endif() diff --git a/admin/win/CMakeLists.txt b/admin/win/CMakeLists.txt new file mode 100644 index 000000000..5395ad9e7 --- /dev/null +++ b/admin/win/CMakeLists.txt @@ -0,0 +1,8 @@ +# MSI package scripts, helper DLL and migration tools +if(BUILD_WIN_MSI) + add_subdirectory(msi) +endif() + +if(BUILD_WIN_MSI OR BUILD_WIN_TOOLS) + add_subdirectory(tools) +endif() diff --git a/admin/win/tools/CMakeLists.txt b/admin/win/tools/CMakeLists.txt new file mode 100644 index 000000000..e2610a69b --- /dev/null +++ b/admin/win/tools/CMakeLists.txt @@ -0,0 +1,61 @@ +cmake_minimum_required(VERSION 3.2) +set(CMAKE_CXX_STANDARD 17) + +if(CMAKE_SIZEOF_VOID_P MATCHES 4) + set(BITNESS 32) +else() + set(BITNESS 64) +endif() + +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} + NCToolsShared +) + +add_definitions(-DUNICODE) +add_definitions(-D_UNICODE) +add_definitions(-DNDEBUG) +add_definitions(-D_WINDOWS) + +# Get APIs from from Vista onwards. +add_definitions(-D_WIN32_WINNT=0x0601) +add_definitions(-DWINVER=0x0601) + +if(MSVC) + # Use automatic overload for suitable CRT safe-functions + # See https://docs.microsoft.com/de-de/cpp/c-runtime-library/security-features-in-the-crt?view=vs-2019 + add_definitions(-D_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES=1) + # Also: Disable compiler warnings because we don't use Windows CRT safe-functions explicitly and don't intend to + # as this is a pure cross-platform source the only alternative would be a ton of ifdefs with calls to the _s version + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + + # Optimize for size + set(COMPILER_FLAGS "/GL /O1 /sdl /Zc:inline /Oi /EHsc /nologo") + set(LINKER_FLAGS "/LTCG /OPT:REF /SUBSYSTEM:WINDOWS /NOLOGO") + + # Enable DEP, ASLR and CFG + set(LINKER_FLAGS "${LINKER_FLAGS} /nxcompat /dynamicbase /guard:cf") + + # x86 only: Enable SafeSEH + if(CMAKE_SIZEOF_VOID_P MATCHES 4) + set(LINKER_FLAGS "${LINKER_FLAGS} /safeseh") + endif() + + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COMPILER_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILER_FLAGS}") + + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${LINKER_FLAGS}") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LINKER_FLAGS}") + + # Use static runtime for all subdirectories + foreach(buildType "" "_DEBUG" "_MINSIZEREL" "_RELEASE" "_RELWITHDEBINFO") + string(REPLACE "/MD" "/MT" "CMAKE_CXX_FLAGS${buildType}" "${CMAKE_CXX_FLAGS${buildType}}") + endforeach() +endif() + +add_subdirectory(NCToolsShared) + +if(BUILD_WIN_MSI) + add_subdirectory(NCMsiHelper) +endif() diff --git a/admin/win/tools/NCMsiHelper/CMakeLists.txt b/admin/win/tools/NCMsiHelper/CMakeLists.txt new file mode 100644 index 000000000..d28d59581 --- /dev/null +++ b/admin/win/tools/NCMsiHelper/CMakeLists.txt @@ -0,0 +1,47 @@ +# Find WiX Toolset +if(NOT DEFINED ENV{WIX}) + # Example: WIX=C:\Program Files (x86)\WiX Toolset v3.11\ + message(FATAL_ERROR "WiX Toolset path not set (environment variable 'WIX'). Please install the WiX Toolset.") +else() + set(WIX_SDK_PATH $ENV{WIX}/SDK/VS2017) + message(STATUS "WiX Toolset SDK path: ${WIX_SDK_PATH}") +endif() + +include_directories( + ${WIX_SDK_PATH}/inc +) + +if(CMAKE_SIZEOF_VOID_P MATCHES 4) + link_directories( + ${WIX_SDK_PATH}/lib/x86 + ) +else() + link_directories( + ${WIX_SDK_PATH}/lib/x64 + ) +endif() + +add_definitions(-D_NCMSIHELPER_EXPORTS) +add_definitions(-D_USRDLL) +add_definitions(-D_WINDLL) + +set(TARGET_NAME NCMsiHelper${BITNESS}) + +add_library(${TARGET_NAME} MODULE + CustomAction.cpp + CustomAction.def + LogResult.cpp + NCMsiHelper.cpp +) + +target_link_libraries(${TARGET_NAME} + NCToolsShared +) + +install(TARGETS ${TARGET_NAME} + DESTINATION msi/ +) +install(FILES + NCMsiHelper.wxs + DESTINATION msi/ +) diff --git a/admin/win/tools/NCMsiHelper/CustomAction.cpp b/admin/win/tools/NCMsiHelper/CustomAction.cpp new file mode 100644 index 000000000..f222a264f --- /dev/null +++ b/admin/win/tools/NCMsiHelper/CustomAction.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Parts of this file are based on: + * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo + * + * Licensed under the The Code Project Open License (CPOL): + * https://www.codeproject.com/info/cpol10.aspx + * + */ + +#include "NCTools.h" +#include "NCMsiHelper.h" + +/** + * Sets up logging for MSIs and then calls the appropriate custom action with argc/argv parameters. + * + * MSI deferred custom action dlls have to handle parameters (properties) a little differently, + * since the deferred action may not have an active session when it begins. Since the easiest + * way to pass parameter(s) is to put them all into a CustomActionData property and then retrieve it, + * the easiest thing to do on this ( C/C++ ) end is to pull the parameter and then split it into + * a list of parameter(s) that we need. + * + * For this implementation, it made sense to treat the single string provided in CustomActionData + * as if it were a command line, and then parse it out just as if it were a command line. Obviously, + * the "program name" isn't going to be the first argument unless the MSI writer is pedantic, but + * otherwise it seems to be a good way to do it. + * + * Since all entry points need to do this same work, it was easiest to have a single function that + * would do the setup, pull the CustomActionData parameter, split it into an argc/argv style of + * argument list, and then pass that argument list into a function that actually does something + * interesting. + * + * @param hInstall The hInstall parameter provided by MSI/WiX. + * @param func The function to be called with argc/argv parameters. + * @param actionName The text description of the function. It will be put in the log. + * @return Returns ERROR_SUCCESS or ERROR_INSTALL_FAILURE. + */ +UINT CustomActionArgcArgv(MSIHANDLE hInstall, CUSTOM_ACTION_ARGC_ARGV func, LPCSTR actionName) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + LPWSTR pszCustomActionData = nullptr; + int argc = 0; + LPWSTR *argv = nullptr; + + hr = WcaInitialize(hInstall, actionName); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Initialized."); + + // Retrieve our custom action property. This is one of + // only three properties we can request on a Deferred + // Custom Action. So, we assume the caller puts all + // parameters in this one property. + pszCustomActionData = nullptr; + hr = WcaGetProperty(L"CustomActionData", &pszCustomActionData); + ExitOnFailure(hr, "Failed to get Custom Action Data."); + WcaLog(LOGMSG_STANDARD, "Custom Action Data = '%ls'.", pszCustomActionData); + + // Convert the string retrieved into a standard argc/arg layout + // (ignoring the fact that the first parameter is whatever was + // passed, not necessarily the application name/path). + argv = CommandLineToArgvW(pszCustomActionData, &argc); + if (argv) + { + hr = HRESULT_FROM_WIN32(GetLastError()); + ExitOnFailure(hr, "Failed to convert Custom Action Data to argc/argv."); + } + + hr = (func)(argc, argv); + ExitOnFailure(hr, "Custom action failed"); + +LExit: + // Resource freeing here! + ReleaseStr(pszCustomActionData); + if (argv) + LocalFree(argv); + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall ExecNsisUninstaller(MSIHANDLE hInstall) +{ + return CustomActionArgcArgv(hInstall, DoExecNsisUninstaller, "ExecNsisUninstaller"); +} + +UINT __stdcall RemoveNavigationPaneEntries(MSIHANDLE hInstall) +{ + return CustomActionArgcArgv(hInstall, DoRemoveNavigationPaneEntries, "RemoveNavigationPaneEntries"); +} + +/** + * DllMain - Initialize and cleanup WiX custom action utils. + */ +extern "C" BOOL WINAPI DllMain( + __in HINSTANCE hInst, + __in ULONG ulReason, + __in LPVOID + ) +{ + switch(ulReason) + { + case DLL_PROCESS_ATTACH: + WcaGlobalInitialize(hInst); + break; + + case DLL_PROCESS_DETACH: + WcaGlobalFinalize(); + break; + } + + return TRUE; +} diff --git a/admin/win/tools/NCMsiHelper/CustomAction.def b/admin/win/tools/NCMsiHelper/CustomAction.def new file mode 100644 index 000000000..dc15a63d1 --- /dev/null +++ b/admin/win/tools/NCMsiHelper/CustomAction.def @@ -0,0 +1,3 @@ +EXPORTS +ExecNsisUninstaller +RemoveNavigationPaneEntries diff --git a/admin/win/tools/NCMsiHelper/LogResult.cpp b/admin/win/tools/NCMsiHelper/LogResult.cpp new file mode 100644 index 000000000..5aff88b31 --- /dev/null +++ b/admin/win/tools/NCMsiHelper/LogResult.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Parts of this file are based on: + * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo + * + * Licensed under the The Code Project Open License (CPOL): + * https://www.codeproject.com/info/cpol10.aspx + * + */ + +#include "NCTools.h" +#include "NCMsiHelper.h" + +// +// This code modified from MSDN article 256348 +// "How to obtain error message descriptions using the FormatMessage API" +// Currently found at http://support.microsoft.com/kb/256348/en-us + +#define ERRMSGBUFFERSIZE 256 + +/** + * Use FormatMessage() to look an error code and log the error text. + * + * @param dwErrorMsgId The error code to be investigated. + */ +void LogError(DWORD dwErrorMsgId) +{ + HLOCAL pBuffer = nullptr; // Buffer to hold the textual error description. + DWORD ret = 0; // Temp space to hold a return value. + HINSTANCE hInst = nullptr; // Instance handle for DLL. + bool doLookup = true; + DWORD dwMessageId = dwErrorMsgId; + LPCSTR pMessage = "Error %d"; + DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS; + + if (HRESULT_FACILITY(dwErrorMsgId) == FACILITY_MSMQ) { + hInst = LoadLibrary(TEXT("MQUTIL.DLL")); + flags |= FORMAT_MESSAGE_FROM_HMODULE; + doLookup = (nullptr != hInst); + } else if (dwErrorMsgId >= NERR_BASE && dwErrorMsgId <= MAX_NERR) { + hInst = LoadLibrary(TEXT("NETMSG.DLL")); + flags |= FORMAT_MESSAGE_FROM_HMODULE; + doLookup = (nullptr != hInst); + } else if (HRESULT_FACILITY(dwErrorMsgId) == FACILITY_WIN32) { + // A "GetLastError" error, drop the HRESULT_FACILITY + dwMessageId &= 0x0000FFFF; + flags |= FORMAT_MESSAGE_FROM_SYSTEM; + } + + if (doLookup) { + ret = FormatMessageA( + flags, + hInst, // Handle to the DLL. + dwMessageId, // Message identifier. + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language. + (LPSTR)&pBuffer, // Buffer that will hold the text string. + ERRMSGBUFFERSIZE, // Allocate at least this many chars for pBuffer. + nullptr // No insert values. + ); + } + + if (0 < ret && nullptr != pBuffer) { + pMessage = (LPSTR)pBuffer; + } + + // Display the string. + if (WcaIsInitialized()) { + WcaLogError(dwErrorMsgId, pMessage, dwMessageId); + } else { + // Log to stdout/stderr + fprintf_s(stderr, pMessage, dwMessageId); + if ('\n' != pMessage[strlen(pMessage) - 1]) { + fprintf_s(stderr, "\n"); + } + } + + // Free the buffer. + LocalFree(pBuffer); +} + +void LogResult( + __in HRESULT hr, + __in_z __format_string PCSTR fmt, ... + ) +{ + // This code taken from MSDN vsprintf example found currently at + // http://msdn.microsoft.com/en-us/library/28d5ce15(v=vs.71).aspx + // ...and then modified... because it doesn't seem to work! + va_list args; + + va_start(args, fmt); +#pragma warning(push) +#pragma warning(disable : 4996) + auto len = _vsnprintf(nullptr, 0, fmt, args) + 1; +#pragma warning(pop) + auto buffer = (char*)malloc(len * sizeof(char)); + +#ifdef _DEBUG + ::ZeroMemory(buffer, len); +#endif // _DEBUG + _vsnprintf_s(buffer, len, len-1, fmt, args); + + // (MSDN code complete) + + // Now that the buffer holds the formatted string, send it to + // the appropriate output. + if (WcaIsInitialized()) + { + if (FAILED(hr)) { + WcaLogError(hr, buffer); + LogError(hr); + } else { + WcaLog(LOGMSG_STANDARD, buffer); + } + } else { // Log to stdout/stderr + if (FAILED(hr)) + { + fprintf_s(stderr, "%s\n", buffer); + LogError(hr); + } else { + fprintf_s(stdout, "%s\n", buffer); + } + } + + free(buffer); +} diff --git a/admin/win/tools/NCMsiHelper/LogResult.h b/admin/win/tools/NCMsiHelper/LogResult.h new file mode 100644 index 000000000..a43a72fca --- /dev/null +++ b/admin/win/tools/NCMsiHelper/LogResult.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Parts of this file are based on: + * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo + * + * Licensed under the The Code Project Open License (CPOL): + * https://www.codeproject.com/info/cpol10.aspx + * + */ + +/** + * Function prototype for LogResult() + */ +#pragma once + +/** + * Log a message. + * + * If the DLL is being used in a WiX MSI environment, LogResult() will + * route any log messages to the MSI log file via WcaLog() or WcaLogError(). + * + * If the DLL is NOT being used in a WiX MSI environment, LogResult() will + * route any log messages to stdout or stderr. + * + * If the result is an error code, LogResult will attempt to gather a + * text version of the error code and place it in the log. For example, + * if the error code means ERROR_FILE_NOT_FOUND, it will look up the appropriate + * message ( via FormatMessage() ) and add "The system cannot find the file specified." + * to the log. + * + * @param hr The HRESULT to be interrogated for success or failure. + * @param fmt The string format for a user-specified error message. + */ +void LogResult( + __in HRESULT hr, + __in_z __format_string PCSTR fmt, ... +); diff --git a/admin/win/tools/NCMsiHelper/NCMsiHelper.cpp b/admin/win/tools/NCMsiHelper/NCMsiHelper.cpp new file mode 100644 index 000000000..2cdbb59d1 --- /dev/null +++ b/admin/win/tools/NCMsiHelper/NCMsiHelper.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "NCTools.h" +#include "utility.h" +#include "LogResult.h" +#include "NCMsiHelper.h" + +using namespace NCTools; + +HRESULT NCMSIHELPER_API DoExecNsisUninstaller(int argc, LPWSTR *argv) +{ + if (argc != 2) { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + auto appShortName = std::wstring(argv[0]); + auto uninstallExePath = std::wstring(argv[1]); + + if (appShortName.empty() + || uninstallExePath.empty()) { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + auto appInstallDir = uninstallExePath; + auto posLastSlash = appInstallDir.find_last_of(PathSeparator); + if (posLastSlash != std::wstring::npos) { + appInstallDir.erase(posLastSlash); + } else { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + // Run uninstaller + std::wstring cmd = L'\"' + uninstallExePath + L"\" /S _?=" + appInstallDir; + LogResult(S_OK, "Running '%ls'.", cmd.data()); + Utility::execCmd(cmd); + + LogResult(S_OK, "Waiting for NSIS uninstaller."); + + // Can't wait for the process because Uninstall.exe (opposed to Setup.exe) immediately returns, so we'll sleep a bit. + Utility::waitForNsisUninstaller(appShortName); + + LogResult(S_OK, "Removing the NSIS uninstaller."); + + // Sleep a bit and clean up the NSIS mess + Sleep(1500); + DeleteFile(uninstallExePath.data()); + RemoveDirectory(appInstallDir.data()); + + LogResult(S_OK, "Finished."); + + return S_OK; +} + +HRESULT NCMSIHELPER_API DoRemoveNavigationPaneEntries(int argc, LPWSTR *argv) +{ + if (argc != 1) { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + auto appName = std::wstring(argv[0]); + + if (appName.empty()) { + return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); + } + + LogResult(S_OK, "Removing '%ls' sync folders from Explorer's Navigation Pane for the current user.", appName.data()); + + Utility::removeNavigationPaneEntries(appName); + + LogResult(S_OK, "Finished."); + + return S_OK; +} diff --git a/admin/win/tools/NCMsiHelper/NCMsiHelper.h b/admin/win/tools/NCMsiHelper/NCMsiHelper.h new file mode 100644 index 000000000..53e3d20f4 --- /dev/null +++ b/admin/win/tools/NCMsiHelper/NCMsiHelper.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Parts of this file are based on: + * https://www.codeproject.com/articles/570751/devmsi-an-example-cplusplus-msi-wix-deferred-custo + * + * Licensed under the The Code Project Open License (CPOL): + * https://www.codeproject.com/info/cpol10.aspx + * + */ + +/** + * Function prototypes for external "C" interfaces into the DLL. + * + * This project builds a "hybrid" DLL that will work either from + * a MSI Custom Action environment or from an external C program. + * The former routes through "C" interface functions defined in + * CustomAction.def. The latter uses the interfaces defined here. + * + * This header is suitable for inclusion by a project wanting to + * call these methods. Note that _NCMSIHELPER_EXPORTS should not be + * defined for the accessing application source code. + */ +#pragma once + +#ifdef _NCMSIHELPER_EXPORTS +# pragma comment (lib, "newdev") +# pragma comment (lib, "setupapi") +# pragma comment (lib, "msi") +# pragma comment (lib, "dutil") +# pragma comment (lib, "wcautil") +# pragma comment (lib, "Version") + +# include +# include +# include + +// WiX Header Files: +# include +# include + +# define NCMSIHELPER_API __declspec(dllexport) +#else +# define NCMSIHELPER_API __declspec(dllimport) +#endif + +/** + * Runs the NSIS uninstaller and waits for its completion. + * + * argc MUST be 2. + * + * argv[0] is APPLICATION_EXECUTABLE, e.g. "nextcloud" + * argv[1] is the full path to "Uninstall.exe" + * + * @param argc The count of valid arguments in argv. + * @param argv An array of string arguments for the function. + * @return Returns an HRESULT indicating success or failure. + */ +HRESULT NCMSIHELPER_API DoExecNsisUninstaller(int argc, LPWSTR *argv); + + +/** + * Removes the Explorer's Navigation Pane entries. + * + * argc MUST be 1. + * + * argv[0] is APPLICATION_NAME, e.g. "Nextcloud" + * + * @param argc The count of valid arguments in argv. + * @param argv An array of string arguments for the function. + * @return Returns an HRESULT indicating success or failure. + */ +HRESULT NCMSIHELPER_API DoRemoveNavigationPaneEntries(int argc, LPWSTR *argv); + +/** + * Standardized function prototype for NCMsiHelper. + * + * Functions in NCMsiHelper can be called through the MSI Custom + * Action DLL or through an external C program. Both + * methods expect to wrap things into this function prototype. + * + * As a result, all functions defined in this header should + * conform to this function prototype. + */ +typedef HRESULT NCMSIHELPER_API (*CUSTOM_ACTION_ARGC_ARGV)( + int argc, LPWSTR *argv); diff --git a/admin/win/tools/NCMsiHelper/NCMsiHelper.wxs b/admin/win/tools/NCMsiHelper/NCMsiHelper.wxs new file mode 100644 index 000000000..5d07850ee --- /dev/null +++ b/admin/win/tools/NCMsiHelper/NCMsiHelper.wxs @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + diff --git a/admin/win/tools/NCToolsShared/CMakeLists.txt b/admin/win/tools/NCToolsShared/CMakeLists.txt new file mode 100644 index 000000000..61429fdf6 --- /dev/null +++ b/admin/win/tools/NCToolsShared/CMakeLists.txt @@ -0,0 +1,4 @@ +add_library(NCToolsShared STATIC + utility_win.cpp + SimpleMutex.cpp +) diff --git a/admin/win/tools/NCToolsShared/NCTools.h b/admin/win/tools/NCToolsShared/NCTools.h new file mode 100644 index 000000000..87f724f29 --- /dev/null +++ b/admin/win/tools/NCToolsShared/NCTools.h @@ -0,0 +1,31 @@ +// NCTools.h : include file for standard system include files +// + +#pragma once + +#include + +// // Including SDKDDKVer.h defines the highest available Windows platform. +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. +#include + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers + +// Windows Header Files +#include +#include +#include +#include +#include + +// C RunTime Header Files +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/admin/win/tools/NCToolsShared/SimpleMutex.cpp b/admin/win/tools/NCToolsShared/SimpleMutex.cpp new file mode 100644 index 000000000..9f216e754 --- /dev/null +++ b/admin/win/tools/NCToolsShared/SimpleMutex.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "NCTools.h" +#include "SimpleMutex.h" + +SimpleMutex::SimpleMutex() +{ +} + +bool SimpleMutex::create(const std::wstring &name) +{ + release(); + + // Mutex + _hMutex = CreateMutex(nullptr, TRUE, name.data()); + + if (GetLastError() == ERROR_ALREADY_EXISTS) { + CloseHandle(_hMutex); + _hMutex = nullptr; + return false; + } + + return true; +} + +void SimpleMutex::release() +{ + // Release mutex + if (_hMutex) { + ReleaseMutex(_hMutex); + CloseHandle(_hMutex); + _hMutex = nullptr; + } +} diff --git a/admin/win/tools/NCToolsShared/SimpleMutex.h b/admin/win/tools/NCToolsShared/SimpleMutex.h new file mode 100644 index 000000000..3a8bf214f --- /dev/null +++ b/admin/win/tools/NCToolsShared/SimpleMutex.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) by Michael Schuster + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include "NCTools.h" + +class SimpleMutex +{ +public: + SimpleMutex(); + + bool create(const std::wstring &name); + void release(); + +private: + HANDLE _hMutex = nullptr; +}; diff --git a/admin/win/tools/NCToolsShared/utility.h b/admin/win/tools/NCToolsShared/utility.h new file mode 100644 index 000000000..212096736 --- /dev/null +++ b/admin/win/tools/NCToolsShared/utility.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) by Klaas Freitag + * Copyright (C) by Daniel Molkentin + * Copyright (C) by Michael Schuster + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "NCTools.h" + +namespace NCTools { + +typedef std::variant> registryVariant; + +static const std::wstring PathSeparator = L"\\"; + +namespace Utility { + // Ported from libsync + registryVariant registryGetKeyValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& valueName); + bool registrySetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName, DWORD type, const registryVariant &value); + bool registryDeleteKeyTree(HKEY hRootKey, const std::wstring &subKey); + bool registryDeleteKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName); + bool registryWalkSubKeys(HKEY hRootKey, const std::wstring &subKey, const std::function &callback); + + // Ported from gui, modified to optionally rename matching files + typedef std::function copy_dir_recursive_callback; + bool copy_dir_recursive(std::wstring from_dir, std::wstring to_dir, copy_dir_recursive_callback* callbackFileNameMatchReplace = nullptr); + + // Created for native Win32 + DWORD execCmd(std::wstring cmd, bool wait = true); + bool killProcess(const std::wstring &exePath); + bool isValidDirectory(const std::wstring &path); + std::wstring getAppRegistryString(const std::wstring &appVendor, const std::wstring &appName, const std::wstring &valueName); + std::wstring getAppPath(const std::wstring &appVendor, const std::wstring &appName); + std::wstring getConfigPath(const std::wstring &appName); + void waitForNsisUninstaller(const std::wstring& appShortName); + void removeNavigationPaneEntries(const std::wstring &appName); +} + +} // namespace NCTools diff --git a/admin/win/tools/NCToolsShared/utility_win.cpp b/admin/win/tools/NCToolsShared/utility_win.cpp new file mode 100644 index 000000000..c6bc279e7 --- /dev/null +++ b/admin/win/tools/NCToolsShared/utility_win.cpp @@ -0,0 +1,475 @@ +/* + * Copyright (C) by Daniel Molkentin + * Copyright (C) by Michael Schuster + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include "NCTools.h" +#include "utility.h" + +#define ASSERT assert +#define Q_ASSERT assert + +namespace NCTools { + +// Ported from libsync + +registryVariant Utility::registryGetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName) +{ + registryVariant value; + + HKEY hKey; + + REGSAM sam = KEY_READ | KEY_WOW64_64KEY; + LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast(subKey.data()), 0, sam, &hKey); + ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND); + if (result != ERROR_SUCCESS) + return value; + + DWORD type = 0, sizeInBytes = 0; + result = RegQueryValueEx(hKey, reinterpret_cast(valueName.data()), 0, &type, nullptr, &sizeInBytes); + ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND); + if (result == ERROR_SUCCESS) { + switch (type) { + case REG_DWORD: + DWORD dword; + Q_ASSERT(sizeInBytes == sizeof(dword)); + if (RegQueryValueEx(hKey, reinterpret_cast(valueName.data()), 0, &type, reinterpret_cast(&dword), &sizeInBytes) == ERROR_SUCCESS) { + value = int(dword); + } + break; + case REG_EXPAND_SZ: + case REG_SZ: { + std::wstring string; + string.resize(sizeInBytes / sizeof(wchar_t)); + result = RegQueryValueEx(hKey, reinterpret_cast(valueName.data()), 0, &type, reinterpret_cast(string.data()), &sizeInBytes); + + if (result == ERROR_SUCCESS) { + int newCharSize = sizeInBytes / sizeof(wchar_t); + // From the doc: + // If the data has the REG_SZ, REG_MULTI_SZ or REG_EXPAND_SZ type, the string may not have been stored with + // the proper terminating null characters. Therefore, even if the function returns ERROR_SUCCESS, + // the application should ensure that the string is properly terminated before using it; otherwise, it may overwrite a buffer. + if (string.at(newCharSize - 1) == wchar_t('\0')) + string.resize(newCharSize - 1); + value = string; + } + break; + } + case REG_BINARY: { + std::vector buffer; + buffer.resize(sizeInBytes); + result = RegQueryValueEx(hKey, reinterpret_cast(valueName.data()), 0, &type, reinterpret_cast(buffer.data()), &sizeInBytes); + if (result == ERROR_SUCCESS) { + value = buffer.at(12); + } + break; + } + default: + break;// Q_UNREACHABLE(); + } + } + ASSERT(result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND); + + RegCloseKey(hKey); + return value; +} + +bool Utility::registrySetKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName, DWORD type, const registryVariant &value) +{ + HKEY hKey; + // KEY_WOW64_64KEY is necessary because CLSIDs are "Redirected and reflected only for CLSIDs that do not specify InprocServer32 or InprocHandler32." + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa384253%28v=vs.85%29.aspx#redirected__shared__and_reflected_keys_under_wow64 + // This shouldn't be an issue in our case since we use shell32.dll as InprocServer32, so we could write those registry keys for both 32 and 64bit. + // FIXME: Not doing so at the moment means that explorer will show the cloud provider, but 32bit processes' open dialogs (like the ownCloud client itself) won't show it. + REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY; + LONG result = RegCreateKeyEx(hRootKey, reinterpret_cast(subKey.data()), 0, nullptr, 0, sam, nullptr, &hKey, nullptr); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) + return false; + + result = -1; + switch (type) { + case REG_DWORD: { + try { + DWORD dword = std::get(value); + result = RegSetValueEx(hKey, reinterpret_cast(valueName.data()), 0, type, reinterpret_cast(&dword), sizeof(dword)); + } + catch (const std::bad_variant_access&) {} + break; + } + case REG_EXPAND_SZ: + case REG_SZ: { + try { + std::wstring string = std::get(value); + result = RegSetValueEx(hKey, reinterpret_cast(valueName.data()), 0, type, reinterpret_cast(string.data()), static_cast((string.size() + 1) * sizeof(wchar_t))); + } + catch (const std::bad_variant_access&) {} + break; + } + default: + break;// Q_UNREACHABLE(); + } + ASSERT(result == ERROR_SUCCESS); + + RegCloseKey(hKey); + return result == ERROR_SUCCESS; +} + +bool Utility::registryDeleteKeyTree(HKEY hRootKey, const std::wstring &subKey) +{ + HKEY hKey; + REGSAM sam = DELETE | KEY_ENUMERATE_SUB_KEYS | KEY_QUERY_VALUE | KEY_SET_VALUE | KEY_WOW64_64KEY; + LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast(subKey.data()), 0, sam, &hKey); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) + return false; + + result = RegDeleteTree(hKey, nullptr); + RegCloseKey(hKey); + ASSERT(result == ERROR_SUCCESS); + + result |= RegDeleteKeyEx(hRootKey, reinterpret_cast(subKey.data()), sam, 0); + ASSERT(result == ERROR_SUCCESS); + + return result == ERROR_SUCCESS; +} + +bool Utility::registryDeleteKeyValue(HKEY hRootKey, const std::wstring &subKey, const std::wstring &valueName) +{ + HKEY hKey; + REGSAM sam = KEY_WRITE | KEY_WOW64_64KEY; + LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast(subKey.data()), 0, sam, &hKey); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) + return false; + + result = RegDeleteValue(hKey, reinterpret_cast(valueName.data())); + ASSERT(result == ERROR_SUCCESS); + + RegCloseKey(hKey); + return result == ERROR_SUCCESS; +} + +bool Utility::registryWalkSubKeys(HKEY hRootKey, const std::wstring &subKey, const std::function &callback) +{ + HKEY hKey; + REGSAM sam = KEY_READ | KEY_WOW64_64KEY; + LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast(subKey.data()), 0, sam, &hKey); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) + return false; + + DWORD maxSubKeyNameSize; + // Get the largest keyname size once instead of relying each call on ERROR_MORE_DATA. + result = RegQueryInfoKey(hKey, nullptr, nullptr, nullptr, nullptr, &maxSubKeyNameSize, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + ASSERT(result == ERROR_SUCCESS); + if (result != ERROR_SUCCESS) { + RegCloseKey(hKey); + return false; + } + + std::wstring subKeyName; + subKeyName.reserve(maxSubKeyNameSize + 1); + + DWORD retCode = ERROR_SUCCESS; + for (DWORD i = 0; retCode == ERROR_SUCCESS; ++i) { + Q_ASSERT(unsigned(subKeyName.capacity()) > maxSubKeyNameSize); + // Make the previously reserved capacity official again. + subKeyName.resize(subKeyName.capacity()); + DWORD subKeyNameSize = static_cast(subKeyName.size()); + retCode = RegEnumKeyEx(hKey, i, reinterpret_cast(subKeyName.data()), &subKeyNameSize, nullptr, nullptr, nullptr, nullptr); + + ASSERT(result == ERROR_SUCCESS || retCode == ERROR_NO_MORE_ITEMS); + if (retCode == ERROR_SUCCESS) { + // subKeyNameSize excludes the trailing \0 + subKeyName.resize(subKeyNameSize); + // Pass only the sub keyname, not the full path. + callback(hKey, subKeyName); + } + } + + RegCloseKey(hKey); + return retCode != ERROR_NO_MORE_ITEMS; +} + +// Created for Win32 + +DWORD Utility::execCmd(std::wstring cmd, bool wait) +{ + // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-processes + STARTUPINFO si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + // Start the child process. + if (!CreateProcess(nullptr, // No module name (use command line) + cmd.data(), // Command line + nullptr, // Process handle not inheritable + nullptr, // Thread handle not inheritable + FALSE, // Set handle inheritance to FALSE + 0, // No creation flags + nullptr, // Use parent's environment block + nullptr, // Use parent's starting directory + &si, // Pointer to STARTUPINFO structure + &pi) // Pointer to PROCESS_INFORMATION structure + ) + { + return ERROR_INVALID_FUNCTION; + } + + DWORD exitCode = 0; + + if (wait) { + // Wait until child process exits. + WaitForSingleObject(pi.hProcess, INFINITE); + + GetExitCodeProcess(pi.hProcess, &exitCode); + } + + // Close process and thread handles. + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return exitCode; +} + +bool Utility::killProcess(const std::wstring &exePath) +{ + // https://docs.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes + // Get the list of process identifiers. + DWORD aProcesses[1024], cbNeeded, cProcesses, i; + + if (!EnumProcesses(aProcesses, sizeof(aProcesses), &cbNeeded)) { + return false; + } + + // Calculate how many process identifiers were returned. + cProcesses = cbNeeded / sizeof(DWORD); + + std::wstring tmpMatch = exePath; + std::transform(tmpMatch.begin(), tmpMatch.end(), tmpMatch.begin(), std::tolower); + + for (i = 0; i < cProcesses; i++) { + if (aProcesses[i] != 0) { + // Get a handle to the process. + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, FALSE, aProcesses[i]); + + // Get the process name. + if (hProcess) { + TCHAR szProcessName[MAX_PATH] = {0}; + DWORD cbSize = sizeof(szProcessName) / sizeof(TCHAR); + + if (QueryFullProcessImageName(hProcess, 0, szProcessName, &cbSize) == TRUE && cbSize > 0) { + std::wstring procName = szProcessName; + std::transform(procName.begin(), procName.end(), procName.begin(), std::tolower); + + if (procName == tmpMatch) { + if (TerminateProcess(hProcess, 0) == TRUE) { + WaitForSingleObject(hProcess, INFINITE); + CloseHandle(hProcess); + return true; + } + } + } + + CloseHandle(hProcess); + } + } + } + + return false; +} + +bool Utility::isValidDirectory(const std::wstring &path) +{ + auto attrib = GetFileAttributes(path.data()); + + if (attrib == INVALID_FILE_ATTRIBUTES || GetLastError() == ERROR_FILE_NOT_FOUND) { + return false; + } + + return (attrib & FILE_ATTRIBUTE_DIRECTORY); +} + +std::wstring Utility::getAppRegistryString(const std::wstring &appVendor, const std::wstring &appName, const std::wstring &valueName) +{ + std::wstring appKey = std::wstring(LR"(SOFTWARE\)") + appVendor + L'\\' + appName; + std::wstring appKeyWow64 = std::wstring(LR"(SOFTWARE\WOW6432Node\)") + appVendor + L'\\' + appName; + + std::vector appKeys = { appKey, appKeyWow64 }; + + for (auto &key : appKeys) { + try { + return std::get(Utility::registryGetKeyValue(HKEY_LOCAL_MACHINE, + key, + valueName)); + } + catch (const std::bad_variant_access&) {} + } + + return {}; +} + +std::wstring Utility::getAppPath(const std::wstring &appVendor, const std::wstring &appName) +{ + return getAppRegistryString(appVendor, appName, L""); // intentionally left empty to get the key's "(default)" value +} + +std::wstring Utility::getConfigPath(const std::wstring &appName) +{ + // On Windows, use AppDataLocation, that's where the roaming data is and where we should store the config file + PWSTR pszPath = nullptr; + if (!SUCCEEDED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pszPath)) || !pszPath) { + return {}; + } + std::wstring path = pszPath + PathSeparator + appName + PathSeparator; + CoTaskMemFree(pszPath); + + auto newLocation = path; + + return newLocation; +} + +void Utility::waitForNsisUninstaller(const std::wstring &appShortName) +{ + // Can't WaitForSingleObject because NSIS Uninstall.exe copies itself to a TEMP directory and creates a new process, + // so we do sort of a hack and wait for its mutex (see nextcloud.nsi). + HANDLE hMutex; + DWORD lastError = ERROR_SUCCESS; + std::wstring name = appShortName + std::wstring(L"Uninstaller"); + + // Give the process enough time to start, to wait for the NSIS mutex. + Sleep(1500); + + do { + hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, name.data()); + lastError = GetLastError(); + if (hMutex) { + CloseHandle(hMutex); + } + + // This is sort of a hack because WaitForSingleObject immediately returns for the NSIS mutex. + Sleep(500); + } while (lastError != ERROR_FILE_NOT_FOUND); +} + +void Utility::removeNavigationPaneEntries(const std::wstring &appName) +{ + if (appName.empty()) { + return; + } + + // Start by looking at every registered namespace extension for the sidebar, and look for an "ApplicationName" value + // that matches ours when we saved. + std::vector entriesToRemove; + Utility::registryWalkSubKeys( + HKEY_CURRENT_USER, + LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace)", + [&entriesToRemove, &appName](HKEY key, const std::wstring &subKey) { + try { + auto curAppName = std::get(Utility::registryGetKeyValue(key, subKey, L"ApplicationName")); + + if (curAppName == appName) { + entriesToRemove.push_back(subKey); + } + } + catch (const std::bad_variant_access&) {} + }); + + for (auto &clsid : entriesToRemove) { + std::wstring clsidStr = clsid; + std::wstring clsidPath = std::wstring(LR"(Software\Classes\CLSID\)") + clsidStr; + std::wstring clsidPathWow64 = std::wstring(LR"(Software\Classes\Wow6432Node\CLSID\)") + clsidStr; + std::wstring namespacePath = std::wstring(LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\)") + clsidStr; + + Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPath); + Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, clsidPathWow64); + Utility::registryDeleteKeyTree(HKEY_CURRENT_USER, namespacePath); + Utility::registryDeleteKeyValue(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\HideDesktopIcons\NewStartPanel)", clsidStr); + } +} + +// Ported from gui, modified to optionally rename matching files +bool Utility::copy_dir_recursive(std::wstring from_dir, std::wstring to_dir, copy_dir_recursive_callback *callbackFileNameMatchReplace) +{ + WIN32_FIND_DATA fileData; + + if (from_dir.empty() || to_dir.empty()) { + return false; + } + + if (from_dir.back() != PathSeparator.front()) + from_dir.append(PathSeparator); + if (to_dir.back() != PathSeparator.front()) + to_dir.append(PathSeparator); + + std::wstring startDir = from_dir; + startDir.append(L"*.*"); + + auto hFind = FindFirstFile(startDir.data(), &fileData); + + if (hFind == INVALID_HANDLE_VALUE) { + return false; + } + + bool success = true; + + do { + if (fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (std::wstring(fileData.cFileName) == L"." || std::wstring(fileData.cFileName) == L"..") { + continue; + } + + std::wstring from = from_dir + fileData.cFileName; + std::wstring to = to_dir + fileData.cFileName; + + if (CreateDirectoryEx(from.data(), to.data(), nullptr) == FALSE) { + success = false; + break; + } + + if (copy_dir_recursive(from, to, callbackFileNameMatchReplace) == false) { + success = false; + break; + } + } else { + std::wstring newFilename = fileData.cFileName; + + if (callbackFileNameMatchReplace) { + (*callbackFileNameMatchReplace)(std::wstring(fileData.cFileName), newFilename); + } + + std::wstring from = from_dir + fileData.cFileName; + std::wstring to = to_dir + newFilename; + + if (CopyFile(from.data(), to.data(), TRUE) == FALSE) { + success = false; + break; + } + } + } while (FindNextFile(hFind, &fileData)); + + FindClose(hFind); + + return success; +} + +} // namespace NCTools