/*
 *  openvpnmsica -- Custom Action DLL to provide OpenVPN-specific support to MSI packages
 *                  https://community.openvpn.net/openvpn/wiki/OpenVPNMSICA
 *
 *  Copyright (C) 2018 Simon Rozman <simon@rozman.si>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2
 *  as published by the Free Software Foundation.
 *
 *  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.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#elif defined(_MSC_VER)
#include <config-msvc.h>
#endif
#include <winsock2.h> /* Must be included _before_ <windows.h> */

#include "openvpnmsica.h"
#include "msica_op.h"
#include "msiex.h"

#include "../tapctl/basic.h"
#include "../tapctl/error.h"
#include "../tapctl/tap.h"

#include <windows.h>
#include <iphlpapi.h>
#include <malloc.h>
#include <memory.h>
#include <msiquery.h>
#include <shellapi.h>
#include <shlwapi.h>
#include <stdbool.h>
#include <stdlib.h>
#include <tchar.h>

#ifdef _MSC_VER
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "shell32.lib")
#pragma comment(lib, "shlwapi.lib")
#pragma comment(lib, "version.lib")
#endif


/**
 * Local constants
 */

#define MSICA_INTERFACE_TICK_SIZE (16*1024) /** Amount of tick space to reserve for one TAP/TUN interface creation/deletition. */


/**
 * Cleanup actions
 */
static const struct {
    LPCTSTR szName;               /** Name of the cleanup action. This name is appended to the deferred custom action name (e.g. "InstallTAPInterfaces" >> "InstallTAPInterfacesCommit"). */
    TCHAR szSuffix[3];            /** Two-character suffix to append to the cleanup operation sequence filename */
} openvpnmsica_cleanup_action_seqs[MSICA_CLEANUP_ACTION_COUNT] =
{
    { TEXT("Commit"  ), TEXT("cm") }, /* MSICA_CLEANUP_ACTION_COMMIT   */
    { TEXT("Rollback"), TEXT("rb") }, /* MSICA_CLEANUP_ACTION_ROLLBACK */
};


/**
 * Creates a new sequence file in the current user's temporary folder and sets MSI property
 * to its absolute path.
 *
 * @param hInstall      Handle to the installation provided to the DLL custom action
 *
 * @param szProperty    MSI property name to set to the absolute path of the sequence file.
 *
 * @param szFilename    String of minimum MAXPATH+1 characters where the zero-terminated
 *                      file absolute path is stored.
 *
 * @return ERROR_SUCCESS on success; An error code otherwise
 */
static DWORD
openvpnmsica_setup_sequence_filename(
    _In_ MSIHANDLE hInstall,
    _In_z_ LPCTSTR szProperty,
    _Out_z_cap_(MAXPATH + 1) LPTSTR szFilename)
{
    DWORD dwResult;

    if (szFilename == NULL)
    {
        return ERROR_BAD_ARGUMENTS;
    }

    /* Generate a random filename in the temporary folder. */
    if (GetTempPath(MAX_PATH + 1, szFilename) == 0)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: GetTempPath failed", __FUNCTION__);
        return dwResult;
    }
    if (GetTempFileName(szFilename, szProperty, 0, szFilename) == 0)
    {
        dwResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: GetTempFileName failed", __FUNCTION__);
        return dwResult;
    }

    /* Store sequence filename to property for deferred custom action. */
    dwResult = MsiSetProperty(hInstall, szProperty, szFilename);
    if (dwResult != ERROR_SUCCESS)
    {
        SetLastError(dwResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szProperty);
        return dwResult;
    }

    /* Generate and store cleanup operation sequence filenames to properties. */
    LPTSTR szExtension = PathFindExtension(szFilename);
    TCHAR szFilenameEx[MAX_PATH + 1 /*dash*/ + 2 /*suffix*/ + 1 /*terminator*/];
    size_t len_property_name = _tcslen(szProperty);
    for (size_t i = 0; i < MSICA_CLEANUP_ACTION_COUNT; i++)
    {
        size_t len_action_name_z = _tcslen(openvpnmsica_cleanup_action_seqs[i].szName) + 1;
        TCHAR *szPropertyEx = (TCHAR *)malloc((len_property_name + len_action_name_z) * sizeof(TCHAR));
        if (szPropertyEx == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, (len_property_name + len_action_name_z) * sizeof(TCHAR));
            return ERROR_OUTOFMEMORY;
        }

        memcpy(szPropertyEx, szProperty, len_property_name * sizeof(TCHAR));
        memcpy(szPropertyEx + len_property_name, openvpnmsica_cleanup_action_seqs[i].szName, len_action_name_z * sizeof(TCHAR));
        _stprintf_s(
            szFilenameEx, _countof(szFilenameEx),
            TEXT("%.*s-%.2s%s"),
            (int)(szExtension - szFilename), szFilename,
            openvpnmsica_cleanup_action_seqs[i].szSuffix,
            szExtension);
        dwResult = MsiSetProperty(hInstall, szPropertyEx, szFilenameEx);
        if (dwResult != ERROR_SUCCESS)
        {
            SetLastError(dwResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szPropertyEx);
            free(szPropertyEx);
            return dwResult;
        }
        free(szPropertyEx);
    }

    return ERROR_SUCCESS;
}


#ifdef _DEBUG

/**
 * Pops up a message box creating a time window to attach a debugger to the installer process in
 * order to debug custom actions.
 *
 * @param szFunctionName  Function name that triggered the pop-up. Displayed in message box's
 *                        title.
 */
