diff --git a/msipackage/package.wix.in b/msipackage/package.wix.in
index 7375cc096d..fc966e0f8e 100644
--- a/msipackage/package.wix.in
+++ b/msipackage/package.wix.in
@@ -1,6 +1,6 @@
-
+
@@ -707,6 +707,17 @@
+
+
+
+
+
@@ -761,6 +772,9 @@
+
+
+
@@ -782,6 +796,11 @@
See: https://learn.microsoft.com/en-us/windows/win32/msi/msirmshutdown
-->
+
+
+
+
+
diff --git a/src/windows/wslinstall/DllMain.cpp b/src/windows/wslinstall/DllMain.cpp
index cc42baf018..b2e62dc70a 100644
--- a/src/windows/wslinstall/DllMain.cpp
+++ b/src/windows/wslinstall/DllMain.cpp
@@ -984,6 +984,19 @@ extern "C" UINT __stdcall EnableWslService(MSIHANDLE install)
return NOERROR;
}
+#ifndef WSL_OFFICIAL_BUILD
+extern "C" __declspec(dllexport) UINT __stdcall WslTestForceInstallFailure(MSIHANDLE install)
+{
+ try
+ {
+ WSL_INSTALL_LOG("WslTestForceInstallFailure", TraceLoggingValue("Forcing install failure for rollback testing", "Reason"));
+ }
+ CATCH_LOG();
+
+ return ERROR_INSTALL_FAILURE;
+}
+#endif
+
EXTERN_C BOOL STDAPICALLTYPE DllMain(_In_ HINSTANCE Instance, _In_ DWORD Reason, _In_opt_ LPVOID Reserved)
{
wil::DLLMain(Instance, Reason, Reserved);
diff --git a/src/windows/wslinstall/wslinstall.def b/src/windows/wslinstall/wslinstall.def
index 13d1b0456a..6b77173b3a 100644
--- a/src/windows/wslinstall/wslinstall.def
+++ b/src/windows/wslinstall/wslinstall.def
@@ -15,4 +15,4 @@ EXPORTS
UnregisterLspCategories
CalculateWslSettingsProtocolIds
DisableWslService
- EnableWslService
\ No newline at end of file
+ EnableWslService
diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp
index 91d9c699c1..31677e7acb 100644
--- a/test/windows/InstallerTests.cpp
+++ b/test/windows/InstallerTests.cpp
@@ -14,6 +14,7 @@ Module Name:
#include "precomp.h"
#include
+#include
#include "Common.h"
#include "registry.hpp"
@@ -21,6 +22,7 @@ Module Name:
#include "wslcsdk.h"
using namespace wsl::windows::common::registry;
+using unique_msi_handle = wil::unique_any;
extern std::wstring g_dumpFolder;
static std::wstring g_pipelineBuildId;
@@ -145,7 +147,7 @@ class InstallerTests
return m_packageManager.FindPackagesForUser(L"", wsl::windows::common::wslutil::c_msixPackageFamilyName).First().HasCurrent();
}
- static void CallMsiExec(const std::wstring& Args)
+ static DWORD RunMsiExec(const std::wstring& Args)
{
std::wstring commandLine;
THROW_IF_FAILED(wil::GetSystemDirectoryW(commandLine));
@@ -168,7 +170,12 @@ class InstallerTests
std::chrono::minutes(2),
[]() { return wil::ResultFromCaughtException() == E_ABORT; });
- VERIFY_ARE_EQUAL(0L, exitCode);
+ return exitCode;
+ }
+
+ static void CallMsiExec(const std::wstring& Args)
+ {
+ VERIFY_ARE_EQUAL(0L, RunMsiExec(Args));
}
std::wstring GetMsiProductCode() const
@@ -345,6 +352,28 @@ class InstallerTests
wsl::windows::common::registry::DeleteKeyValue(msiKey.get(), L"ProductCode");
}
+ // Queries the MSI database for the sequence number of the given action in InstallExecuteSequence.
+ // Returns -1 if the action is not found.
+ int GetMsiSequenceNumber(MSIHANDLE database, LPCWSTR action)
+ {
+ unique_msi_handle view;
+ THROW_IF_WIN32_ERROR(MsiDatabaseOpenViewW(database, L"SELECT `Sequence` FROM `InstallExecuteSequence` WHERE `Action` = ?", &view));
+ unique_msi_handle rec{MsiCreateRecord(1)};
+ THROW_IF_WIN32_ERROR(MsiRecordSetStringW(rec.get(), 1, action));
+ THROW_IF_WIN32_ERROR(MsiViewExecute(view.get(), rec.get()));
+
+ MSIHANDLE hResult = 0;
+ auto fetchResult = MsiViewFetch(view.get(), &hResult);
+ if (fetchResult == ERROR_NO_MORE_ITEMS)
+ {
+ return -1;
+ }
+
+ THROW_IF_WIN32_ERROR(fetchResult);
+ unique_msi_handle result{hResult};
+ return MsiRecordGetInteger(result.get(), 1);
+ }
+
void InstallGitHubRelease(const std::wstring& version)
{
auto arch = wsl::shared::Arm64 ? L".0.arm64" : L".0.x64";
@@ -694,6 +723,76 @@ class InstallerTests
output);
}
+ TEST_METHOD(MsiRemoveExistingProductsScheduledInsideTransaction)
+ {
+ // Verify that RemoveExistingProducts is scheduled between InstallInitialize
+ // and InstallFinalize in the MSI sequence table. This is the effect of
+ // Schedule="afterInstallInitialize" on in package.wix.in.
+
+ unique_msi_handle database;
+ THROW_IF_WIN32_ERROR(MsiOpenDatabaseW(m_msiPath.c_str(), MSIDBOPEN_READONLY, &database));
+
+ auto installInitialize = GetMsiSequenceNumber(database.get(), L"InstallInitialize");
+ auto removeExistingProducts = GetMsiSequenceNumber(database.get(), L"RemoveExistingProducts");
+ auto installFinalize = GetMsiSequenceNumber(database.get(), L"InstallFinalize");
+
+ LogInfo("MSI sequence: InstallInitialize=%d, RemoveExistingProducts=%d, InstallFinalize=%d", installInitialize, removeExistingProducts, installFinalize);
+
+ VERIFY_ARE_NOT_EQUAL(-1, installInitialize);
+ VERIFY_ARE_NOT_EQUAL(-1, removeExistingProducts);
+ VERIFY_ARE_NOT_EQUAL(-1, installFinalize);
+
+ VERIFY_IS_GREATER_THAN(
+ removeExistingProducts, installInitialize, L"RemoveExistingProducts must be after InstallInitialize");
+
+ VERIFY_IS_LESS_THAN(removeExistingProducts, installFinalize, L"RemoveExistingProducts must precede InstallFinalize");
+ }
+
+ TEST_METHOD(MsiUpgradeFailureRestoresFiles)
+ {
+#ifdef WSL_OFFICIAL_BUILD
+ Log::Comment(L"TestSkipped: This test case requires the test-only WslTestForceInstallFailure CA");
+ return;
+#else
+ // End-to-end rollback test: install an older version, then attempt a major
+ // upgrade that fails inside the transaction. Verify the old version's files
+ // are restored by MSI rollback.
+
+ UninstallMsi();
+ InstallGitHubRelease(L"2.0.2");
+
+ auto restore = wil::scope_exit([this]() { InstallMsi(); });
+
+ VERIFY_IS_TRUE(IsMsiPackageInstalled());
+
+ const auto wslExe = m_installedPath / L"wsl.exe";
+ const auto wslService = m_installedPath / L"wslservice.exe";
+ const auto systemVhd = m_installedPath / L"system.vhd";
+
+ VERIFY_IS_TRUE(std::filesystem::exists(wslExe), L"wsl.exe must exist before upgrade");
+ VERIFY_IS_TRUE(std::filesystem::exists(wslService), L"wslservice.exe must exist before upgrade");
+ VERIFY_IS_TRUE(std::filesystem::exists(systemVhd), L"system.vhd must exist before upgrade");
+
+ // Attempt a major upgrade forced to fail after RemoveExistingProducts.
+ PrepareForMsiOperation();
+ auto msiArgs = std::format(
+ L"/qn /norestart /i \"{}\" WSL_TEST_FORCE_INSTALL_FAILURE=1 SKIPVALIDATION=1 "
+ L"/L*V \"{}\"",
+ m_msiPath,
+ GenerateMsiLogPath());
+ auto exitCode = RunMsiExec(msiArgs);
+
+ VERIFY_ARE_NOT_EQUAL(0L, exitCode, L"Upgrade should have failed due to forced failure CA");
+
+ // Verify rollback restored the previous installation
+ VERIFY_IS_TRUE(IsMsiPackageInstalled(), L"MSI package must still be installed after rollback");
+ VERIFY_IS_TRUE(std::filesystem::exists(wslExe), L"wsl.exe must be restored after rollback");
+ VERIFY_IS_TRUE(std::filesystem::exists(wslService), L"wslservice.exe must be restored after rollback");
+ VERIFY_IS_TRUE(std::filesystem::exists(systemVhd), L"system.vhd must be restored after rollback");
+ ValidateInstalledVersion(L"2.0.2");
+#endif
+ }
+
TEST_METHOD(WslUpdateNoNewVersion)
{
constexpr auto endpoint = L"http://127.0.0.1:12345/";