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