static void
_openvpnmsica_debug_popup(_In_z_ LPCTSTR szFunctionName)
{
    TCHAR szTitle[0x100], szMessage[0x100+MAX_PATH], szProcessPath[MAX_PATH];

    /* Compose pop-up title. The dialog title will contain function name to ease the process
     * locating. Mind that Visual Studio displays window titles on the process list. */
    _stprintf_s(szTitle, _countof(szTitle), TEXT("%s v%s"), szFunctionName, TEXT(PACKAGE_VERSION));

    /* Get process name. */
    GetModuleFileName(NULL, szProcessPath, _countof(szProcessPath));
    LPCTSTR szProcessName = _tcsrchr(szProcessPath, TEXT('\\'));
    szProcessName = szProcessName ? szProcessName + 1 : szProcessPath;

    /* Compose the pop-up message. */
    _stprintf_s(
        szMessage, _countof(szMessage),
        TEXT("The %s process (PID: %u) has started to execute the %s custom action.\r\n")
        TEXT("\r\n")
        TEXT("If you would like to debug the custom action, attach a debugger to this process and set breakpoints before dismissing this dialog.\r\n")
        TEXT("\r\n")
        TEXT("If you are not debugging this custom action, you can safely ignore this message."),
        szProcessName,
        GetCurrentProcessId(),
        szFunctionName);

    MessageBox(NULL, szMessage, szTitle, MB_OK);
}

#define openvpnmsica_debug_popup(f) _openvpnmsica_debug_popup(f)
#else  /* ifdef _DEBUG */
#define openvpnmsica_debug_popup(f)
#endif /* ifdef _DEBUG */


/**
 * Detects Windows version and sets DRIVERCERTIFICATION property to "", "whql", or "attsgn"
 * accordingly.
 *
 * @param hInstall      Handle to the installation provided to the DLL custom action
 *
 * @return ERROR_SUCCESS on success; An error code otherwise
 *         See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx
 */
static UINT
openvpnmsica_set_driver_certification(_In_ MSIHANDLE hInstall)
{
    UINT uiResult;

    /* Get Windows version. */
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4996) /* 'GetVersionExW': was declared deprecated. */
#endif
    OSVERSIONINFOEX ver_info = { .dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX) };
    if (!GetVersionEx((LPOSVERSIONINFO)&ver_info))
    {
        uiResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: GetVersionEx() failed", __FUNCTION__);
        return uiResult;
    }
#ifdef _MSC_VER
#pragma warning(pop)
#endif

    /* The Windows version is usually spoofed, check using RtlGetVersion(). */
    TCHAR szDllPath[0x1000];
    ExpandEnvironmentStrings(TEXT("%SystemRoot%\\System32\\ntdll.dll"), szDllPath,
#ifdef UNICODE
                             _countof(szDllPath)
#else
                             _countof(szDllPath) - 1
#endif
                             );
    HMODULE hNtDllModule = LoadLibrary(szDllPath);
    if (hNtDllModule)
    {
        typedef NTSTATUS (WINAPI* fnRtlGetVersion)(PRTL_OSVERSIONINFOW);
        fnRtlGetVersion RtlGetVersion = (fnRtlGetVersion)GetProcAddress(hNtDllModule, "RtlGetVersion");
        if (RtlGetVersion)
        {
            RTL_OSVERSIONINFOW rtl_ver_info = { .dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOW) };
            if (RtlGetVersion(&rtl_ver_info) == 0)
            {
                if (
                    rtl_ver_info.dwMajorVersion >  ver_info.dwMajorVersion
                    || rtl_ver_info.dwMajorVersion == ver_info.dwMajorVersion && rtl_ver_info.dwMinorVersion >  ver_info.dwMinorVersion
                    || rtl_ver_info.dwMajorVersion == ver_info.dwMajorVersion && rtl_ver_info.dwMinorVersion == ver_info.dwMinorVersion && rtl_ver_info.dwBuildNumber > ver_info.dwBuildNumber)
                {
                    /* We got RtlGetVersion() and it reported newer version than GetVersionEx(). */
                    ver_info.dwMajorVersion = rtl_ver_info.dwMajorVersion;
                    ver_info.dwMinorVersion = rtl_ver_info.dwMinorVersion;
                    ver_info.dwBuildNumber  = rtl_ver_info.dwBuildNumber;
                    ver_info.dwPlatformId   = rtl_ver_info.dwPlatformId;
                }
            }
        }

        FreeLibrary(hNtDllModule);
    }

    /* We don't trust RtlGetVersion() either. Check the version resource of kernel32.dll. */
    ExpandEnvironmentStrings(TEXT("%SystemRoot%\\System32\\kernel32.dll"), szDllPath,
#ifdef UNICODE
                             _countof(szDllPath)
#else
                             _countof(szDllPath) - 1
#endif
                             );

    DWORD dwHandle;
    DWORD dwVerInfoSize = GetFileVersionInfoSize(szDllPath, &dwHandle);
    if (dwVerInfoSize)
    {
        LPVOID pVersionInfo = malloc(dwVerInfoSize);
        if (pVersionInfo)
        {
            /* Read version info. */
            if (GetFileVersionInfo(szDllPath, dwHandle, dwVerInfoSize, pVersionInfo))
            {
                /* Get the value for the root block. */
                UINT uiSize = 0;
                VS_FIXEDFILEINFO *pVSFixedFileInfo = NULL;
                if (VerQueryValue(pVersionInfo, TEXT("\\"), &pVSFixedFileInfo, &uiSize) && uiSize && pVSFixedFileInfo)
                {
                    if (HIWORD(pVSFixedFileInfo->dwProductVersionMS) >  ver_info.dwMajorVersion
                        || HIWORD(pVSFixedFileInfo->dwProductVersionMS) == ver_info.dwMajorVersion && LOWORD(pVSFixedFileInfo->dwProductVersionMS) >  ver_info.dwMinorVersion
                        || HIWORD(pVSFixedFileInfo->dwProductVersionMS) == ver_info.dwMajorVersion && LOWORD(pVSFixedFileInfo->dwProductVersionMS) == ver_info.dwMinorVersion && HIWORD(pVSFixedFileInfo->dwProductVersionLS) > ver_info.dwBuildNumber)
                    {
                        /* We got kernel32.dll version and it is newer. */
                        ver_info.dwMajorVersion = HIWORD(pVSFixedFileInfo->dwProductVersionMS);
                        ver_info.dwMinorVersion = LOWORD(pVSFixedFileInfo->dwProductVersionMS);
                        ver_info.dwBuildNumber  = HIWORD(pVSFixedFileInfo->dwProductVersionLS);
                    }
                }
            }

            free(pVersionInfo);
        }
        else
        {
            msg(M_NONFATAL, "%s: malloc(%u) failed", __FUNCTION__, dwVerInfoSize);
        }
    }

    uiResult = MsiSetProperty(hInstall, TEXT("DRIVERCERTIFICATION"), ver_info.dwMajorVersion >= 10 ? ver_info.wProductType > VER_NT_WORKSTATION ? TEXT("whql") : TEXT("attsgn") : TEXT(""));
    if (uiResult != ERROR_SUCCESS)
    {
        SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"DRIVERCERTIFICATION\") failed", __FUNCTION__);
        return uiResult;
    }

    return ERROR_SUCCESS;
}


