1
+/*++
2
+
3
+Copyright (c) Microsoft. All rights reserved.
4
+
5
+Module Name:
6
+
7
+ install.cpp
8
+
9
+Abstract:
10
+
11
+ This file contains MSI/Wintrust install helper functions.
12
+ Split from wslutil.cpp to avoid pulling msi.dll/wintrust.dll
13
+ into targets that don't need them.
14
+
15
+--*/
16
+
17
+#include "precomp.h"
18
+#include "install.h"
19
+#include "wslutil.h"
20
+#include "WslPluginApi.h"
21
+#include "wslinstallerservice.h"
22
+
23
+#include "ConsoleProgressBar.h"
24
+#include "ExecutionContext.h"
25
+#include "MsiQuery.h"
26
+
27
+using winrt::Windows::Foundation::Uri;
28
+using winrt::Windows::Management::Deployment::DeploymentOptions;
29
+using wsl::shared::Localization;
30
+using wsl::windows::common::Context;
31
+using namespace wsl::windows::common::registry;
32
+using namespace wsl::windows::common::wslutil;
33
+using namespace wsl::windows::common::install;
34
+
35
+namespace {
36
+
37
+bool PromptForKeyPress()
38
+{
39
+ THROW_IF_WIN32_BOOL_FALSE(FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE)));
40
+
41
+ // Note: Ctrl-c causes _getch to return 0x3.
42
+ return _getch() != 0x3;
43
+}
44
+
45
+bool PromptForKeyPressWithTimeout()
46
+{
47
+ // Run PromptForKeyPress on a separate thread so we can apply a timeout.
48
+ // If PromptForKeyPress fails, fulfill the promise with false so the caller doesn't hang.
49
+ std::promise<bool> pressedKey;
50
+ auto thread = std::thread([&pressedKey]() {
51
+ try
52
+ {
53
+ pressedKey.set_value(PromptForKeyPress());
54
+ }
55
+ catch (...)
56
+ {
57
+ LOG_CAUGHT_EXCEPTION();
58
+ try
59
+ {
60
+ pressedKey.set_value(false);
61
+ }
62
+ CATCH_LOG()
63
+ }
64
+ });
65
+
66
+ auto cancelRead = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&thread]() {
67
+ if (thread.joinable())
68
+ {
69
+ LOG_IF_WIN32_BOOL_FALSE(CancelSynchronousIo(thread.native_handle()));
70
+ thread.join();
71
+ }
72
+ });
73
+
74
+ auto future = pressedKey.get_future();
75
+ const auto waitResult = future.wait_for(std::chrono::minutes(1));
76
+
77
+ return waitResult == std::future_status::ready && future.get();
78
+}
79
+
80
+int UpdatePackageImpl(bool preRelease, bool repair)
81
+{
82
+ if (!repair)
83
+ {
84
+ PrintMessage(Localization::MessageCheckingForUpdates());
85
+ }
86
+
87
+ auto [version, release] = GetLatestGitHubRelease(preRelease);
88
+
89
+ if (!repair && ParseWslPackageVersion(version) <= wsl::shared::PackageVersion)
90
+ {
91
+ PrintMessage(Localization::MessageUpdateNotNeeded());
92
+ return 0;
93
+ }
94
+
95
+ PrintMessage(Localization::MessageUpdatingToVersion(version.c_str()));
96
+
97
+ const bool msiInstall = wsl::shared::string::EndsWith<wchar_t>(release.name, L".msi");
98
+ const auto downloadPath = DownloadFile(release.url, release.name);
99
+ if (msiInstall)
100
+ {
101
+ auto logFile = std::filesystem::temp_directory_path() / L"wsl-install-logs.txt";
102
+ auto clearLogs =
103
+ wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&logFile]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFile(logFile.c_str())); });
104
+
105
+ const auto exitCode = UpgradeViaMsi(downloadPath.c_str(), L"", logFile.c_str(), &MsiMessageCallback);
106
+
107
+ if (exitCode != 0)
108
+ {
109
+ clearLogs.release();
110
+ THROW_HR_WITH_USER_ERROR(
111
+ HRESULT_FROM_WIN32(exitCode),
112
+ wsl::shared::Localization::MessageUpdateFailed(exitCode) + L"\r\n" +
113
+ wsl::shared::Localization::MessageSeeLogFile(logFile.c_str()));
114
+ }
115
+ }
116
+ else
117
+ {
118
+ // Set FILE_FLAG_DELETE_ON_CLOSE on the file to make sure it's deleted when the installation completes.
119
+ const wil::unique_hfile package{CreateFileW(
120
+ downloadPath.c_str(), DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, nullptr)};
121
+
122
+ THROW_LAST_ERROR_IF(!package);
123
+
124
+ const winrt::Windows::Management::Deployment::PackageManager packageManager;
125
+ const auto result = packageManager.AddPackageAsync(
126
+ Uri{downloadPath.c_str()}, nullptr, DeploymentOptions::ForceApplicationShutdown | DeploymentOptions::ForceTargetApplicationShutdown);
127
+
128
+ THROW_IF_FAILED(result.get().ExtendedErrorCode());
129
+
130
+ // Note: If the installation is successful, this process is expected to receive and Ctrl-C and exit
131
+ }
132
+
133
+ return 0;
134
+}
135
+
136
+void WaitForMsiInstall()
137
+{
138
+ wil::com_ptr_t<IWslInstaller> installer;
139
+
140
+ auto retry_pred = []() {
141
+ const auto errorCode = wil::ResultFromCaughtException();
142
+ return errorCode == REGDB_E_CLASSNOTREG;
143
+ };
144
+
145
+ wsl::shared::retry::RetryWithTimeout<void>(
146
+ [&installer]() { installer = wil::CoCreateInstance<IWslInstaller>(__uuidof(WslInstaller), CLSCTX_LOCAL_SERVER); },
147
+ std::chrono::seconds(1),
148
+ std::chrono::minutes(1),
149
+ retry_pred);
150
+
151
+ fputws(wsl::shared::Localization::MessageFinishMsiInstallation().c_str(), stderr);
152
+
153
+ auto finishLine = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, []() { fputws(L"\n", stderr); });
154
+
155
+ UINT exitCode = -1;
156
+ wil::unique_cotaskmem_string message{};
157
+ THROW_IF_FAILED(installer->Install(&exitCode, &message));
158
+
159
+ if (message && *message.get() != UNICODE_NULL)
160
+ {
161
+ finishLine.release();
162
+ wprintf(L"\n%ls\n", message.get());
163
+ }
164
+
165
+ if (exitCode != 0)
166
+ {
167
+ THROW_HR_WITH_USER_ERROR(HRESULT_FROM_WIN32(exitCode), wsl::shared::Localization::MessageUpdateFailed(exitCode));
168
+ }
169
+}
170
+
171
+wil::unique_handle CreateJob()
172
+{
173
+ // Create a job object that will terminate all processes in the job on
174
+ // close but will not terminate the children of the processes in the job.
175
+ // This is used to ensure that when forwarding from an inbox binary (I)
176
+ // to a lifted binary (L), if I is terminated L is terminated as well but
177
+ // any children of L (e.g. wslhost.exe) continue to run.
178
+ wil::unique_handle job{CreateJobObject(nullptr, nullptr)};
179
+ THROW_LAST_ERROR_IF_NULL(job.get());
180
+
181
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
182
+ info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
183
+ THROW_IF_WIN32_BOOL_FALSE(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &info, sizeof(info)));
184
+
185
+ return job;
186
+}
187
+
188
+int WINAPI InstallRecordHandler(void* context, UINT messageType, LPCWSTR message)
189
+{
190
+ try
191
+ {
192
+ WSL_LOG("MSIMessage", TraceLoggingValue(messageType, "type"), TraceLoggingValue(message, "message"));
193
+ auto type = (INSTALLMESSAGE)(0xFF000000 & (UINT)messageType);
194
+
195
+ if (type == INSTALLMESSAGE_ERROR || type == INSTALLMESSAGE_FATALEXIT || type == INSTALLMESSAGE_WARNING)
196
+ {
197
+ WriteInstallLog(std::format("MSI message: {}", message));
198
+ }
199
+
200
+ auto* callback = reinterpret_cast<const std::function<void(INSTALLMESSAGE, LPCWSTR)>*>(context);
201
+ if (callback != nullptr)
202
+ {
203
+ (*callback)(type, message);
204
+ }
205
+ }
206
+ CATCH_LOG();
207
+
208
+ return IDOK;
209
+}
210
+
211
+void ConfigureMsiLogging(_In_opt_ LPCWSTR LogFile, _In_ const std::function<void(INSTALLMESSAGE, LPCWSTR)>& Callback)
212
+{
213
+ if (LogFile != nullptr)
214
+ {
215
+ LOG_IF_WIN32_ERROR(MsiEnableLog(INSTALLLOGMODE_VERBOSE | INSTALLLOGMODE_EXTRADEBUG | INSTALLLOGMODE_PROGRESS, LogFile, 0));
216
+ }
217
+
218
+ MsiSetExternalUI(
219
+ &InstallRecordHandler,
220
+ INSTALLLOGMODE_FATALEXIT | INSTALLLOGMODE_ERROR | INSTALLLOGMODE_WARNING | INSTALLLOGMODE_USER | INSTALLLOGMODE_INFO |
221
+ INSTALLLOGMODE_RESOLVESOURCE | INSTALLLOGMODE_OUTOFDISKSPACE | INSTALLLOGMODE_ACTIONSTART | INSTALLLOGMODE_ACTIONDATA |
222
+ INSTALLLOGMODE_COMMONDATA | INSTALLLOGMODE_INITIALIZE | INSTALLLOGMODE_TERMINATE | INSTALLLOGMODE_SHOWDIALOG,
223
+ (void*)&Callback);
224
+
225
+ MsiSetInternalUI(INSTALLUILEVEL(INSTALLUILEVEL_NONE | INSTALLUILEVEL_UACONLY | INSTALLUILEVEL_SOURCERESONLY), nullptr);
226
+}
227
+
228
+} // namespace
229
+
230
+int wsl::windows::common::install::CallMsiPackage()
231
+{
232
+ wsl::windows::common::ExecutionContext context(wsl::windows::common::CallMsi);
233
+
234
+ auto msiPath = GetMsiPackagePath();
235
+ if (!msiPath.has_value())
236
+ {
237
+ wsl::windows::common::ExecutionContext context(wsl::windows::common::Install);
238
+
239
+ try
240
+ {
241
+ WaitForMsiInstall();
242
+ msiPath = GetMsiPackagePath();
243
+ }
244
+ catch (...)
245
+ {
246
+ LOG_CAUGHT_EXCEPTION();
247
+
248
+ // GetMsiPackagePath() will generate a user error if the registry access fails.
249
+ // Save the error from GetMsiPackagePath() to return a proper 'install failed' message.
250
+ auto savedError = context.ReportedError();
251
+
252
+ // There is a race where the service might stop before returning the install result.
253
+ // if this happens, only fail if the MSI still isn't installed.
254
+ msiPath = GetMsiPackagePath();
255
+ if (!msiPath.has_value())
256
+ {
257
+ // Offer to directly install the MSI package if the MsixInstaller logic fails
258
+ // This can trigger a UAC so only do it
259
+ if (IsInteractiveConsole())
260
+ {
261
+ auto errorCode = savedError.has_value() ? ErrorToString(savedError.value()).Code
262
+ : ErrorCodeToString(wil::ResultFromCaughtException());
263
+
264
+ EMIT_USER_WARNING(wsl::shared::Localization::MessageInstallationCorrupted(errorCode));
265
+
266
+ if (PromptForKeyPressWithTimeout())
267
+ {
268
+ return UpdatePackage(false, true);
269
+ }
270
+ }
271
+
272
+ if (savedError.has_value())
273
+ {
274
+ THROW_HR_WITH_USER_ERROR(savedError->Code, savedError->Message.value_or(L""));
275
+ }
276
+
277
+ throw;
278
+ }
279
+ }
280
+
281
+ THROW_HR_IF(E_UNEXPECTED, !msiPath.has_value());
282
+ }
283
+
284
+ auto target = msiPath.value() + L"\\" WSL_BINARY_NAME;
285
+
286
+ SubProcess process(target.c_str(), GetCommandLine());
287
+ process.SetDesktopAppPolicy(PROCESS_CREATION_DESKTOP_APP_BREAKAWAY_ENABLE_PROCESS_TREE);
288
+ auto runningProcess = process.Start();
289
+
290
+ // N.B. The job cannot be assigned at process creation time as the packaged process
291
+ // creation path will assign the new process to a per package job object.
292
+ // In the case of multiple processes running in a single package, assigning
293
+ // the new process to the per package job object will fail for the second request
294
+ // since both jobs already have processes which prevents a job hierarchy from
295
+ // being established.
296
+ auto job = CreateJob();
297
+
298
+ // Assign the process to the job, ignoring failures when the process has
299
+ // terminated.
300
+ //
301
+ // N.B. Assigning the job after process creation without CREATE_SUSPENDED is
302
+ // safe to do here since only the new child process will be in the job
303
+ // object. None of the grandchildren processes are included since the
304
+ // job is created with JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK.
305
+ if (!AssignProcessToJobObject(job.get(), runningProcess.get()))
306
+ {
307
+ auto lastError = GetLastError();
308
+ if (lastError != ERROR_ACCESS_DENIED)
309
+ {
310
+ THROW_WIN32(lastError);
311
+ }
312
+ }
313
+
314
+ return static_cast<int>(SubProcess::GetExitCode(runningProcess.get()));
315
+}
316
+
317
+void wsl::windows::common::install::MsiMessageCallback(INSTALLMESSAGE type, LPCWSTR message)
318
+{
319
+ switch (type)
320
+ {
321
+ case INSTALLMESSAGE_ERROR:
322
+ case INSTALLMESSAGE_FATALEXIT:
323
+ case INSTALLMESSAGE_WARNING:
324
+ wprintf(L"%ls\n", message);
325
+ break;
326
+
327
+ default:
328
+ break;
329
+ }
330
+}
331
+
332
+int wsl::windows::common::install::UpdatePackage(bool PreRelease, bool Repair)
333
+{
334
+ // Register a console control handler so "^C" is not printed when the app platform terminates the process.
335
+ THROW_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(
336
+ [](DWORD ctrlType) {
337
+ if (ctrlType == CTRL_C_EVENT)
338
+ {
339
+ ExitProcess(0);
340
+ }
341
+ return FALSE;
342
+ },
343
+ TRUE));
344
+
345
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [] { SetConsoleCtrlHandler(nullptr, FALSE); });
346
+
347
+ try
348
+ {
349
+ return UpdatePackageImpl(PreRelease, Repair);
350
+ }
351
+ catch (...)
352
+ {
353
+ // Rethrowing via WIL is required for the error context to be properly set in case a winrt exception was thrown.
354
+ THROW_HR(wil::ResultFromCaughtException());
355
+ }
356
+}
357
+
358
+UINT wsl::windows::common::install::UpgradeViaMsi(
359
+ _In_ LPCWSTR PackageLocation, _In_opt_ LPCWSTR ExtraArgs, _In_opt_ LPCWSTR LogFile, _In_ const std::function<void(INSTALLMESSAGE, LPCWSTR)>& Callback)
360
+{
361
+ WriteInstallLog(std::format("Upgrading via MSI package: {}. Args: {}", PackageLocation, ExtraArgs != nullptr ? ExtraArgs : L""));
362
+
363
+ ConfigureMsiLogging(LogFile, Callback);
364
+
365
+ auto result = MsiInstallProduct(PackageLocation, ExtraArgs);
366
+ WSL_LOG(
367
+ "MsiInstallResult",
368
+ TraceLoggingValue(result, "result"),
369
+ TraceLoggingValue(ExtraArgs != nullptr ? ExtraArgs : L"", "ExtraArgs"));
370
+
371
+ WriteInstallLog(std::format("MSI upgrade result: {}", result));
372
+
373
+ return result;
374
+}
375
+
376
+UINT wsl::windows::common::install::UninstallViaMsi(_In_opt_ LPCWSTR LogFile, _In_ const std::function<void(INSTALLMESSAGE, LPCWSTR)>& Callback)
377
+{
378
+ const auto key = OpenLxssMachineKey(KEY_READ);
379
+ const auto productCode = ReadString(key.get(), L"Msi", L"ProductCode", nullptr);
380
+
381
+ WriteInstallLog(std::format("Uninstalling MSI package: {}", productCode));
382
+
383
+ ConfigureMsiLogging(LogFile, Callback);
384
+
385
+ auto result = MsiConfigureProduct(productCode.c_str(), 0, INSTALLSTATE_ABSENT);
386
+ WSL_LOG("MsiUninstallResult", TraceLoggingValue(result, "result"));
387
+
388
+ WriteInstallLog(std::format("MSI package uninstall result: {}", result));
389
+
390
+ return result;
391
+}
392
+
393
+wil::unique_hfile wsl::windows::common::install::ValidateFileSignature(LPCWSTR Path)
394
+{
395
+ wil::unique_hfile fileHandle{CreateFileW(Path, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr)};
396
+ THROW_LAST_ERROR_IF(!fileHandle);
397
+
398
+ GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
399
+ WINTRUST_DATA trust{};
400
+ trust.cbStruct = sizeof(trust);
401
+ trust.dwUIChoice = WTD_UI_NONE;
402
+ trust.dwUnionChoice = WTD_CHOICE_FILE;
403
+ trust.dwStateAction = WTD_STATEACTION_VERIFY;
404
+
405
+ WINTRUST_FILE_INFO file = {0};
406
+ file.cbStruct = sizeof(file);
407
+ file.hFile = fileHandle.get();
408
+ trust.pFile = &file;
409
+
410
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
411
+ trust.dwStateAction = WTD_STATEACTION_CLOSE;
412
+ WinVerifyTrust(nullptr, &action, &trust);
413
+ });
414
+
415
+ THROW_IF_WIN32_ERROR(WinVerifyTrust(nullptr, &action, &trust));
416
+
417
+ return fileHandle;
418
+}
419
+
420
+void wsl::windows::common::install::WriteInstallLog(const std::string& Content)
421
+try
422
+{
423
+ static std::wstring path = wil::GetWindowsDirectoryW<std::wstring>() + L"\\temp\\wsl-install-log.txt";
424
+
425
+ // Wait up to 10 seconds for the log file mutex
426
+ wil::unique_handle mutex{CreateMutex(nullptr, true, L"Global\\WslInstallLog")};
427
+ THROW_LAST_ERROR_IF(!mutex);
428
+
429
+ THROW_LAST_ERROR_IF(WaitForSingleObject(mutex.get(), 10 * 1000) != WAIT_OBJECT_0);
430
+
431
+ wil::unique_handle file{CreateFile(
432
+ path.c_str(), GENERIC_ALL, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, 0, nullptr)};
433
+
434
+ THROW_LAST_ERROR_IF(!file);
435
+
436
+ LARGE_INTEGER size{};
437
+ THROW_IF_WIN32_BOOL_FALSE(GetFileSizeEx(file.get(), &size));
438
+
439
+ // Append to the file if its size is below 10MB, otherwise truncate.
440
+ if (size.QuadPart < 10 * _1MB)
441
+ {
442
+ THROW_LAST_ERROR_IF(SetFilePointer(file.get(), 0, nullptr, FILE_END) == INVALID_SET_FILE_POINTER);
443
+ }
444
+ else
445
+ {
446
+ THROW_IF_WIN32_BOOL_FALSE(SetEndOfFile(file.get()));
447
+ }
448
+
449
+ static auto processName = wil::GetModuleFileNameW<std::wstring>();
450
+ auto logLine = std::format("{:%FT%TZ} {}[{}]: {}\n", std::chrono::system_clock::now(), processName, WSL_PACKAGE_VERSION, Content);
451
+
452
+ DWORD bytesWritten{};
453
+ THROW_IF_WIN32_BOOL_FALSE(WriteFile(file.get(), logLine.c_str(), static_cast<DWORD>(logLine.size()), &bytesWritten, nullptr));
454
+}
455
+CATCH_LOG();