diff --git a/doc/Settings.md b/doc/Settings.md
index 7285c9e1a0..bb2e078d85 100644
--- 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
index cb5b67f196..768158ccff 100644
--- 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",
@@ -233,6 +249,12 @@
},
"additionalItems": true
},
+ {
+ "properties": {
+ "interactivity": { "$ref": "#/definitions/Interactivity" }
+ },
+ "additionalItems": true
+ },
{
"properties": {
"experimentalFeatures": { "$ref": "#/definitions/Experimental" }
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index f09cd3c3ab..4cc8f5d9da 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -260,7 +260,7 @@
-
+
@@ -283,6 +283,7 @@
+
@@ -336,6 +337,7 @@
+
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
index f0925e10b9..4904eab23d 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
@@ -173,7 +173,7 @@
Workflows
-
+
Public
@@ -181,7 +181,10 @@
Workflows
-
+
+
+ Workflows
+
@@ -328,9 +331,12 @@
Workflows
+
+ Workflows
+
Workflows
-
+
diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp
index a871a3b387..6af4585099 100644
--- 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
index 5cd9cde1e8..fa282661cc 100644
--- 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
index 5f52898c81..7a820a2817 100644
--- 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
index 80568e1b89..bafda0b0cf 100644
--- 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
index d2f0deb301..6fe4d8c444 100644
--- 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
index c70e78a858..88808bc50a 100644
--- 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
index e262f44a6a..f31c1469b0 100644
--- 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
index 2367f53d76..157fc4bd9f 100644
--- 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
index 4c91903834..b24920b684 100644
--- 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
#include
#include
@@ -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();
- auto agreements = manifest.CurrentLocalization.Get();
-
- 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())
- {
- // Show agreements for each package that has one
- auto agreements = packageContext->Get().CurrentLocalization.Get();
- 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())
{
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
index 62136565de..7e62becb94 100644
--- 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
new file mode 100644
index 0000000000..1df6f3ea17
--- /dev/null
+++ 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
+
+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())
+ {
+ 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& 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().CurrentLocalization.Get();
+ return !agreements.empty();
+ }
+
+ void PromptForSinglePackage(Execution::Context& context) override
+ {
+ ShowPackageAgreements(context);
+ EnsurePackageAgreementsAcceptance(context, /* showPrompt */ true);
+ }
+
+ void PromptForMultiplePackages(Execution::Context& context, std::vector& 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();
+ auto agreements = manifest.CurrentLocalization.Get();
+
+ 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()) {}
+
+ bool PackageNeedsPrompt(Execution::Context& context) override
+ {
+ if (context.Get()->InstallLocationRequired &&
+ !context.Args.Contains(Execution::Args::Type::InstallLocation))
+ {
+ AICLI_LOG(CLI, Info, << "Package [" << context.Get().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& 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().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()->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& 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> GetPackagePrompts(bool ensureAgreementsAcceptance = true)
+ {
+ std::vector> result;
+ result.push_back(std::make_unique(ensureAgreementsAcceptance));
+ result.push_back(std::make_unique());
+ result.push_back(std::make_unique());
+ 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 packagesToPrompt;
+ for (auto& packageContext : context.Get())
+ {
+ 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
new file mode 100644
index 0000000000..0c79df84b6
--- /dev/null
+++ 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
index 9dbb3c2e91..d6c45556df 100644
--- 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
index aaf5a52f9e..d18c690894 100644
--- 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
@@ -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 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();
- 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();
- ReportIdentity(context, manifest.CurrentLocalization.Get(), manifest.Id);
+ ReportIdentity(context, {}, Resource::String::ReportIdentityFound, manifest.CurrentLocalization.Get(), manifest.Id);
}
- void ReportManifestIdentityWithVersion(Execution::Context& context)
+ void ReportManifestIdentityWithVersion::operator()(Execution::Context& context) const
{
const auto& manifest = context.Get();
- ReportIdentity(context, manifest.CurrentLocalization.Get(), manifest.Id, manifest.Version);
+ ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get(), 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
index ab526554db..03f81d0d47 100644
--- 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
#include
@@ -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 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
index c26fe05e7d..b2d034d42f 100644
--- a/src/AppInstallerCLIE2ETests/Constants.cs
+++ b/src/AppInstallerCLIE2ETests/Constants.cs
@@ -197,6 +197,7 @@ public class ErrorCode
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
index f5705c9943..4058653011 100644
--- 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.
The nested installer type is not supported
+
+ This installer is known to restart the terminal or shell
+
+
+ This package requires an install location
+
+
+ The following installers are known to restart the terminal or shell:
+
+
+ The following installers require an install location:
+
+
+ Specify the install root:
+
+
+ Do you want to proceed?
+
+
+ Agreements for
+ This will be followed by a package name, and then a list of license agreements
+
+
+ Install location is required by the package but it was not provided
+
+
+ Disable interactive prompts
+ Description for a command line argument, shown next to it in the help
+
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index ede8a06710..93e345fd0c 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -265,6 +265,12 @@
true
+
+ true
+
+
+ true
+
true
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index 39c1c9e126..04bd47fbb5 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -507,12 +507,18 @@
TestData
+
+ TestData
+
TestData
TestData
+
+ TestData
+
TestData
diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_AbortsTerminal.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_AbortsTerminal.yaml
new file mode 100644
index 0000000000..4d7980b285
--- /dev/null
+++ 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
new file mode 100644
index 0000000000..4c0e987253
--- /dev/null
+++ 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
+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
index cefc35414d..9dd0b07047 100644
--- a/src/AppInstallerCLITests/WorkFlow.cpp
+++ b/src/AppInstallerCLITests/WorkFlow.cpp
@@ -10,19 +10,20 @@
#include
#include
#include
-#include
+#include
+#include
#include
+#include
#include
#include
-#include
-#include
-#include
-#include
-#include
#include
+#include
+#include
+#include
#include
#include
-#include
+#include
+#include
#include
#include
#include
@@ -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(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(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
index 642bb5e44a..346d5936a4 100644
--- 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
index 11d36b9550..1a5014f00d 100644
--- 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
index ef66182813..7f9130fb05 100644
--- 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::vector, {}, ".installBehavior.preferences.architectures"sv);
SETTINGMAPPING_SPECIALIZATION(Setting::InstallArchitectureRequirement, std::vector, std::vector, {}, ".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::vector, {}, ".installBehavior.preferences.locale"sv);
SETTINGMAPPING_SPECIALIZATION(Setting::InstallLocaleRequirement, std::vector, std::vector, {}, ".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
inline auto Deduce(std::index_sequence) { return std::variant(I)>::value_t...>{}; }
diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp
index b80b19ce3e..56e62e0851 100644
--- 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(S)>(root, settings, warnings));
}
+
+ std::optional 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::Validate(value);
+ return ValidatePathValue(value);
}
WINGET_VALIDATE_SIGNATURE(InstallArchitecturePreference)
@@ -318,6 +324,11 @@ namespace AppInstaller::Settings
return SettingMapping::Validate(value);
}
+ WINGET_VALIDATE_SIGNATURE(InstallDefaultRoot)
+ {
+ return ValidatePathValue(value);
+ }
+
WINGET_VALIDATE_SIGNATURE(NetworkDownloader)
{
static constexpr std::string_view s_downloader_default = "default";