/**
 * Detects if the OpenVPNService service is in use (running or paused) and sets
 * OPENVPNSERVICE to the service process PID, or its path if it is set to
 * auto-start, but not running.
 *
 * @param hInstall      Handle to the installation provided to the DLL custom action
 *
 * @return ERROR_SUCCESS on success; An error code otherwise
 *         See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa368072.aspx
 */
static UINT
openvpnmsica_set_openvpnserv_state(_In_ MSIHANDLE hInstall)
{
    UINT uiResult;

    /* Get Service Control Manager handle. */
    SC_HANDLE hSCManager = OpenSCManager(NULL, SERVICES_ACTIVE_DATABASE, SC_MANAGER_CONNECT);
    if (hSCManager == NULL)
    {
        uiResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: OpenSCManager() failed", __FUNCTION__);
        return uiResult;
    }

    /* Get OpenVPNService service handle. */
    SC_HANDLE hService = OpenService(hSCManager, TEXT("OpenVPNService"), SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG);
    if (hService == NULL)
    {
        uiResult = GetLastError();
        if (uiResult == ERROR_SERVICE_DOES_NOT_EXIST)
        {
            /* This is not actually an error. */
            goto cleanup_OpenSCManager;
        }
        msg(M_NONFATAL | M_ERRNO, "%s: OpenService(\"OpenVPNService\") failed", __FUNCTION__);
        goto cleanup_OpenSCManager;
    }

    /* Query service status. */
    SERVICE_STATUS_PROCESS ssp;
    DWORD dwBufSize;
    if (QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, sizeof(ssp), &dwBufSize))
    {
        switch (ssp.dwCurrentState)
        {
            case SERVICE_START_PENDING:
            case SERVICE_RUNNING:
            case SERVICE_STOP_PENDING:
            case SERVICE_PAUSE_PENDING:
            case SERVICE_PAUSED:
            case SERVICE_CONTINUE_PENDING:
            {
                /* Service is started (kind of). Set OPENVPNSERVICE property to service PID. */
                TCHAR szPID[10 /*MAXDWORD in decimal*/ + 1 /*terminator*/];
                _stprintf_s(
                    szPID, _countof(szPID),
                    TEXT("%u"),
                    ssp.dwProcessId);

                uiResult = MsiSetProperty(hInstall, TEXT("OPENVPNSERVICE"), szPID);
                if (uiResult != ERROR_SUCCESS)
                {
                    SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
                    msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"OPENVPNSERVICE\") failed", __FUNCTION__);
                }

                /* We know user is using the service. Skip auto-start setting check. */
                goto cleanup_OpenService;
            }
            break;
        }
    }
    else
    {
        uiResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: QueryServiceStatusEx(\"OpenVPNService\") failed", __FUNCTION__);
    }

    /* Service is not started. Is it set to auto-start? */
    /* MSDN describes the maximum buffer size for QueryServiceConfig() to be 8kB. */
    /* This is small enough to fit on stack. */
    BYTE _buffer_8k[8192];
    LPQUERY_SERVICE_CONFIG pQsc = (LPQUERY_SERVICE_CONFIG)_buffer_8k;
    dwBufSize = sizeof(_buffer_8k);
    if (!QueryServiceConfig(hService, pQsc, dwBufSize, &dwBufSize))
    {
        uiResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: QueryServiceStatusEx(\"QueryServiceConfig\") failed", __FUNCTION__);
        goto cleanup_OpenService;
    }

    if (pQsc->dwStartType <= SERVICE_AUTO_START)
    {
        /* Service is set to auto-start. Set OPENVPNSERVICE property to its path. */
        uiResult = MsiSetProperty(hInstall, TEXT("OPENVPNSERVICE"), pQsc->lpBinaryPathName);
        if (uiResult != ERROR_SUCCESS)
        {
            SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"OPENVPNSERVICE\") failed", __FUNCTION__);
            goto cleanup_OpenService;
        }
    }

    uiResult = ERROR_SUCCESS;

cleanup_OpenService:
    CloseServiceHandle(hService);
cleanup_OpenSCManager:
    CloseServiceHandle(hSCManager);
    return uiResult;
}


UINT __stdcall
FindSystemInfo(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL));

    OPENVPNMSICA_SAVE_MSI_SESSION(hInstall);

    openvpnmsica_set_driver_certification(hInstall);
    openvpnmsica_set_openvpnserv_state(hInstall);

    if (bIsCoInitialized)
    {
        CoUninitialize();
    }
    return ERROR_SUCCESS;
}


