ShellExecuteInstallerHandler.cpp (24460B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ShellExecuteInstallerHandler.h" 5 #include <AppInstallerFileLogger.h> 6 #include <AppInstallerRuntime.h> 7 #include <winget/Filesystem.h> 8 9 using namespace AppInstaller::CLI; 10 using namespace AppInstaller::Utility; 11 using namespace AppInstaller::Manifest; 12 using namespace AppInstaller::Repository; 13 14 namespace AppInstaller::CLI::Workflow 15 { 16 namespace 17 { 18 // ShellExecutes the given path. 19 std::optional<DWORD> InvokeShellExecuteEx(const std::filesystem::path& filePath, const std::string& args, bool useRunAs, int show, IProgressCallback& progress) 20 { 21 AICLI_LOG(CLI, Info, << "Starting: '" << filePath.u8string() << "' with arguments '" << args << '\''); 22 23 SHELLEXECUTEINFOW execInfo = { 0 }; 24 execInfo.cbSize = sizeof(execInfo); 25 execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; 26 execInfo.lpFile = filePath.c_str(); 27 std::wstring argsUtf16 = Utility::ConvertToUTF16(args); 28 execInfo.lpParameters = argsUtf16.c_str(); 29 execInfo.nShow = show; 30 31 // This installer must be run elevated, but we are not currently. 32 // Have ShellExecute elevate the installer since it won't do so itself. 33 if (useRunAs) 34 { 35 execInfo.lpVerb = L"runas"; 36 } 37 38 THROW_LAST_ERROR_IF(!ShellExecuteExW(&execInfo) || !execInfo.hProcess); 39 40 wil::unique_process_handle process{ execInfo.hProcess }; 41 42 // Wait for installation to finish 43 while (!progress.IsCancelledBy(CancelReason::User)) 44 { 45 DWORD waitResult = WaitForSingleObject(process.get(), 250); 46 if (waitResult == WAIT_OBJECT_0) 47 { 48 break; 49 } 50 if (waitResult != WAIT_TIMEOUT) 51 { 52 THROW_LAST_ERROR_MSG("Unexpected WaitForSingleObjectResult: %lu", waitResult); 53 } 54 } 55 56 if (progress.IsCancelledBy(CancelReason::Any)) 57 { 58 return {}; 59 } 60 else 61 { 62 DWORD exitCode = 0; 63 GetExitCodeProcess(process.get(), &exitCode); 64 return exitCode; 65 } 66 } 67 68 std::optional<DWORD> InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) 69 { 70 // Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete. 71 // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. 72 return InvokeShellExecuteEx(filePath, args, false, SW_SHOW, progress); 73 } 74 75 // Gets the escaped installer args. 76 std::string GetInstallerArgsTemplate(Execution::Context& context) 77 { 78 bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); 79 bool isRepair = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseRepair); 80 81 const auto& installer = context.Get<Execution::Data::Installer>(); 82 const auto& installerSwitches = installer->Switches; 83 std::string installerArgs = {}; 84 85 // Construct install experience arg. 86 // SilentWithProgress is default, so look for it first. 87 auto experienceArgsItr = installerSwitches.find(InstallerSwitchType::SilentWithProgress); 88 89 if (context.Args.Contains(Execution::Args::Type::Interactive)) 90 { 91 // If interactive requested, always use Interactive (or nothing). If the installer supports 92 // interactive it is usually the default, and thus it is cumbersome to put a blank entry in 93 // the manifest. 94 experienceArgsItr = installerSwitches.find(InstallerSwitchType::Interactive); 95 } 96 // If no SilentWithProgress exists, or Silent requested, try to find Silent. 97 else if (experienceArgsItr == installerSwitches.end() || context.Args.Contains(Execution::Args::Type::Silent)) 98 { 99 auto silentItr = installerSwitches.find(InstallerSwitchType::Silent); 100 // If Silent requested, but doesn't exist, then continue using SilentWithProgress. 101 if (silentItr != installerSwitches.end()) 102 { 103 experienceArgsItr = silentItr; 104 } 105 } 106 107 if (experienceArgsItr != installerSwitches.end()) 108 { 109 installerArgs += experienceArgsItr->second; 110 } 111 112 // Construct log path arg. 113 if (installerSwitches.find(InstallerSwitchType::Log) != installerSwitches.end()) 114 { 115 installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Log); 116 } 117 118 // Construct repair arg. Custom switches and other args are not applicable for repair scenario so we can return here. 119 if (isRepair) 120 { 121 if (installerSwitches.find(InstallerSwitchType::Repair) != installerSwitches.end()) 122 { 123 installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Repair); 124 } 125 126 return installerArgs; 127 } 128 129 // Construct custom arg. 130 if (installerSwitches.find(InstallerSwitchType::Custom) != installerSwitches.end()) 131 { 132 installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Custom); 133 } 134 135 // Construct custom arg passed in by cli arg 136 if (context.Args.Contains(Execution::Args::Type::CustomSwitches)) 137 { 138 std::string_view customSwitches = context.Args.GetArg(Execution::Args::Type::CustomSwitches); 139 // Since these arguments are appended to the installer at runtime, it doesn't make sense to append them if empty or whitespace 140 if (!Utility::IsEmptyOrWhitespace(customSwitches)) 141 { 142 installerArgs += ' ' + std::string{ customSwitches }; 143 } 144 } 145 146 // Construct update arg if applicable 147 if (isUpdate && installerSwitches.find(InstallerSwitchType::Update) != installerSwitches.end()) 148 { 149 installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::Update); 150 } 151 152 // Construct install location arg if necessary. 153 if (context.Args.Contains(Execution::Args::Type::InstallLocation) && 154 installerSwitches.find(InstallerSwitchType::InstallLocation) != installerSwitches.end()) 155 { 156 installerArgs += ' ' + installerSwitches.at(InstallerSwitchType::InstallLocation); 157 } 158 159 return installerArgs; 160 } 161 162 // Applies values to the template. 163 void PopulateInstallerArgsTemplate(Execution::Context& context, std::string& installerArgs) 164 { 165 // Populate <LogPath> with value from command line or temp path. 166 std::string logPath; 167 if (context.Args.Contains(Execution::Args::Type::Log)) 168 { 169 logPath = context.Args.GetArg(Execution::Args::Type::Log); 170 } 171 else 172 { 173 const auto& manifest = context.Get<Execution::Data::Manifest>(); 174 175 auto path = Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation); 176 path /= Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); 177 path += '-'; 178 path += Utility::GetCurrentTimeForFilename(true); 179 path += Logging::FileLogger::DefaultExt(); 180 181 logPath = path.u8string(); 182 } 183 184 if (Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_LOGPATH), logPath)) 185 { 186 context.Add<Execution::Data::LogPath>(Utility::ConvertToUTF16(logPath)); 187 } 188 189 // Populate <InstallPath> with value from command line. 190 if (context.Args.Contains(Execution::Args::Type::InstallLocation)) 191 { 192 Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_INSTALLPATH), context.Args.GetArg(Execution::Args::Type::InstallLocation)); 193 } 194 195 // Todo: language token support will be implemented later 196 } 197 198 // Gets the arguments for uninstalling an MSI with MsiExec 199 std::string GetMsiExecUninstallArgs(Execution::Context& context, const Utility::LocIndString& productCode) 200 { 201 std::string args = "/x" + productCode.get(); 202 203 // https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options 204 if (context.Args.Contains(Execution::Args::Type::Silent)) 205 { 206 args += " /quiet /norestart"; 207 } 208 else if (!context.Args.Contains(Execution::Args::Type::Interactive)) 209 { 210 args += " /passive /norestart"; 211 } 212 213 return args; 214 } 215 216 // Gets the arguments for repairing an MSI with MsiExec 217 std::string GetMsiExecRepairArgs(Execution::Context& context, const Utility::LocIndString& productCode) 218 { 219 // https://learn.microsoft.com/en-us/windows/win32/msi/command-line-options 220 // Available Options for '/f [p|o|e|d|c|a|u|m|s|v] <Product.msi | ProductCode>' 221 // Default parameter for '/f' is 'omus' 222 // o - Reinstall all files regardless of version 223 // m - Rewrite all required registry entries (This is the default option) 224 // u - Rewrite all required user-specific registry entries (This is the default option) 225 // s - Overwrite all existing shortcuts (This is the default option) 226 std::string args = "/f " + productCode.get(); 227 228 // https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options 229 if (context.Args.Contains(Execution::Args::Type::Silent)) 230 { 231 args += " /quiet /norestart"; 232 } 233 else if (!context.Args.Contains(Execution::Args::Type::Interactive)) 234 { 235 args += " /passive /norestart"; 236 } 237 238 return args; 239 } 240 } 241 242 void ShellExecuteInstallImpl(Execution::Context& context) 243 { 244 bool isRepair = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseRepair); 245 246 if (isRepair) 247 { 248 context.Reporter.Info() << Resource::String::RepairFlowStartingPackageRepair << std::endl; 249 } 250 else 251 { 252 context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl; 253 } 254 255 const auto& installer = context.Get<Execution::Data::Installer>(); 256 const std::string& installerArgs = context.Get<Execution::Data::InstallerArgs>(); 257 258 // Inform of elevation requirements 259 bool isElevated = Runtime::IsRunningAsAdmin(); 260 261 // The installer will run elevated, either by direct request or through the installer itself doing so. 262 if ((installer->ElevationRequirement == ElevationRequirementEnum::ElevationRequired || 263 installer->ElevationRequirement == ElevationRequirementEnum::ElevatesSelf) 264 && !isElevated) 265 { 266 context.Reporter.Warn() << Resource::String::InstallerElevationExpected << std::endl; 267 } 268 269 // Some installers force UI. Setting to SW_HIDE will hide installer UI and installation will never complete. 270 // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. 271 auto installResult = context.Reporter.ExecuteWithProgress( 272 std::bind(InvokeShellExecuteEx, 273 context.Get<Execution::Data::InstallerPath>(), 274 installerArgs, 275 installer->ElevationRequirement == ElevationRequirementEnum::ElevationRequired && !isElevated, 276 SW_SHOW, 277 std::placeholders::_1)); 278 279 if (!installResult) 280 { 281 if (isRepair) 282 { 283 context.Reporter.Warn() << Resource::String::RepairAbandoned << std::endl; 284 } 285 else 286 { 287 context.Reporter.Warn() << Resource::String::InstallAbandoned << std::endl; 288 } 289 290 AICLI_TERMINATE_CONTEXT(E_ABORT); 291 } 292 else 293 { 294 context.Add<Execution::Data::OperationReturnCode>(installResult.value()); 295 } 296 } 297 298 void GetInstallerArgs(Execution::Context& context) 299 { 300 // If override switch is specified, use the override value as installer args. 301 if (context.Args.Contains(Execution::Args::Type::Override)) 302 { 303 context.Add<Execution::Data::InstallerArgs>(std::string{ context.Args.GetArg(Execution::Args::Type::Override) }); 304 return; 305 } 306 307 std::string installerArgs = GetInstallerArgsTemplate(context); 308 309 PopulateInstallerArgsTemplate(context, installerArgs); 310 311 AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs); 312 context.Add<Execution::Data::InstallerArgs>(std::move(installerArgs)); 313 } 314 315 void ShellExecuteUninstallImpl(Execution::Context& context) 316 { 317 context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; 318 std::wstring commandUtf16 = Utility::ConvertToUTF16(context.Get<Execution::Data::UninstallString>()); 319 320 // Parse the command string as application and command line for CreateProcess 321 wil::unique_cotaskmem_string app = nullptr; 322 wil::unique_cotaskmem_string args = nullptr; 323 THROW_IF_FAILED(SHEvaluateSystemCommandTemplate(commandUtf16.c_str(), &app, NULL, &args)); 324 325 auto uninstallResult = context.Reporter.ExecuteWithProgress( 326 std::bind(InvokeShellExecute, 327 std::filesystem::path(app.get()), 328 Utility::ConvertToUTF8(args.get()), 329 std::placeholders::_1)); 330 331 if (!uninstallResult) 332 { 333 context.Reporter.Warn() << Resource::String::UninstallAbandoned << std::endl; 334 AICLI_TERMINATE_CONTEXT(E_ABORT); 335 } 336 else 337 { 338 context.Add<Execution::Data::OperationReturnCode>(uninstallResult.value()); 339 } 340 } 341 342 void ShellExecuteRepairImpl(Execution::Context& context) 343 { 344 context.Reporter.Info() << Resource::String::RepairFlowStartingPackageRepair << std::endl; 345 346 std::wstring commandUtf16 = Utility::ConvertToUTF16(context.Get<Execution::Data::RepairString>()); 347 348 // When running as admin, block attempt to repair user scope installed package. 349 // [NOTE:] This check is to address the security concern related to above scenario. 350 if (Runtime::IsRunningAsAdmin()) 351 { 352 auto installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 353 const std::string installedScopeString = installedPackageVersion->GetMetadata()[PackageVersionMetadata::InstalledScope]; 354 auto scopeEnum = ConvertToScopeEnum(installedScopeString); 355 356 if (scopeEnum == ScopeEnum::User) 357 { 358 context.Reporter.Error() << Resource::String::NoAdminRepairForUserScopePackage << std::endl; 359 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED); 360 } 361 } 362 363 // Parse the command string as application and command line for CreateProcess 364 wil::unique_cotaskmem_string app = nullptr; 365 wil::unique_cotaskmem_string args = nullptr; 366 THROW_IF_FAILED(SHEvaluateSystemCommandTemplate(commandUtf16.c_str(), &app, NULL, &args)); 367 368 auto repairResult = context.Reporter.ExecuteWithProgress( 369 std::bind(InvokeShellExecute, 370 std::filesystem::path(app.get()), 371 Utility::ConvertToUTF8(args.get()), 372 std::placeholders::_1)); 373 374 if (!repairResult) 375 { 376 context.Reporter.Error() << Resource::String::RepairAbandoned << std::endl; 377 AICLI_TERMINATE_CONTEXT(E_ABORT); 378 } 379 else 380 { 381 context.Add<Execution::Data::OperationReturnCode>(repairResult.value()); 382 } 383 } 384 385 void ShellExecuteMsiExecUninstall(Execution::Context& context) 386 { 387 const auto& productCodes = context.Get<Execution::Data::ProductCodes>(); 388 context.Reporter.Info() << Resource::String::UninstallFlowStartingPackageUninstall << std::endl; 389 390 const std::filesystem::path msiexecPath{ ExpandEnvironmentVariables(L"%windir%\\system32\\msiexec.exe") }; 391 392 for (const auto& productCode : productCodes) 393 { 394 AICLI_LOG(CLI, Info, << "Removing: " << productCode); 395 auto uninstallResult = context.Reporter.ExecuteWithProgress( 396 std::bind(InvokeShellExecute, 397 msiexecPath, 398 GetMsiExecUninstallArgs(context, productCode), 399 std::placeholders::_1)); 400 401 if (!uninstallResult) 402 { 403 context.Reporter.Error() << Resource::String::UninstallAbandoned << std::endl; 404 AICLI_TERMINATE_CONTEXT(E_ABORT); 405 } 406 else 407 { 408 context.Add<Execution::Data::OperationReturnCode>(uninstallResult.value()); 409 } 410 } 411 } 412 413 void ShellExecuteMsiExecRepair(Execution::Context& context) 414 { 415 const auto& productCodes = context.Get<Execution::Data::ProductCodes>(); 416 context.Reporter.Info() << Resource::String::RepairFlowStartingPackageRepair << std::endl; 417 418 const std::filesystem::path msiexecPath{ ExpandEnvironmentVariables(L"%windir%\\system32\\msiexec.exe") }; 419 420 for (const auto& productCode : productCodes) 421 { 422 AICLI_LOG(CLI, Info, << "Repairing: " << productCode); 423 auto repairResult = context.Reporter.ExecuteWithProgress( 424 std::bind(InvokeShellExecute, 425 msiexecPath, 426 GetMsiExecRepairArgs(context, productCode), 427 std::placeholders::_1)); 428 429 if (!repairResult) 430 { 431 context.Reporter.Error() << Resource::String::RepairAbandoned << std::endl; 432 AICLI_TERMINATE_CONTEXT(E_ABORT); 433 } 434 else 435 { 436 context.Add<Execution::Data::OperationReturnCode>(repairResult.value()); 437 } 438 } 439 } 440 441 #ifndef AICLI_DISABLE_TEST_HOOKS 442 std::optional<DWORD> s_EnableWindowsFeatureResult_Override{}; 443 444 void TestHook_SetEnableWindowsFeatureResult_Override(std::optional<DWORD>&& result) 445 { 446 s_EnableWindowsFeatureResult_Override = std::move(result); 447 } 448 449 std::optional<DWORD> s_DoesWindowsFeatureExistResult_Override{}; 450 451 void TestHook_SetDoesWindowsFeatureExistResult_Override(std::optional<DWORD>&& result) 452 { 453 s_DoesWindowsFeatureExistResult_Override = std::move(result); 454 } 455 #endif 456 457 std::filesystem::path GetDismExecutablePath() 458 { 459 return AppInstaller::Filesystem::GetExpandedPath("%windir%\\system32\\dism.exe"); 460 } 461 462 std::optional<DWORD> DoesWindowsFeatureExist(Execution::Context& context, std::string_view featureName) 463 { 464 #ifndef AICLI_DISABLE_TEST_HOOKS 465 if (s_DoesWindowsFeatureExistResult_Override) 466 { 467 return s_DoesWindowsFeatureExistResult_Override; 468 } 469 #endif 470 471 std::string args = "/Online /Get-FeatureInfo /FeatureName:" + std::string{ featureName }; 472 auto dismExecPath = GetDismExecutablePath(); 473 474 auto getFeatureInfoResult = context.Reporter.ExecuteWithProgress( 475 std::bind(InvokeShellExecuteEx, 476 dismExecPath, 477 args, 478 false, 479 SW_HIDE, 480 std::placeholders::_1)); 481 482 return getFeatureInfoResult; 483 } 484 485 std::optional<DWORD> EnableWindowsFeature(Execution::Context& context, std::string_view featureName) 486 { 487 #ifndef AICLI_DISABLE_TEST_HOOKS 488 if (s_EnableWindowsFeatureResult_Override) 489 { 490 return s_EnableWindowsFeatureResult_Override; 491 } 492 #endif 493 494 std::string args = "/Online /Enable-Feature /NoRestart /FeatureName:" + std::string{ featureName }; 495 auto dismExecPath = GetDismExecutablePath(); 496 497 AICLI_LOG(Core, Info, << "Enabling Windows Feature [" << featureName << "]"); 498 499 auto enableFeatureResult = context.Reporter.ExecuteWithProgress( 500 std::bind(InvokeShellExecuteEx, 501 dismExecPath, 502 args, 503 false, 504 SW_HIDE, 505 std::placeholders::_1)); 506 507 return enableFeatureResult; 508 } 509 510 void ShellExecuteEnableWindowsFeature::operator()(Execution::Context& context) const 511 { 512 Utility::LocIndView locIndFeatureName{ m_featureName }; 513 514 std::optional<DWORD> doesFeatureExistResult = DoesWindowsFeatureExist(context, m_featureName); 515 516 if (!doesFeatureExistResult) 517 { 518 AICLI_TERMINATE_CONTEXT(E_ABORT); 519 } 520 else if (doesFeatureExistResult.value() != ERROR_SUCCESS) 521 { 522 context.Add<Execution::Data::OperationReturnCode>(doesFeatureExistResult.value()); 523 return; 524 } 525 526 context.Reporter.Info() << Resource::String::EnablingWindowsFeature(locIndFeatureName) << std::endl; 527 528 std::optional<DWORD> enableFeatureResult = EnableWindowsFeature(context, m_featureName); 529 530 if (!enableFeatureResult) 531 { 532 AICLI_TERMINATE_CONTEXT(E_ABORT); 533 } 534 else 535 { 536 context.Add<Execution::Data::OperationReturnCode>(enableFeatureResult.value()); 537 } 538 } 539 540 #ifndef AICLI_DISABLE_TEST_HOOKS 541 std::optional<DWORD> s_ExtractArchiveWithTarResult_Override{}; 542 543 void TestHook_SetExtractArchiveWithTarResult_Override(std::optional<DWORD>&& result) 544 { 545 s_ExtractArchiveWithTarResult_Override = std::move(result); 546 } 547 #endif 548 549 void ShellExecuteExtractArchive::operator()(Execution::Context& context) const 550 { 551 auto tarExecPath = AppInstaller::Filesystem::GetExpandedPath("%windir%\\system32\\tar.exe"); 552 553 std::string args = "-xf \"" + m_archivePath.u8string() + "\" -C \"" + m_destPath.u8string() + "\""; 554 555 std::optional<DWORD> extractArchiveResult; 556 #ifndef AICLI_DISABLE_TEST_HOOKS 557 if (s_ExtractArchiveWithTarResult_Override) 558 { 559 extractArchiveResult = *s_ExtractArchiveWithTarResult_Override; 560 } 561 else 562 #endif 563 { 564 extractArchiveResult = context.Reporter.ExecuteWithProgress( 565 std::bind(InvokeShellExecuteEx, 566 tarExecPath, 567 args, 568 false, 569 SW_HIDE, 570 std::placeholders::_1)); 571 } 572 573 if (!extractArchiveResult) 574 { 575 AICLI_TERMINATE_CONTEXT(E_ABORT); 576 } 577 578 if (extractArchiveResult.value() == ERROR_SUCCESS) 579 { 580 AICLI_LOG(CLI, Info, << "Successfully extracted archive"); 581 context.Reporter.Info() << Resource::String::ExtractArchiveSucceeded << std::endl; 582 } 583 else 584 { 585 AICLI_LOG(CLI, Info, << "Failed to extract archive with exit code " << extractArchiveResult.value()); 586 context.Reporter.Error() << Resource::String::ExtractArchiveFailed << std::endl; 587 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED); 588 } 589 } 590 }