瀏覽代碼

Windows MSI: Add helper DLL and shared migration tools code

The helper DLL will be utilized by Windows Installer with Custom Actions defined in the NCMsiHelper.wxs WiX fragment.

Exports:
- ExecNsisUninstaller
- RemoveNavigationPaneEntries

Signed-off-by: Michael Schuster <michael@schuster.ms>
Michael Schuster 5 年之前
父節點
當前提交
68776fe319

+ 7 - 2
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()

+ 8 - 0
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()

+ 61 - 0
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()

+ 47 - 0
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/
+)

+ 125 - 0
admin/win/tools/NCMsiHelper/CustomAction.cpp

@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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;
+}

+ 3 - 0
admin/win/tools/NCMsiHelper/CustomAction.def

@@ -0,0 +1,3 @@
+EXPORTS
+ExecNsisUninstaller
+RemoveNavigationPaneEntries

+ 137 - 0
admin/win/tools/NCMsiHelper/LogResult.cpp

@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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);
+}

+ 48 - 0
admin/win/tools/NCMsiHelper/LogResult.h

@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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, ...
+);

+ 85 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.cpp

@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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;
+}

+ 96 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.h

@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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 <msiquery.h>
+#  include <stdlib.h>
+#  include <lmerr.h>
+
+// WiX Header Files:
+#  include <wcautil.h>
+#  include <strutil.h>
+
+#  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);

+ 43 - 0
admin/win/tools/NCMsiHelper/NCMsiHelper.wxs

@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ *
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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.
+ *
+-->
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+    <Fragment>
+
+        <?if $(var.Platform) = x64 ?>
+        <?define bitness = "64" ?>
+        <?else ?>
+        <?define bitness = "32" ?>
+        <?endif ?>
+
+        <Binary Id="NCMsiHelper" SourceFile="NCMsiHelper$(var.bitness).dll" />
+
+        <CustomAction Id="ExecNsisUninstaller"
+                    Return="ignore"
+                    BinaryKey="NCMsiHelper"
+                    DllEntry="ExecNsisUninstaller"
+                    Execute="deferred"
+                    Impersonate="no" />
+
+        <CustomAction Id="RemoveNavigationPaneEntries"
+                    Return="ignore"
+                    BinaryKey="NCMsiHelper"
+                    DllEntry="RemoveNavigationPaneEntries"
+                    Execute="deferred"
+                    Impersonate="yes" />
+
+    </Fragment>
+</Wix>

+ 4 - 0
admin/win/tools/NCToolsShared/CMakeLists.txt

@@ -0,0 +1,4 @@
+add_library(NCToolsShared STATIC
+    utility_win.cpp
+    SimpleMutex.cpp
+)

+ 31 - 0
admin/win/tools/NCToolsShared/NCTools.h

@@ -0,0 +1,31 @@
+// NCTools.h : include file for standard system include files
+//
+
+#pragma once
+
+#include <WinSDKVer.h>
+
+// // 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 <SDKDDKVer.h>
+
+#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
+
+// Windows Header Files
+#include <windows.h>
+#include <shellapi.h>
+#include <Shlobj.h>
+#include <psapi.h>
+#include <wincred.h>
+
+// C RunTime Header Files
+#include <cstdlib>
+#include <malloc.h>
+#include <memory.h>
+#include <tchar.h>
+#include <algorithm>
+#include <functional>
+#include <string>
+#include <vector>
+#include <variant>

+ 46 - 0
admin/win/tools/NCToolsShared/SimpleMutex.cpp

@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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;
+    }
+}

+ 29 - 0
admin/win/tools/NCToolsShared/SimpleMutex.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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;
+};

+ 54 - 0
admin/win/tools/NCToolsShared/utility.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
+ * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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<int, std::wstring, std::vector<unsigned char>> 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<void(HKEY, const std::wstring&)> &callback);
+
+    // Ported from gui, modified to optionally rename matching files
+    typedef std::function<void(const std::wstring&, std::wstring&)> 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

+ 475 - 0
admin/win/tools/NCToolsShared/utility_win.cpp

@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) by Daniel Molkentin <danimo@owncloud.com>
+ * Copyright (C) by Michael Schuster <michael.schuster@nextcloud.com>
+ *
+ * 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 <cassert>
+#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<LPCWSTR>(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<LPCWSTR>(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<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(&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<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(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<unsigned char> buffer;
+            buffer.resize(sizeInBytes);
+            result = RegQueryValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, &type, reinterpret_cast<LPBYTE>(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<LPCWSTR>(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<int>(value);
+            result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, type, reinterpret_cast<const BYTE *>(&dword), sizeof(dword));
+        }
+        catch (const std::bad_variant_access&) {}
+        break;
+    }
+    case REG_EXPAND_SZ:
+    case REG_SZ: {
+        try {
+            std::wstring string = std::get<std::wstring>(value);
+            result = RegSetValueEx(hKey, reinterpret_cast<LPCWSTR>(valueName.data()), 0, type, reinterpret_cast<const BYTE *>(string.data()), static_cast<DWORD>((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<LPCWSTR>(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<LPCWSTR>(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<LPCWSTR>(subKey.data()), 0, sam, &hKey);
+    ASSERT(result == ERROR_SUCCESS);
+    if (result != ERROR_SUCCESS)
+        return false;
+
+    result = RegDeleteValue(hKey, reinterpret_cast<LPCWSTR>(valueName.data()));
+    ASSERT(result == ERROR_SUCCESS);
+
+    RegCloseKey(hKey);
+    return result == ERROR_SUCCESS;
+}
+
+bool Utility::registryWalkSubKeys(HKEY hRootKey, const std::wstring &subKey, const std::function<void(HKEY, const std::wstring&)> &callback)
+{
+    HKEY hKey;
+    REGSAM sam = KEY_READ | KEY_WOW64_64KEY;
+    LONG result = RegOpenKeyEx(hRootKey, reinterpret_cast<LPCWSTR>(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<DWORD>(subKeyName.size());
+        retCode = RegEnumKeyEx(hKey, i, reinterpret_cast<LPWSTR>(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<std::wstring> appKeys = { appKey, appKeyWow64 };
+
+    for (auto &key : appKeys) {
+        try {
+            return std::get<std::wstring>(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<std::wstring> 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<std::wstring>(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