master
cpp 1,232 lines 46.6 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 InstallerTests.cpp
8
9 Abstract:
10
11 This file contains test cases for the installation process.
12
13 --*/
14
15 #include "precomp.h"
16 #include <Sfc.h>
17 #include <msiquery.h>
18
19 #include "Common.h"
20 #include "registry.hpp"
21 #include "PluginTests.h"
22 #include "wslcsdk.h"
23
24 using namespace wsl::windows::common::registry;
25 using unique_msi_handle = wil::unique_any<MSIHANDLE, decltype(MsiCloseHandle), &MsiCloseHandle>;
26
27 extern std::wstring g_dumpFolder;
28 extern std::wstring g_pipelineBuildId;
29
30 class InstallerTests
31 {
32 std::wstring m_msixPackagePath;
33 std::wstring m_msiPath;
34 std::wstring m_msixInstalledPath;
35 std::filesystem::path m_installedPath;
36 bool m_initialized = false;
37 winrt::Windows::Management::Deployment::PackageManager m_packageManager;
38 wil::unique_hkey m_lxssKey = wsl::windows::common::registry::OpenLxssMachineKey(KEY_ALL_ACCESS);
39 wil::unique_schandle m_scm{OpenSCManagerW(nullptr, SERVICES_ACTIVE_DATABASE, GENERIC_READ | GENERIC_EXECUTE)};
40
41 wil::unique_hfile nulDevice{CreateFileW(
42 L"nul", GENERIC_READ, (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE), nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
43
44 WSL_TEST_CLASS(InstallerTests)
45
46 TEST_CLASS_SETUP(TestClassSetup)
47 {
48 VERIFY_ARE_EQUAL(LxsstuInitialize(FALSE), TRUE);
49 m_initialized = true;
50
51 WEX::Common::String MsixPackagePath;
52 WEX::TestExecution::RuntimeParameters::TryGetValue(L"Package", MsixPackagePath);
53 m_msixPackagePath = wsl::windows::common::filesystem::GetCanonicalPath(static_cast<std::wstring>(MsixPackagePath)).wstring();
54 VERIFY_IS_FALSE(m_msixPackagePath.empty());
55
56 for (const auto& e : m_packageManager.FindPackages(wsl::windows::common::wslutil::c_msixPackageFamilyName))
57 {
58 VERIFY_IS_TRUE(m_msixInstalledPath.empty());
59 m_msixInstalledPath = std::wstring(e.InstalledLocation().Path().c_str());
60 }
61
62 #ifdef WSL_DEV_THIN_MSI_PACKAGE
63
64 m_msiPath = wsl::windows::common::filesystem::GetCanonicalPath(WSL_DEV_THIN_MSI_PACKAGE).wstring();
65
66 #else
67
68 VERIFY_IS_TRUE(IsInstallerMsixInstalled(), L"Installer MSIX absent, can't run the tests");
69 m_msiPath = (std::filesystem::temp_directory_path() / L"wsl.msi").wstring();
70 VERIFY_IS_TRUE(std::filesystem::copy_file(m_msixInstalledPath + L"\\wsl.msi", m_msiPath, std::filesystem::copy_options::overwrite_existing));
71 #endif
72
73 auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
74 VERIFY_IS_TRUE(installPath.has_value());
75
76 m_installedPath = std::move(installPath.value());
77
78 wsl::windows::common::helpers::SetHandleInheritable(nulDevice.get());
79
80 return true;
81 }
82
83 TEST_CLASS_CLEANUP(TestClassCleanup)
84 {
85
86 #ifndef WSL_DEV_THIN_MSI_PACKAGE
87
88 if (!m_msiPath.empty())
89 {
90 std::filesystem::remove(m_msiPath);
91 }
92
93 #endif
94
95 if (m_initialized)
96 {
97 LxsstuUninitialize(FALSE);
98 }
99
100 return true;
101 }
102
103 DWORD GetWslInstallerServiceState() const
104 {
105 const wil::unique_schandle service{OpenServiceW(m_scm.get(), L"Wslinstaller", SERVICE_QUERY_STATUS)};
106 VERIFY_IS_NOT_NULL(service);
107
108 SERVICE_STATUS status{};
109 VERIFY_IS_TRUE(QueryServiceStatus(service.get(), &status));
110
111 return status.dwCurrentState;
112 }
113
114 void WaitForInstallerServiceStop()
115 {
116 DWORD lastState = -1;
117 auto pred = [&]() {
118 lastState = GetWslInstallerServiceState();
119 THROW_HR_IF(E_FAIL, lastState != SERVICE_STOPPED);
120 };
121
122 try
123 {
124 wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::hours(3), std::chrono::minutes(2));
125 }
126 catch (...)
127 {
128 LogError("InstallerService state: %lu", lastState);
129 VERIFY_FAIL("Timed out waiting for the installer service to stop");
130 }
131 }
132
133 static std::wstring GenerateMsiLogPath()
134 {
135 // Note: canonical is required because msiexec seems to be unable to deal with symlinks.
136 static int counter = 0;
137 return std::format(L"{}\\msi-install-{}.txt", std::filesystem::canonical(g_dumpFolder).c_str(), counter++);
138 }
139
140 bool IsInstallerMsixInstalled() const
141 {
142 return std::filesystem::exists(m_msixInstalledPath + L"\\wslinstaller.exe");
143 }
144
145 bool IsMsixInstalled() const
146 {
147 return m_packageManager.FindPackagesForUser(L"", wsl::windows::common::wslutil::c_msixPackageFamilyName).First().HasCurrent();
148 }
149
150 static DWORD RunMsiExec(const std::wstring& Args)
151 {
152 std::wstring commandLine;
153 THROW_IF_FAILED(wil::GetSystemDirectoryW(commandLine));
154 commandLine += L"\\msiexec.exe " + Args;
155
156 LogInfo("Calling msiexec: %ls", commandLine.c_str());
157
158 // There is a potential race condition given that we have no easy way to know when the msiexec process
159 // created by the installer service will release the MSI mutex.
160 // If the mutex is still held when we call msiexec, it will fails with ERROR_INSTALL_ALREADY_RUNNING
161 // so retry for up to two minutes if we get that error back.
162
163 DWORD exitCode = -1;
164 wsl::shared::retry::RetryWithTimeout<void>(
165 [&]() {
166 exitCode = LxsstuRunCommand(commandLine.data());
167 THROW_HR_IF(E_ABORT, exitCode == ERROR_INSTALL_ALREADY_RUNNING);
168 },
169 std::chrono::seconds(1),
170 std::chrono::minutes(2),
171 []() { return wil::ResultFromCaughtException() == E_ABORT; });
172
173 return exitCode;
174 }
175
176 static void CallMsiExec(const std::wstring& Args)
177 {
178 auto exitCode = RunMsiExec(Args);
179 if (exitCode != ERROR_SUCCESS && exitCode != ERROR_SUCCESS_REBOOT_REQUIRED)
180 {
181 LogError("msiexec failed with exit code %lu", exitCode);
182 VERIFY_FAIL();
183 }
184 }
185
186 std::wstring GetMsiProductCode() const
187 {
188 return wsl::windows::common::registry::ReadString(m_lxssKey.get(), L"MSI", L"ProductCode", L"");
189 }
190
191 // Release any in-process COM DLLs (e.g. wslserviceproxystub.dll loaded by prior tests)
192 // so the Restart Manager doesn't detect the test process as holding files.
193 // This avoids install failures on older Server SKUs where the RM has stricter silent-mode behavior.
194 static void PrepareForMsiOperation()
195 {
196 CoFreeUnusedLibrariesEx(0, 0);
197 }
198
199 void UninstallMsi()
200 {
201 PrepareForMsiOperation();
202 auto productCode = GetMsiProductCode();
203 VERIFY_IS_FALSE(productCode.empty());
204
205 CallMsiExec(std::format(L"/qn /norestart /x \"{}\" /L*V \"{}\"", productCode, GenerateMsiLogPath()));
206 }
207
208 void InstallMsi()
209 {
210 PrepareForMsiOperation();
211 CallMsiExec(std::format(L"/qn /norestart /i \"{}\" /L*V \"{}\"", m_msiPath, GenerateMsiLogPath()));
212 }
213
214 void InstallMsix() const
215 {
216 const auto result =
217 m_packageManager
218 .AddPackageAsync(winrt::Windows::Foundation::Uri{m_msixPackagePath}, nullptr, winrt::Windows::Management::Deployment::DeploymentOptions::None)
219 .get();
220
221 VERIFY_ARE_EQUAL(result.ExtendedErrorCode(), S_OK);
222 }
223
224 void UninstallMsix() const
225 {
226 auto result = m_packageManager.DeprovisionPackageForAllUsersAsync(wsl::windows::common::wslutil::c_msixPackageFamilyName).get();
227 auto errorCode = result.ExtendedErrorCode();
228 if (FAILED(errorCode) && errorCode != HRESULT_FROM_WIN32(ERROR_NOT_FOUND))
229 {
230 LogError("Error deprovisioning the package: 0x%x, %ls", errorCode, result.ErrorText().c_str());
231 VERIFY_FAIL();
232 }
233
234 for (const auto& e : m_packageManager.FindPackages(wsl::windows::common::wslutil::c_msixPackageFamilyName))
235 {
236 // Remove the package for the current user first.
237 result = m_packageManager.RemovePackageAsync(e.Id().FullName()).get();
238 errorCode = result.ExtendedErrorCode();
239 if (FAILED(errorCode) && errorCode != HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
240 {
241 LogError("Error deprovisioning the package: 0x%x, %ls", errorCode, result.ErrorText().c_str());
242 VERIFY_FAIL();
243 }
244
245 // then remove the package for all users.
246 result = m_packageManager
247 .RemovePackageAsync(e.Id().FullName(), winrt::Windows::Management::Deployment::RemovalOptions::RemoveForAllUsers)
248 .get();
249 errorCode = result.ExtendedErrorCode();
250 if (FAILED(errorCode) && errorCode != HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND))
251 {
252 LogError("Error deprovisioning the package: 0x%x, %ls", errorCode, result.ErrorText().c_str());
253 VERIFY_FAIL();
254 }
255 }
256 }
257
258 bool IsMsiPackageInstalled()
259 {
260 // Check for wslservice to be installed because MSI installs registry keys before installing services
261 return !GetMsiProductCode().empty() && wsl::windows::common::helpers::IsServicePresent(L"wslservice");
262 }
263
264 static bool IsMsixInstallerInstalled()
265 {
266 return wsl::windows::common::helpers::IsServicePresent(L"wslinstaller");
267 }
268
269 void WaitForMsiPackageInstall()
270 {
271 auto pred = [this]() { THROW_HR_IF(E_FAIL, !IsMsiPackageInstalled()); };
272
273 try
274 {
275 // It is possible for the 'DeprovisionMsix' stage of the MSI installation to take a long time.
276 // On vb_release, up to 7 minutes have been observed. Wait for up to 20 minutes to be safe.
277 wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(20));
278 }
279 catch (...)
280 {
281 VERIFY_FAIL("Timed out waiting for MSI package installation");
282 }
283 }
284
285 static void ValidateInstalledVersion(LPCWSTR expectedVersion)
286 {
287 auto pred = [expectedVersion]() {
288 // Validate that we're not using inbox WSL
289 auto [output, _] = LxsstuLaunchWslAndCaptureOutput(L"--version");
290 if (output.find(expectedVersion) == std::string::npos)
291 {
292 LogInfo("Package is not installed yet. Output: %ls", output.c_str());
293 THROW_HR(E_FAIL);
294 }
295
296 LogInfo("Package is properly installed. Output: %ls", output.c_str());
297 };
298
299 // Sadly the provisioning of MSIX packages for user accounts isn't synchronous so we need to wait until the package
300 // becomes visible.
301 try
302 {
303 wsl::shared::retry::RetryWithTimeout<void>(pred, std::chrono::seconds(1), std::chrono::minutes(2));
304 }
305 catch (...)
306 {
307 VERIFY_FAIL("Timed out waiting for MSIX package to be available");
308 }
309 }
310
311 void ValidatePackageInstalledProperly(LPCWSTR expectedVersion = WIDEN(WSL_PACKAGE_VERSION))
312 {
313 ValidateInstalledVersion(expectedVersion);
314
315 auto checkInstalled = []() {
316 auto commandLine = LxssGenerateWslCommandLine(L"echo OK");
317 auto [output, _, exitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(commandLine.data());
318 if (exitCode != 0 && output.find(L"Wsl/ERROR_SHARING_VIOLATION") != std::wstring::npos)
319 {
320 THROW_HR(HRESULT_FROM_WIN32(ERROR_RETRY));
321 }
322
323 return std::make_pair(exitCode, output);
324 };
325
326 // When upgrading from MSIX, wsl.exe might not be available right away. Retry if we get Wsl/ERROR_SHARING_VIOLATION back.
327 std::wstring output;
328 int exitCode = -1;
329 try
330 {
331 std::tie(exitCode, output) = wsl::shared::retry::RetryWithTimeout<std::pair<int, std::wstring>>(
332 checkInstalled, std::chrono::seconds(1), std::chrono::minutes(2), []() {
333 return wil::ResultFromCaughtException() == HRESULT_FROM_WIN32(ERROR_RETRY);
334 });
335 }
336 catch (...)
337 {
338 VERIFY_FAIL("Timed out waiting for WSL to be installed.");
339 }
340
341 VERIFY_ARE_EQUAL(exitCode, 0);
342 VERIFY_ARE_EQUAL(output, L"OK\n");
343
344 // Check that the installed version is the one we expect
345 const auto key = wsl::windows::common::registry::OpenLxssMachineKey();
346 const auto version = wsl::windows::common::registry::ReadString(key.get(), L"MSI", L"Version", L"");
347
348 VERIFY_ARE_EQUAL(version, expectedVersion);
349 VERIFY_IS_FALSE(GetMsiProductCode().empty());
350
351 // TODO: check wslsupport, wslapi and p9rdr
352 }
353
354 void DeleteProductCode() const
355 {
356 const auto msiKey = wsl::windows::common::registry::OpenKey(m_lxssKey.get(), L"MSI", KEY_ALL_ACCESS);
357 wsl::windows::common::registry::DeleteKeyValue(msiKey.get(), L"ProductCode");
358 }
359
360 // Queries the MSI database for the sequence number of the given action in InstallExecuteSequence.
361 // Returns -1 if the action is not found.
362 int GetMsiSequenceNumber(MSIHANDLE database, LPCWSTR action)
363 {
364 unique_msi_handle view;
365 THROW_IF_WIN32_ERROR(MsiDatabaseOpenViewW(database, L"SELECT `Sequence` FROM `InstallExecuteSequence` WHERE `Action` = ?", &view));
366 unique_msi_handle rec{MsiCreateRecord(1)};
367 THROW_IF_WIN32_ERROR(MsiRecordSetStringW(rec.get(), 1, action));
368 THROW_IF_WIN32_ERROR(MsiViewExecute(view.get(), rec.get()));
369
370 MSIHANDLE hResult = 0;
371 auto fetchResult = MsiViewFetch(view.get(), &hResult);
372 if (fetchResult == ERROR_NO_MORE_ITEMS)
373 {
374 return -1;
375 }
376
377 THROW_IF_WIN32_ERROR(fetchResult);
378 unique_msi_handle result{hResult};
379 return MsiRecordGetInteger(result.get(), 1);
380 }
381
382 void InstallGitHubRelease(const std::wstring& version)
383 {
384 auto arch = wsl::shared::Arm64 ? L".0.arm64" : L".0.x64";
385
386 std::wstring installerFile;
387 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&installerFile]() { DeleteFile(installerFile.c_str()); });
388
389 if (auto found = L"wsl." + version + arch + L".msi"; PathFileExists(found.c_str()))
390 {
391 installerFile = wsl::windows::common::filesystem::GetCanonicalPath(found);
392 cleanup.release();
393 }
394 else if (auto found = L"Microsoft.WSL_" + version + L".0_x64_ARM64.msixbundle"; PathFileExists(found.c_str()))
395 {
396 installerFile = wsl::windows::common::filesystem::GetCanonicalPath(found);
397 cleanup.release();
398 }
399 else
400 {
401 LogInfo("Downloading: %ls", version.c_str());
402
403 VERIFY_IS_TRUE(g_pipelineBuildId.empty()); // Pipeline builds should have the installers already available
404 auto release = wsl::windows::common::wslutil::GetGitHubReleaseByTag(version);
405 auto asset = wsl::windows::common::wslutil::GetGitHubAssetFromRelease(release);
406 VERIFY_IS_TRUE(asset.has_value());
407
408 auto downloadPath = wsl::windows::common::wslutil::DownloadFile(asset->second.url, asset->second.name);
409 installerFile = std::move(downloadPath);
410 }
411
412 LogInfo("Installing: %ls", installerFile.c_str());
413 if (wsl::shared::string::EndsWith<wchar_t>(installerFile, L".msi"))
414 {
415 PrepareForMsiOperation();
416 CallMsiExec(std::format(L"/qn /norestart /i \"{}\" /L*V \"{}\"", installerFile, GenerateMsiLogPath()));
417 }
418 else
419 {
420 auto result =
421 m_packageManager
422 .AddPackageAsync(winrt::Windows::Foundation::Uri{installerFile}, nullptr, winrt::Windows::Management::Deployment::DeploymentOptions::None)
423 .get();
424
425 VERIFY_SUCCEEDED(result.ExtendedErrorCode());
426 }
427
428 ValidateInstalledVersion(version.c_str());
429 }
430
431 TEST_METHOD(UpgradeFromWsl130)
432 {
433 UninstallMsi();
434 InstallGitHubRelease(L"1.3.17");
435
436 // Note: we can't use wsl --update here because GitHubUrlOverride was introduced in 2.0.0
437 InstallMsi();
438 ValidatePackageInstalledProperly();
439 }
440
441 TEST_METHOD(UpgradeFromWsl200)
442 {
443 UninstallMsi();
444
445 // Note: we can't use wsl --update here because wsl 2.0.0 passes REINSTALL=ALL to msiexec
446 InstallGitHubRelease(L"2.0.0");
447 InstallMsi();
448 ValidatePackageInstalledProperly();
449 }
450
451 TEST_METHOD(UpgradeFromWsl202)
452 {
453 UninstallMsi();
454 InstallGitHubRelease(L"2.0.2");
455 CallWslUpdateViaMsi();
456 }
457
458 WSLC_TEST_METHOD(WslcSdkVersionDetection)
459 {
460 auto restore = wil::scope_exit([this]() { InstallMsi(); });
461
462 UninstallMsi();
463
464 // Validate that the SDK detects that the WSL package is not installed.
465 WslcComponentFlags flags{};
466 VERIFY_SUCCEEDED(WslcGetMissingComponents(&flags));
467 VERIFY_ARE_EQUAL(flags, WSLC_COMPONENT_FLAG_WSL_PACKAGE);
468
469 // Validate that the SDK detects that the installed version of WSL is too old.
470 InstallGitHubRelease(L"2.0.2");
471
472 VERIFY_SUCCEEDED(WslcGetMissingComponents(&flags));
473 VERIFY_ARE_EQUAL(flags, WSLC_COMPONENT_FLAG_WSL_PACKAGE);
474
475 restore.reset();
476
477 // Validate that the SDK supports the current package.
478 VERIFY_SUCCEEDED(WslcGetMissingComponents(&flags));
479 VERIFY_ARE_EQUAL(flags, 0);
480
481 // TODO: Add test coverage for a more recent version of the package that doesn't support the SDK, if ever needed.
482 // In the meantime, the below block can be commented to manual test this scenario with a manual code change.
483 /*VERIFY_SUCCEEDED(WslcGetMissingComponents(&flags));
484 VERIFY_ARE_EQUAL(flags, WSLC_COMPONENT_FLAG_SDK_NEEDS_UPDATE);
485
486 WslcSessionSettings sessionSettings{};
487 VERIFY_SUCCEEDED(WslcInitSessionSettings(L"should-fail", L"C:\\", &sessionSettings));
488
489 WslcSession session{};
490 VERIFY_ARE_EQUAL(WslcCreateSession(&sessionSettings, &session, nullptr), WSLC_E_SDK_UPDATE_NEEDED);*/
491 }
492
493 TEST_METHOD(MsrdcPluginKey)
494 {
495 // Remove the MSI package.
496 UninstallMsi();
497
498 // Create a volatile registry key to emulate what the old MSIX would do.
499 const auto key = wsl::windows::common::registry::CreateKey(
500 HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Terminal Server Client\\Default", KEY_ALL_ACCESS);
501 VERIFY_IS_TRUE(!!key);
502
503 wsl::windows::common::registry::DeleteKey(key.get(), L"OptionalAddIns\\WSLDVC_PACKAGE");
504
505 const auto volatileKey =
506 wsl::windows::common::registry::CreateKey(key.get(), L"OptionalAddIns\\WSLDVC_PACKAGE", KEY_READ, nullptr, REG_OPTION_VOLATILE);
507 VERIFY_IS_TRUE(wsl::windows::common::registry::IsKeyVolatile(volatileKey.get()));
508
509 // Install the MSI.
510 InstallMsi();
511
512 // Validate that the key is correctly written to and isn't volatile anymore.
513 auto pluginPath = wsl::windows::common::wslutil::GetMsiPackagePath().value_or(L"");
514 VERIFY_IS_FALSE(pluginPath.empty());
515
516 pluginPath += L"WSLDVCPlugin.dll";
517
518 const auto pluginKey = wsl::windows::common::registry::OpenKey(
519 HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Terminal Server Client\\Default\\OptionalAddIns\\WSLDVC_PACKAGE", KEY_READ);
520 const auto value = wsl::windows::common::registry::ReadString(pluginKey.get(), nullptr, L"Name", L"");
521 VERIFY_ARE_EQUAL(value, pluginPath);
522
523 VERIFY_IS_FALSE(wsl::windows::common::registry::IsKeyVolatile(pluginKey.get()));
524 }
525
526 TEST_METHOD(UninstallingMsiRemovesInstallerMsix)
527 {
528 UninstallMsi();
529 VERIFY_IS_FALSE(IsMsiPackageInstalled());
530 VERIFY_IS_FALSE(IsMsixInstalled());
531
532 InstallMsix();
533 WaitForMsiPackageInstall();
534 VERIFY_IS_TRUE(IsMsiPackageInstalled());
535 VERIFY_IS_TRUE(IsMsixInstalled());
536 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
537
538 ValidatePackageInstalledProperly();
539 }
540
541 TEST_METHOD(InstallingMsiInstallsGluePackage)
542 {
543 // Remove the MSI package
544 UninstallMsi();
545 VERIFY_IS_FALSE(IsMsiPackageInstalled());
546 VERIFY_IS_FALSE(IsMsixInstalled());
547
548 // Installing it again and validate that the glue package was added
549 InstallMsi();
550 VERIFY_IS_TRUE(IsMsiPackageInstalled());
551 VERIFY_IS_TRUE(IsMsixInstalled());
552 VERIFY_IS_FALSE(IsMsixInstallerInstalled());
553 ValidatePackageInstalledProperly();
554
555 // Validate that removing it removes the glue package
556 UninstallMsi();
557 VERIFY_IS_FALSE(IsMsiPackageInstalled());
558 VERIFY_IS_FALSE(IsMsixInstalled());
559
560 // Validate that installing the installer gets the MSI installed properly again
561 InstallMsix();
562 WaitForMsiPackageInstall();
563 VERIFY_IS_TRUE(IsMsiPackageInstalled());
564 VERIFY_IS_TRUE(IsMsixInstalled());
565 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
566 ValidatePackageInstalledProperly();
567 }
568
569 TEST_METHOD(UpgradeInstallsTheMsiPackage)
570 {
571 // Remove the MSIX package
572 UninstallMsix();
573 VERIFY_IS_FALSE(IsMsixInstalled());
574
575 // Validate that upgrading the MSI installs the MSIX again
576 InstallMsi();
577 VERIFY_IS_TRUE(IsMsiPackageInstalled());
578 VERIFY_IS_TRUE(IsMsixInstalled());
579 VERIFY_IS_FALSE(IsMsixInstallerInstalled());
580 ValidatePackageInstalledProperly();
581 }
582
583 TEST_METHOD(MsixInstallerUpgrades)
584 {
585 // Remove the MSIX package
586 UninstallMsix();
587 VERIFY_IS_FALSE(IsMsixInstalled());
588
589 // Remove the MSI package
590 UninstallMsi();
591 VERIFY_IS_FALSE(IsMsiPackageInstalled());
592 VERIFY_IS_FALSE(IsMsixInstalled());
593
594 RegistryKeyChange<std::wstring> change(
595 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"Version", L"1.0.0");
596
597 DeleteProductCode();
598 VERIFY_IS_TRUE(GetMsiProductCode().empty());
599
600 // Validate that upgrading the MSIX upgrades the MSI
601 InstallMsix();
602 WaitForMsiPackageInstall();
603 VERIFY_IS_TRUE(IsMsiPackageInstalled());
604 VERIFY_IS_TRUE(IsMsixInstalled());
605 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
606
607 // Validate that the version got upgraded
608 const auto key = wsl::windows::common::registry::OpenLxssMachineKey();
609 const auto versionValue = wsl::windows::common::registry::ReadString(key.get(), L"MSI", L"Version");
610
611 VERIFY_ARE_EQUAL(versionValue, WIDEN(WSL_PACKAGE_VERSION));
612 }
613
614 TEST_METHOD(MsixInstallerUpgradesWithLogFile)
615 {
616 // Remove the MSIX package
617 UninstallMsix();
618 VERIFY_IS_FALSE(IsMsixInstalled());
619
620 // Remove the MSI package
621 UninstallMsi();
622 VERIFY_IS_FALSE(IsMsiPackageInstalled());
623 VERIFY_IS_FALSE(IsMsixInstalled());
624
625 RegistryKeyChange<std::wstring> version(
626 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"Version", L"1.0.0");
627
628 auto logFilePath = std::filesystem::current_path() / "msi-install-logs.txt";
629
630 RegistryKeyChange<std::wstring> logFile(
631 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"UpgradeLogFile", logFilePath.c_str());
632
633 auto cleanup =
634 wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFilePath]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFilePath.c_str())); });
635
636 DeleteProductCode();
637 VERIFY_IS_TRUE(GetMsiProductCode().empty());
638
639 // Validate that upgrading the MSIX upgrades the MSI
640 InstallMsix();
641 WaitForMsiPackageInstall();
642 VERIFY_IS_TRUE(IsMsiPackageInstalled());
643 VERIFY_IS_TRUE(IsMsixInstalled());
644 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
645
646 // Validate that the version got upgraded
647 const auto key = wsl::windows::common::registry::OpenLxssMachineKey();
648 const auto versionValue = wsl::windows::common::registry::ReadString(key.get(), L"MSI", L"Version");
649
650 VERIFY_ARE_EQUAL(versionValue, WIDEN(WSL_PACKAGE_VERSION));
651
652 // Validate that the log file exists and is not empty
653 VERIFY_IS_TRUE(std::filesystem::exists(logFilePath));
654 VERIFY_ARE_NOT_EQUAL(std::filesystem::file_size(logFilePath), 0);
655 }
656
657 TEST_METHOD(MsixInstallerDoesntDowngrade)
658 {
659 // Remove the MSIX package
660 UninstallMsix();
661 VERIFY_IS_FALSE(IsMsixInstalled());
662
663 RegistryKeyChange<std::wstring> change(
664 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"Version", L"999.0.0");
665
666 // Validate that upgrading the MSIX doesn't downgrade the MSI
667 InstallMsix();
668 WaitForInstallerServiceStop();
669 VERIFY_IS_TRUE(IsMsiPackageInstalled());
670 VERIFY_IS_TRUE(IsMsixInstalled());
671 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
672
673 // Validate that the version dit not get upgrade
674 const auto key = wsl::windows::common::registry::OpenLxssMachineKey();
675 const auto versionValue = wsl::windows::common::registry::ReadString(key.get(), L"MSI", L"Version");
676
677 VERIFY_ARE_EQUAL(versionValue, L"999.0.0");
678 }
679
680 TEST_METHOD(MsixUpgradeManual)
681 {
682 // Registry key to disable auto upgrade on service start
683 RegistryKeyChange<DWORD> change(
684 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"AutoUpgradeViaMsix", static_cast<DWORD>(0));
685
686 // Remove the MSI package
687 UninstallMsi();
688 VERIFY_IS_FALSE(IsMsiPackageInstalled());
689 VERIFY_IS_FALSE(IsMsixInstalled());
690
691 // Install the installer MSIX
692 InstallMsix();
693
694 // Validate that calling wsl.exe triggers the install
695 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok");
696 VERIFY_ARE_EQUAL(L"ok\n", output);
697 VERIFY_ARE_EQUAL(L"WSL is finishing an upgrade...\r\n", warnings);
698
699 ValidatePackageInstalledProperly();
700 }
701
702 TEST_METHOD(MsixUpgradeFails)
703 {
704 // Remove the MSI package
705 UninstallMsi();
706 VERIFY_IS_FALSE(IsMsiPackageInstalled());
707 VERIFY_IS_FALSE(IsMsixInstalled());
708
709 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { InstallMsi(); });
710
711 // Open a handle to wsl.exe in the install directory which will cause the installation to fail.
712 //
713 // N.B. The file handle will be closed before the cleanup lambda runs.
714 std::filesystem::create_directories(m_installedPath);
715 wil::unique_hfile fileHandle(::CreateFileW(
716 (m_installedPath / WSL_BINARY_NAME).c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
717
718 // Install the installer MSIX
719 InstallMsix();
720
721 // Validate that calling wsl.exe triggers the install.
722 auto [output, warnings] = LxsstuLaunchWslAndCaptureOutput(L"echo ok", -1, nulDevice.get());
723 VERIFY_ARE_EQUAL(
724 FormatErrorMessage(
725 L"\r\nAnother application has exclusive access to the file 'C:\\Program Files\\WSL\\wsl.exe'. Please shut "
726 L"down all other applications, then click Retry.\r\nUpdate failed (exit code: 1603).",
727 L"Wsl/CallMsi/Install/ERROR_INSTALL_FAILURE"),
728 output);
729 }
730
731 TEST_METHOD(MsiRemoveExistingProductsScheduledInsideTransaction)
732 {
733 // Verify that RemoveExistingProducts is scheduled between InstallInitialize
734 // and InstallFinalize in the MSI sequence table. This is the effect of
735 // Schedule="afterInstallInitialize" on <MajorUpgrade> in package.wix.in.
736
737 unique_msi_handle database;
738 THROW_IF_WIN32_ERROR(MsiOpenDatabaseW(m_msiPath.c_str(), MSIDBOPEN_READONLY, &database));
739
740 auto installInitialize = GetMsiSequenceNumber(database.get(), L"InstallInitialize");
741 auto removeExistingProducts = GetMsiSequenceNumber(database.get(), L"RemoveExistingProducts");
742 auto installFinalize = GetMsiSequenceNumber(database.get(), L"InstallFinalize");
743
744 LogInfo("MSI sequence: InstallInitialize=%d, RemoveExistingProducts=%d, InstallFinalize=%d", installInitialize, removeExistingProducts, installFinalize);
745
746 VERIFY_ARE_NOT_EQUAL(-1, installInitialize);
747 VERIFY_ARE_NOT_EQUAL(-1, removeExistingProducts);
748 VERIFY_ARE_NOT_EQUAL(-1, installFinalize);
749
750 VERIFY_IS_GREATER_THAN(
751 removeExistingProducts, installInitialize, L"RemoveExistingProducts must be after InstallInitialize");
752
753 VERIFY_IS_LESS_THAN(removeExistingProducts, installFinalize, L"RemoveExistingProducts must precede InstallFinalize");
754 }
755
756 TEST_METHOD(MsiUpgradeFailureRestoresFiles)
757 {
758 #ifdef WSL_OFFICIAL_BUILD
759 WEX::Logging::Log::Comment(L"TestSkipped: This test case requires the test-only WslTestForceInstallFailure CA");
760 return;
761 #else
762 // End-to-end rollback test: install an older version, then attempt a major
763 // upgrade that fails inside the transaction. Verify the old version's files
764 // are restored by MSI rollback.
765
766 UninstallMsi();
767 InstallGitHubRelease(L"2.0.2");
768
769 auto restore = wil::scope_exit([this]() { InstallMsi(); });
770
771 VERIFY_IS_TRUE(IsMsiPackageInstalled());
772
773 const auto wslExe = m_installedPath / L"wsl.exe";
774 const auto wslService = m_installedPath / L"wslservice.exe";
775 const auto systemVhd = m_installedPath / L"system.vhd";
776
777 VERIFY_IS_TRUE(std::filesystem::exists(wslExe), L"wsl.exe must exist before upgrade");
778 VERIFY_IS_TRUE(std::filesystem::exists(wslService), L"wslservice.exe must exist before upgrade");
779 VERIFY_IS_TRUE(std::filesystem::exists(systemVhd), L"system.vhd must exist before upgrade");
780
781 // Attempt a major upgrade forced to fail after RemoveExistingProducts.
782 PrepareForMsiOperation();
783 auto msiArgs = std::format(
784 L"/qn /norestart /i \"{}\" WSL_TEST_FORCE_INSTALL_FAILURE=1 SKIPVALIDATION=1 "
785 L"/L*V \"{}\"",
786 m_msiPath,
787 GenerateMsiLogPath());
788 auto exitCode = RunMsiExec(msiArgs);
789
790 VERIFY_ARE_NOT_EQUAL(0L, exitCode, L"Upgrade should have failed due to forced failure CA");
791
792 // Verify rollback restored the previous installation
793 VERIFY_IS_TRUE(IsMsiPackageInstalled(), L"MSI package must still be installed after rollback");
794 VERIFY_IS_TRUE(std::filesystem::exists(wslExe), L"wsl.exe must be restored after rollback");
795 VERIFY_IS_TRUE(std::filesystem::exists(wslService), L"wslservice.exe must be restored after rollback");
796 VERIFY_IS_TRUE(std::filesystem::exists(systemVhd), L"system.vhd must be restored after rollback");
797 ValidateInstalledVersion(L"2.0.2");
798 #endif
799 }
800
801 TEST_METHOD(WslUpdateNoNewVersion)
802 {
803 constexpr auto endpoint = L"http://127.0.0.1:12345/";
804 RegistryKeyChange<std::wstring> change(
805 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss", wsl::windows::common::wslutil::c_githubUrlOverrideRegistryValue, endpoint);
806
807 constexpr auto GitHubApiResponse =
808 LR"({
809 \"name\": \"1.0.0\",
810 \"created_at\": \"2023-06-14T16:56:30Z\",
811 \"assets\": [
812 {
813 \"url\": \"https://api.github.com/repos/microsoft/WSL/releases/assets/112754644\",
814 \"id\": 112754644,
815 \"name\": \"Microsoft.WSL_1.0.0.0_x64_ARM64.msixbundle\"
816 }
817 ]
818 })";
819
820 UniqueWebServer server(endpoint, GitHubApiResponse);
821
822 auto [out, _] = LxsstuLaunchWslAndCaptureOutput(L"--update");
823 VERIFY_ARE_EQUAL(
824 out, L"Checking for updates.\r\nThe most recent version of Windows Subsystem for Linux is already installed.\r\n");
825 }
826
827 TEST_METHOD(InstallRemovesStaleComRegistration)
828 {
829 constexpr auto clsid = L"{A9B7A1B9-0671-405C-95F1-E0612CB4CE7E}";
830
831 // Remove the MSI package
832 UninstallMsi();
833 VERIFY_IS_FALSE(IsMsiPackageInstalled());
834 VERIFY_IS_FALSE(IsMsixInstalled());
835
836 // Emulate a leaked packaged COM registration
837 auto packagedComClassIndex{wsl::windows::common::registry::OpenKey(
838 HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\PackagedCom\\ClassIndex", KEY_CREATE_SUB_KEY | KEY_READ)};
839
840 auto keyExists = [&packagedComClassIndex]() {
841 wil::unique_hkey subKey;
842
843 const auto result = RegOpenKeyEx(packagedComClassIndex.get(), clsid, 0, KEY_READ, &subKey);
844
845 return result == NO_ERROR;
846 };
847
848 wsl::windows::common::registry::CreateKey(packagedComClassIndex.get(), clsid);
849
850 VERIFY_IS_TRUE(keyExists());
851
852 // Install the package and validate that the registry key was removed.
853 InstallMsi();
854 VERIFY_IS_TRUE(IsMsiPackageInstalled());
855 VERIFY_IS_TRUE(IsMsixInstalled());
856 VERIFY_IS_FALSE(IsMsixInstallerInstalled());
857
858 VERIFY_IS_FALSE(keyExists());
859 ValidatePackageInstalledProperly();
860 }
861
862 TEST_METHOD(InstallRemovesStaleServiceRegistration)
863 {
864 // Remove the MSI package.
865 UninstallMsi();
866 VERIFY_IS_FALSE(IsMsiPackageInstalled());
867 VERIFY_IS_FALSE(IsMsixInstalled());
868
869 // Emulate a leaked packaged service registration.
870 const wil::unique_schandle manager{OpenSCManager(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE)};
871
872 wil::unique_schandle service{CreateService(
873 manager.get(),
874 L"wslservice",
875 L"WSL test service",
876 GENERIC_ALL,
877 SERVICE_WIN32_OWN_PROCESS,
878 SERVICE_DISABLED,
879 SERVICE_ERROR_IGNORE,
880 L"C:\\windows\\system32\\cmd.exe",
881 nullptr,
882 nullptr,
883 nullptr,
884 nullptr,
885 nullptr)};
886
887 service.reset();
888
889 wil::unique_hkey key = wsl::windows::common::registry::OpenKey(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Services", KEY_WRITE);
890 THROW_LAST_ERROR_IF(!key);
891
892 wsl::windows::common::registry::WriteString(key.get(), L"wslservice", L"AppUserModelId", L"Dummy");
893 key.reset();
894
895 auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
896 service.reset(OpenService(manager.get(), L"wslservice", DELETE));
897 THROW_LAST_ERROR_IF(!service);
898
899 LOG_IF_WIN32_BOOL_FALSE(DeleteService(service.get()));
900 });
901
902 // Install the MSI package. It should delete the wslservice.
903 InstallMsi();
904 cleanup.release();
905
906 VERIFY_IS_TRUE(IsMsiPackageInstalled());
907 VERIFY_IS_TRUE(IsMsixInstalled());
908
909 ValidatePackageInstalledProperly();
910
911 // Validate that the AppUserModelId registry value is gone.
912 VERIFY_ARE_EQUAL(wsl::windows::common::registry::ReadString(key.get(), L"wslservice", L"AppUserModelId", L""), L"");
913 }
914
915 TEST_METHOD(InstallSetsLspRegistration)
916 {
917 auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
918 VERIFY_IS_TRUE(installPath.has_value());
919
920 // Remove the MSI package.
921 UninstallMsi();
922 VERIFY_IS_FALSE(IsMsiPackageInstalled());
923 VERIFY_IS_FALSE(IsMsixInstalled());
924
925 // Validate that LSP flags are not set
926 auto getLspFlags = [&installPath](const auto* path) -> std::optional<DWORD> {
927 DWORD flags{};
928 INT error{};
929
930 if (WSCGetApplicationCategory(path, static_cast<DWORD>(wcslen(path)), nullptr, 0, &flags, &error) == SOCKET_ERROR)
931 {
932 if (error != WSASERVICE_NOT_FOUND)
933 {
934 LogError("WSCGetApplicationCategory failed for: %ls, error: %i", path, error);
935 }
936
937 return {};
938 }
939
940 return flags;
941 };
942
943 const std::vector<LPCWSTR> executables = {L"wsl.exe", L"wslhost.exe", L"wslrelay.exe", L"wslg.exe"};
944 for (const auto& e : executables)
945 {
946 auto fullPath = installPath.value() + e;
947 VERIFY_ARE_EQUAL(getLspFlags(fullPath.c_str()).value_or(0), 0);
948 }
949
950 // Install the package
951 InstallMsi();
952
953 VERIFY_IS_TRUE(IsMsiPackageInstalled());
954 VERIFY_IS_TRUE(IsMsixInstalled());
955 ValidatePackageInstalledProperly();
956
957 // Validate that the LSP flags were correctly set
958 for (const auto& e : executables)
959 {
960 auto fullPath = installPath.value() + e;
961 VERIFY_ARE_EQUAL(getLspFlags(fullPath.c_str()).value_or(0), LSP_SYSTEM);
962 }
963 }
964
965 TEST_METHOD(InstallClearsExplorerState)
966 {
967 constexpr auto valueName = L"Attributes";
968
969 // Put the explorer in a state where the WSL shortcut is hidden.
970
971 const auto key = wsl::windows::common::registry::CreateKey(
972 HKEY_CURRENT_USER,
973 LR"(Software\Microsoft\Windows\CurrentVersion\Explorer\CLSID\{B2B4A4D1-2754-4140-A2EB-9A76D9D7CDC6}\ShellFolder)",
974 KEY_READ | KEY_WRITE);
975
976 wsl::windows::common::registry::WriteDword(key.get(), nullptr, valueName, SFGAO_NONENUMERATED);
977
978 // Install the package
979 InstallMsi();
980
981 VERIFY_IS_TRUE(IsMsiPackageInstalled());
982 VERIFY_IS_TRUE(IsMsixInstalled());
983 ValidatePackageInstalledProperly();
984
985 // Validate that the installer removed the problematic flag
986
987 VERIFY_ARE_EQUAL(wsl::windows::common::registry::ReadDword(key.get(), nullptr, valueName, 0), 0);
988 }
989
990 TEST_METHOD(InstallUnprotectsKeys)
991 {
992 const auto installPath = wsl::windows::common::wslutil::GetMsiPackagePath();
993 VERIFY_IS_TRUE(installPath.has_value());
994
995 constexpr auto keyPath = LR"(SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\IdListAliasTranslations\WSL)";
996
997 // Create a protected key that the installer will write to
998 {
999 auto [localAdministratorsSid, adminSidBuffer] =
1000 wsl::windows::common::security::CreateSid(SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
1001
1002 EXPLICIT_ACCESS aces[2];
1003 aces[0].grfAccessMode = SET_ACCESS;
1004 aces[0].grfAccessPermissions = KEY_READ;
1005 aces[0].grfInheritance = NO_INHERITANCE;
1006 BuildTrusteeWithSid(&aces[0].Trustee, localAdministratorsSid);
1007
1008 aces[1].grfAccessMode = GRANT_ACCESS;
1009 aces[1].grfAccessPermissions = KEY_ALL_ACCESS;
1010 aces[1].grfInheritance = NO_INHERITANCE;
1011 std::wstring trustedInstaller = L"NT Service\\TrustedInstaller";
1012 BuildTrusteeWithName(&aces[1].Trustee, trustedInstaller.data());
1013
1014 wsl::windows::common::security::unique_acl newAcl{};
1015 THROW_IF_WIN32_ERROR(SetEntriesInAcl(2, aces, nullptr, &newAcl));
1016
1017 SECURITY_DESCRIPTOR newDescriptor{};
1018 THROW_IF_WIN32_BOOL_FALSE(InitializeSecurityDescriptor(&newDescriptor, SECURITY_DESCRIPTOR_REVISION));
1019 THROW_IF_WIN32_BOOL_FALSE(SetSecurityDescriptorDacl(&newDescriptor, true, newAcl.get(), false));
1020
1021 auto restore = wsl::windows::common::security::AcquirePrivileges({SE_BACKUP_NAME, SE_RESTORE_NAME});
1022
1023 const auto key =
1024 wsl::windows::common::registry::CreateKey(HKEY_LOCAL_MACHINE, keyPath, KEY_ALL_ACCESS, nullptr, REG_OPTION_BACKUP_RESTORE);
1025 THROW_IF_WIN32_ERROR(RegSetKeySecurity(key.get(), DACL_SECURITY_INFORMATION, &newDescriptor));
1026 }
1027
1028 VERIFY_IS_TRUE(SfcIsKeyProtected(HKEY_LOCAL_MACHINE, keyPath, KEY_WOW64_64KEY));
1029
1030 // Install the package
1031 InstallMsi();
1032
1033 VERIFY_IS_TRUE(IsMsiPackageInstalled());
1034 VERIFY_IS_TRUE(IsMsixInstalled());
1035 ValidatePackageInstalledProperly();
1036
1037 // Verify that key was unprotected.
1038 VERIFY_IS_FALSE(SfcIsKeyProtected(HKEY_LOCAL_MACHINE, keyPath, KEY_WOW64_64KEY));
1039 }
1040
1041 void ValidateDcatRegistration()
1042 {
1043 const auto versionValue =
1044 wsl::windows::common::registry::ReadString(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), L"Version");
1045 VERIFY_ARE_EQUAL(versionValue, WIDEN(WSL_PACKAGE_VERSION));
1046 }
1047
1048 TEST_METHOD(InstallerRegistersWithDcat)
1049 {
1050 // Uninstalling should remove the registration
1051 UninstallMsi();
1052 VERIFY_IS_FALSE(IsMsiPackageInstalled());
1053 VERIFY_IS_FALSE(IsMsixInstalled());
1054
1055 VERIFY_ARE_EQUAL(
1056 wsl::windows::common::registry::OpenKeyNoThrow(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), KEY_READ).second,
1057 HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
1058
1059 // Installing should add the registration
1060 InstallMsi();
1061 VERIFY_IS_TRUE(IsMsiPackageInstalled());
1062 VERIFY_IS_TRUE(IsMsixInstalled());
1063
1064 ValidateDcatRegistration();
1065 }
1066
1067 TEST_METHOD(ServiceRemediatesDcatRegistration)
1068 {
1069 // Starting the service should create the registration if it is missing
1070 StopWslService();
1071 VERIFY_ARE_EQUAL(wsl::windows::common::registry::DeleteKey(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY)), true);
1072 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--list"), 0);
1073 ValidateDcatRegistration();
1074
1075 // Starting the service should fix the registration if needed
1076 StopWslService();
1077 wsl::windows::common::registry::WriteString(HKEY_LOCAL_MACHINE, WIDEN(DCAT_REGISTRATION_KEY), L"Version", L"1.0.0");
1078 VERIFY_ARE_EQUAL(LxsstuLaunchWsl(L"--list"), 0);
1079 ValidateDcatRegistration();
1080 }
1081
1082 void CallWslUpdateViaMsi()
1083 {
1084
1085 #ifdef WSL_DEV_THIN_MSI_PACKAGE
1086
1087 LogSkipped("This test case cannot run with a thin MSI package");
1088 return;
1089
1090 #endif
1091
1092 constexpr auto endpoint = L"http://127.0.0.1:12345/";
1093 RegistryKeyChange<std::wstring> change(
1094 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss", wsl::windows::common::wslutil::c_githubUrlOverrideRegistryValue, endpoint);
1095
1096 constexpr auto GitHubApiResponse =
1097 LR"({
1098 \"name\": \"999.0.0\",
1099 \"created_at\": \"2023-06-14T16:56:30Z\",
1100 \"assets\": [
1101 {
1102 \"url\": \"http://127.0.0.1:12346/wsl.testpackage.x64.msi\",
1103 \"id\": 0,
1104 \"name\": \"wsl.testpackage.x64.msi\"
1105 }
1106 ]
1107 })";
1108
1109 UniqueWebServer apiServer(endpoint, GitHubApiResponse);
1110 UniqueWebServer fileServer(L"http://127.0.0.1:12346/", std::filesystem::path(m_msiPath));
1111
1112 // DeleteProductCode();
1113 // TODO: It would be good to remove something to validate that the MSI actually gets installed,
1114 // but this doesn't work during the tests because the ProductCode is the same so the components
1115 // don't actually get reinstalled and we can't use REINSTALLMODE because this would skip component removal
1116 // during upgrade.
1117
1118 // The MSI upgrade can send a ctrl-c to wsl.exe, so create a new console so the test process doesn't receive the ctrl-c.
1119 const auto commandLine = LxssGenerateWslCommandLine(L"--update");
1120 wsl::windows::common::SubProcess process(nullptr, commandLine.data(), CREATE_NEW_CONSOLE);
1121 process.SetShowWindow(SW_HIDE);
1122 LogInfo("wsl --update exited with: %lu", process.Run());
1123
1124 // wsl --update isn't synchronous since wsl.exe will be killed during the installation.
1125 WaitForMsiPackageInstall();
1126
1127 ValidatePackageInstalledProperly();
1128 VERIFY_IS_TRUE(IsMsixInstalled());
1129 VERIFY_IS_TRUE(!GetMsiProductCode().empty());
1130 }
1131
1132 TEST_METHOD(WslUpdateViaMsi)
1133 {
1134 CallWslUpdateViaMsi();
1135 }
1136
1137 TEST_METHOD(WslUpdateViaMsix)
1138 {
1139 constexpr auto endpoint = L"http://127.0.0.1:12345/";
1140 RegistryKeyChange<std::wstring> change(
1141 HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss", wsl::windows::common::wslutil::c_githubUrlOverrideRegistryValue, endpoint);
1142
1143 constexpr auto GitHubApiResponse =
1144 LR"({
1145 \"name\": \"999.0.0\",
1146 \"created_at\": \"2023-06-14T16:56:30Z\",
1147 \"assets\": [
1148 {
1149 \"url\": \"http://127.0.0.1:12346/wsl.testpackage.msixbundle\",
1150 \"id\": 0,
1151 \"name\": \"wsl.testpackage.x64.msixbundle\"
1152 }
1153 ]
1154 })";
1155
1156 UniqueWebServer apiServer(endpoint, GitHubApiResponse);
1157 UniqueWebServer fileServer(L"http://127.0.0.1:12346/", std::filesystem::path(m_msixPackagePath));
1158
1159 UninstallMsix();
1160 VERIFY_IS_FALSE(IsMsixInstalled());
1161
1162 const auto installLocation = wsl::windows::common::wslutil::GetMsiPackagePath();
1163 VERIFY_IS_TRUE(installLocation.has_value());
1164 auto cmd = installLocation.value() + L"\\wsl.exe --update";
1165 LxsstuRunCommand(cmd.data()); // Ignore the error code since wsl.exe will be killed by msiexec
1166
1167 VERIFY_IS_TRUE(IsMsiPackageInstalled());
1168 VERIFY_IS_TRUE(IsMsixInstalled());
1169 VERIFY_IS_TRUE(IsMsixInstallerInstalled());
1170 ValidatePackageInstalledProperly();
1171 }
1172
1173 static bool WslSettingsProtocolAssociationExists()
1174 {
1175 wil::com_ptr<IEnumAssocHandlers> enumAssocHandlers;
1176 if (FAILED(SHAssocEnumHandlersForProtocolByApplication(L"wsl-settings", IID_PPV_ARGS(&enumAssocHandlers))))
1177 {
1178 return false;
1179 }
1180
1181 ULONG elementsReturned;
1182 wil::com_ptr<IAssocHandler> currentAssoc;
1183 while (SUCCEEDED(enumAssocHandlers->Next(1, &currentAssoc, &elementsReturned)))
1184 {
1185 wil::unique_cotaskmem_string name;
1186 if (FAILED(currentAssoc->GetName(&name)))
1187 {
1188 LogError("Failed to get association name, continuing...");
1189 continue;
1190 }
1191
1192 if (::_wcsicmp(name.get(), L"WSL Settings") != 0)
1193 {
1194 return true;
1195 }
1196 }
1197
1198 return false;
1199 }
1200
1201 void VerifyWslSettingsProtocolAssociationExistsWithRetry()
1202 {
1203 VERIFY_NO_THROW(wsl::shared::retry::RetryWithTimeout<void>(
1204 [&]() { THROW_HR_IF(E_UNEXPECTED, !WslSettingsProtocolAssociationExists()); }, std::chrono::seconds(1), std::chrono::minutes(2)));
1205 }
1206
1207 TEST_METHOD(WslValidateWslSettingsProtocol)
1208 {
1209 WSL_SETTINGS_TEST();
1210
1211 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
1212 VerifyWslSettingsProtocolAssociationExistsWithRetry();
1213
1214 UninstallMsi();
1215 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
1216
1217 wil::com_ptr<IEnumAssocHandlers> enumAssocHandlers;
1218 const HRESULT hr = SHAssocEnumHandlersForProtocolByApplication(L"wsl-settings", IID_PPV_ARGS(&enumAssocHandlers));
1219 if (FAILED(hr))
1220 {
1221 VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NO_ASSOCIATION));
1222 }
1223 else
1224 {
1225 VERIFY_IS_FALSE(WslSettingsProtocolAssociationExists());
1226 }
1227
1228 InstallMsi();
1229 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
1230 VerifyWslSettingsProtocolAssociationExistsWithRetry();
1231 }
1232 };