diff --git a/localization/strings/en-US/Resources.resw b/localization/strings/en-US/Resources.resw index 5a8097efca..577c9a7c84 100644 --- a/localization/strings/en-US/Resources.resw +++ b/localization/strings/en-US/Resources.resw @@ -1008,6 +1008,10 @@ Falling back to NAT networking. WSL is finishing an upgrade... + + WSL was updated but a system restart is required to complete the installation. Please reboot your machine and try again. + {Locked="WSL"} + Update failed (exit code: {}). {FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated diff --git a/src/windows/common/install.cpp b/src/windows/common/install.cpp index a268f8bf63..46e75271f0 100644 --- a/src/windows/common/install.cpp +++ b/src/windows/common/install.cpp @@ -106,6 +106,7 @@ int UpdatePackageImpl(bool preRelease, bool repair) if (exitCode == ERROR_SUCCESS_REBOOT_REQUIRED) { + wsl::windows::common::install::SetRebootRequiredMarker(); PrintSystemError(ERROR_SUCCESS_REBOOT_REQUIRED); } else if (exitCode != 0) @@ -116,6 +117,11 @@ int UpdatePackageImpl(bool preRelease, bool repair) wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" + wsl::shared::Localization::MessageSeeLogFile(logFile.c_str())); } + else + { + // Clean install — clear any pending reboot marker from a prior 3010-result install. + wsl::windows::common::install::ClearRebootRequiredMarker(); + } } else { @@ -166,6 +172,16 @@ void WaitForMsiInstall() wprintf(L"\n%ls\n", message.get()); } + if (exitCode == ERROR_SUCCESS_REBOOT_REQUIRED) + { + // The MSI completed but one or more files (typically system.vhd or wslservice.exe) + // were in use and have been scheduled for replacement on the next reboot. Surface + // this distinctly so the caller does not proceed to launch WSL against a + // half-installed package — notably, the previous system.vhd has been renamed away + // to %WINDIR%\Installer\Config.Msi\*.rbf and the new one is not yet in place. + THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED), wsl::shared::Localization::MessageUpdateRebootRequired()); + } + if (exitCode != 0) { THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode)); @@ -231,10 +247,47 @@ void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function LxssUserSessionImpl::_CreateInstance(_In_op { ExecutionContext context(Context::CreateInstance); + // If a previous MSI install is pending reboot (files like system.vhd have been + // renamed away and are waiting for delayed replacement), block instance creation + // with a clear error rather than launching against a broken install. + if (wsl::windows::common::install::IsRebootRequired()) + { + THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(ERROR_SUCCESS_REBOOT_REQUIRED), wsl::shared::Localization::MessageUpdateRebootRequired()); + } + // Validate flags. THROW_HR_IF(E_INVALIDARG, (WI_IsAnyFlagSet(Flags, ~LXSS_CREATE_INSTANCE_FLAGS_ALL))); diff --git a/src/windows/wslinstaller/exe/WslInstaller.cpp b/src/windows/wslinstaller/exe/WslInstaller.cpp index 7ac796d6c9..b279d0f76f 100644 --- a/src/windows/wslinstaller/exe/WslInstaller.cpp +++ b/src/windows/wslinstaller/exe/WslInstaller.cpp @@ -97,13 +97,28 @@ std::pair InstallMsipackageImpl() auto result = wsl::windows::common::install::UpgradeViaMsi( GetMsiPackagePath().c_str(), L"SKIPMSIX=1", logFile.has_value() ? logFile->path.c_str() : nullptr, messageCallback); - // ERROR_SUCCESS_REBOOT_REQUIRED (3010) means the install succeeded but some files - // will be replaced on the next reboot. Treat as success since the service runs - // silently with no user-facing console. + // ERROR_SUCCESS_REBOOT_REQUIRED (3010) means MSI completed its database changes but + // one or more files (e.g. system.vhd, wslservice.exe) were in use and have been moved + // to .rbf backups under %WINDIR%\Installer\Config.Msi with their replacements scheduled + // via MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT). Until the user reboots, the install + // location is in a half-replaced state — notably, the old system.vhd has been renamed + // away and the new one is not yet in place. Propagate this distinctly so the client + // does not proceed to launch WSL against a broken install (which surfaces to users as + // "my system.vhd disappeared after the update"). const bool rebootRequired = (result == ERROR_SUCCESS_REBOOT_REQUIRED); + + // Write a volatile (auto-cleared on reboot) registry marker so subsequent wsl.exe + // invocations know the install is incomplete. Without this, CallMsiPackage() would + // short-circuit and launch against the half-replaced install directory. if (rebootRequired) { - result = ERROR_SUCCESS; + wsl::windows::common::install::SetRebootRequiredMarker(); + } + else if (result == ERROR_SUCCESS) + { + // A clean install means any previously-pending reboot has been resolved (the new + // files are in place). Clear the marker so the user can resume without a reboot. + wsl::windows::common::install::ClearRebootRequiredMarker(); } WSL_LOG( @@ -112,7 +127,10 @@ std::pair InstallMsipackageImpl() TraceLoggingValue(rebootRequired, "rebootRequired"), TraceLoggingValue(errors.c_str(), "errorMessage")); - if (result != ERROR_SUCCESS && result != ERROR_SUCCESS_REBOOT_REQUIRED) + // Preserve MSI logs on anything other than a clean success — including + // ERROR_SUCCESS_REBOOT_REQUIRED, since the log identifies which file(s) forced the + // delayed rename. + if (result != ERROR_SUCCESS) { clearLogs.release(); } diff --git a/test/windows/InstallerTests.cpp b/test/windows/InstallerTests.cpp index 91d9c699c1..19cdb94320 100644 --- a/test/windows/InstallerTests.cpp +++ b/test/windows/InstallerTests.cpp @@ -17,6 +17,7 @@ Module Name: #include "Common.h" #include "registry.hpp" +#include "install.h" #include "PluginTests.h" #include "wslcsdk.h" @@ -1125,4 +1126,111 @@ class InstallerTests SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); VerifyWslSettingsProtocolAssociationExistsWithRetry(); } + + TEST_METHOD(UpgradeWithLockedFileReportsRebootRequired) + { + // Ensure the MSI is installed cleanly first. If a prior test run was + // interrupted, the MSI may be missing — reinstall it. + if (!IsMsiPackageInstalled()) + { + InstallMsi(); + } + + VERIFY_IS_TRUE(IsMsiPackageInstalled()); + + // Stop the WSL service so nothing holds files open. + StopWslService(); + + // Uninstall the MSI. MsiInstallProduct on an already-registered ProductCode + // enters maintenance mode and won't replace files. We need a fresh install + // so the MSI actually writes files and hits the lock. + UninstallMsi(); + + // Create a dummy system.vhd in the install directory so we have something to lock. + // When the MSI does a fresh install it will try to write its real system.vhd here, + // but can't because the dummy is memory-mapped — resulting in 3010. + std::filesystem::create_directories(m_installedPath); + auto systemVhdPath = m_installedPath / L"system.vhd"; + { + wil::unique_hfile dummyHandle{ + CreateFileW(systemVhdPath.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)}; + VERIFY_IS_TRUE(dummyHandle.is_valid()); + BYTE pad = 0; + DWORD written = 0; + VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(dummyHandle.get(), &pad, 1, &written, nullptr)); + } + + // Memory-map the dummy to simulate a running VM. A memory-mapped file cannot + // be renamed or deleted regardless of directory permissions — this forces the MSI + // to schedule a delayed rename (MoveFileEx MOVEFILE_DELAY_UNTIL_REBOOT) and return 3010. + wil::unique_hfile lockedHandle{CreateFileW( + systemVhdPath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; + VERIFY_IS_TRUE(lockedHandle.is_valid()); + + wil::unique_handle mapping{CreateFileMappingW(lockedHandle.get(), nullptr, PAGE_READONLY, 0, 0, nullptr)}; + VERIFY_IS_TRUE(mapping.is_valid()); + + auto* mapView = MapViewOfFile(mapping.get(), FILE_MAP_READ, 0, 0, 1); + VERIFY_IS_NOT_NULL(mapView); + auto unmapOnExit = wil::scope_exit([mapView]() { UnmapViewOfFile(mapView); }); + + // Fake a stale version so the WslInstaller service thinks an upgrade is needed. + RegistryKeyChange version( + HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Lxss\\MSI", L"Version", L"1.0.0"); + + // Remove the MSIX so we can reinstall it to trigger the WslInstaller service. + UninstallMsix(); + VERIFY_IS_FALSE(IsMsixInstalled()); + + // Install the MSIX — this starts the WslInstaller service which detects the + // stale version and runs the MSI. With system.vhd locked, the MSI returns 3010 + // and WslInstaller calls SetRebootRequiredMarker(). + InstallMsix(); + + // Wait for the reboot-required marker — this is the signal that the installer + // completed the MSI install and hit the locked file (3010). + auto waitForMarker = []() { THROW_HR_IF(E_FAIL, !wsl::windows::common::install::IsRebootRequired()); }; + + try + { + wsl::shared::retry::RetryWithTimeout(waitForMarker, std::chrono::seconds(1), std::chrono::minutes(5)); + } + catch (...) + { + VERIFY_FAIL("Timed out waiting for reboot-required marker to be set by WslInstaller"); + } + + // Release the memory map and handle — the file has been renamed to .rbf by MSI. + unmapOnExit.reset(); + mapping.reset(); + lockedHandle.reset(); + + // Verify that launching wsl.exe (a command that goes through CallMsiPackage) fails + // with the reboot-required error. + auto wslCommandLine = LxssGenerateWslCommandLine(L"echo OK"); + auto [output, warnings, wslExitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(wslCommandLine.data()); + + LogInfo("wsl echo OK output: %ls", output.c_str()); + LogInfo("wsl echo OK warnings: %ls", warnings.c_str()); + VERIFY_ARE_NOT_EQUAL(wslExitCode, 0); + + // The error message should mention a restart is required. + auto combined = output + warnings; + VERIFY_IS_TRUE(combined.find(L"restart") != std::wstring::npos); + + // Non-distro commands (--version, --list, --shutdown, --update) must keep working + // even with the marker set — they go through CallMsiPackage but don't reach the + // service's _CreateInstance gate, so they should not be blocked. + std::wstring versionCmd = wsl::windows::common::wslutil::GetMsiPackagePath().value_or(L"") + L"\\wsl.exe --version"; + auto [versionOutput, versionWarnings, versionExitCode] = LxsstuLaunchCommandAndCaptureOutputWithResult(versionCmd.data()); + LogInfo("wsl --version output: %ls", versionOutput.c_str()); + VERIFY_ARE_EQUAL(versionExitCode, 0L); + + // Clean up: delete the volatile marker and reinstall cleanly. + wsl::windows::common::registry::DeleteKey(OpenLxssMachineKey(KEY_ALL_ACCESS).get(), L"MSI\\RebootPending"); + VERIFY_IS_FALSE(wsl::windows::common::install::IsRebootRequired()); + + InstallMsi(); + ValidatePackageInstalledProperly(); + } };