RepairFlow.cpp (23905B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "RepairFlow.h" 5 #include "Workflows/ShellExecuteInstallerHandler.h" 6 #include "Workflows/WorkflowBase.h" 7 #include "Workflows/DownloadFlow.h" 8 #include "Workflows/ArchiveFlow.h" 9 #include "Workflows/InstallFlow.h" 10 #include "Workflows/PromptFlow.h" 11 #include "winget/ManifestCommon.h" 12 #include "AppInstallerDeployment.h" 13 #include "AppInstallerMsixInfo.h" 14 #include "AppInstallerSynchronization.h" 15 #include "MSStoreInstallerHandler.h" 16 #include <winget/ManifestComparator.h> 17 #include <winget/PackageVersionSelection.h> 18 19 using namespace AppInstaller::Manifest; 20 using namespace AppInstaller::Msix; 21 using namespace AppInstaller::Repository; 22 using namespace AppInstaller::CLI::Execution; 23 24 namespace AppInstaller::CLI::Workflow 25 { 26 // Internal implementation details 27 namespace 28 { 29 30 void SetUninstallStringInContext(Execution::Context& context) 31 { 32 const auto& installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 33 IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); 34 35 // Default to silent unless it is not present or interactivity is requested 36 auto uninstallCommandItr = packageMetadata.find(PackageVersionMetadata::SilentUninstallCommand); 37 38 if ((!context.Args.Contains(Execution::Args::Type::Silent) && uninstallCommandItr == packageMetadata.end()) 39 || context.Args.Contains(Execution::Args::Type::Interactive)) 40 { 41 auto interactiveItr = packageMetadata.find(PackageVersionMetadata::StandardUninstallCommand); 42 if (interactiveItr != packageMetadata.end()) 43 { 44 uninstallCommandItr = interactiveItr; 45 } 46 } 47 48 if (uninstallCommandItr == packageMetadata.end()) 49 { 50 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 51 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 52 } 53 54 context.Add<Execution::Data::UninstallString>(uninstallCommandItr->second); 55 } 56 57 void SetModifyPathInContext(Execution::Context& context) 58 { 59 const auto& installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 60 IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); 61 62 // Default to silent unless it is not present or interactivity is requested 63 auto modifyPathItr = packageMetadata.find(PackageVersionMetadata::StandardModifyCommand); 64 if (modifyPathItr == packageMetadata.end()) 65 { 66 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 67 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 68 } 69 70 context.Add<Execution::Data::ModifyPath>(modifyPathItr->second); 71 } 72 73 void SetProductCodesInContext(Execution::Context& context) 74 { 75 const auto& installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 76 auto productCodes = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); 77 78 if (productCodes.empty()) 79 { 80 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 81 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 82 } 83 84 context.Add<Execution::Data::ProductCodes>(productCodes); 85 } 86 87 void SetPackageFamilyNamesInContext(Execution::Context& context) 88 { 89 const auto& installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 90 91 auto packageFamilyNames = installedPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName); 92 if (packageFamilyNames.empty()) 93 { 94 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 95 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 96 } 97 98 context.Add<Execution::Data::PackageFamilyNames>(packageFamilyNames); 99 } 100 101 InstallerTypeEnum GetInstalledType(Execution::Context& context) 102 { 103 const auto& installedPackage = context.Get<Execution::Data::InstalledPackageVersion>(); 104 std::string installedType = installedPackage->GetMetadata()[PackageVersionMetadata::InstalledType]; 105 return ConvertToInstallerTypeEnum(installedType); 106 } 107 108 void ApplicabilityCheckForInstalledPackage(Execution::Context& context) 109 { 110 // Installed Package repair applicability check 111 const auto& installedPackageVersion = context.Get<Execution::Data::InstalledPackageVersion>(); 112 113 const std::string installerType = context.Get<Execution::Data::InstalledPackageVersion>()->GetMetadata()[PackageVersionMetadata::InstalledType]; 114 InstallerTypeEnum installerTypeEnum = ConvertToInstallerTypeEnum(installerType); 115 116 if (installerTypeEnum == InstallerTypeEnum::Portable || installerTypeEnum == InstallerTypeEnum::Unknown) 117 { 118 context.Reporter.Error() << Resource::String::RepairOperationNotSupported << std::endl; 119 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED); 120 } 121 122 IPackageVersion::Metadata packageMetadata = installedPackageVersion->GetMetadata(); 123 124 auto noModifyItr = packageMetadata.find(PackageVersionMetadata::NoModify); 125 std::string noModifyARPFlag = noModifyItr != packageMetadata.end() ? noModifyItr->second : std::string(); 126 127 auto noRepairItr = packageMetadata.find(PackageVersionMetadata::NoRepair); 128 std::string noRepairARPFlag = noRepairItr != packageMetadata.end() ? noRepairItr->second : std::string(); 129 130 if (Utility::IsDwordFlagSet(noModifyARPFlag) || Utility::IsDwordFlagSet(noRepairARPFlag)) 131 { 132 context.Reporter.Error() << Resource::String::RepairOperationNotSupported << std::endl; 133 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED); 134 } 135 } 136 137 void ApplicabilityCheckForAvailablePackage(Execution::Context& context) 138 { 139 // Skip the Available Package applicability check for MSI and MSIX repair as they aren't needed. 140 if (!context.Contains(Execution::Data::Installer)) 141 { 142 return; 143 } 144 145 // Selected Installer repair applicability check 146 auto installerType = context.Get<Execution::Data::Installer>()->EffectiveInstallerType(); 147 auto repairBehavior = context.Get<Execution::Data::Installer>()->RepairBehavior; 148 149 if (installerType == InstallerTypeEnum::Portable || installerType == InstallerTypeEnum::Unknown) 150 { 151 context.Reporter.Error() << Resource::String::RepairOperationNotSupported << std::endl; 152 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_REPAIR_NOT_SUPPORTED); 153 } 154 155 // Repair behavior is required for Burn, Inno, Nullsoft, Exe installers 156 if (DoesInstallerTypeRequireRepairBehaviorForRepair(installerType) && 157 repairBehavior == RepairBehaviorEnum::Unknown) 158 { 159 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 160 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 161 } 162 } 163 164 void HandleModifyRepairBehavior(Execution::Context& context, std::string& repairCommand) 165 { 166 SetModifyPathInContext(context); 167 repairCommand += context.Get<Execution::Data::ModifyPath>(); 168 } 169 170 void HandleInstallerRepairBehavior(Execution::Context& context, InstallerTypeEnum installerType) 171 { 172 context << 173 ShowInstallationDisclaimer << 174 ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << 175 DownloadInstaller; 176 177 if (installerType == InstallerTypeEnum::Zip) 178 { 179 context << 180 ScanArchiveFromLocalManifest << 181 ExtractFilesFromArchive << 182 VerifyAndSetNestedInstaller; 183 } 184 } 185 186 void HandleUninstallerRepairBehavior(Execution::Context& context, std::string& repairCommand) 187 { 188 SetUninstallStringInContext(context); 189 repairCommand += context.Get<Execution::Data::UninstallString>(); 190 } 191 192 void GenerateRepairString(Execution::Context& context) 193 { 194 const auto& installer = context.Get<Execution::Data::Installer>(); 195 auto installerType = installer->BaseInstallerType; 196 auto repairBehavior = installer->RepairBehavior; 197 198 std::string repairCommand; 199 200 switch (repairBehavior) 201 { 202 case RepairBehaviorEnum::Modify: 203 HandleModifyRepairBehavior(context, repairCommand); 204 break; 205 case RepairBehaviorEnum::Installer: 206 HandleInstallerRepairBehavior(context, installerType); 207 break; 208 case RepairBehaviorEnum::Uninstaller: 209 HandleUninstallerRepairBehavior(context, repairCommand); 210 break; 211 case RepairBehaviorEnum::Unknown: 212 default: 213 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 214 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 215 } 216 217 context << 218 GetInstallerArgs; 219 220 // If the repair behavior is set to 'Installer', we can proceed with the repair command as is. 221 // For repair behaviors other than 'Installer', subsequent steps will be necessary to prepare the repair command. 222 if (repairBehavior == RepairBehaviorEnum::Installer) 223 { 224 return; 225 } 226 227 if (repairCommand.empty()) 228 { 229 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 230 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 231 } 232 233 repairCommand += " "; 234 repairCommand += context.Get<Execution::Data::InstallerArgs>(); 235 context.Add<Execution::Data::RepairString>(repairCommand); 236 } 237 238 bool IsInstallerMappingRequired(Execution::Context& context) 239 { 240 InstallerTypeEnum installerTypeEnum = GetInstalledType(context); 241 242 switch (installerTypeEnum) 243 { 244 case InstallerTypeEnum::Msi: 245 return false; 246 case InstallerTypeEnum::Msix: 247 // For MSIX packages that are from the Microsoft Store, selecting an installer is required. 248 if (context.Contains(Execution::Data::Package)) 249 { 250 auto availablePackages = context.Get<Execution::Data::Package>()->GetAvailable(); 251 252 if (availablePackages.size() == 1 && availablePackages[0]->GetSource() == WellKnownSource::MicrosoftStore) 253 { 254 return true; 255 } 256 } 257 258 // For MSIX packages that are not from the Microsoft Store, selecting an installer is not required. 259 return false; 260 default: 261 return true; 262 } 263 } 264 265 void HandleModifyOrUninstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) 266 { 267 context << 268 ShellExecuteRepairImpl << 269 ReportRepairResult(RepairBehaviorToString(repairBehavior), APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); 270 } 271 272 void HandleInstallerRepair(Execution::Context& context, RepairBehaviorEnum repairBehavior) 273 { 274 context << 275 ShellExecuteInstallImpl << 276 ReportInstallerResult(RepairBehaviorToString(repairBehavior), APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); 277 } 278 } 279 280 void RunRepairForRepairBehaviorBasedInstaller(Execution::Context& context) 281 { 282 const auto& installer = context.Get<Execution::Data::Installer>(); 283 auto repairBehavior = installer->RepairBehavior; 284 285 switch (repairBehavior) 286 { 287 case RepairBehaviorEnum::Modify: 288 case RepairBehaviorEnum::Uninstaller: 289 HandleModifyOrUninstallerRepair(context, repairBehavior); 290 break; 291 case RepairBehaviorEnum::Installer: 292 HandleInstallerRepair(context, repairBehavior); 293 break; 294 default: 295 context.Reporter.Error() << Resource::String::NoRepairInfoFound << std::endl; 296 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_REPAIR_INFO_FOUND); 297 } 298 } 299 300 void RepairMsiBasedInstaller(Execution::Context& context) 301 { 302 context << 303 ShellExecuteMsiExecRepair << 304 ReportRepairResult("MsiExec", APPINSTALLER_CLI_ERROR_EXEC_REPAIR_FAILED); 305 } 306 307 void RepairApplicabilityCheck(Execution::Context& context) 308 { 309 context << 310 ApplicabilityCheckForInstalledPackage << 311 ApplicabilityCheckForAvailablePackage; 312 } 313 314 void ExecuteRepair(Execution::Context& context) 315 { 316 InstallerTypeEnum installerTypeEnum = context.Contains(Execution::Data::Installer) ? context.Get<Execution::Data::Installer>()->EffectiveInstallerType() : GetInstalledType(context); 317 318 Synchronization::CrossProcessInstallLock lock; 319 320 if (!ExemptFromSingleInstallLocking(installerTypeEnum)) 321 { 322 // Acquire the lock , if the operation is cancelled it will return false so we will also return. 323 if (!context.Reporter.ExecuteWithProgress([&](IProgressCallback& callback) 324 { 325 callback.SetProgressMessage(Resource::String::InstallWaitingOnAnother()); 326 return lock.Acquire(callback); 327 })) 328 { 329 AICLI_LOG(CLI, Info, << "Abandoning attempt to acquire repair lock due to cancellation"); 330 return; 331 } 332 } 333 334 switch (installerTypeEnum) 335 { 336 case InstallerTypeEnum::Exe: 337 case InstallerTypeEnum::Burn: 338 case InstallerTypeEnum::Inno: 339 case InstallerTypeEnum::Nullsoft: 340 { 341 context << 342 RunRepairForRepairBehaviorBasedInstaller; 343 } 344 break; 345 case InstallerTypeEnum::Msi: 346 case InstallerTypeEnum::Wix: 347 { 348 context << 349 RepairMsiBasedInstaller; 350 } 351 break; 352 case InstallerTypeEnum::Msix: 353 { 354 context << 355 RepairMsixPackage; 356 } 357 break; 358 case InstallerTypeEnum::MSStore: 359 { 360 context << 361 EnsureStorePolicySatisfied << 362 MSStoreRepair; 363 } 364 break; 365 case InstallerTypeEnum::Portable: 366 default: 367 THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); 368 } 369 } 370 371 void GetRepairInfo(Execution::Context& context) 372 { 373 InstallerTypeEnum installerTypeEnum = context.Contains(Execution::Data::Installer) ? context.Get<Execution::Data::Installer>()->BaseInstallerType : GetInstalledType(context); 374 375 switch (installerTypeEnum) 376 { 377 // Exe based installers, for installed package all gets mapped to exe extension. 378 case InstallerTypeEnum::Burn: 379 case InstallerTypeEnum::Exe: 380 case InstallerTypeEnum::Inno: 381 case InstallerTypeEnum::Nullsoft: 382 { 383 context << 384 GenerateRepairString; 385 } 386 break; 387 // MSI based installers, for installed package all gets mapped to msi extension. 388 case InstallerTypeEnum::Msi: 389 case InstallerTypeEnum::Wix: 390 { 391 context << 392 SetProductCodesInContext; 393 } 394 break; 395 // MSIX based installers, msix. 396 case InstallerTypeEnum::Msix: 397 { 398 context << 399 SetPackageFamilyNamesInContext; 400 } 401 break; 402 case InstallerTypeEnum::MSStore: 403 break; 404 case InstallerTypeEnum::Portable: 405 default: 406 THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); 407 } 408 } 409 410 void RepairMsixPackage(Execution::Context& context) 411 { 412 bool isMachineScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Machine; 413 414 const auto& packageFamilyNames = context.Get<Execution::Data::PackageFamilyNames>(); 415 context.Reporter.Info() << Resource::String::RepairFlowStartingPackageRepair << std::endl; 416 417 for (const auto& packageFamilyName : packageFamilyNames) 418 { 419 auto packageFullName = Msix::GetPackageFullNameFromFamilyName(packageFamilyName); 420 421 if (!packageFullName.has_value()) 422 { 423 AICLI_LOG(CLI, Warning, << "No package found with family name: " << packageFamilyName); 424 continue; 425 } 426 427 AICLI_LOG(CLI, Info, << "Repairing package: " << packageFullName.value()); 428 429 try 430 { 431 if (!isMachineScope) 432 { 433 // Best effort repair by registering the package. 434 context.Reporter.ExecuteWithProgress(std::bind(Deployment::RegisterPackage, packageFamilyName, std::placeholders::_1)); 435 } 436 else 437 { 438 context.Reporter.Error() << Resource::String::RepairFlowReturnCodeSystemNotSupported << std::endl; 439 context.Add<Execution::Data::OperationReturnCode>(static_cast<DWORD>(APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED)); 440 AICLI_LOG(CLI, Error, << "Device wide repair for msix type is not supported."); 441 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_SYSTEM_NOT_SUPPORTED); 442 } 443 } 444 catch (const wil::ResultException& re) 445 { 446 context.Add<Execution::Data::OperationReturnCode>(re.GetErrorCode()); 447 context << ReportRepairResult("MSIX", re.GetErrorCode(), true); 448 return; 449 } 450 } 451 452 context.Reporter.Info() << Resource::String::RepairFlowRepairSuccess << std::endl; 453 } 454 455 void RepairSinglePackage(Execution::Context& context) 456 { 457 context << 458 RepairApplicabilityCheck << 459 GetRepairInfo << 460 ReportExecutionStage(ExecutionStage::Execution) << 461 ExecuteRepair << 462 ReportExecutionStage(ExecutionStage::PostExecution); 463 } 464 465 void SelectApplicablePackageVersion(Execution::Context& context) 466 { 467 // If the repair flow is initiated with manifest, then we don't need to select the applicable package version. 468 if (context.Args.Contains(Args::Type::Manifest)) 469 { 470 return; 471 } 472 473 const auto& installedPackage = context.Get<Execution::Data::InstalledPackageVersion>(); 474 475 Utility::Version installedVersion = Utility::Version(installedPackage->GetProperty(PackageVersionProperty::Version)); 476 if (installedVersion.IsUnknown()) 477 { 478 context.Reporter.Error() << Resource::String::NoApplicableInstallers << std::endl; 479 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); 480 } 481 482 std::string_view requestedVersion = context.Args.Contains(Execution::Args::Type::TargetVersion) ? context.Args.GetArg(Execution::Args::Type::TargetVersion) : installedVersion.ToString(); 483 // If it's Store source with only one version unknown, use the unknown version for available version mapping. 484 const auto& package = context.Get<Execution::Data::Package>(); 485 auto packageVersions = GetAvailableVersionsForInstalledVersion(package, installedPackage); 486 auto versionKeys = packageVersions->GetVersionKeys(); 487 if (versionKeys.size() == 1) 488 { 489 auto packageVersion = packageVersions->GetVersion(versionKeys.at(0)); 490 if (packageVersion->GetSource().IsWellKnownSource(WellKnownSource::MicrosoftStore) && 491 Utility::Version{ packageVersion->GetProperty(PackageVersionProperty::Version) }.IsUnknown()) 492 { 493 requestedVersion = ""; 494 } 495 } 496 497 context << 498 GetManifestWithVersionFromPackage( 499 requestedVersion, 500 context.Args.GetArg(Execution::Args::Type::Channel), false); 501 } 502 503 void SelectApplicableInstallerIfNecessary(Execution::Context& context) 504 { 505 // For MSI installers, the platform provides built-in support for repair via msiexec, hence no need to select an installer. 506 // Similarly, for MSIX packages that are not from the Microsoft Store, selecting an installer is not required. 507 if (IsInstallerMappingRequired(context)) 508 { 509 context << 510 SelectApplicablePackageVersion << 511 SelectInstaller << 512 EnsureApplicableInstaller; 513 } 514 } 515 516 void ReportRepairResult::operator()(Execution::Context& context) const 517 { 518 DWORD repairResult = context.Get<Execution::Data::OperationReturnCode>(); 519 520 if (repairResult != 0) 521 { 522 auto& repairPackage = context.Contains(Execution::Data::PackageVersion) ? 523 context.Get<Execution::Data::PackageVersion>() : 524 context.Get<Execution::Data::InstalledPackageVersion>(); 525 526 Logging::Telemetry().LogRepairFailure( 527 repairPackage->GetProperty(PackageVersionProperty::Id), 528 repairPackage->GetProperty(PackageVersionProperty::Version), 529 m_repairType, 530 repairResult); 531 532 if (m_isHResult) 533 { 534 context.Reporter.Error() 535 << Resource::String::RepairFailedWithCode(Utility::LocIndView{ GetUserPresentableMessage(repairResult) }) 536 << std::endl; 537 } 538 else 539 { 540 context.Reporter.Error() 541 << Resource::String::RepairFailedWithCode(repairResult) 542 << std::endl; 543 } 544 545 // Show log path if available 546 if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get<Execution::Data::LogPath>())) 547 { 548 auto installerLogPath = Utility::LocIndString{ context.Get<Execution::Data::LogPath>().u8string() }; 549 context.Reporter.Info() << Resource::String::InstallerLogAvailable(installerLogPath) << std::endl; 550 } 551 552 AICLI_TERMINATE_CONTEXT(m_hr); 553 } 554 else 555 { 556 context.Reporter.Info() << Resource::String::RepairFlowRepairSuccess << std::endl; 557 } 558 } 559 }