master
cpp 441 lines 15.4 KB
Raw
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 }