UINT __stdcall
FindTAPInterfaces(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    UINT uiResult;
    BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL));

    OPENVPNMSICA_SAVE_MSI_SESSION(hInstall);

    /* Get available network interfaces. */
    struct tap_interface_node *pInterfaceList = NULL;
    uiResult = tap_list_interfaces(NULL, &pInterfaceList);
    if (uiResult != ERROR_SUCCESS)
    {
        goto cleanup_CoInitialize;
    }

    /* Get IPv4/v6 info for all network interfaces. Actually, we're interested in link status only: up/down? */
    PIP_ADAPTER_ADDRESSES pAdapterAdresses = NULL;
    ULONG ulAdapterAdressesSize = 16*1024;
    for (size_t iteration = 0; iteration < 2; iteration++)
    {
        pAdapterAdresses = (PIP_ADAPTER_ADDRESSES)malloc(ulAdapterAdressesSize);
        if (pAdapterAdresses == NULL)
        {
            msg(M_NONFATAL, "%s: malloc(%u) failed", __FUNCTION__, ulAdapterAdressesSize);
            uiResult = ERROR_OUTOFMEMORY; goto cleanup_tap_list_interfaces;
        }

        ULONG ulResult = GetAdaptersAddresses(
            AF_UNSPEC,
            GAA_FLAG_SKIP_UNICAST | GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME | GAA_FLAG_INCLUDE_ALL_INTERFACES,
            NULL,
            pAdapterAdresses,
            &ulAdapterAdressesSize);

        if (ulResult == ERROR_SUCCESS)
        {
            break;
        }

        free(pAdapterAdresses);
        if (ulResult != ERROR_BUFFER_OVERFLOW)
        {
            SetLastError(ulResult); /* MSDN does not mention GetAdaptersAddresses() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: GetAdaptersAddresses() failed", __FUNCTION__);
            uiResult = ulResult; goto cleanup_tap_list_interfaces;
        }
    }

    /* Enumerate interfaces. */
    struct interface_node
    {
        const struct tap_interface_node *iface;
        struct interface_node *next;
    } *interfaces_head = NULL, *interfaces_tail = NULL;
    size_t interface_count = 0;
    MSIHANDLE hRecord = MsiCreateRecord(1);
    for (struct tap_interface_node *pInterface = pInterfaceList; pInterface; pInterface = pInterface->pNext)
    {
        for (LPCTSTR hwid = pInterface->szzHardwareIDs; hwid[0]; hwid += _tcslen(hwid) + 1)
        {
            if (_tcsicmp(hwid, TEXT(TAP_WIN_COMPONENT_ID)) == 0
                || _tcsicmp(hwid, TEXT("root\\") TEXT(TAP_WIN_COMPONENT_ID)) == 0)
            {
                /* TAP interface found. */

                /* Report the GUID of the interface to installer. */
                LPOLESTR szInterfaceId = NULL;
                StringFromIID((REFIID)&pInterface->guid, &szInterfaceId);
                MsiRecordSetString(hRecord, 1, szInterfaceId);
                MsiProcessMessage(hInstall, INSTALLMESSAGE_ACTIONDATA, hRecord);
                CoTaskMemFree(szInterfaceId);

                /* Append interface to the list. */
                struct interface_node *node = (struct interface_node *)malloc(sizeof(struct interface_node));
                if (node == NULL)
                {
                    MsiCloseHandle(hRecord);
                    msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, sizeof(struct interface_node));
                    uiResult = ERROR_OUTOFMEMORY; goto cleanup_pAdapterAdresses;
                }

                node->iface = pInterface;
                node->next = NULL;
                if (interfaces_head)
                {
                    interfaces_tail = interfaces_tail->next = node;
                }
                else
                {
                    interfaces_head = interfaces_tail = node;
                }
                interface_count++;
                break;
            }
        }
    }
    MsiCloseHandle(hRecord);

    if (interface_count)
    {
        /* Prepare semicolon delimited list of TAP interface ID(s) and active TAP interface ID(s). */
        LPTSTR
            szTAPInterfaces     = (LPTSTR)malloc(interface_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)),
            szTAPInterfacesTail = szTAPInterfaces;
        if (szTAPInterfaces == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, interface_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR));
            uiResult = ERROR_OUTOFMEMORY; goto cleanup_pAdapterAdresses;
        }

        LPTSTR
            szTAPInterfacesActive     = (LPTSTR)malloc(interface_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR)),
            szTAPInterfacesActiveTail = szTAPInterfacesActive;
        if (szTAPInterfacesActive == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, interface_count * (38 /*GUID*/ + 1 /*separator/terminator*/) * sizeof(TCHAR));
            uiResult = ERROR_OUTOFMEMORY; goto cleanup_szTAPInterfaces;
        }

        while (interfaces_head)
        {
            /* Convert interface GUID to UTF-16 string. (LPOLESTR defaults to LPWSTR) */
            LPOLESTR szInterfaceId = NULL;
            StringFromIID((REFIID)&interfaces_head->iface->guid, &szInterfaceId);

            /* Append to the list of TAP interface ID(s). */
            if (szTAPInterfaces < szTAPInterfacesTail)
            {
                *(szTAPInterfacesTail++) = TEXT(';');
            }
            memcpy(szTAPInterfacesTail, szInterfaceId, 38 * sizeof(TCHAR));
            szTAPInterfacesTail += 38;

            /* If this interface is active (connected), add it to the list of active TAP interface ID(s). */
            for (PIP_ADAPTER_ADDRESSES p = pAdapterAdresses; p; p = p->Next)
            {
                OLECHAR szId[38 + 1];
                GUID guid;
                if (MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, p->AdapterName, -1, szId, _countof(szId)) > 0
                    && SUCCEEDED(IIDFromString(szId, &guid))
                    && memcmp(&guid, &interfaces_head->iface->guid, sizeof(GUID)) == 0)
                {
                    if (p->OperStatus == IfOperStatusUp)
                    {
                        /* This TAP interface is active (connected). */
                        if (szTAPInterfacesActive < szTAPInterfacesActiveTail)
                        {
                            *(szTAPInterfacesActiveTail++) = TEXT(';');
                        }
                        memcpy(szTAPInterfacesActiveTail, szInterfaceId, 38 * sizeof(TCHAR));
                        szTAPInterfacesActiveTail += 38;
                    }
                    break;
                }
            }
            CoTaskMemFree(szInterfaceId);

            struct interface_node *p = interfaces_head;
            interfaces_head = interfaces_head->next;
            free(p);
        }
        szTAPInterfacesTail      [0] = 0;
        szTAPInterfacesActiveTail[0] = 0;

        /* Set Installer TAPINTERFACES property. */
        uiResult = MsiSetProperty(hInstall, TEXT("TAPINTERFACES"), szTAPInterfaces);
        if (uiResult != ERROR_SUCCESS)
        {
            SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"TAPINTERFACES\") failed", __FUNCTION__);
            goto cleanup_szTAPInterfacesActive;
        }

        /* Set Installer ACTIVETAPINTERFACES property. */
        uiResult = MsiSetProperty(hInstall, TEXT("ACTIVETAPINTERFACES"), szTAPInterfacesActive);
        if (uiResult != ERROR_SUCCESS)
        {
            SetLastError(uiResult); /* MSDN does not mention MsiSetProperty() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: MsiSetProperty(\"ACTIVETAPINTERFACES\") failed", __FUNCTION__);
            goto cleanup_szTAPInterfacesActive;
        }

cleanup_szTAPInterfacesActive:
        free(szTAPInterfacesActive);
cleanup_szTAPInterfaces:
        free(szTAPInterfaces);
    }
    else
    {
        uiResult = ERROR_SUCCESS;
    }

cleanup_pAdapterAdresses:
    free(pAdapterAdresses);
cleanup_tap_list_interfaces:
    tap_free_interface_list(pInterfaceList);
cleanup_CoInitialize:
    if (bIsCoInitialized)
    {
        CoUninitialize();
    }
    return uiResult;
}


UINT __stdcall
CloseOpenVPNGUI(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif
    UNREFERENCED_PARAMETER(hInstall); /* This CA is does not interact with MSI session (report errors, access properties, tables, etc.). */

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    /* Find OpenVPN GUI window. */
    HWND hWnd = FindWindow(TEXT("OpenVPN-GUI"), NULL);
    if (hWnd)
    {
        /* Ask it to close and wait for 100ms. Unfortunately, this will succeed only for recent OpenVPN GUI that do not run elevated. */
        SendMessage(hWnd, WM_CLOSE, 0, 0);
        Sleep(100);
    }

    return ERROR_SUCCESS;
}


