| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WslInstall.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains implementations for installing WSL distributions |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include "WslInstall.h" |
| 17 | #include "registry.hpp" |
| 18 | #include "wslutil.h" |
| 19 | #include "Distribution.h" |
| 20 | #include "HandleConsoleProgressBar.h" |
| 21 | #include "svccomm.hpp" |
| 22 | #include "WmiService.h" |
| 23 | |
| 24 | extern HINSTANCE g_dllInstance; |
| 25 | |
| 26 | constexpr LPCWSTR c_optionalFeatureInstallStatus = L"InstallStatus"; |
| 27 | |
| 28 | using wsl::shared::Localization; |
| 29 | using namespace wsl::windows::common::distribution; |
| 30 | using namespace wsl::windows::common::wslutil; |
| 31 | |
| 32 | namespace { |
| 33 | void EnforceFileHash(HANDLE file, const std::wstring& expectedHash) |
| 34 | { |
| 35 | wsl::windows::common::ExecutionContext context(wsl::windows::common::VerifyChecksum); |
| 36 | |
| 37 | const auto fileHash = wsl::windows::common::wslutil::HashFile(file, CALG_SHA_256); |
| 38 | |
| 39 | THROW_LAST_ERROR_IF(SetFilePointer(file, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER); |
| 40 | if (fileHash != wsl::windows::common::string::HexToBytes(expectedHash)) |
| 41 | { |
| 42 | THROW_HR_WITH_USER_ERROR( |
| 43 | TRUST_E_BAD_DIGEST, |
| 44 | wsl::shared::Localization::MessageHashMismatch( |
| 45 | expectedHash.c_str(), wsl::windows::common::string::BytesToHex(fileHash).c_str())); |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | std::vector<std::wstring> GetInstalledOptionalComponents() |
| 50 | { |
| 51 | // Query the list of optional components that have already been installed. |
| 52 | const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey(); |
| 53 | auto [key, error] = wsl::windows::common::registry::OpenKeyNoThrow(lxssKey.get(), c_optionalFeatureInstallStatus, KEY_READ); |
| 54 | std::vector<std::wstring> installedComponents; |
| 55 | if (key) |
| 56 | { |
| 57 | const auto components = wsl::windows::common::registry::ReadString(key.get(), nullptr, nullptr, L""); |
| 58 | installedComponents = wsl::shared::string::Split(components, L','); |
| 59 | } |
| 60 | |
| 61 | return installedComponents; |
| 62 | } |
| 63 | |
| 64 | }; // namespace |
| 65 | |
| 66 | HRESULT WslInstall::InstallDistribution( |
| 67 | _Out_ InstallResult& installResult, |
| 68 | _In_ const std::optional<std::wstring>& distributionName, |
| 69 | _In_ const std::optional<ULONG>& version, |
| 70 | _In_ bool launchAfterInstall, |
| 71 | _In_ bool useGitHub, |
| 72 | _In_ bool legacy, |
| 73 | _In_ bool fixedVhd, |
| 74 | _In_ const std::optional<std::wstring>& localName, |
| 75 | _In_ const std::optional<std::wstring>& location, |
| 76 | _In_ const std::optional<uint64_t>& vhdSize) |
| 77 | try |
| 78 | { |
| 79 | wsl::windows::common::ExecutionContext context(wsl::windows::common::InstallDistro); |
| 80 | |
| 81 | try |
| 82 | { |
| 83 | const auto distributions = wsl::windows::common::distribution::GetAvailable(); |
| 84 | |
| 85 | if (distributionName.has_value()) |
| 86 | { |
| 87 | installResult.Distribution = LookupByName(distributions, distributionName->c_str(), legacy); |
| 88 | } |
| 89 | else |
| 90 | { |
| 91 | if (legacy) |
| 92 | { |
| 93 | THROW_HR_IF( |
| 94 | E_UNEXPECTED, !distributions.Manifest.Distributions.has_value() || distributions.Manifest.Distributions->empty()); |
| 95 | installResult.Distribution = (*distributions.Manifest.Distributions)[0]; |
| 96 | } |
| 97 | else |
| 98 | { |
| 99 | if (distributions.OverrideManifest.has_value() && distributions.OverrideManifest->Default.has_value()) |
| 100 | { |
| 101 | installResult.Distribution = LookupByName(distributions, distributions.OverrideManifest->Default->c_str(), false); |
| 102 | } |
| 103 | else |
| 104 | { |
| 105 | if (!distributions.Manifest.Default.has_value()) |
| 106 | { |
| 107 | THROW_HR_WITH_USER_ERROR(E_UNEXPECTED, wsl::shared::Localization::MessageNoInstallDefault()); |
| 108 | } |
| 109 | |
| 110 | installResult.Distribution = LookupByName(distributions, distributions.Manifest.Default->c_str(), false); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | if (const auto* distro = std::get_if<ModernDistributionVersion>(&*installResult.Distribution)) |
| 116 | { |
| 117 | std::tie(installResult.Name, installResult.Id) = |
| 118 | InstallModernDistribution(*distro, version, localName, location, vhdSize, fixedVhd); |
| 119 | |
| 120 | installResult.InstalledViaGitHub = true; |
| 121 | } |
| 122 | else if (const auto* distro = std::get_if<Distribution>(&*installResult.Distribution)) |
| 123 | { |
| 124 | std::list<std::pair<bool, LPCWSTR>> unsupportedArguments = { |
| 125 | {localName.has_value(), WSL_INSTALL_ARG_NAME_LONG}, |
| 126 | {location.has_value(), WSL_INSTALL_ARG_LOCATION_LONG}, |
| 127 | {vhdSize.has_value(), WSL_INSTALL_ARG_VHD_SIZE}, |
| 128 | {fixedVhd, WSL_INSTALL_ARG_FIXED_VHD}}; |
| 129 | |
| 130 | for (const auto& [condition, argument] : unsupportedArguments) |
| 131 | { |
| 132 | if (condition) |
| 133 | { |
| 134 | THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_USAGE, Localization::MessageNotSupportedOnLegacyDistros(argument).c_str()); |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | installResult.Alreadyinstalled = wsl::windows::common::distribution::IsInstalled(*distro, useGitHub); |
| 139 | if (!installResult.Alreadyinstalled) |
| 140 | { |
| 141 | EMIT_USER_WARNING(Localization::MessageUsingLegacyDistribution()); |
| 142 | if (version.has_value()) |
| 143 | { |
| 144 | THROW_HR_WITH_USER_ERROR(WSL_E_INVALID_USAGE, Localization::MessageLegacyDistributionVersionArgNotSupported()); |
| 145 | } |
| 146 | |
| 147 | // If downloading from the store fails, attempt to download from GitHub. |
| 148 | if (!useGitHub) |
| 149 | { |
| 150 | auto hr = |
| 151 | wil::ResultFromException([&]() { wsl::windows::common::distribution::LegacyInstallViaStore(*distro); }); |
| 152 | if (FAILED(hr)) |
| 153 | { |
| 154 | useGitHub = true; |
| 155 | auto errorString = wsl::windows::common::wslutil::GetErrorString(hr); |
| 156 | wsl::windows::common::wslutil::PrintMessage( |
| 157 | Localization::MessageDistroStoreInstallFailed(distro->Name.c_str(), errorString.c_str()), stdout); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | if (useGitHub) |
| 162 | { |
| 163 | wsl::windows::common::distribution::LegacyInstallViaGithub(*distro); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | installResult.Name = distro->FriendlyName; |
| 168 | installResult.InstalledViaGitHub = useGitHub; |
| 169 | } |
| 170 | else |
| 171 | { |
| 172 | THROW_HR(E_UNEXPECTED); |
| 173 | } |
| 174 | } |
| 175 | catch (...) |
| 176 | { |
| 177 | // Rethrowing via WIL is required for the error context to be properly set |
| 178 | // in case a winrt exception was thrown. |
| 179 | THROW_HR(wil::ResultFromCaughtException()); |
| 180 | } |
| 181 | |
| 182 | return S_OK; |
| 183 | } |
| 184 | CATCH_RETURN() |
| 185 | |
| 186 | std::pair<bool, std::vector<std::wstring>> WslInstall::CheckForMissingOptionalComponents(_In_ bool requireWslOptionalComponent) |
| 187 | { |
| 188 | // Include the WSL optional component if it was requested, or if the OS is not Windows 11 or later. |
| 189 | std::vector<std::wstring> missingComponents; |
| 190 | requireWslOptionalComponent |= !wsl::windows::common::helpers::IsWindows11OrAbove(); |
| 191 | if (requireWslOptionalComponent && !wsl::windows::common::helpers::IsServicePresent(L"lxssmanager")) |
| 192 | { |
| 193 | missingComponents.emplace_back(c_optionalFeatureNameWsl); |
| 194 | } |
| 195 | |
| 196 | if (!wsl::windows::common::wslutil::IsVirtualMachinePlatformInstalled()) |
| 197 | { |
| 198 | missingComponents.emplace_back(c_optionalFeatureNameVmp); |
| 199 | } |
| 200 | |
| 201 | // If any required components are not present, a reboot is required. |
| 202 | bool rebootRequired = !missingComponents.empty(); |
| 203 | |
| 204 | // Query the list of optional components that have already been installed. |
| 205 | const auto installedComponents = GetInstalledOptionalComponents(); |
| 206 | for (const auto& component : installedComponents) |
| 207 | { |
| 208 | std::erase(missingComponents, component); |
| 209 | } |
| 210 | |
| 211 | return {rebootRequired, std::move(missingComponents)}; |
| 212 | } |
| 213 | |
| 214 | DWORD WslInstall::InstallOptionalComponent(LPCWSTR component, bool consoleOutput) |
| 215 | { |
| 216 | std::wstring systemDirectory; |
| 217 | THROW_IF_FAILED(wil::GetSystemDirectoryW(systemDirectory)); |
| 218 | |
| 219 | const auto dismPath = std::filesystem::path(std::move(systemDirectory)) / L"dism.exe"; |
| 220 | |
| 221 | auto commandLine = std::format(L"{} /Online /NoRestart /enable-feature /featurename:{}", dismPath.native(), component); |
| 222 | |
| 223 | wsl::windows::common::SubProcess process(nullptr, commandLine.c_str()); |
| 224 | if (!consoleOutput) |
| 225 | { |
| 226 | process.SetFlags(CREATE_NEW_CONSOLE); |
| 227 | process.SetShowWindow(SW_HIDE); |
| 228 | } |
| 229 | |
| 230 | return process.Run(); |
| 231 | } |
| 232 | |
| 233 | void WslInstall::InstallOptionalComponents(const std::vector<std::wstring>& components) |
| 234 | { |
| 235 | for (const auto& component : components) |
| 236 | { |
| 237 | wsl::windows::common::wslutil::PrintMessage(Localization::MessageInstallingWindowsComponent(component)); |
| 238 | |
| 239 | const auto exitCode = InstallOptionalComponent(component.c_str(), true); |
| 240 | if (exitCode != 0 && exitCode != ERROR_SUCCESS_REBOOT_REQUIRED) |
| 241 | { |
| 242 | THROW_HR_WITH_USER_ERROR(WSL_E_INSTALL_COMPONENT_FAILED, Localization::MessageOptionalComponentInstallFailed(component, exitCode)); |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | // Update the list of optional components that have been installed. |
| 247 | auto installedComponents = GetInstalledOptionalComponents(); |
| 248 | installedComponents.insert(installedComponents.end(), components.begin(), components.end()); |
| 249 | const auto lxssKey = wsl::windows::common::registry::OpenLxssUserKey(); |
| 250 | const auto key = |
| 251 | wsl::windows::common::registry::CreateKey(lxssKey.get(), c_optionalFeatureInstallStatus, KEY_ALL_ACCESS, nullptr, REG_OPTION_VOLATILE); |
| 252 | wsl::windows::common::registry::WriteString(key.get(), nullptr, nullptr, wsl::shared::string::Join(installedComponents, L',').c_str()); |
| 253 | } |
| 254 | |
| 255 | bool WslInstall::IsOptionalComponentInstalled(LPCWSTR component) |
| 256 | { |
| 257 | constexpr int32_t c_enabled = 1; |
| 258 | |
| 259 | wsl::core::WmiService service(L"ROOT\\CIMV2"); |
| 260 | wsl::core::WmiEnumerate optionalFeatures(service); |
| 261 | const auto query = std::format(L"SELECT InstallState FROM Win32_OptionalFeature WHERE Name = '{}'", component); |
| 262 | const auto& results = optionalFeatures.query(query.c_str()); |
| 263 | const auto result = results.begin(); |
| 264 | THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_NOT_FOUND), result == results.end(), "Optional component not found: %ls", component); |
| 265 | |
| 266 | int32_t installState{}; |
| 267 | THROW_HR_IF_MSG(E_UNEXPECTED, !result->get(L"InstallState", &installState), "Invalid optional component state: %ls", component); |
| 268 | return installState == c_enabled; |
| 269 | } |
| 270 | |
| 271 | std::pair<std::wstring, GUID> WslInstall::InstallModernDistribution( |
| 272 | const ModernDistributionVersion& distribution, |
| 273 | const std::optional<ULONG>& version, |
| 274 | const std::optional<std::wstring>& name, |
| 275 | const std::optional<std::wstring>& location, |
| 276 | const std::optional<uint64_t>& vhdSize, |
| 277 | const bool fixedVhd) |
| 278 | { |
| 279 | wsl::windows::common::SvcComm service; |
| 280 | |
| 281 | // Fail early if the distributions name is already in use. |
| 282 | auto result = wil::ResultFromException([&]() { |
| 283 | service.GetDistributionId(name.has_value() ? name->c_str() : distribution.Name.c_str(), LXSS_GET_DISTRO_ID_LIST_ALL); |
| 284 | }); |
| 285 | |
| 286 | THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), SUCCEEDED(result)); |
| 287 | LOG_HR_IF(result, result != WSL_E_DISTRO_NOT_FOUND); |
| 288 | |
| 289 | const auto downloadInfo = wsl::shared::Arm64 ? distribution.Arm64Url : distribution.Amd64Url; |
| 290 | THROW_HR_IF(E_UNEXPECTED, !downloadInfo.has_value()); |
| 291 | |
| 292 | std::wstring installPath; |
| 293 | bool fileDownloaded{}; |
| 294 | auto deleteFile = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { |
| 295 | if (fileDownloaded) |
| 296 | { |
| 297 | THROW_IF_WIN32_BOOL_FALSE(DeleteFileW(installPath.c_str())); |
| 298 | } |
| 299 | }); |
| 300 | |
| 301 | if (auto localFile = wsl::windows::common::filesystem::TryGetPathFromFileUrl(downloadInfo->Url)) |
| 302 | { |
| 303 | installPath = std::move(localFile.value()); |
| 304 | } |
| 305 | else |
| 306 | { |
| 307 | PrintMessage(Localization::MessageDownloading(distribution.FriendlyName.c_str()), stdout); |
| 308 | installPath = DownloadFile(downloadInfo->Url, distribution.Name + L".wsl"); |
| 309 | fileDownloaded = true; |
| 310 | } |
| 311 | |
| 312 | PrintMessage(Localization::MessageInstalling(distribution.FriendlyName.c_str()), stdout); |
| 313 | |
| 314 | wil::unique_handle file{CreateFile(installPath.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)}; |
| 315 | THROW_LAST_ERROR_IF(!file); |
| 316 | |
| 317 | EnforceFileHash(file.get(), downloadInfo->Sha256); |
| 318 | |
| 319 | wsl::windows::common::HandleConsoleProgressBar progressBar(file.get(), Localization::MessageImportProgress()); |
| 320 | |
| 321 | auto [id, installedName] = service.RegisterDistribution( |
| 322 | name.has_value() ? name->c_str() : distribution.Name.c_str(), |
| 323 | version.value_or(LXSS_WSL_VERSION_DEFAULT), |
| 324 | file.get(), |
| 325 | location.has_value() ? location->c_str() : nullptr, |
| 326 | fixedVhd ? LXSS_IMPORT_DISTRO_FLAGS_FIXED_VHD : 0, |
| 327 | vhdSize); |
| 328 | |
| 329 | return {installedName.get(), id}; |
| 330 | } |