PromptFlow.cpp (19876B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "PromptFlow.h" 5 #include "ShowFlow.h" 6 #include <winget/UserSettings.h> 7 8 using namespace AppInstaller::CLI::Execution; 9 using namespace AppInstaller::Settings; 10 using namespace AppInstaller::Utility::literals; 11 12 namespace AppInstaller::CLI::Workflow 13 { 14 namespace 15 { 16 bool IsInteractivityAllowed(Execution::Context& context) 17 { 18 // Interactivity can be disabled for several reasons: 19 // * We are running in a non-interactive context (e.g., COM call) 20 // * It is disabled in the settings 21 // * It was disabled from the command line 22 23 if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::DisableInteractivity)) 24 { 25 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled due to non-interactive context."); 26 return false; 27 } 28 29 if (context.Args.Contains(Execution::Args::Type::DisableInteractivity)) 30 { 31 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled by command line argument."); 32 return false; 33 } 34 35 if (Settings::User().Get<Settings::Setting::InteractivityDisable>()) 36 { 37 AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled in settings."); 38 return false; 39 } 40 41 return true; 42 } 43 44 bool HandleSourceAgreementsForOneSource(Execution::Context& context, const Repository::Source& source) 45 { 46 auto details = source.GetDetails(); 47 AICLI_LOG(CLI, Verbose, << "Checking Source agreements for source: " << details.Name); 48 49 if (source.CheckSourceAgreements()) 50 { 51 AICLI_LOG(CLI, Verbose, << "Source agreements satisfied. Source: " << details.Name); 52 return true; 53 } 54 55 // Show source agreements 56 context.Reporter.Info() 57 << Execution::SourceInfoEmphasis 58 << Resource::String::SourceAgreementsTitle(Utility::LocIndView{ details.Name }) 59 << std::endl; 60 61 const auto& agreements = source.GetInformation().SourceAgreements; 62 63 for (const auto& agreement : agreements) 64 { 65 if (!agreement.Label.empty()) 66 { 67 context.Reporter.Info() << Execution::SourceInfoEmphasis << Utility::LocIndString{ agreement.Label } << ": "_liv; 68 } 69 70 if (!agreement.Text.empty()) 71 { 72 context.Reporter.Info() << Utility::LocIndString{ agreement.Text } << std::endl; 73 } 74 75 if (!agreement.Url.empty()) 76 { 77 context.Reporter.Info() << Utility::LocIndString{ agreement.Url } << std::endl; 78 } 79 } 80 81 // Show message for each individual implicit agreement field 82 auto fields = source.GetAgreementFieldsFromSourceInformation(); 83 if (WI_IsFlagSet(fields, Repository::ImplicitAgreementFieldEnum::Market)) 84 { 85 context.Reporter.Info() << Resource::String::SourceAgreementsMarketMessage << std::endl; 86 } 87 88 context.Reporter.Info() << std::endl; 89 90 bool accepted = context.Args.Contains(Execution::Args::Type::AcceptSourceAgreements); 91 92 if (!accepted && IsInteractivityAllowed(context)) 93 { 94 accepted = context.Reporter.PromptForBoolResponse(Resource::String::SourceAgreementsPrompt); 95 } 96 97 if (accepted) 98 { 99 AICLI_LOG(CLI, Verbose, << "Source agreements accepted. Source: " << details.Name); 100 source.SaveAcceptedSourceAgreements(); 101 } 102 else 103 { 104 AICLI_LOG(CLI, Verbose, << "Source agreements not accepted. Source: " << details.Name); 105 } 106 107 return accepted; 108 } 109 110 // An interface for defining prompts to the user regarding a package. 111 // Note that each prompt may behave differently when running non-interactively 112 // (e.g. failing if it is needed vs. continuing silently), and they may 113 // do some work while checking if the prompt is needed even if no prompt is shown, 114 // so they need to always run. 115 struct PackagePrompt 116 { 117 virtual ~PackagePrompt() = default; 118 119 // Determines whether a package needs this prompt. 120 // Inputs: Manifest, Installer 121 // Outputs: None 122 virtual bool PackageNeedsPrompt(Execution::Context& context) = 0; 123 124 // Prompts for the information needed for a single package. 125 // Inputs: Manifest, Installer 126 // Outputs: None 127 virtual void PromptForSinglePackage(Execution::Context& context) = 0; 128 129 // Prompts for the information needed for multiple packages. 130 // Inputs: Manifest, Installer (for each sub context) 131 // Outputs: None 132 virtual void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) = 0; 133 }; 134 135 // Prompt for accepting package agreements. 136 struct PackageAgreementsPrompt : public PackagePrompt 137 { 138 PackageAgreementsPrompt(bool ensureAgreementsAcceptance) : m_ensureAgreementsAcceptance(ensureAgreementsAcceptance) {} 139 140 bool PackageNeedsPrompt(Execution::Context& context) override 141 { 142 const auto& agreements = context.Get<Execution::Data::Manifest>().CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); 143 return !agreements.empty(); 144 } 145 146 void PromptForSinglePackage(Execution::Context& context) override 147 { 148 ShowPackageAgreements(context); 149 EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true); 150 } 151 152 void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override 153 { 154 for (auto packageContext : packagesToPrompt) 155 { 156 // Show agreements for each package 157 Execution::Context& showContext = *packageContext; 158 auto previousThreadGlobals = showContext.SetForCurrentThread(); 159 160 ShowPackageAgreements(showContext); 161 if (showContext.IsTerminated()) 162 { 163 AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR()); 164 } 165 } 166 167 EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true); 168 } 169 170 private: 171 void ShowPackageAgreements(Execution::Context& context) 172 { 173 const auto& manifest = context.Get<Execution::Data::Manifest>(); 174 auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); 175 176 if (agreements.empty()) 177 { 178 // Nothing to do 179 return; 180 } 181 182 context << Workflow::ReportManifestIdentityWithVersion(Resource::String::ReportIdentityForAgreements) << Workflow::ShowAgreementsInfo; 183 context.Reporter.EmptyLine(); 184 } 185 186 void EnsurePackageAgreementsAcceptance(Execution::Context& context, bool showPrompt) const 187 { 188 if (!m_ensureAgreementsAcceptance) 189 { 190 return; 191 } 192 193 if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements)) 194 { 195 AICLI_LOG(CLI, Info, << "Package agreements accepted by CLI flag"); 196 return; 197 } 198 199 if (showPrompt) 200 { 201 AICLI_LOG(CLI, Verbose, << "Prompting to accept package agreements"); 202 if (IsInteractivityAllowed(context)) 203 { 204 bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PackageAgreementsPrompt); 205 if (accepted) 206 { 207 AICLI_LOG(CLI, Info, << "Package agreements accepted in prompt"); 208 return; 209 } 210 else 211 { 212 AICLI_LOG(CLI, Info, << "Package agreements not accepted in prompt"); 213 } 214 } 215 } 216 217 AICLI_LOG(CLI, Error, << "Package agreements were not agreed to."); 218 context.Reporter.Error() << Resource::String::PackageAgreementsNotAgreedTo << std::endl; 219 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); 220 } 221 222 bool m_ensureAgreementsAcceptance; 223 }; 224 225 // Prompt for getting the install root when a package requires it and it is not 226 // specified by the settings. 227 struct InstallRootPrompt : public PackagePrompt 228 { 229 InstallRootPrompt() : m_installLocation(User().Get<Setting::InstallDefaultRoot>()) {} 230 231 bool PackageNeedsPrompt(Execution::Context& context) override 232 { 233 if (context.Get<Execution::Data::Installer>()->InstallLocationRequired && 234 !context.Args.Contains(Execution::Args::Type::InstallLocation)) 235 { 236 AICLI_LOG(CLI, Info, << "Package [" << context.Get<Execution::Data::Manifest>().Id << "] requires an install location."); 237 238 // An install location is required but one wasn't provided. 239 // Check if there is a default one from settings. 240 if (m_installLocation.empty()) 241 { 242 // We need to prompt 243 return true; 244 } 245 else 246 { 247 // Use the default 248 SetInstallLocation(context); 249 } 250 } 251 252 return false; 253 } 254 255 void PromptForSinglePackage(Execution::Context& context) override 256 { 257 context.Reporter.Info() << Resource::String::InstallerRequiresInstallLocation << std::endl; 258 PromptForInstallRoot(context); 259 260 // When prompting for a single package, we use the provided location directly. 261 // This is different from when we prompt for multiple packages or use the root in the settings. 262 context.Args.AddArg(Execution::Args::Type::InstallLocation, m_installLocation.u8string()); 263 } 264 265 void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override 266 { 267 // Report packages that will be affected. 268 context.Reporter.Info() << Resource::String::InstallersRequireInstallLocation << std::endl; 269 for (auto packageContext : packagesToPrompt) 270 { 271 *packageContext << ReportManifestIdentityWithVersion(" - "_liv, Execution::Reporter::Level::Warning); 272 if (packageContext->IsTerminated()) 273 { 274 AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR()); 275 } 276 } 277 278 PromptForInstallRoot(context); 279 280 // Set the install location for each package. 281 for (auto packageContext : packagesToPrompt) 282 { 283 SetInstallLocation(*packageContext); 284 } 285 } 286 287 private: 288 void PromptForInstallRoot(Execution::Context& context) 289 { 290 if (!IsInteractivityAllowed(context)) 291 { 292 AICLI_LOG(CLI, Error, << "Install location is required but was not provided."); 293 context.Reporter.Error() << Resource::String::InstallLocationNotProvided << std::endl; 294 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED); 295 } 296 297 AICLI_LOG(CLI, Info, << "Prompting for install root."); 298 m_installLocation = context.Reporter.PromptForPath(Resource::String::PromptForInstallRoot); 299 if (m_installLocation.empty()) 300 { 301 AICLI_LOG(CLI, Error, << "Install location is required but the provided path was empty."); 302 context.Reporter.Error() << Resource::String::InstallLocationNotProvided << std::endl; 303 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED); 304 } 305 AICLI_LOG(CLI, Info, << "Proceeding with installation using install root: " << m_installLocation); 306 } 307 308 // Sets the install location for an execution context. 309 // The install location is obtained by appending the package ID to the install root. 310 // This function assumes that m_installLocation is set, either from settings or from the prompt, 311 // and that the context does not already have an install location. 312 void SetInstallLocation(Execution::Context& context) 313 { 314 auto packageId = context.Get<Execution::Data::Manifest>().Id; 315 auto installLocation = m_installLocation; 316 installLocation += "\\" + packageId; 317 AICLI_LOG(CLI, Info, << "Setting install location for package [" << packageId << "] to: " << installLocation); 318 context.Args.AddArg(Execution::Args::Type::InstallLocation, installLocation.u8string()); 319 } 320 321 std::filesystem::path m_installLocation; 322 }; 323 324 // Prompt asking whether to continue when an installer will abort the terminal. 325 struct InstallerAbortsTerminalPrompt : public PackagePrompt 326 { 327 bool PackageNeedsPrompt(Execution::Context& context) override 328 { 329 return context.Get<Execution::Data::Installer>()->InstallerAbortsTerminal; 330 } 331 332 void PromptForSinglePackage(Execution::Context& context) override 333 { 334 AICLI_LOG(CLI, Info, << "This installer may abort the terminal"); 335 context.Reporter.Warn() << Resource::String::InstallerAbortsTerminal << std::endl; 336 PromptToProceed(context); 337 } 338 339 void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override 340 { 341 AICLI_LOG(CLI, Info, << "One or more installers may abort the terminal"); 342 context.Reporter.Warn() << Resource::String::InstallersAbortTerminal << std::endl; 343 for (auto packageContext : packagesToPrompt) 344 { 345 *packageContext << ReportManifestIdentityWithVersion(" - "_liv, Execution::Reporter::Level::Warning); 346 if (packageContext->IsTerminated()) 347 { 348 AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR()); 349 } 350 } 351 352 PromptToProceed(context); 353 } 354 355 private: 356 void PromptToProceed(Execution::Context& context) 357 { 358 AICLI_LOG(CLI, Info, << "Prompting before proceeding with installer that aborts terminal."); 359 if (!IsInteractivityAllowed(context)) 360 { 361 return; 362 } 363 364 bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PromptToProceed, Reporter::Level::Warning, true); 365 if (accepted) 366 { 367 AICLI_LOG(CLI, Info, << "Proceeding with installation"); 368 } 369 else 370 { 371 AICLI_LOG(CLI, Error, << "Aborting installation"); 372 context.Reporter.Error() << Resource::String::Cancelled << std::endl; 373 AICLI_TERMINATE_CONTEXT(E_ABORT); 374 } 375 } 376 }; 377 378 // Gets all the prompts that may be displayed, in order of appearance 379 std::vector<std::unique_ptr<PackagePrompt>> GetPackagePrompts(bool ensureAgreementsAcceptance = true, bool installerDownloadOnly = false) 380 { 381 std::vector<std::unique_ptr<PackagePrompt>> result; 382 383 if (installerDownloadOnly) 384 { 385 result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); 386 } 387 else 388 { 389 result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); 390 result.push_back(std::make_unique<InstallRootPrompt>()); 391 result.push_back(std::make_unique<InstallerAbortsTerminalPrompt>()); 392 } 393 394 return result; 395 } 396 } 397 398 void HandleSourceAgreements::operator()(Execution::Context& context) const 399 { 400 bool allAccepted = true; 401 402 if (m_source.IsComposite()) 403 { 404 for (auto const& source : m_source.GetAvailableSources()) 405 { 406 if (!HandleSourceAgreementsForOneSource(context, source)) 407 { 408 allAccepted = false; 409 } 410 } 411 } 412 else 413 { 414 allAccepted = HandleSourceAgreementsForOneSource(context, m_source); 415 } 416 417 if (!allAccepted) 418 { 419 context.Reporter.Error() << Resource::String::SourceAgreementsNotAgreedTo << std::endl; 420 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); 421 } 422 } 423 424 void ShowPromptsForSinglePackage::operator()(Execution::Context& context) const 425 { 426 bool installerDownloadOnly = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly); 427 428 for (auto& prompt : GetPackagePrompts(true, installerDownloadOnly)) 429 { 430 // Show the prompt if needed 431 if (prompt->PackageNeedsPrompt(context)) 432 { 433 prompt->PromptForSinglePackage(context); 434 } 435 436 if (context.IsTerminated()) 437 { 438 return; 439 } 440 } 441 } 442 443 void ShowPromptsForMultiplePackages::operator()(Execution::Context& context) const 444 { 445 for (auto& prompt : GetPackagePrompts(m_ensureAgreementsAcceptance, m_installerDownloadOnly)) 446 { 447 // Find which packages need this prompt 448 std::vector<Execution::Context*> packagesToPrompt; 449 for (auto& packageContext : context.Get<Execution::Data::PackageSubContexts>()) 450 { 451 if (prompt->PackageNeedsPrompt(*packageContext)) 452 { 453 packagesToPrompt.push_back(packageContext.get()); 454 } 455 } 456 457 // Prompt only if needed 458 if (!packagesToPrompt.empty()) 459 { 460 prompt->PromptForMultiplePackages(context, packagesToPrompt); 461 if (context.IsTerminated()) 462 { 463 return; 464 } 465 } 466 } 467 } 468 469 void RequireInteractivity::operator()(Execution::Context& context) const 470 { 471 if (!IsInteractivityAllowed(context)) 472 { 473 AICLI_TERMINATE_CONTEXT(m_nonInteractiveError); 474 } 475 } 476 }