UINT __stdcall
StartOpenVPNGUI(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    UINT uiResult;
    BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL));

    OPENVPNMSICA_SAVE_MSI_SESSION(hInstall);

    /* Create and populate a MSI record. */
    MSIHANDLE hRecord = MsiCreateRecord(1);
    if (!hRecord)
    {
        uiResult = ERROR_INVALID_HANDLE;
        msg(M_NONFATAL, "%s: MsiCreateRecord failed", __FUNCTION__);
        goto cleanup_CoInitialize;
    }
    uiResult = MsiRecordSetString(hRecord, 0, TEXT("\"[#bin.openvpn_gui.exe]\""));
    if (uiResult != ERROR_SUCCESS)
    {
        SetLastError(uiResult); /* MSDN does not mention MsiRecordSetString() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiRecordSetString failed", __FUNCTION__);
        goto cleanup_MsiCreateRecord;
    }

    /* Format string. */
    TCHAR szStackBuf[MAX_PATH];
    DWORD dwPathSize = _countof(szStackBuf);
    LPTSTR szPath = szStackBuf;
    uiResult = MsiFormatRecord(hInstall, hRecord, szPath, &dwPathSize);
    if (uiResult == ERROR_MORE_DATA)
    {
        /* Allocate buffer on heap (+1 for terminator), and retry. */
        szPath = (LPTSTR)malloc((++dwPathSize) * sizeof(TCHAR));
        if (szPath == NULL)
        {
            msg(M_FATAL, "%s: malloc(%u) failed", __FUNCTION__, dwPathSize * sizeof(TCHAR));
            uiResult = ERROR_OUTOFMEMORY; goto cleanup_MsiCreateRecord;
        }

        uiResult = MsiFormatRecord(hInstall, hRecord, szPath, &dwPathSize);
    }
    if (uiResult != ERROR_SUCCESS)
    {
        SetLastError(uiResult); /* MSDN does not mention MsiFormatRecord() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiFormatRecord failed", __FUNCTION__);
        goto cleanup_malloc_szPath;
    }

    /* Launch the OpenVPN GUI. */
    SHELLEXECUTEINFO sei = {
        .cbSize = sizeof(SHELLEXECUTEINFO),
        .fMask  = SEE_MASK_FLAG_NO_UI, /* Don't show error UI, we'll display it. */
        .lpFile = szPath,
        .nShow  = SW_SHOWNORMAL
    };
    if (!ShellExecuteEx(&sei))
    {
        uiResult = GetLastError();
        msg(M_NONFATAL | M_ERRNO, "%s: ShellExecuteEx(%s) failed", __FUNCTION__, szPath);
        goto cleanup_malloc_szPath;
    }

    uiResult = ERROR_SUCCESS;

cleanup_malloc_szPath:
    if (szPath != szStackBuf)
    {
        free(szPath);
    }
cleanup_MsiCreateRecord:
    MsiCloseHandle(hRecord);
cleanup_CoInitialize:
    if (bIsCoInitialized)
    {
        CoUninitialize();
    }
    return uiResult;
}


UINT __stdcall
EvaluateTAPInterfaces(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    UINT uiResult;
    BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL));

    OPENVPNMSICA_SAVE_MSI_SESSION(hInstall);

    /* List of deferred custom actions EvaluateTAPInterfaces prepares operation sequence for. */
    static const LPCTSTR szActionNames[] =
    {
        TEXT("InstallTAPInterfaces"),
        TEXT("UninstallTAPInterfaces"),
    };
    struct msica_op_seq exec_seq[_countof(szActionNames)];
    for (size_t i = 0; i < _countof(szActionNames); i++)
    {
        msica_op_seq_init(&exec_seq[i]);
    }

    {
        /* Check and store the rollback enabled state. */
        TCHAR szValue[128];
        DWORD dwLength = _countof(szValue);
        bool enable_rollback = MsiGetProperty(hInstall, TEXT("RollbackDisabled"), szValue, &dwLength) == ERROR_SUCCESS ?
                               _ttoi(szValue) || _totlower(szValue[0]) == TEXT('y') ? false : true :
                               true;
        for (size_t i = 0; i < _countof(szActionNames); i++)
        {
            msica_op_seq_add_tail(
                &exec_seq[i],
                msica_op_create_bool(
                    msica_op_rollback_enable,
                    0,
                    NULL,
                    enable_rollback));
        }
    }

    /* Open MSI database. */
    MSIHANDLE hDatabase = MsiGetActiveDatabase(hInstall);
    if (hDatabase == 0)
    {
        msg(M_NONFATAL, "%s: MsiGetActiveDatabase failed", __FUNCTION__);
        uiResult = ERROR_INVALID_HANDLE; goto cleanup_exec_seq;
    }

    /* Check if TAPInterface table exists. If it doesn't exist, there's nothing to do. */
    switch (MsiDatabaseIsTablePersistent(hDatabase, TEXT("TAPInterface")))
    {
        case MSICONDITION_FALSE:
        case MSICONDITION_TRUE: break;

        default:
            uiResult = ERROR_SUCCESS;
            goto cleanup_hDatabase;
    }

    /* Prepare a query to get a list/view of interfaces. */
    MSIHANDLE hViewST = 0;
    LPCTSTR szQuery = TEXT("SELECT `Interface`,`DisplayName`,`Condition`,`Component_` FROM `TAPInterface`");
    uiResult = MsiDatabaseOpenView(hDatabase, szQuery, &hViewST);
    if (uiResult != ERROR_SUCCESS)
    {
        SetLastError(uiResult); /* MSDN does not mention MsiDatabaseOpenView() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiDatabaseOpenView(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szQuery);
        goto cleanup_hDatabase;
    }

    /* Execute query! */
    uiResult = MsiViewExecute(hViewST, 0);
    if (uiResult != ERROR_SUCCESS)
    {
        SetLastError(uiResult); /* MSDN does not mention MsiViewExecute() to set GetLastError(). But we do have an error code. Set last error manually. */
        msg(M_NONFATAL | M_ERRNO, "%s: MsiViewExecute(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szQuery);
        goto cleanup_hViewST;
    }

    /* Create a record to report progress with. */
    MSIHANDLE hRecordProg = MsiCreateRecord(2);
    if (!hRecordProg)
    {
        uiResult = ERROR_INVALID_HANDLE;
        msg(M_NONFATAL, "%s: MsiCreateRecord failed", __FUNCTION__);
        goto cleanup_hViewST_close;
    }

    for (;;)
    {
        /* Fetch one record from the view. */
        MSIHANDLE hRecord = 0;
        uiResult = MsiViewFetch(hViewST, &hRecord);
        if (uiResult == ERROR_NO_MORE_ITEMS)
        {
            uiResult = ERROR_SUCCESS;
            break;
        }
        else if (uiResult != ERROR_SUCCESS)
        {
            SetLastError(uiResult); /* MSDN does not mention MsiViewFetch() to set GetLastError(). But we do have an error code. Set last error manually. */
            msg(M_NONFATAL | M_ERRNO, "%s: MsiViewFetch failed", __FUNCTION__);
            goto cleanup_hRecordProg;
        }

        INSTALLSTATE iInstalled, iAction;
        {
            /* Read interface component ID (`Component_` is field #4). */
            LPTSTR szValue = NULL;
            uiResult = msi_get_record_string(hRecord, 4, &szValue);
            if (uiResult != ERROR_SUCCESS)
            {
                goto cleanup_hRecord;
            }

            /* Get the component state. */
            uiResult = MsiGetComponentState(hInstall, szValue, &iInstalled, &iAction);
            if (uiResult != ERROR_SUCCESS)
            {
                SetLastError(uiResult); /* MSDN does not mention MsiGetComponentState() to set GetLastError(). But we do have an error code. Set last error manually. */
                msg(M_NONFATAL | M_ERRNO, "%s: MsiGetComponentState(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szValue);
                free(szValue);
                goto cleanup_hRecord;
            }
            free(szValue);
        }

        /* Get interface display name (`DisplayName` is field #2). */
        LPTSTR szDisplayName = NULL;
        uiResult = msi_format_field(hInstall, hRecord, 2, &szDisplayName);
        if (uiResult != ERROR_SUCCESS)
        {
            goto cleanup_hRecord;
        }

        if (iAction > INSTALLSTATE_BROKEN)
        {
            if (iAction >= INSTALLSTATE_LOCAL)
            {
                /* Read and evaluate interface condition (`Condition` is field #3). */
                LPTSTR szValue = NULL;
                uiResult = msi_get_record_string(hRecord, 3, &szValue);
                if (uiResult != ERROR_SUCCESS)
                {
                    goto cleanup_szDisplayName;
                }
#ifdef __GNUC__
/*
 * warning: enumeration value ‘MSICONDITION_TRUE’ not handled in switch
 * warning: enumeration value ‘MSICONDITION_NONE’ not handled in switch
 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
#endif
                switch (MsiEvaluateCondition(hInstall, szValue))
                {
                    case MSICONDITION_FALSE:
                        free(szValue);
                        goto cleanup_szDisplayName;

                    case MSICONDITION_ERROR:
                        uiResult = ERROR_INVALID_FIELD;
                        msg(M_NONFATAL | M_ERRNO, "%s: MsiEvaluateCondition(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szValue);
                        free(szValue);
                        goto cleanup_szDisplayName;
                }
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
                free(szValue);

                /* Component is or should be installed. Schedule interface creation. */
                msica_op_seq_add_tail(
                    &exec_seq[0],
                    msica_op_create_string(
                        msica_op_tap_interface_create,
                        MSICA_INTERFACE_TICK_SIZE,
                        NULL,
                        szDisplayName));
            }
            else
            {
                /* Component is installed, but should be degraded to advertised/removed. Schedule interface deletition. */
                msica_op_seq_add_tail(
                    &exec_seq[1],
                    msica_op_create_string(
                        msica_op_tap_interface_delete_by_name,
                        MSICA_INTERFACE_TICK_SIZE,
                        NULL,
                        szDisplayName));
            }

            /* The amount of tick space to add for each interface to progress indicator. */
            MsiRecordSetInteger(hRecordProg, 1, 3 /* OP3 = Add ticks to the expected total number of progress of the progress bar */);
            MsiRecordSetInteger(hRecordProg, 2, MSICA_INTERFACE_TICK_SIZE);
            if (MsiProcessMessage(hInstall, INSTALLMESSAGE_PROGRESS, hRecordProg) == IDCANCEL)
            {
                uiResult = ERROR_INSTALL_USEREXIT;
                goto cleanup_szDisplayName;
            }
        }

cleanup_szDisplayName:
        free(szDisplayName);
cleanup_hRecord:
        MsiCloseHandle(hRecord);
        if (uiResult != ERROR_SUCCESS)
        {
            goto cleanup_hRecordProg;
        }
    }

    /*
     * Write sequence files.
     * The InstallTAPInterfaces and UninstallTAPInterfaces are deferred custom actions, thus all this information
     * will be unavailable to them. Therefore save all required operations and their info to sequence files.
     */
    TCHAR szSeqFilename[_countof(szActionNames)][MAX_PATH + 1];
    for (size_t i = 0; i < _countof(szActionNames); i++)
    {
        szSeqFilename[i][0] = 0;
    }
    for (size_t i = 0; i < _countof(szActionNames); i++)
    {
        uiResult = openvpnmsica_setup_sequence_filename(hInstall, szActionNames[i], szSeqFilename[i]);
        if (uiResult != ERROR_SUCCESS)
        {
            goto cleanup_szSeqFilename;
        }
        HANDLE hSeqFile = CreateFile(
            szSeqFilename[i],
            GENERIC_WRITE,
            FILE_SHARE_READ,
            NULL,
            CREATE_ALWAYS,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
            NULL);
        if (hSeqFile == INVALID_HANDLE_VALUE)
        {
            uiResult = GetLastError();
            msg(M_NONFATAL | M_ERRNO, "%s: CreateFile(\"%.*" PRIsLPTSTR "\") failed", __FUNCTION__, _countof(szSeqFilename[i]), szSeqFilename[i]);
            goto cleanup_szSeqFilename;
        }
        uiResult = msica_op_seq_save(&exec_seq[i], hSeqFile);
        CloseHandle(hSeqFile);
        if (uiResult != ERROR_SUCCESS)
        {
            goto cleanup_szSeqFilename;
        }
    }

    uiResult = ERROR_SUCCESS;

cleanup_szSeqFilename:
    if (uiResult != ERROR_SUCCESS)
    {
        /* Clean-up sequence files. */
        for (size_t i = _countof(szActionNames); i--;)
        {
            if (szSeqFilename[i][0])
            {
                DeleteFile(szSeqFilename[i]);
            }
        }
    }
cleanup_hRecordProg:
    MsiCloseHandle(hRecordProg);
cleanup_hViewST_close:
    MsiViewClose(hViewST);
cleanup_hViewST:
    MsiCloseHandle(hViewST);
cleanup_hDatabase:
    MsiCloseHandle(hDatabase);
cleanup_exec_seq:
    for (size_t i = 0; i < _countof(szActionNames); i++)
    {
        msica_op_seq_free(&exec_seq[i]);
    }
    if (bIsCoInitialized)
    {
        CoUninitialize();
    }
    return uiResult;
}


UINT __stdcall
ProcessDeferredAction(_In_ MSIHANDLE hInstall)
{
#ifdef _MSC_VER
#pragma comment(linker, DLLEXP_EXPORT)
#endif

    openvpnmsica_debug_popup(TEXT(__FUNCTION__));

    UINT uiResult;
    BOOL bIsCoInitialized = SUCCEEDED(CoInitialize(NULL));

    OPENVPNMSICA_SAVE_MSI_SESSION(hInstall);

    BOOL bIsCleanup = MsiGetMode(hInstall, MSIRUNMODE_COMMIT) || MsiGetMode(hInstall, MSIRUNMODE_ROLLBACK);

    /* Get sequence filename and open the file. */
    LPTSTR szSeqFilename = NULL;
    uiResult = msi_get_string(hInstall, TEXT("CustomActionData"), &szSeqFilename);
    if (uiResult != ERROR_SUCCESS)
    {
        goto cleanup_CoInitialize;
    }
    struct msica_op_seq seq = { .head = NULL, .tail = NULL };
    {
        HANDLE hSeqFile = CreateFile(
            szSeqFilename,
            GENERIC_READ,
            FILE_SHARE_READ,
            NULL,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
            NULL);
        if (hSeqFile == INVALID_HANDLE_VALUE)
        {
            uiResult = GetLastError();
            if (uiResult == ERROR_FILE_NOT_FOUND && bIsCleanup)
            {
                /*
                 * Sequence file not found and this is rollback/commit action. Either of the following scenarios are possible:
                 * - The delayed action failed to save the rollback/commit sequence to file. The delayed action performed cleanup itself. No further operation is required.
                 * - Somebody removed the rollback/commit file between delayed action and rollback/commit action. No further operation is possible.
                 */
                uiResult = ERROR_SUCCESS;
                goto cleanup_szSeqFilename;
            }
            msg(M_NONFATAL | M_ERRNO, "%s: CreateFile(\"%" PRIsLPTSTR "\") failed", __FUNCTION__, szSeqFilename);
            goto cleanup_szSeqFilename;
        }

        /* Load sequence. */
        uiResult = msica_op_seq_load(&seq, hSeqFile);
        CloseHandle(hSeqFile);
        if (uiResult != ERROR_SUCCESS)
        {
            goto cleanup_seq;
        }
    }

    /* Prepare session context. */
    struct msica_session session;
    openvpnmsica_session_init(
        &session,
        hInstall,
        bIsCleanup, /* In case of commit/rollback, continue sequence on error, to do as much cleanup as possible. */
        false);

    /* Execute sequence. */
    uiResult = msica_op_seq_process(&seq, &session);
    if (!bIsCleanup)
    {
        /*
         * Save cleanup scripts of delayed action regardless of action's execution status.
         * Rollback action MUST be scheduled in InstallExecuteSequence before this action! Otherwise cleanup won't be performed in case this action execution failed.
         */
        DWORD dwResultEx; /* Don't overwrite uiResult. */
        LPCTSTR szExtension = PathFindExtension(szSeqFilename);
        TCHAR szFilenameEx[MAX_PATH + 1 /*dash*/ + 2 /*suffix*/ + 1 /*terminator*/];
        for (size_t i = 0; i < MSICA_CLEANUP_ACTION_COUNT; i++)
        {
            _stprintf_s(
                szFilenameEx, _countof(szFilenameEx),
                TEXT("%.*s-%.2s%s"),
                (int)(szExtension - szSeqFilename), szSeqFilename,
                openvpnmsica_cleanup_action_seqs[i].szSuffix,
                szExtension);

            /* After commit, delete rollback file. After rollback, delete commit file. */
            msica_op_seq_add_tail(
                &session.seq_cleanup[MSICA_CLEANUP_ACTION_COUNT - 1 - i],
                msica_op_create_string(
                    msica_op_file_delete,
                    0,
                    NULL,
                    szFilenameEx));
        }
        for (size_t i = 0; i < MSICA_CLEANUP_ACTION_COUNT; i++)
        {
            _stprintf_s(
                szFilenameEx, _countof(szFilenameEx),
                TEXT("%.*s-%.2s%s"),
                (int)(szExtension - szSeqFilename), szSeqFilename,
                openvpnmsica_cleanup_action_seqs[i].szSuffix,
                szExtension);

            /* Save the cleanup sequence file. */
            HANDLE hSeqFile = CreateFile(
                szFilenameEx,
                GENERIC_WRITE,
                FILE_SHARE_READ,
                NULL,
                CREATE_ALWAYS,
                FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
                NULL);
            if (hSeqFile == INVALID_HANDLE_VALUE)
            {
                dwResultEx = GetLastError();
                msg(M_NONFATAL | M_ERRNO, "%s: CreateFile(\"%.*" PRIsLPTSTR "\") failed", __FUNCTION__, _countof(szFilenameEx), szFilenameEx);
                goto cleanup_session;
            }
            dwResultEx = msica_op_seq_save(&session.seq_cleanup[i], hSeqFile);
            CloseHandle(hSeqFile);
            if (dwResultEx != ERROR_SUCCESS)
            {
                goto cleanup_session;
            }
        }

cleanup_session:
        if (dwResultEx != ERROR_SUCCESS)
        {
            /* The commit and/or rollback scripts were not written to file successfully. Perform the cleanup immediately. */
            struct msica_session session_cleanup;
            openvpnmsica_session_init(
                &session_cleanup,
                hInstall,
                true,
                false);
            msica_op_seq_process(&session.seq_cleanup[MSICA_CLEANUP_ACTION_ROLLBACK], &session_cleanup);

            szExtension = PathFindExtension(szSeqFilename);
            for (size_t i = 0; i < MSICA_CLEANUP_ACTION_COUNT; i++)
            {
                _stprintf_s(
                    szFilenameEx, _countof(szFilenameEx),
                    TEXT("%.*s-%.2s%s"),
                    (int)(szExtension - szSeqFilename), szSeqFilename,
                    openvpnmsica_cleanup_action_seqs[i].szSuffix,
                    szExtension);
                DeleteFile(szFilenameEx);
            }
        }
    }
    else
    {
        /* No cleanup after cleanup support. */
        uiResult = ERROR_SUCCESS;
    }

    for (size_t i = MSICA_CLEANUP_ACTION_COUNT; i--;)
    {
        msica_op_seq_free(&session.seq_cleanup[i]);
    }
    DeleteFile(szSeqFilename);
cleanup_seq:
    msica_op_seq_free(&seq);
cleanup_szSeqFilename:
    free(szSeqFilename);
cleanup_CoInitialize:
    if (bIsCoInitialized)
    {
        CoUninitialize();
    }
    return uiResult;
}