winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit d9f2688aa770b26cb9f5f23fc79ce736e304674b
parent 5390e1c7873bf9455261be189083498bcb2e8dc1
Author: Flor Chacón <lechacon@users.noreply.github.com>
Date:   Thu, 11 Aug 2022 18:17:20 -0600

Add prompts when installer aborts terminal or needs install location (#1814)


Diffstat:
Mdoc/Settings.md | 24++++++++++++++++++++++++
Mschemas/JSON/settings/settings.schema.0.2.json | 22++++++++++++++++++++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 4+++-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 12+++++++++---
Msrc/AppInstallerCLICore/Argument.cpp | 1+
Msrc/AppInstallerCLICore/COMContext.h | 2++
Msrc/AppInstallerCLICore/Command.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionArgs.h | 14+++++++++-----
Msrc/AppInstallerCLICore/ExecutionContext.h | 1+
Msrc/AppInstallerCLICore/ExecutionReporter.cpp | 28+++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/ExecutionReporter.h | 3+++
Msrc/AppInstallerCLICore/Resources.h | 11++++++++++-
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 102+++++++------------------------------------------------------------------------
Msrc/AppInstallerCLICore/Workflows/InstallFlow.h | 39+--------------------------------------
Asrc/AppInstallerCLICore/Workflows/PromptFlow.cpp | 461+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/PromptFlow.h | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/SourceFlow.cpp | 1+
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 133++++++++++++++++---------------------------------------------------------------
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 30+++++++++++++++---------------
Msrc/AppInstallerCLIE2ETests/Constants.cs | 1+
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 29+++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 6++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 6++++++
Asrc/AppInstallerCLITests/TestData/InstallFlowTest_AbortsTerminal.yaml | 16++++++++++++++++
Asrc/AppInstallerCLITests/TestData/InstallFlowTest_InstallLocationRequired.yaml | 18++++++++++++++++++
Msrc/AppInstallerCLITests/WorkFlow.cpp | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/AppInstallerCommonCore/Errors.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 42+++++++++++++++++++++++++++++++++---------
Msrc/AppInstallerCommonCore/UserSettings.cpp | 29++++++++++++++++++++---------
30 files changed, 972 insertions(+), 290 deletions(-)

diff --git a/doc/Settings.md b/doc/Settings.md @@ -135,6 +135,16 @@ The `purgePortablePackage` behavior affects the default behavior for uninstallin }, ``` +### Default install root + +The `defaultInstallRoot` affects the install location when a package requires one. This can be overridden by the `--location` parameter. This setting is only used when a package manifest includes `InstallLocationRequired`, and the actual location is obtained by appending the package ID to the root. + +```json + "installBehavior": { + "defaultInstallRoot": "C:\installRoot" + }, +``` + ## Telemetry The `telemetry` settings control whether winget writes ETW events that may be sent to Microsoft on a default installation of Windows. @@ -183,6 +193,20 @@ The `doProgressTimeoutInSeconds` setting updates the number of seconds to wait w } ``` +## Interactivity + +The `interactivity` settings control whether winget may show interactive prompts during execution. Note that this refers only to prompts shown by winget itself and not to those shown by package installers. + +### disable + +```json + "interactivity": { + "disable": true + }, +``` + +If set to true, the `interactivity.disable` setting will prevent any interactive prompt from being shown. + ## Experimental Features To allow work to be done and distributed to early adopters for feedback, settings can be used to enable "experimental" features. diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -116,6 +116,11 @@ "description": "The default root directory where packages are installed to under Machine scope. Applies to the portable installer type.", "type": "string", "default": "%PROGRAMFILES%/WinGet/Packages/" + }, + "defaultInstallRoot": { + "description": "Default install location to use for packages that require it when not specified", + "type": "string", + "maxLength": "32767" } } }, @@ -164,6 +169,17 @@ } } }, + "Interactivity": { + "description": "Interactivity settings", + "type": "object", + "properties": { + "disable": { + "description": "Controls whether interactive prompts are shown by the Windows Package Manager client", + "type": "boolean", + "default": false + } + } + }, "Experimental": { "description": "Experimental Features", "type": "object", @@ -235,6 +251,12 @@ }, { "properties": { + "interactivity": { "$ref": "#/definitions/Interactivity" } + }, + "additionalItems": true + }, + { + "properties": { "experimentalFeatures": { "$ref": "#/definitions/Experimental" } }, "additionalItems": true diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -260,7 +260,7 @@ <ClInclude Include="Commands\SettingsCommand.h" /> <ClInclude Include="CompletionData.h" /> <ClInclude Include="ContextOrchestrator.h" /> - <ClInclude Include="Public\COMContext.h" /> + <ClInclude Include="COMContext.h" /> <ClInclude Include="Workflows\DependenciesFlow.h" /> <ClInclude Include="ExecutionArgs.h" /> <ClInclude Include="ExecutionContextData.h" /> @@ -283,6 +283,7 @@ <ClInclude Include="Workflows\MsiInstallFlow.h" /> <ClInclude Include="Workflows\MSStoreInstallerHandler.h" /> <ClInclude Include="Workflows\PortableFlow.h" /> + <ClInclude Include="Workflows\PromptFlow.h" /> <ClInclude Include="Workflows\SettingsFlow.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> @@ -336,6 +337,7 @@ <ClCompile Include="Workflows\MsiInstallFlow.cpp" /> <ClCompile Include="Workflows\MSStoreInstallerHandler.cpp" /> <ClCompile Include="Workflows\PortableFlow.cpp" /> + <ClCompile Include="Workflows\PromptFlow.cpp" /> <ClCompile Include="Workflows\SettingsFlow.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -173,7 +173,7 @@ <ClInclude Include="Workflows\DependencyNodeProcessor.h"> <Filter>Workflows</Filter> </ClInclude> - <ClInclude Include="Public\COMContext.h"> + <ClInclude Include="COMContext.h"> <Filter>Public</Filter> </ClInclude> <ClInclude Include="Workflows\PortableFlow.h"> @@ -181,7 +181,10 @@ </ClInclude> <ClInclude Include="Workflows\ArchiveFlow.h"> <Filter>Workflows</Filter> - </ClInclude> + </ClInclude> + <ClInclude Include="Workflows\PromptFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -328,9 +331,12 @@ <ClCompile Include="Workflows\PortableFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Workflows\PromptFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> <ClCompile Include="Workflows\ArchiveFlow.cpp"> <Filter>Workflows</Filter> - </ClCompile> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -110,6 +110,7 @@ namespace AppInstaller::CLI args.push_back(ForType(Args::Type::RainbowStyle)); args.push_back(ForType(Args::Type::RetroStyle)); args.push_back(ForType(Args::Type::VerboseLogs)); + args.emplace_back("disable-interactivity", NoAlias, Args::Type::DisableInteractivity, Resource::String::DisableInteractivityArgumentDescription, ArgumentType::Flag, false); } std::string Argument::GetUsageString() const diff --git a/src/AppInstallerCLICore/COMContext.h b/src/AppInstallerCLICore/COMContext.h @@ -41,12 +41,14 @@ namespace AppInstaller::CLI::Execution Reporter.SetChannel(Reporter::Channel::Disabled); Reporter.SetProgressSink(this); SetFlags(CLI::Execution::ContextFlag::AgreementsAcceptedByCaller); + SetFlags(CLI::Execution::ContextFlag::DisableInteractivity); } COMContext(std::ostream& out, std::istream& in) : CLI::Execution::Context(out, in) { Reporter.SetProgressSink(this); SetFlags(CLI::Execution::ContextFlag::AgreementsAcceptedByCaller); + SetFlags(CLI::Execution::ContextFlag::DisableInteractivity); } ~COMContext() = default; diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -835,6 +835,7 @@ namespace AppInstaller::CLI { ExecuteInternal(context); } + if (context.Args.Contains(Execution::Args::Type::Wait)) { context.Reporter.PromptForEnter(); diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -79,22 +79,26 @@ namespace AppInstaller::CLI::Execution AdminSettingEnable, AdminSettingDisable, - // Upgrade Command - All, // Update all installed packages to latest - IncludeUnknown, // Allow upgrades of packages with unknown versions + // Upgrade command + All, // Used in Update command to update all installed packages to latest + IncludeUnknown, // Used in Upgrade command to allow upgrades of packages with unknown versions - // Other + // Show command ListVersions, // Used in Show command to list all available versions of an app + + // Common arguments NoVT, // Disable VirtualTerminal outputs RetroStyle, // Makes progress display as retro RainbowStyle, // Makes progress display as a rainbow Help, // Show command usage Info, // Show general info about WinGet VerboseLogs, // Increases winget logging level to verbose + DisableInteractivity, // Disable interactive prompts + Wait, // Prompts the user to press any key before exiting + DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header AcceptSourceAgreements, // Accept all source agreements - Wait, // Prompts the user to press any key before exiting // Used for demonstration purposes ExperimentalArg, diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -64,6 +64,7 @@ namespace AppInstaller::CLI::Execution // TODO: Remove when the source interface is refactored. TreatSourceFailuresAsWarning = 0x10, ShowSearchResultsOnPartialFailure = 0x20, + DisableInteractivity = 0x40, }; DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -163,6 +163,32 @@ namespace AppInstaller::CLI::Execution m_in.get(); } + std::filesystem::path Reporter::PromptForPath(Resource::LocString message, Level level) + { + auto out = GetOutputStream(level); + + // Try prompting until we get a valid answer + for (;;) + { + out << message << ' '; + + // Read the response + std::string response; + if (!std::getline(m_in, response)) + { + THROW_HR(APPINSTALLER_CLI_ERROR_PROMPT_INPUT_ERROR); + } + + // Validate the path + std::filesystem::path path{ response }; + if (path.is_absolute()) + { + return path; + } + } + + } + void Reporter::ShowIndefiniteProgress(bool running) { if (m_spinner) @@ -186,7 +212,7 @@ namespace AppInstaller::CLI::Execution m_progressBar->ShowProgress(current, maximum, type); } } - + void Reporter::BeginProgress() { GetBasicOutputStream() << VirtualTerminal::Cursor::Visibility::DisableShow; diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -102,6 +102,9 @@ namespace AppInstaller::CLI::Execution // Prompts the user, continues when Enter is pressed void PromptForEnter(Level level = Level::Info); + // Prompts the user for a path. + std::filesystem::path PromptForPath(Resource::LocString message, Level level = Level::Info); + // Used to show indefinite progress. Currently an indefinite spinner is the form of // showing indefinite progress. // running: shows indefinite progress if set to true, stops indefinite progress if set to false diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -45,6 +45,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(CountArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowInstall); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceNotFound); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowSourceTooManyMatches); @@ -56,8 +57,8 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DependenciesFlowContainsLoop); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementError); WINGET_DEFINE_RESOURCE_STRINGID(DependenciesManagementExitMessage); - WINGET_DEFINE_RESOURCE_STRINGID(CountOutOfBoundsError); WINGET_DEFINE_RESOURCE_STRINGID(DisabledByGroupPolicy); + WINGET_DEFINE_RESOURCE_STRINGID(DisableInteractivityArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Done); WINGET_DEFINE_RESOURCE_STRINGID(ExactArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ExperimentalArgumentDescription); @@ -121,6 +122,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageNotAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstalledPackageVersionNotAvailable); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerAbortsTerminal); WINGET_DEFINE_RESOURCE_STRINGID(InstallerElevationExpected); WINGET_DEFINE_RESOURCE_STRINGID(InstallerBlockedByPolicy); WINGET_DEFINE_RESOURCE_STRINGID(InstallerFailedSecurityCheck); @@ -133,6 +135,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallerHashVerified); WINGET_DEFINE_RESOURCE_STRINGID(InstallerLogAvailable); WINGET_DEFINE_RESOURCE_STRINGID(InstallerProhibitsElevation); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerRequiresInstallLocation); + WINGET_DEFINE_RESOURCE_STRINGID(InstallersAbortTerminal); + WINGET_DEFINE_RESOURCE_STRINGID(InstallersRequireInstallLocation); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowRegistrationDeferred); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeAlreadyInstalled); @@ -152,6 +157,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeRebootRequiredToFinish); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowStartingPackageInstall); WINGET_DEFINE_RESOURCE_STRINGID(InstallForceArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstallLocationNotProvided); WINGET_DEFINE_RESOURCE_STRINGID(InstallScopeDescription); WINGET_DEFINE_RESOURCE_STRINGID(InteractiveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(InvalidAliasError); @@ -231,8 +237,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(PressEnterToContinue); WINGET_DEFINE_RESOURCE_STRINGID(PrivacyStatement); WINGET_DEFINE_RESOURCE_STRINGID(ProductCodeArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(PromptForInstallRoot); WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionNo); WINGET_DEFINE_RESOURCE_STRINGID(PromptOptionYes); + WINGET_DEFINE_RESOURCE_STRINGID(PromptToProceed); WINGET_DEFINE_RESOURCE_STRINGID(PurgeArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(PurgeInstallDirectory); WINGET_DEFINE_RESOURCE_STRINGID(QueryArgumentDescription); @@ -240,6 +248,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(RelatedLink); WINGET_DEFINE_RESOURCE_STRINGID(RenameArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ReparsePointsNotSupportedError); + WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityForAgreements); WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityFound); WINGET_DEFINE_RESOURCE_STRINGID(RequiredArgError); WINGET_DEFINE_RESOURCE_STRINGID(ReservedFilenameError); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -12,7 +12,8 @@ #include "ArchiveFlow.h" #include "PortableFlow.h" #include "WorkflowBase.h" -#include "Workflows/DependenciesFlow.h" +#include "DependenciesFlow.h" +#include "PromptFlow.h" #include <AppInstallerDeployment.h> #include <winget/ARPCorrelation.h> #include <winget/Archive.h> @@ -28,6 +29,7 @@ using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; using namespace AppInstaller::Utility; +using namespace AppInstaller::Utility::literals; namespace AppInstaller::CLI::Workflow { @@ -245,91 +247,6 @@ namespace AppInstaller::CLI::Workflow } } - void ShowPackageAgreements::operator()(Execution::Context& context) const - { - const auto& manifest = context.Get<Execution::Data::Manifest>(); - auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); - - if (agreements.empty()) - { - // Nothing to do - return; - } - - context << Workflow::ShowPackageInfo; - context.Reporter.Info() << std::endl; - - if (m_ensureAcceptance) - { - context << Workflow::EnsurePackageAgreementsAcceptance(/* showPrompt */ true); - } - } - - void EnsurePackageAgreementsAcceptance::operator()(Execution::Context& context) const - { - if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) - { - AICLI_LOG(CLI, Info, << "Skipping package agreements acceptance check because AgreementsAcceptedByCaller flag is set."); - return; - } - - if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements)) - { - AICLI_LOG(CLI, Info, << "Package agreements accepted by CLI flag"); - return; - } - - if (m_showPrompt) - { - bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PackageAgreementsPrompt); - if (accepted) - { - AICLI_LOG(CLI, Info, << "Package agreements accepted in prompt"); - return; - } - else - { - AICLI_LOG(CLI, Info, << "Package agreements not accepted in prompt"); - } - } - - AICLI_LOG(CLI, Error, << "Package agreements were not agreed to."); - context.Reporter.Error() << Resource::String::PackageAgreementsNotAgreedTo << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); - } - - void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context) - { - bool hasPackageAgreements = false; - for (auto& packageContext : context.Get<Execution::Data::PackagesToInstall>()) - { - // Show agreements for each package that has one - auto agreements = packageContext->Get<Execution::Data::Manifest>().CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); - if (agreements.empty()) - { - continue; - } - Execution::Context& showContext = *packageContext; - auto previousThreadGlobals = showContext.SetForCurrentThread(); - - showContext << - Workflow::ReportManifestIdentityWithVersion << - Workflow::ShowPackageAgreements(/* ensureAcceptance */ false); - if (showContext.IsTerminated()) - { - AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR()); - } - - hasPackageAgreements |= true; - } - - // If any package has agreements, ensure they are accepted - if (hasPackageAgreements) - { - context << Workflow::EnsurePackageAgreementsAcceptance(/* showPrompt */ false); - } - } - void ExecuteInstallerForType::operator()(Execution::Context& context) const { bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); @@ -513,7 +430,7 @@ namespace AppInstaller::CLI::Workflow void ReportIdentityAndInstallationDisclaimer(Execution::Context& context) { context << - Workflow::ReportManifestIdentityWithVersion << + Workflow::ReportManifestIdentityWithVersion() << Workflow::ShowInstallationDisclaimer; } @@ -535,7 +452,7 @@ namespace AppInstaller::CLI::Workflow { context << Workflow::ReportIdentityAndInstallationDisclaimer << - Workflow::ShowPackageAgreements(/* ensureAcceptance */ true) << + Workflow::ShowPromptsForSinglePackage(/* ensureAcceptance */ true) << Workflow::GetDependenciesFromInstaller << Workflow::ReportDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << Workflow::ManagePackageDependencies(Resource::String::InstallAndUpgradeCommandsReportDependencies) << @@ -561,11 +478,8 @@ namespace AppInstaller::CLI::Workflow void InstallMultiplePackages::operator()(Execution::Context& context) const { - if (m_ensurePackageAgreements) - { - // Show all license agreements before installing anything - context << Workflow::EnsurePackageAgreementsAcceptanceForMultipleInstallers; - } + // Show all prompts needed for every package before installing anything + context << Workflow::ShowPromptsForMultiplePackages(m_ensurePackageAgreements); if (context.IsTerminated()) { @@ -592,7 +506,7 @@ namespace AppInstaller::CLI::Workflow for (auto& packageContext : context.Get<Execution::Data::PackagesToInstall>()) { packagesProgress++; - context.Reporter.Info() << "(" << packagesProgress << "/" << packagesCount << ") "; + context.Reporter.Info() << '(' << packagesProgress << '/' << packagesCount << ") "_liv; // We want to do best effort to install all packages regardless of previous failures Execution::Context& installContext = *packageContext; diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -35,44 +35,7 @@ namespace AppInstaller::CLI::Workflow // Outputs: None void CheckForUnsupportedArgs(Execution::Context& context); - // Shows the license agreements if the application has them. - // Required Args: None - // Inputs: Manifest - // Outputs: None - struct ShowPackageAgreements : public WorkflowTask - { - ShowPackageAgreements(bool ensureAcceptance) : WorkflowTask("ShowPackageAgreements"), m_ensureAcceptance(ensureAcceptance) {} - - void operator()(Execution::Context& context) const override; - - private: - // Whether we need to ensure that the agreements are accepted, or only show them. - bool m_ensureAcceptance; - }; - - // Ensure the user accepted the license agreements. - // Required Args: None - // Inputs: None - // Outputs: None - struct EnsurePackageAgreementsAcceptance : public WorkflowTask - { - EnsurePackageAgreementsAcceptance(bool showPrompt) : WorkflowTask("EnsurePackageAgreementsAcceptance"), m_showPrompt(showPrompt) {} - - void operator()(Execution::Context& context) const override; - - private: - // Whether to show an interactive prompt - bool m_showPrompt; - }; - - // Ensure that the user accepted all the license agreements when there are - // multiple installers. - // Required Args: None - // Inputs: PackagesToInstall - // Outputs: None - void EnsurePackageAgreementsAcceptanceForMultipleInstallers(Execution::Context& context); - - // Starts execution of the installer. + // Composite flow that chooses what to do based on the installer type. // Required Args: None // Inputs: Installer, InstallerPath // Outputs: None diff --git a/src/AppInstallerCLICore/Workflows/PromptFlow.cpp b/src/AppInstallerCLICore/Workflows/PromptFlow.cpp @@ -0,0 +1,461 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "PromptFlow.h" +#include "ShowFlow.h" +#include <winget/UserSettings.h> + +using namespace AppInstaller::Settings; +using namespace AppInstaller::Utility::literals; + +namespace AppInstaller::CLI::Workflow +{ + namespace + { + bool IsInteractivityAllowed(Execution::Context& context) + { + // Interactivity can be disabled for several reasons: + // * We are running in a non-interactive context (e.g., COM call) + // * It is disabled in the settings + // * It was disabled from the command line + + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::DisableInteractivity)) + { + AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled due to non-interactive context."); + return false; + } + + if (context.Args.Contains(Execution::Args::Type::DisableInteractivity)) + { + AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled by command line argument."); + return false; + } + + if (Settings::User().Get<Settings::Setting::InteractivityDisable>()) + { + AICLI_LOG(CLI, Verbose, << "Skipping prompt. Interactivity is disabled in settings."); + return false; + } + + return true; + } + + bool HandleSourceAgreementsForOneSource(Execution::Context& context, const Repository::Source& source) + { + auto details = source.GetDetails(); + AICLI_LOG(CLI, Verbose, << "Checking Source agreements for source: " << details.Name); + + if (source.CheckSourceAgreements()) + { + AICLI_LOG(CLI, Verbose, << "Source agreements satisfied. Source: " << details.Name); + return true; + } + + // Show source agreements + std::string agreementsTitleMessage = Resource::LocString{ Resource::String::SourceAgreementsTitle }; + context.Reporter.Info() << Execution::SourceInfoEmphasis << + Utility::LocIndString{ Utility::FindAndReplaceMessageToken(agreementsTitleMessage, details.Name) } << std::endl; + + const auto& agreements = source.GetInformation().SourceAgreements; + + for (const auto& agreement : agreements) + { + if (!agreement.Label.empty()) + { + context.Reporter.Info() << Execution::SourceInfoEmphasis << Utility::LocIndString{ agreement.Label } << ": "_liv; + } + + if (!agreement.Text.empty()) + { + context.Reporter.Info() << Utility::LocIndString{ agreement.Text } << std::endl; + } + + if (!agreement.Url.empty()) + { + context.Reporter.Info() << Utility::LocIndString{ agreement.Url } << std::endl; + } + } + + // Show message for each individual implicit agreement field + auto fields = source.GetAgreementFieldsFromSourceInformation(); + if (WI_IsFlagSet(fields, Repository::ImplicitAgreementFieldEnum::Market)) + { + context.Reporter.Info() << Resource::String::SourceAgreementsMarketMessage << std::endl; + } + + context.Reporter.Info() << std::endl; + + bool accepted = context.Args.Contains(Execution::Args::Type::AcceptSourceAgreements); + + if (!accepted && IsInteractivityAllowed(context)) + { + accepted = context.Reporter.PromptForBoolResponse(Resource::String::SourceAgreementsPrompt); + } + + if (accepted) + { + AICLI_LOG(CLI, Verbose, << "Source agreements accepted. Source: " << details.Name); + source.SaveAcceptedSourceAgreements(); + } + else + { + AICLI_LOG(CLI, Verbose, << "Source agreements not accepted. Source: " << details.Name); + } + + return accepted; + } + + // An interface for defining prompts to the user regarding a package. + // Note that each prompt may behave differently when running non-interactively + // (e.g. failing if it is needed vs. continuing silently), and they may + // do some work while checking if the prompt is needed even if no prompt is shown, + // so they need to always run. + struct PackagePrompt + { + virtual ~PackagePrompt() = default; + + // Determines whether a package needs this prompt. + // Inputs: Manifest, Installer + // Outputs: None + virtual bool PackageNeedsPrompt(Execution::Context& context) = 0; + + // Prompts for the information needed for a single package. + // Inputs: Manifest, Installer + // Outputs: None + virtual void PromptForSinglePackage(Execution::Context& context) = 0; + + // Prompts for the information needed for multiple packages. + // Inputs: Manifest, Installer (for each sub context) + // Outputs: None + virtual void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) = 0; + }; + + // Prompt for accepting package agreements. + struct PackageAgreementsPrompt : public PackagePrompt + { + PackageAgreementsPrompt(bool ensureAgreementsAcceptance) : m_ensureAgreementsAcceptance(ensureAgreementsAcceptance) {} + + bool PackageNeedsPrompt(Execution::Context& context) override + { + const auto& agreements = context.Get<Execution::Data::Manifest>().CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); + return !agreements.empty(); + } + + void PromptForSinglePackage(Execution::Context& context) override + { + ShowPackageAgreements(context); + EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true); + } + + void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override + { + for (auto packageContext : packagesToPrompt) + { + // Show agreements for each package + Execution::Context& showContext = *packageContext; + auto previousThreadGlobals = showContext.SetForCurrentThread(); + + ShowPackageAgreements(showContext); + if (showContext.IsTerminated()) + { + AICLI_TERMINATE_CONTEXT(showContext.GetTerminationHR()); + } + } + + EnsurePackageAgreementsAcceptance(context, /* showPrompt */ false); + } + + private: + void ShowPackageAgreements(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + auto agreements = manifest.CurrentLocalization.Get<AppInstaller::Manifest::Localization::Agreements>(); + + if (agreements.empty()) + { + // Nothing to do + return; + } + + context << Workflow::ReportManifestIdentityWithVersion(Resource::String::ReportIdentityForAgreements) << Workflow::ShowPackageInfo; + context.Reporter.EmptyLine(); + } + + void EnsurePackageAgreementsAcceptance(Execution::Context& context, bool showPrompt) const + { + if (!m_ensureAgreementsAcceptance) + { + return; + } + + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) + { + AICLI_LOG(CLI, Info, << "Skipping package agreements acceptance check because AgreementsAcceptedByCaller flag is set."); + return; + } + + if (context.Args.Contains(Execution::Args::Type::AcceptPackageAgreements)) + { + AICLI_LOG(CLI, Info, << "Package agreements accepted by CLI flag"); + return; + } + + if (showPrompt) + { + AICLI_LOG(CLI, Verbose, << "Prompting to accept package agreements"); + if (IsInteractivityAllowed(context)) + { + bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PackageAgreementsPrompt); + if (accepted) + { + AICLI_LOG(CLI, Info, << "Package agreements accepted in prompt"); + return; + } + else + { + AICLI_LOG(CLI, Info, << "Package agreements not accepted in prompt"); + } + } + } + + AICLI_LOG(CLI, Error, << "Package agreements were not agreed to."); + context.Reporter.Error() << Resource::String::PackageAgreementsNotAgreedTo << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); + } + + bool m_ensureAgreementsAcceptance; + }; + + // Prompt for getting the install root when a package requires it and it is not + // specified by the settings. + struct InstallRootPrompt : public PackagePrompt + { + InstallRootPrompt() : m_installLocation(User().Get<Setting::InstallDefaultRoot>()) {} + + bool PackageNeedsPrompt(Execution::Context& context) override + { + if (context.Get<Execution::Data::Installer>()->InstallLocationRequired && + !context.Args.Contains(Execution::Args::Type::InstallLocation)) + { + AICLI_LOG(CLI, Info, << "Package [" << context.Get<Execution::Data::Manifest>().Id << "] requires an install location."); + + // An install location is required but one wasn't provided. + // Check if there is a default one from settings. + if (m_installLocation.empty()) + { + // We need to prompt + return true; + } + else + { + // Use the default + SetInstallLocation(context); + } + } + + return false; + } + + void PromptForSinglePackage(Execution::Context& context) override + { + context.Reporter.Info() << Resource::String::InstallerRequiresInstallLocation << std::endl; + PromptForInstallRoot(context); + + // When prompting for a single package, we use the provided location directly. + // This is different from when we prompt for multiple packages or use the root in the settings. + context.Args.AddArg(Execution::Args::Type::InstallLocation, m_installLocation.string()); + } + + void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override + { + // Report packages that will be affected. + context.Reporter.Info() << Resource::String::InstallersRequireInstallLocation << std::endl; + for (auto packageContext : packagesToPrompt) + { + *packageContext << ReportManifestIdentityWithVersion(" - "_liv, Execution::Reporter::Level::Warning); + if (packageContext->IsTerminated()) + { + AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR()); + } + } + + PromptForInstallRoot(context); + + // Set the install location for each package. + for (auto packageContext : packagesToPrompt) + { + SetInstallLocation(*packageContext); + } + } + + private: + void PromptForInstallRoot(Execution::Context& context) + { + if (!IsInteractivityAllowed(context)) + { + AICLI_LOG(CLI, Error, << "Install location is required but was not provided."); + context.Reporter.Error() << Resource::String::InstallLocationNotProvided << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED); + } + + AICLI_LOG(CLI, Info, << "Prompting for install root."); + m_installLocation = context.Reporter.PromptForPath(Resource::String::PromptForInstallRoot); + AICLI_LOG(CLI, Info, << "Proceeding with installation using install root: " << m_installLocation); + } + + // Sets the install location for an execution context. + // The install location is obtained by appending the package ID to the install root. + // This function assumes that m_installLocation is set, either from settings or from the prompt, + // and that the context does not already have an install location. + void SetInstallLocation(Execution::Context& context) + { + auto packageId = context.Get<Execution::Data::Manifest>().Id; + auto installLocation = m_installLocation; + installLocation += "\\" + packageId; + AICLI_LOG(CLI, Info, << "Setting install location for package [" << packageId << "] to: " << installLocation); + context.Args.AddArg(Execution::Args::Type::InstallLocation, installLocation.string()); + } + + std::filesystem::path m_installLocation; + }; + + // Prompt asking whether to continue when an installer will abort the terminal. + struct InstallerAbortsTerminalPrompt : public PackagePrompt + { + bool PackageNeedsPrompt(Execution::Context& context) override + { + return context.Get<Execution::Data::Installer>()->InstallerAbortsTerminal; + } + + void PromptForSinglePackage(Execution::Context& context) override + { + AICLI_LOG(CLI, Info, << "This installer may abort the terminal"); + context.Reporter.Warn() << Resource::String::InstallerAbortsTerminal << std::endl; + PromptToProceed(context); + } + + void PromptForMultiplePackages(Execution::Context& context, std::vector<Execution::Context*>& packagesToPrompt) override + { + AICLI_LOG(CLI, Info, << "One or more installers may abort the terminal"); + context.Reporter.Warn() << Resource::String::InstallersAbortTerminal << std::endl; + for (auto packageContext : packagesToPrompt) + { + *packageContext << ReportManifestIdentityWithVersion(" - "_liv, Execution::Reporter::Level::Warning); + if (packageContext->IsTerminated()) + { + AICLI_TERMINATE_CONTEXT(packageContext->GetTerminationHR()); + } + } + + PromptToProceed(context); + } + + private: + void PromptToProceed(Execution::Context& context) + { + AICLI_LOG(CLI, Info, << "Prompting before proceeding with installer that aborts terminal."); + if (!IsInteractivityAllowed(context)) + { + return; + } + + bool accepted = context.Reporter.PromptForBoolResponse(Resource::String::PromptToProceed, Execution::Reporter::Level::Warning); + if (accepted) + { + AICLI_LOG(CLI, Info, << "Proceeding with installation"); + } + else + { + AICLI_LOG(CLI, Error, << "Aborting installation"); + context.Reporter.Error() << Resource::String::Cancelled << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); + } + } + }; + + // Gets all the prompts that may be displayed, in order of appearance + std::vector<std::unique_ptr<PackagePrompt>> GetPackagePrompts(bool ensureAgreementsAcceptance = true) + { + std::vector<std::unique_ptr<PackagePrompt>> result; + result.push_back(std::make_unique<PackageAgreementsPrompt>(ensureAgreementsAcceptance)); + result.push_back(std::make_unique<InstallRootPrompt>()); + result.push_back(std::make_unique<InstallerAbortsTerminalPrompt>()); + return result; + } + } + + void HandleSourceAgreements::operator()(Execution::Context& context) const + { + if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) + { + AICLI_LOG(CLI, Info, << "Skipping source agreements acceptance check because AgreementsAcceptedByCaller flag is set."); + return; + } + + bool allAccepted = true; + + if (m_source.IsComposite()) + { + for (auto const& source : m_source.GetAvailableSources()) + { + if (!HandleSourceAgreementsForOneSource(context, source)) + { + allAccepted = false; + } + } + } + else + { + allAccepted = HandleSourceAgreementsForOneSource(context, m_source); + } + + if (!allAccepted) + { + context.Reporter.Error() << Resource::String::SourceAgreementsNotAgreedTo << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); + } + } + + void ShowPromptsForSinglePackage::operator()(Execution::Context& context) const + { + for (auto& prompt : GetPackagePrompts()) + { + // Show the prompt if needed + if (prompt->PackageNeedsPrompt(context)) + { + prompt->PromptForSinglePackage(context); + } + + if (context.IsTerminated()) + { + return; + } + } + } + + void ShowPromptsForMultiplePackages::operator()(Execution::Context& context) const + { + for (auto& prompt : GetPackagePrompts(m_ensureAgreementsAcceptance)) + { + // Find which packages need this prompt + std::vector<Execution::Context*> packagesToPrompt; + for (auto& packageContext : context.Get<Execution::Data::PackagesToInstall>()) + { + if (prompt->PackageNeedsPrompt(*packageContext)) + { + packagesToPrompt.push_back(packageContext.get()); + } + } + + // Prompt only if needed + if (!packagesToPrompt.empty()) + { + prompt->PromptForMultiplePackages(context, packagesToPrompt); + if (context.IsTerminated()) + { + return; + } + } + } + } +} diff --git a/src/AppInstallerCLICore/Workflows/PromptFlow.h b/src/AppInstallerCLICore/Workflows/PromptFlow.h @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Handles all opened source(s) agreements if needed. + // Required Args: The source to be checked for agreements + // Inputs: None + // Outputs: None + struct HandleSourceAgreements : public WorkflowTask + { + HandleSourceAgreements(Repository::Source source) : WorkflowTask("HandleSourceAgreements"), m_source(std::move(source)) {} + + void operator()(Execution::Context& context) const override; + + private: + Repository::Source m_source; + }; + + // Shows all the prompts required for a single package, e.g. for package agreements + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + struct ShowPromptsForSinglePackage : public WorkflowTask + { + ShowPromptsForSinglePackage(bool ensureAgreementsAcceptance) : + WorkflowTask("ShowPromptsForSinglePackage"), m_ensureAgreementsAcceptance(ensureAgreementsAcceptance) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_ensureAgreementsAcceptance; + }; + + // Shows all the prompts required for multiple package, e.g. for package agreements + // Required Args: None + // Inputs: PackagesToInstall + // Outputs: None + struct ShowPromptsForMultiplePackages : public WorkflowTask + { + ShowPromptsForMultiplePackages(bool ensureAgreementsAcceptance) : + WorkflowTask("ShowPromptsForMultiplePackages"), m_ensureAgreementsAcceptance(ensureAgreementsAcceptance) {} + + void operator()(Execution::Context& context) const override; + + private: + bool m_ensureAgreementsAcceptance; + }; +}+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Resources.h" #include "SourceFlow.h" +#include "PromptFlow.h" #include "TableOutput.h" #include "WorkflowBase.h" diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -4,6 +4,7 @@ #include "WorkflowBase.h" #include "ExecutionContext.h" #include "ManifestComparator.h" +#include "PromptFlow.h" #include "TableOutput.h" #include <winget/ManifestYamlParser.h> @@ -31,14 +32,29 @@ namespace AppInstaller::CLI::Workflow } } - void ReportIdentity(Execution::Context& context, std::string_view name, std::string_view id) + void ReportIdentity( + Execution::Context& context, + Utility::LocIndView prefix, + std::optional<Resource::StringId> label, + std::string_view name, + std::string_view id, + std::string_view version = {}, + Execution::Reporter::Level level = Execution::Reporter::Level::Info) { - context.Reporter.Info() << Resource::String::ReportIdentityFound << ' ' << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << ']' << std::endl; - } + auto out = context.Reporter.GetOutputStream(level); + out << prefix; + if (label) + { + out << *label << ' '; + } + out << Execution::NameEmphasis << name << " ["_liv << Execution::IdEmphasis << id << ']'; - void ReportIdentity(Execution::Context& context, std::string_view name, std::string_view id, std::string_view version) - { - context.Reporter.Info() << Resource::String::ReportIdentityFound << ' ' << Execution::NameEmphasis << name << " [" << Execution::IdEmphasis << id << "] " << Resource::String::ShowVersion << ' ' << version << std::endl; + if (!version.empty()) + { + out << ' ' << Resource::String::ShowVersion << ' ' << version; + } + + out << std::endl; } Repository::Source OpenNamedSource(Execution::Context& context, std::string_view sourceName) @@ -154,71 +170,6 @@ namespace AppInstaller::CLI::Workflow } } - bool HandleSourceAgreementsForOneSource(Execution::Context& context, const Source& source) - { - auto details = source.GetDetails(); - AICLI_LOG(CLI, Verbose, << "Checking Source agreements for source: " << details.Name); - - if (source.CheckSourceAgreements()) - { - AICLI_LOG(CLI, Verbose, << "Source agreements satisfied. Source: " << details.Name); - return true; - } - - // Show source agreements - std::string agreementsTitleMessage = Resource::LocString{ Resource::String::SourceAgreementsTitle }; - context.Reporter.Info() << Execution::SourceInfoEmphasis << - Utility::LocIndString{ Utility::FindAndReplaceMessageToken(agreementsTitleMessage, details.Name) } << std::endl; - - const auto& agreements = source.GetInformation().SourceAgreements; - - for (const auto& agreement : agreements) - { - if (!agreement.Label.empty()) - { - context.Reporter.Info() << Execution::SourceInfoEmphasis << Utility::LocIndString{ agreement.Label } << ": "_liv; - } - - if (!agreement.Text.empty()) - { - context.Reporter.Info() << Utility::LocIndString{ agreement.Text } << std::endl; - } - - if (!agreement.Url.empty()) - { - context.Reporter.Info() << Utility::LocIndString{ agreement.Url } << std::endl; - } - } - - // Show message for each individual implicit agreement field - auto fields = source.GetAgreementFieldsFromSourceInformation(); - if (WI_IsFlagSet(fields, ImplicitAgreementFieldEnum::Market)) - { - context.Reporter.Info() << Resource::String::SourceAgreementsMarketMessage << std::endl; - } - - context.Reporter.Info() << std::endl; - - bool accepted = context.Args.Contains(Execution::Args::Type::AcceptSourceAgreements); - - if (!accepted) - { - accepted = context.Reporter.PromptForBoolResponse(Resource::String::SourceAgreementsPrompt); - } - - if (accepted) - { - AICLI_LOG(CLI, Verbose, << "Source agreements accepted. Source: " << details.Name); - source.SaveAcceptedSourceAgreements(); - } - else - { - AICLI_LOG(CLI, Verbose, << "Source agreements rejected. Source: " << details.Name); - } - - return accepted; - } - // Data shown on a line of a table displaying installed packages struct InstalledPackagesTableLine { @@ -996,19 +947,19 @@ namespace AppInstaller::CLI::Workflow void ReportPackageIdentity(Execution::Context& context) { auto package = context.Get<Execution::Data::Package>(); - ReportIdentity(context, package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); } void ReportManifestIdentity(Execution::Context& context) { const auto& manifest = context.Get<Execution::Data::Manifest>(); - ReportIdentity(context, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id); } - void ReportManifestIdentityWithVersion(Execution::Context& context) + void ReportManifestIdentityWithVersion::operator()(Execution::Context& context) const { const auto& manifest = context.Get<Execution::Data::Manifest>(); - ReportIdentity(context, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id, manifest.Version); + ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get<Manifest::Localization::PackageName>(), manifest.Id, manifest.Version, m_level); } void GetManifest(Execution::Context& context) @@ -1169,38 +1120,6 @@ namespace AppInstaller::CLI::Workflow { context.SetExecutionStage(m_stage); } - - void HandleSourceAgreements::operator()(Execution::Context& context) const - { - if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::AgreementsAcceptedByCaller)) - { - AICLI_LOG(CLI, Info, << "Skipping source agreements acceptance check because AgreementsAcceptedByCaller flag is set."); - return; - } - - bool allAccepted = true; - - if (m_source.IsComposite()) - { - for (auto const& source : m_source.GetAvailableSources()) - { - if (!HandleSourceAgreementsForOneSource(context, source)) - { - allAccepted = false; - } - } - } - else - { - allAccepted = HandleSourceAgreementsForOneSource(context, m_source); - } - - if (!allAccepted) - { - context.Reporter.Error() << Resource::String::SourceAgreementsNotAgreedTo << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_AGREEMENTS_NOT_ACCEPTED); - } - } } AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include "ExecutionArgs.h" +#include "ExecutionReporter.h" #include <winget/ExperimentalFeature.h> #include <winget/RepositorySearch.h> @@ -300,7 +301,20 @@ namespace AppInstaller::CLI::Workflow // Required Args: None // Inputs: Manifest // Outputs: None - void ReportManifestIdentityWithVersion(Execution::Context& context); + struct ReportManifestIdentityWithVersion : public WorkflowTask + { + ReportManifestIdentityWithVersion(Utility::LocIndView prefix, Execution::Reporter::Level level = Execution::Reporter::Level::Info) : + WorkflowTask("ReportManifestIdentityWithVersion"), m_prefix(prefix), m_level(level) {} + ReportManifestIdentityWithVersion(Resource::StringId label = Resource::String::ReportIdentityFound, Execution::Reporter::Level level = Execution::Reporter::Level::Info) : + WorkflowTask("ReportManifestIdentityWithVersion"), m_label(label), m_level(level) {} + + void operator()(Execution::Context& context) const override; + + private: + Utility::LocIndView m_prefix; + std::optional<Resource::StringId> m_label; + Execution::Reporter::Level m_level; + }; // Composite flow that produces a manifest; either from one given on the command line or by searching. // Required Args: None @@ -359,20 +373,6 @@ namespace AppInstaller::CLI::Workflow private: ExecutionStage m_stage; }; - - // Handles all opened source(s) agreements if needed. - // Required Args: The source to be checked for agreements - // Inputs: None - // Outputs: None - struct HandleSourceAgreements : public WorkflowTask - { - HandleSourceAgreements(Repository::Source source) : WorkflowTask("HandleSourceAgreements"), m_source(std::move(source)) {} - - void operator()(Execution::Context& context) const override; - - private: - Repository::Source m_source; - }; } // Passes the context to the function if it has not been terminated; returns the context. diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -197,6 +197,7 @@ namespace AppInstallerCLIE2ETests public const int ERROR_EXTRACT_ARCHIVE_FAILED = unchecked((int)0x8A15005C); public const int ERROR_NESTEDINSTALLER_INVALID_PATH = unchecked((int)0x8A15005D); public const int ERROR_PINNED_CERTIFICATE_MISMATCH = unchecked((int)0x8A15005E); + public const int ERROR_INSTALL_LOCATION_REQUIRED = unchecked((int)0x8A15005F); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -1400,4 +1400,33 @@ Please specify one of them using the `--source` option to proceed.</value> <data name="NestedInstallerNotSupported" xml:space="preserve"> <value>The nested installer type is not supported</value> </data> + <data name="InstallerAbortsTerminal" xml:space="preserve"> + <value>This installer is known to restart the terminal or shell</value> + </data> + <data name="InstallerRequiresInstallLocation" xml:space="preserve"> + <value>This package requires an install location</value> + </data> + <data name="InstallersAbortTerminal" xml:space="preserve"> + <value>The following installers are known to restart the terminal or shell:</value> + </data> + <data name="InstallersRequireInstallLocation" xml:space="preserve"> + <value>The following installers require an install location:</value> + </data> + <data name="PromptForInstallRoot" xml:space="preserve"> + <value>Specify the install root:</value> + </data> + <data name="PromptToProceed" xml:space="preserve"> + <value>Do you want to proceed?</value> + </data> + <data name="ReportIdentityForAgreements" xml:space="preserve"> + <value>Agreements for</value> + <comment>This will be followed by a package name, and then a list of license agreements</comment> + </data> + <data name="InstallLocationNotProvided" xml:space="preserve"> + <value>Install location is required by the package but it was not provided</value> + </data> + <data name="DisableInteractivityArgumentDescription" xml:space="preserve"> + <value>Disable interactive prompts</value> + <comment>Description for a command line argument, shown next to it in the help</comment> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -265,6 +265,12 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_MSStore.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_AbortsTerminal.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_InstallLocationRequired.yaml"> + <DeploymentContent>true</DeploymentContent> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_NoApplicableArchitecture.yaml"> <DeploymentContent>true</DeploymentContent> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -507,12 +507,18 @@ <CopyFileToFolders Include="TestData\InstallFlowTest_MSStore.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_AbortsTerminal.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_ExpectedReturnCodes.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_InstallationNotes.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> + <CopyFileToFolders Include="TestData\InstallFlowTest_InstallLocationRequired.yaml"> + <Filter>TestData</Filter> + </CopyFileToFolders> <CopyFileToFolders Include="TestData\InstallFlowTest_Portable.yaml"> <Filter>TestData</Filter> </CopyFileToFolders> diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_AbortsTerminal.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_AbortsTerminal.yaml @@ -0,0 +1,16 @@ +PackageIdentifier: AppInstallerCliTest.TestInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +ShortDescription: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +InstallerAbortsTerminal: true +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_InstallLocationRequired.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_InstallLocationRequired.yaml @@ -0,0 +1,18 @@ +PackageIdentifier: AppInstallerCliTest.TestInstaller +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: AppInstaller Test Installer +ShortDescription: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +InstallLocationRequired: true +InstallerSwitches: + InstallLocation: /InstallDir <INSTALLPATH> +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B +ManifestType: singleton +ManifestVersion: 1.1.0 diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -10,19 +10,20 @@ #include <AppInstallerLogging.h> #include <AppInstallerDownloader.h> #include <AppInstallerStrings.h> -#include <Workflows/ImportExportFlow.h> +#include <Workflows/ArchiveFlow.h> +#include <Workflows/DependenciesFlow.h> #include <Workflows/DownloadFlow.h> +#include <Workflows/ImportExportFlow.h> #include <Workflows/InstallFlow.h> #include <Workflows/MsiInstallFlow.h> -#include <Workflows/PortableFlow.h> -#include <Workflows/ArchiveFlow.h> -#include <Workflows/UninstallFlow.h> -#include <Workflows/UpdateFlow.h> -#include <Workflows/DependenciesFlow.h> #include <Workflows/MSStoreInstallerHandler.h> +#include <Workflows/PortableFlow.h> +#include <Workflows/PromptFlow.h> +#include <Workflows/ShellExecuteInstallerHandler.h> #include <Workflows/ShowFlow.h> #include <Workflows/SourceFlow.h> -#include <Workflows/ShellExecuteInstallerHandler.h> +#include <Workflows/UninstallFlow.h> +#include <Workflows/UpdateFlow.h> #include <Workflows/WorkflowBase.h> #include <Public/winget/RepositorySource.h> #include <Commands/ExportCommand.h> @@ -3345,3 +3346,156 @@ TEST_CASE("AdminSetting_LocalManifestFiles", "[LocalManifests][workflow]") REQUIRE_THROWS(installCommand3.ValidateArguments(args3)); } } + +TEST_CASE("PromptFlow_InteractivityDisabled", "[PromptFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_LicenseAgreement.yaml").GetPath().u8string()); + + SECTION("Disabled by setting") + { + testSettings.Set<Setting::InteractivityDisable>(true); + } + SECTION("Disabled by arg") + { + context.Args.AddArg(Execution::Args::Type::DisableInteractivity); + } + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify prompt is not shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageAgreementsPrompt).get()) == std::string::npos); + + // Verify installation failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_PACKAGE_AGREEMENTS_NOT_ACCEPTED); + REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::PackageAgreementsNotAgreedTo).get()) != std::string::npos); +} + +TEST_CASE("PromptFlow_InstallerAbortsTerminal_Proceed", "[PromptFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + // Accept that the installer may abort the terminal by saying "Yes" at the prompt + std::istringstream installInput{ "y" }; + + std::ostringstream installOutput; + TestContext context{ installOutput, installInput }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_AbortsTerminal.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify prompt is shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallerAbortsTerminal).get()) != std::string::npos); + + // Verify Installer is called. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("PromptFlow_InstallerAbortsTerminal_Cancel", "[PromptFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + // Cancel the installation by saying "No" at the prompt that the installer may abort the terminal + std::istringstream installInput{ "n" }; + + std::ostringstream installOutput; + TestContext context{ installOutput, installInput }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_AbortsTerminal.yaml").GetPath().u8string()); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify prompt is shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallerAbortsTerminal).get()) != std::string::npos); + + // Verify installation failed + REQUIRE_TERMINATED_WITH(context, E_ABORT); + REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); +} + +TEST_CASE("PromptFlow_InstallLocationRequired", "[PromptFlow][workflow]") +{ + TestCommon::TempDirectory installLocation("TempDirectory"); + TestCommon::TestUserSettings testSettings; + + std::istringstream installInput; + std::ostringstream installOutput; + TestContext context{ installOutput, installInput }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_InstallLocationRequired.yaml").GetPath().u8string()); + + bool shouldShowPrompt = false; + std::filesystem::path installResultPath = installLocation.GetPath() / "TestExeInstalled.txt"; + SECTION("From argument") + { + context.Args.AddArg(Execution::Args::Type::InstallLocation, installLocation.GetPath().string()); + } + SECTION("From settings") + { + testSettings.Set<Setting::InstallDefaultRoot>(installLocation.GetPath().string()); + + // When using the default location from settings, the Package ID is appended to the root + auto installLocationWithPackageId = installLocation.GetPath() / "AppInstallerCliTest.TestInstaller"; + std::filesystem::create_directory(installLocationWithPackageId); + installResultPath = installLocationWithPackageId / "TestExeInstalled.txt"; + } + SECTION("From prompt") + { + installInput.str(installLocation.GetPath().string()); + shouldShowPrompt = true; + } + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + bool promptShown = installOutput.str().find(Resource::LocString(Resource::String::InstallerRequiresInstallLocation).get()) != std::string::npos; + REQUIRE(shouldShowPrompt == promptShown); + + // Verify Installer is called with the right parameters + REQUIRE(std::filesystem::exists(installResultPath)); + std::ifstream installResultFile(installResultPath); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + const auto installDirArgument = "/InstallDir " + installLocation.GetPath().string(); + REQUIRE(installResultStr.find(installDirArgument) != std::string::npos); +} + +TEST_CASE("PromptFlow_InstallLocationRequired_Missing", "[PromptFlow][workflow]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_InstallLocationRequired.yaml").GetPath().u8string()); + // Disable interactivity so that there is not prompt and we cannot get the required location + context.Args.AddArg(Execution::Args::Type::DisableInteractivity); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + // Verify prompt is shown + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InstallerRequiresInstallLocation).get()) != std::string::npos); + + // Verify installation failed + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED); + REQUIRE_FALSE(std::filesystem::exists(installResultPath.GetPath())); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Errors.cpp b/src/AppInstallerCommonCore/Errors.cpp @@ -156,6 +156,8 @@ namespace AppInstaller return "Source agreements were not agreed to"; case APPINSTALLER_CLI_ERROR_CUSTOMHEADER_EXCEEDS_MAXLENGTH: return "Header size exceeds the allowable limit of 1024 characters. Please reduce the size and try again."; + case APPINSTALLER_CLI_ERROR_MISSING_RESOURCE_FILE: + return "Missing resource file"; case APPINSTALLER_CLI_ERROR_MSI_INSTALL_FAILED: return "Running MSI install failed"; case APPINSTALLER_CLI_ERROR_INVALID_MSIEXEC_ARGUMENT: @@ -188,6 +190,8 @@ namespace AppInstaller return "Failed to uninstall portable package"; case APPINSTALLER_CLI_ERROR_ARP_VERSION_VALIDATION_FAILED: return "Failed to validate DisplayVersion values against index."; + case APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED: + return "Install location required but not provided"; case APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE: return "Application is currently running.Exit the application then try again."; case APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS: diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -107,6 +107,7 @@ #define APPINSTALLER_CLI_ERROR_EXTRACT_ARCHIVE_FAILED ((HRESULT)0x8A15005C) #define APPINSTALLER_CLI_ERROR_NESTEDINSTALLER_INVALID_PATH ((HRESULT)0x8A15005D) #define APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH ((HRESULT)0x8A15005E) +#define APPINSTALLER_CLI_ERROR_INSTALL_LOCATION_REQUIRED ((HRESULT)0x8A15005F) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -68,29 +68,41 @@ namespace AppInstaller::Settings // Validate will be called by ValidateAll without any more changes. enum class Setting : size_t { + // Visual ProgressBarVisualStyle, + // Source AutoUpdateTimeInMinutes, + // Experimental EFExperimentalCmd, EFExperimentalArg, EFDependencies, + EFDirectMSI, EFZipInstall, + // Telemetry TelemetryDisable, + // Install behavior InstallScopePreference, InstallScopeRequirement, - NetworkDownloader, - NetworkDOProgressTimeoutInSeconds, InstallArchitecturePreference, InstallArchitectureRequirement, InstallLocalePreference, InstallLocaleRequirement, - EFDirectMSI, - EnableSelfInitiatedMinidump, - LoggingLevelPreference, + InstallDefaultRoot, InstallIgnoreWarnings, DisableInstallNotes, PortablePackageUserRoot, PortablePackageMachineRoot, + // Network + NetworkDownloader, + NetworkDOProgressTimeoutInSeconds, + // Logging + LoggingLevelPreference, + // Uninstall behavior UninstallPurgePortablePackage, + // Interactivity + InteractivityDisable, + // Debug + EnableSelfInitiatedMinidump, Max }; @@ -124,30 +136,42 @@ namespace AppInstaller::Settings #define SETTINGMAPPING_SPECIALIZATION(_setting_, _json_, _value_, _default_, _path_) \ SETTINGMAPPING_SPECIALIZATION_POLICY(_setting_, _json_, _value_, _default_, _path_, ValuePolicy::None) + // Visual SETTINGMAPPING_SPECIALIZATION(Setting::ProgressBarVisualStyle, std::string, VisualStyle, VisualStyle::Accent, ".visual.progressBar"sv); + // Source SETTINGMAPPING_SPECIALIZATION_POLICY(Setting::AutoUpdateTimeInMinutes, uint32_t, std::chrono::minutes, 5min, ".source.autoUpdateIntervalInMinutes"sv, ValuePolicy::SourceAutoUpdateIntervalInMinutes); + // Experimental SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalCmd, bool, bool, false, ".experimentalFeatures.experimentalCmd"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDependencies, bool, bool, false, ".experimentalFeatures.dependencies"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFZipInstall, bool, bool, false, ".experimentalFeatures.zipInstall"sv); + // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); + // Install behavior SETTINGMAPPING_SPECIALIZATION(Setting::InstallArchitecturePreference, std::vector<std::string>, std::vector<Utility::Architecture>, {}, ".installBehavior.preferences.architectures"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallArchitectureRequirement, std::vector<std::string>, std::vector<Utility::Architecture>, {}, ".installBehavior.requirements.architectures"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopePreference, std::string, ScopePreference, ScopePreference::User, ".installBehavior.preferences.scope"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallScopeRequirement, std::string, ScopePreference, ScopePreference::None, ".installBehavior.requirements.scope"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDownloader, std::string, InstallerDownloader, InstallerDownloader::Default, ".network.downloader"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDOProgressTimeoutInSeconds, uint32_t, std::chrono::seconds, 60s, ".network.doProgressTimeoutInSeconds"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocalePreference, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.preferences.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocaleRequirement, std::vector<std::string>, std::vector<std::string>, {}, ".installBehavior.requirements.locale"sv); SETTINGMAPPING_SPECIALIZATION(Setting::InstallIgnoreWarnings, bool, bool, false, ".installBehavior.ignoreWarnings"sv); SETTINGMAPPING_SPECIALIZATION(Setting::DisableInstallNotes, bool, bool, false, ".installBehavior.disableInstallNotes"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageUserRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageUserRoot"sv); SETTINGMAPPING_SPECIALIZATION(Setting::PortablePackageMachineRoot, std::string, std::filesystem::path, {}, ".installBehavior.portablePackageMachineRoot"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::InstallDefaultRoot, std::string, std::filesystem::path, {}, ".installBehavior.defaultInstallRoot"sv); + // Uninstall behavior SETTINGMAPPING_SPECIALIZATION(Setting::UninstallPurgePortablePackage, bool, bool, false, ".uninstallBehavior.purgePortablePackage"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); + // Network + SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDownloader, std::string, InstallerDownloader, InstallerDownloader::Default, ".network.downloader"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::NetworkDOProgressTimeoutInSeconds, uint32_t, std::chrono::seconds, 60s, ".network.doProgressTimeoutInSeconds"sv); + // Debug SETTINGMAPPING_SPECIALIZATION(Setting::EnableSelfInitiatedMinidump, bool, bool, false, ".debugging.enableSelfInitiatedMinidump"sv); + // Logging SETTINGMAPPING_SPECIALIZATION(Setting::LoggingLevelPreference, std::string, Logging::Level, Logging::Level::Info, ".logging.level"sv); - + // Interactivity + SETTINGMAPPING_SPECIALIZATION(Setting::InteractivityDisable, bool, bool, false, ".interactivity.disable"sv); + // Used to deduce the SettingVariant type; making a variant that includes std::monostate and all SettingMapping types. template <size_t... I> inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, typename SettingMapping<static_cast<Setting>(I)>::value_t...>{}; } diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -189,6 +189,17 @@ namespace AppInstaller::Settings // Use folding to call each setting validate function. (FoldHelper{}, ..., Validate<static_cast<Setting>(S)>(root, settings, warnings)); } + + std::optional<std::filesystem::path> ValidatePathValue(std::string_view value) + { + std::filesystem::path path = ConvertToUTF16(value); + if (!path.is_absolute()) + { + return {}; + } + + return path; + } } namespace details @@ -235,9 +246,10 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalCmd) WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDependencies) + WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFZipInstall) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) - WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) + WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) WINGET_VALIDATE_PASS_THROUGH(EnableSelfInitiatedMinidump) WINGET_VALIDATE_PASS_THROUGH(InstallIgnoreWarnings) WINGET_VALIDATE_PASS_THROUGH(DisableInstallNotes) @@ -245,18 +257,12 @@ namespace AppInstaller::Settings WINGET_VALIDATE_SIGNATURE(PortablePackageUserRoot) { - std::filesystem::path root = ConvertToUTF16(value); - if (!root.is_absolute()) - { - return {}; - } - - return root; + return ValidatePathValue(value); } WINGET_VALIDATE_SIGNATURE(PortablePackageMachineRoot) { - return SettingMapping<Setting::PortablePackageUserRoot>::Validate(value); + return ValidatePathValue(value); } WINGET_VALIDATE_SIGNATURE(InstallArchitecturePreference) @@ -318,6 +324,11 @@ namespace AppInstaller::Settings return SettingMapping<Setting::InstallLocalePreference>::Validate(value); } + WINGET_VALIDATE_SIGNATURE(InstallDefaultRoot) + { + return ValidatePathValue(value); + } + WINGET_VALIDATE_SIGNATURE(NetworkDownloader) { static constexpr std::string_view s_downloader_default = "default";