| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | Distribution.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | This file contains implementations for distribution app download, install and launch. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include "Distribution.h" |
| 17 | #include "ConsoleProgressBar.h" |
| 18 | #include "registry.hpp" |
| 19 | |
| 20 | constexpr auto c_defaultDistroListUrl = |
| 21 | L"https://raw.githubusercontent.com/microsoft/WSL/master/distributions/DistributionInfo.json"; |
| 22 | constexpr auto StoreClientId = L"wsl-install-lifted"; |
| 23 | |
| 24 | using namespace winrt::Windows::ApplicationModel::Store::Preview::InstallControl; |
| 25 | using namespace winrt::Windows::System; |
| 26 | using namespace wsl::windows::common::distribution; |
| 27 | using wsl::shared::Localization; |
| 28 | |
| 29 | namespace { |
| 30 | std::wstring GetFamilyNameFromStorePackage(const winrt::Windows::Services::Store::StoreProduct& Package) |
| 31 | { |
| 32 | const auto extendedJson = Package.ExtendedJsonData(); |
| 33 | |
| 34 | const auto json = nlohmann::json::parse(wsl::shared::string::WideToMultiByte(extendedJson.c_str())); |
| 35 | THROW_HR_IF_MSG(E_UNEXPECTED, !json.contains("Properties"), "Failed to deserialize store json : '%ls'", extendedJson.c_str()); |
| 36 | |
| 37 | const auto properties = json.at("Properties"); |
| 38 | THROW_HR_IF_MSG( |
| 39 | E_UNEXPECTED, |
| 40 | !properties.is_object() || !properties.contains("PackageFamilyName"), |
| 41 | "Failed to deserialize store json : '%ls'", |
| 42 | extendedJson.c_str()); |
| 43 | |
| 44 | return properties.at("PackageFamilyName").get<std::wstring>(); |
| 45 | } |
| 46 | |
| 47 | winrt::Windows::Services::Store::StoreProduct GetStorePackage(LPCWSTR AppId) |
| 48 | { |
| 49 | const auto storeContext = winrt::Windows::Services::Store::StoreContext::GetDefault(); |
| 50 | |
| 51 | const auto productKinds = winrt::single_threaded_vector<winrt::hstring>({L"Application"}); |
| 52 | const auto productIds = winrt::single_threaded_vector<winrt::hstring>({AppId}); |
| 53 | const auto packages = storeContext.GetStoreProductsAsync(productKinds, productIds).get().Products(); |
| 54 | THROW_HR_IF_MSG( |
| 55 | E_UNEXPECTED, |
| 56 | packages.Size() != 1, |
| 57 | "Unexpected store package count AppId=%ls, Count=%zu", |
| 58 | AppId, |
| 59 | static_cast<size_t>(packages.Size())); |
| 60 | |
| 61 | return packages.First().Current().Value(); |
| 62 | } |
| 63 | |
| 64 | std::optional<winrt::Windows::ApplicationModel::Package> GetInstalledPackage(LPCWSTR PackageFamilyName) |
| 65 | { |
| 66 | const winrt::Windows::Management::Deployment::PackageManager packageManager; |
| 67 | const auto familyCollection = packageManager.FindPackagesForUser(L"", PackageFamilyName); |
| 68 | const auto iter = familyCollection.First(); |
| 69 | if (!iter.HasCurrent()) |
| 70 | { |
| 71 | return {}; |
| 72 | } |
| 73 | |
| 74 | auto package = iter.Current(); |
| 75 | LOG_HR_IF_MSG(E_UNEXPECTED, iter.MoveNext(), "More than one package found for packageFamily=%ls", PackageFamilyName); |
| 76 | |
| 77 | return package; |
| 78 | } |
| 79 | |
| 80 | std::wstring GetFamilyName(const Distribution& distro, bool directDownload) |
| 81 | { |
| 82 | if (directDownload) |
| 83 | { |
| 84 | THROW_HR_IF(E_UNEXPECTED, !distro.PackageFamilyName.has_value()); |
| 85 | return *distro.PackageFamilyName; |
| 86 | } |
| 87 | |
| 88 | return GetFamilyNameFromStorePackage(GetStorePackage(distro.StoreAppId.c_str())); |
| 89 | } |
| 90 | |
| 91 | DistributionList ReadFromManifest(const std::wstring& url) |
| 92 | { |
| 93 | using namespace wsl::windows::common::distribution; |
| 94 | |
| 95 | try |
| 96 | { |
| 97 | std::wstring content; |
| 98 | if (const auto localFile = wsl::windows::common::filesystem::TryGetPathFromFileUrl(url)) |
| 99 | { |
| 100 | content = wsl::shared::string::ReadFile<wchar_t, wchar_t>(localFile->c_str()); |
| 101 | } |
| 102 | else |
| 103 | { |
| 104 | const winrt::Windows::Web::Http::Filters::HttpBaseProtocolFilter filter; |
| 105 | filter.CacheControl().WriteBehavior(winrt::Windows::Web::Http::Filters::HttpCacheWriteBehavior::NoCache); |
| 106 | filter.CacheControl().ReadBehavior(winrt::Windows::Web::Http::Filters::HttpCacheReadBehavior::NoCache); |
| 107 | |
| 108 | const winrt::Windows::Web::Http::HttpClient client(filter); |
| 109 | const auto response = client.GetAsync(winrt::Windows::Foundation::Uri(url)).get(); |
| 110 | response.EnsureSuccessStatusCode(); |
| 111 | |
| 112 | content = response.Content().ReadAsStringAsync().get(); |
| 113 | } |
| 114 | |
| 115 | auto distros = wsl::shared::FromJson<DistributionList, nlohmann::ordered_json>(content.c_str()); |
| 116 | |
| 117 | if (distros.Distributions.has_value()) |
| 118 | { |
| 119 | std::erase_if(*distros.Distributions, [](const auto& e) { |
| 120 | if constexpr (wsl::shared::Arm64) |
| 121 | { |
| 122 | return !e.Arm64; |
| 123 | } |
| 124 | else |
| 125 | { |
| 126 | return !e.Amd64; |
| 127 | } |
| 128 | }); |
| 129 | } |
| 130 | |
| 131 | if (distros.ModernDistributions.has_value()) |
| 132 | { |
| 133 | for (auto& [_, versions] : *distros.ModernDistributions) |
| 134 | { |
| 135 | std::erase_if(versions, [](const auto& e) { |
| 136 | if constexpr (wsl::shared::Arm64) |
| 137 | { |
| 138 | return !e.Arm64Url.has_value(); |
| 139 | } |
| 140 | else |
| 141 | { |
| 142 | return !e.Amd64Url.has_value(); |
| 143 | } |
| 144 | }); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | // The "Default" string takes precedence. If not present, use the first legacy distro entry. |
| 149 | if (!distros.Default.has_value() && distros.Distributions.has_value() && distros.Distributions->size() > 0) |
| 150 | { |
| 151 | distros.Default = (*distros.Distributions)[0].Name; |
| 152 | } |
| 153 | |
| 154 | return distros; |
| 155 | } |
| 156 | catch (...) |
| 157 | { |
| 158 | const auto hr = wil::ResultFromCaughtException(); |
| 159 | THROW_HR_WITH_USER_ERROR( |
| 160 | hr, wsl::shared::Localization::MessageCouldFetchDistributionList(url.c_str(), wsl::windows::common::wslutil::GetSystemErrorString(hr))); |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | std::optional<TDistribution> LookupDistributionInManifest(const DistributionList& manifest, LPCWSTR name, bool legacy) |
| 165 | { |
| 166 | // First check if the name matches a distribution, or a distribution version in the modern entries |
| 167 | |
| 168 | const auto utf8name = wsl::shared::string::WideToMultiByte(name); |
| 169 | if (!legacy && manifest.ModernDistributions.has_value()) |
| 170 | { |
| 171 | for (const auto& [distributionName, versions] : *manifest.ModernDistributions) |
| 172 | { |
| 173 | bool useDefault = false; |
| 174 | if (wsl::shared::string::IsEqual(distributionName, utf8name, true)) |
| 175 | { |
| 176 | useDefault = true; |
| 177 | } |
| 178 | |
| 179 | for (const auto& e : versions) |
| 180 | { |
| 181 | if (useDefault && e.Default.value_or(false)) |
| 182 | { |
| 183 | return e; |
| 184 | } |
| 185 | |
| 186 | if (wsl::shared::string::IsEqual(e.Name, name, true)) |
| 187 | { |
| 188 | return e; |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | // If no modern distribution is found, or --legacy is passed, look for a legacy registration |
| 195 | |
| 196 | if (!manifest.Distributions.has_value()) |
| 197 | { |
| 198 | return {}; |
| 199 | } |
| 200 | |
| 201 | const auto it = std::find_if(manifest.Distributions->begin(), manifest.Distributions->end(), [&](const auto e) { |
| 202 | return wsl::shared::string::IsEqual(e.Name, name, true); |
| 203 | }); |
| 204 | |
| 205 | if (it == manifest.Distributions->end()) |
| 206 | { |
| 207 | return {}; |
| 208 | } |
| 209 | |
| 210 | return *it; |
| 211 | } |
| 212 | |
| 213 | } // namespace |
| 214 | |
| 215 | AvailableDistributions wsl::windows::common::distribution::GetAvailable() |
| 216 | { |
| 217 | AvailableDistributions distributions{}; |
| 218 | |
| 219 | std::wstring url = c_defaultDistroListUrl; |
| 220 | std::optional<std::wstring> appendUrl; |
| 221 | try |
| 222 | { |
| 223 | const auto registryKey = registry::OpenLxssMachineKey(); |
| 224 | url = registry::ReadString(registryKey.get(), nullptr, c_distroUrlRegistryValue, c_defaultDistroListUrl); |
| 225 | if (url != c_defaultDistroListUrl) |
| 226 | { |
| 227 | WSL_LOG("Found custom URL for distribution list", TraceLoggingValue(url.c_str(), "url")); |
| 228 | } |
| 229 | |
| 230 | appendUrl = registry::ReadOptionalString(registryKey.get(), nullptr, c_distroUrlAppendRegistryValue); |
| 231 | } |
| 232 | CATCH_LOG() |
| 233 | |
| 234 | distributions.Manifest = ReadFromManifest(url); |
| 235 | |
| 236 | if (appendUrl.has_value()) |
| 237 | { |
| 238 | WSL_LOG("Found append URL for distribution list", TraceLoggingValue(appendUrl->c_str(), "url")); |
| 239 | |
| 240 | distributions.OverrideManifest = ReadFromManifest(appendUrl.value()); |
| 241 | } |
| 242 | |
| 243 | return distributions; |
| 244 | } |
| 245 | |
| 246 | std::variant<Distribution, ModernDistributionVersion> wsl::windows::common::distribution::LookupByName( |
| 247 | const AvailableDistributions& manifest, LPCWSTR name, bool legacy) |
| 248 | { |
| 249 | if (manifest.OverrideManifest.has_value()) |
| 250 | { |
| 251 | auto distribution = LookupDistributionInManifest(manifest.OverrideManifest.value(), name, legacy); |
| 252 | if (distribution.has_value()) |
| 253 | { |
| 254 | EMIT_USER_WARNING(wsl::shared::Localization::MessageDistributionOverridden(name)); |
| 255 | return distribution.value(); |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | auto distribution = LookupDistributionInManifest(manifest.Manifest, name, legacy); |
| 260 | |
| 261 | if (!distribution.has_value()) |
| 262 | { |
| 263 | THROW_HR_WITH_USER_ERROR(WSL_E_DISTRO_NOT_FOUND, Localization::MessageInvalidDistributionName(name)); |
| 264 | } |
| 265 | |
| 266 | return distribution.value(); |
| 267 | } |
| 268 | |
| 269 | bool wsl::windows::common::distribution::IsInstalled(const Distribution& distro, bool directDownload) |
| 270 | { |
| 271 | const auto familyName = GetFamilyName(distro, directDownload); |
| 272 | return GetInstalledPackage(familyName.c_str()).has_value(); |
| 273 | } |
| 274 | |
| 275 | void wsl::windows::common::distribution::LegacyInstallViaGithub(const Distribution& distro) |
| 276 | { |
| 277 | decltype(distro.Amd64PackageUrl) downloadUrl; |
| 278 | |
| 279 | if constexpr (wsl::shared::Arm64) |
| 280 | { |
| 281 | downloadUrl = distro.Arm64PackageUrl; |
| 282 | } |
| 283 | else |
| 284 | { |
| 285 | downloadUrl = distro.Amd64PackageUrl; |
| 286 | } |
| 287 | |
| 288 | THROW_HR_IF(WSL_E_DISTRO_ONLY_AVAILABLE_FROM_STORE, !downloadUrl.has_value()); |
| 289 | |
| 290 | wslutil::PrintMessage(Localization::MessageDownloading(distro.FriendlyName.c_str()), stdout); |
| 291 | |
| 292 | // Note: The appx extensions is required for the installation to succeed. |
| 293 | const auto downloadPath = wslutil::DownloadFile(*downloadUrl, distro.Name + L".appx"); |
| 294 | auto deleteFile = |
| 295 | wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] { THROW_IF_WIN32_BOOL_FALSE(DeleteFileW(downloadPath.c_str())); }); |
| 296 | |
| 297 | wslutil::PrintMessage(Localization::MessageInstalling(distro.FriendlyName), stdout); |
| 298 | |
| 299 | const winrt::Windows::Management::Deployment::PackageManager packageManager; |
| 300 | auto installedPackage = packageManager |
| 301 | .AddPackageAsync( |
| 302 | winrt::Windows::Foundation::Uri{downloadPath}, |
| 303 | nullptr, |
| 304 | winrt::Windows::Management::Deployment::DeploymentOptions::None, |
| 305 | wsl::windows::common::wslutil::GetSystemVolume()) |
| 306 | .get(); |
| 307 | |
| 308 | wslutil::PrintMessage(Localization::MessageDownloadComplete(distro.FriendlyName), stdout); |
| 309 | } |
| 310 | |
| 311 | void wsl::windows::common::distribution::LegacyInstallViaStore(const Distribution& distro) |
| 312 | { |
| 313 | const AppInstallOptions options; |
| 314 | options.CompletedInstallToastNotificationMode(AppInstallationToastNotificationMode::NoToast); |
| 315 | |
| 316 | const AppInstallManager manager; |
| 317 | const auto entries = |
| 318 | manager.StartProductInstallAsync(distro.StoreAppId.c_str(), winrt::hstring{}, StoreClientId, winrt::hstring{}, options).get(); |
| 319 | |
| 320 | // Cancel the app deployment if something goes wrong |
| 321 | auto cancel = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { |
| 322 | for (const auto& e : entries) |
| 323 | { |
| 324 | e.Cancel(); |
| 325 | } |
| 326 | }); |
| 327 | |
| 328 | wslutil::PrintMessage(Localization::MessageDownloading(distro.FriendlyName), stdout); |
| 329 | |
| 330 | // Print install progress. |
| 331 | auto complete = [&]() { |
| 332 | for (uint32_t i = 0; i < entries.Size(); i++) |
| 333 | { |
| 334 | if (entries.GetAt(i).GetCurrentStatus().InstallState() != AppInstallState::Completed) |
| 335 | { |
| 336 | return false; |
| 337 | } |
| 338 | } |
| 339 | return true; |
| 340 | }; |
| 341 | |
| 342 | ConsoleProgressBar progressBar; |
| 343 | const auto total = std::lround(100 * entries.Size()); |
| 344 | while (!complete()) |
| 345 | { |
| 346 | double percentComplete = 0; |
| 347 | for (const auto& e : entries) |
| 348 | { |
| 349 | const auto& status = e.GetCurrentStatus(); |
| 350 | THROW_IF_FAILED(static_cast<HRESULT>(status.ErrorCode())); |
| 351 | |
| 352 | percentComplete += status.PercentComplete(); |
| 353 | } |
| 354 | |
| 355 | progressBar.Print(static_cast<UINT>(percentComplete), total); |
| 356 | Sleep(100); |
| 357 | } |
| 358 | |
| 359 | progressBar.Clear(); |
| 360 | cancel.release(); |
| 361 | |
| 362 | wslutil::PrintMessage(Localization::MessageDownloadComplete(distro.FriendlyName), stdout); |
| 363 | |
| 364 | // Sanity check |
| 365 | THROW_HR_IF(E_UNEXPECTED, !IsInstalled(distro, false)); |
| 366 | } |
| 367 | |
| 368 | void wsl::windows::common::distribution::Launch(const Distribution& distro, bool directDownload, bool throwOnError) |
| 369 | { |
| 370 | const std::wstring familyName = GetFamilyName(distro, directDownload); |
| 371 | |
| 372 | try |
| 373 | { |
| 374 | wil::unique_cotaskmem_string appsPath; |
| 375 | THROW_IF_FAILED(::SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_NO_APPCONTAINER_REDIRECTION, nullptr, &appsPath)); |
| 376 | |
| 377 | const std::filesystem::path appsFolder{appsPath.get()}; |
| 378 | |
| 379 | std::optional<std::filesystem::path> entryPoint; |
| 380 | for (const auto& e : std::filesystem::directory_iterator(appsFolder / L"Microsoft" / "WindowsApps" / familyName)) |
| 381 | { |
| 382 | if (e.path().has_extension() && |
| 383 | wsl::windows::common::string::IsPathComponentEqual(e.path().extension().native(), L".exe")) |
| 384 | { |
| 385 | if (entryPoint.has_value()) |
| 386 | { |
| 387 | // Note: Can't use THROW_HR_IF* here because entryPoint.value() should only be called if entryPoint has a value. |
| 388 | THROW_HR_MSG( |
| 389 | E_UNEXPECTED, |
| 390 | "Found multiple entrypoints for app: %ls (%ls, %ls), falling back to LaunchAsync()", |
| 391 | familyName.c_str(), |
| 392 | entryPoint.value().c_str(), |
| 393 | e.path().c_str()); |
| 394 | } |
| 395 | |
| 396 | entryPoint = e.path(); |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | THROW_HR_IF_MSG( |
| 401 | E_UNEXPECTED, |
| 402 | !entryPoint.has_value(), |
| 403 | "No entrypoint found for app: %ls, path: %ls", |
| 404 | familyName.c_str(), |
| 405 | (appsFolder / L"Microsoft" / "WindowsApps" / familyName).c_str()); |
| 406 | |
| 407 | auto commandLine = entryPoint->wstring(); |
| 408 | const auto exitCode = wsl::windows::common::helpers::RunProcess(commandLine); |
| 409 | if (throwOnError && exitCode != 0) |
| 410 | { |
| 411 | THROW_HR_WITH_USER_ERROR(WSL_E_INSTALL_PROCESS_FAILED, wsl::shared::Localization::MessageInstallProcessFailed(distro.Name, exitCode)); |
| 412 | } |
| 413 | return; |
| 414 | } |
| 415 | catch (...) |
| 416 | { |
| 417 | if (wil::ResultFromCaughtException() == WSL_E_INSTALL_PROCESS_FAILED) |
| 418 | { |
| 419 | throw; |
| 420 | } |
| 421 | else |
| 422 | { |
| 423 | LOG_CAUGHT_EXCEPTION(); |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | // Fallback to the old launch logic in case something went wrong looking up the app execution alias. |
| 428 | |
| 429 | const auto package = GetInstalledPackage(familyName.c_str()); |
| 430 | THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !package.has_value()); |
| 431 | |
| 432 | const auto entryPoints = package->GetAppListEntries(); |
| 433 | THROW_HR_IF_MSG( |
| 434 | E_UNEXPECTED, |
| 435 | entryPoints.Size() != 1, |
| 436 | "Unexpected number of entry points for app: %ls, %i", |
| 437 | distro.StoreAppId.c_str(), |
| 438 | !entryPoints.Size()); |
| 439 | |
| 440 | entryPoints.GetAt(0).LaunchAsync().get(); |
| 441 | } |