commit 1ea38f17a42e55a5bdef1badebe5fcb770f6689d parent 07c6b7aeb783c23c69f87fd314417792e5aa7bb0 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Mon, 6 Apr 2020 15:33:26 -0700 Take Workflows apart and put back together (#79) Diffstat:
44 files changed, 1728 insertions(+), 1130 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -190,14 +190,11 @@ <ClInclude Include="Public\AppInstallerCLICore.h" /> <ClInclude Include="Search\Search.h" /> <ClInclude Include="VTSupport.h" /> - <ClInclude Include="Workflows\Common.h" /> - <ClInclude Include="Workflows\SearchFlow.h" /> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> - <ClInclude Include="Workflows\InstallerHandlerBase.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> <ClInclude Include="Workflows\ManifestComparator.h" /> - <ClInclude Include="Workflows\MsixInstallerHandler.h" /> <ClInclude Include="Workflows\ShowFlow.h" /> + <ClInclude Include="Workflows\SourceFlow.h" /> <ClInclude Include="Workflows\WorkflowBase.h" /> </ItemGroup> <ItemGroup> @@ -211,18 +208,17 @@ <ClCompile Include="Commands\SourceCommand.cpp" /> <ClCompile Include="Commands\ValidateCommand.cpp" /> <ClCompile Include="Core.cpp" /> + <ClCompile Include="ExecutionContext.cpp" /> <ClCompile Include="ExecutionReporter.cpp" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="VTSupport.cpp" /> - <ClCompile Include="Workflows\SearchFlow.cpp" /> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> - <ClCompile Include="Workflows\InstallerHandlerBase.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> <ClCompile Include="Workflows\ManifestComparator.cpp" /> - <ClCompile Include="Workflows\MsixInstallerHandler.cpp" /> <ClCompile Include="Workflows\ShowFlow.cpp" /> + <ClCompile Include="Workflows\SourceFlow.cpp" /> <ClCompile Include="Workflows\WorkflowBase.cpp" /> </ItemGroup> <ItemGroup> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -54,15 +54,6 @@ <ClInclude Include="Workflows\ManifestComparator.h"> <Filter>Workflows</Filter> </ClInclude> - <ClInclude Include="Workflows\Common.h"> - <Filter>Workflows</Filter> - </ClInclude> - <ClInclude Include="Workflows\InstallerHandlerBase.h"> - <Filter>Workflows</Filter> - </ClInclude> - <ClInclude Include="Workflows\MsixInstallerHandler.h"> - <Filter>Workflows</Filter> - </ClInclude> <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h"> <Filter>Workflows</Filter> </ClInclude> @@ -78,9 +69,6 @@ <ClInclude Include="Commands\SearchCommand.h"> <Filter>Commands</Filter> </ClInclude> - <ClInclude Include="Workflows\SearchFlow.h"> - <Filter>Workflows</Filter> - </ClInclude> <ClInclude Include="Workflows\ShowFlow.h"> <Filter>Workflows</Filter> </ClInclude> @@ -102,6 +90,9 @@ <ClInclude Include="Argument.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Workflows\SourceFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> <ClInclude Include="Commands\ValidateCommand.h"> <Filter>Commands</Filter> </ClInclude> @@ -125,12 +116,6 @@ <ClCompile Include="Workflows\ManifestComparator.cpp"> <Filter>Workflows</Filter> </ClCompile> - <ClCompile Include="Workflows\InstallerHandlerBase.cpp"> - <Filter>Workflows</Filter> - </ClCompile> - <ClCompile Include="Workflows\MsixInstallerHandler.cpp"> - <Filter>Workflows</Filter> - </ClCompile> <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp"> <Filter>Workflows</Filter> </ClCompile> @@ -143,9 +128,6 @@ <ClCompile Include="Commands\SearchCommand.cpp"> <Filter>Commands</Filter> </ClCompile> - <ClCompile Include="Workflows\SearchFlow.cpp"> - <Filter>Workflows</Filter> - </ClCompile> <ClCompile Include="Commands\SourceCommand.cpp"> <Filter>Commands</Filter> </ClCompile> @@ -167,6 +149,12 @@ <ClCompile Include="Argument.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Workflows\SourceFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="ExecutionContext.cpp"> + <Filter>Source Files</Filter> + </ClCompile> <ClCompile Include="Commands\ValidateCommand.cpp"> <Filter>Commands</Filter> </ClCompile> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -60,6 +60,12 @@ namespace AppInstaller::CLI return Argument{ "versions", None, Args::Type::ListVersions, LOCME("Show available versions of the app"), ArgumentType::Flag }; case Args::Type::Help: return Argument{ "help", APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR, Args::Type::Help, LOCME("Shows help about the selected command"), ArgumentType::Flag }; + case Args::Type::SourceName: + return Argument{ "name", 'n', Args::Type::SourceName, LOCME("Name of the source"), ArgumentType::Positional, false }; + case Args::Type::SourceArg: + return Argument{ "arg", 'a', Args::Type::SourceArg, LOCME("Argument given to the source"), ArgumentType::Positional, true }; + case Args::Type::SourceType: + return Argument{ "type", 't', Args::Type::SourceType, LOCME("Type of the source"), ArgumentType::Positional }; case Args::Type::ValidateManifest: return Argument{ "manifest", None, Args::Type::ValidateManifest, LOCME("The path to the manifest to be validated"), ArgumentType::Positional, true }; default: diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -476,7 +476,7 @@ namespace AppInstaller::CLI void Command::ExecuteInternal(Execution::Context& context) const { - context.Reporter.ShowMsg(LOCME("Oops, we forgot to do this..."), Execution::Reporter::Level::Error); + context.Reporter.Error() << LOCME("Oops, we forgot to do this...") << std::endl; THROW_HR(E_NOTIMPL); } } diff --git a/src/AppInstallerCLICore/Commands/HashCommand.cpp b/src/AppInstallerCLICore/Commands/HashCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "HashCommand.h" #include "Localization.h" +#include "Workflows/WorkflowBase.h" namespace AppInstaller::CLI { @@ -28,27 +29,33 @@ namespace AppInstaller::CLI void HashCommand::ExecuteInternal(Execution::Context& context) const { - auto inputFile = context.Args.GetArg(Execution::Args::Type::HashFile); - std::ifstream inStream{ inputFile, std::ifstream::binary }; - - context.Reporter.ShowMsg("File Hash: " + Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHash(inStream))); - - if (context.Args.Contains(Execution::Args::Type::Msix)) + context << + Workflow::VerifyFile(Execution::Args::Type::HashFile) << + [](Execution::Context& context) { - try - { - Msix::MsixInfo msixInfo{ inputFile }; - auto signature = msixInfo.GetSignature(); - auto signatureHash = Utility::SHA256::ComputeHash(signature.data(), static_cast<uint32_t>(signature.size())); + auto inputFile = context.Args.GetArg(Execution::Args::Type::HashFile); + std::ifstream inStream{ inputFile, std::ifstream::binary }; - context.Reporter.ShowMsg("Signature Hash: " + Utility::SHA256::ConvertToString(signatureHash)); - } - catch (const wil::ResultException&) + context.Reporter.Info() << "File Hash: " + Utility::SHA256::ConvertToString(Utility::SHA256::ComputeHash(inStream)) << std::endl; + + if (context.Args.Contains(Execution::Args::Type::Msix)) { - context.Reporter.ShowMsg( - "Failed to calculate signature hash. Please verify the input file is a valid signed msix.", - Execution::Reporter::Level::Warning); + try + { + Msix::MsixInfo msixInfo{ inputFile }; + auto signature = msixInfo.GetSignature(); + auto signatureHash = Utility::SHA256::ComputeHash(signature.data(), static_cast<uint32_t>(signature.size())); + + context.Reporter.Info() << "Signature Hash: " + Utility::SHA256::ConvertToString(signatureHash) << std::endl; + } + catch (const wil::ResultException& re) + { + context.Reporter.Warn() << + "Failed to calculate MSIX signature hash." << std::endl << + "Please verify that the input file is a valid, signed MSIX." << std::endl; + AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); + } } - } + }; } } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -3,17 +3,17 @@ #include "pch.h" #include "InstallCommand.h" #include "Localization.h" -#include "Manifest\Manifest.h" -#include "Workflows\InstallFlow.h" +#include "Workflows/InstallFlow.h" +#include "Workflows/WorkflowBase.h" using namespace AppInstaller::Manifest; -using namespace AppInstaller::Workflow; +using namespace AppInstaller::CLI::Workflow; namespace AppInstaller::CLI { using namespace std::string_view_literals; - constexpr std::string_view s_InstallCommand_ArgName_QueryOrManifest = "query|manifest"sv; + constexpr std::string_view s_InstallCommand_ArgName_SilentAndInteractive = "silent|interactive"sv; std::vector<Argument> InstallCommand::GetArguments() const { @@ -48,22 +48,21 @@ namespace AppInstaller::CLI void InstallCommand::ExecuteInternal(Execution::Context& context) const { - InstallFlow appInstall(context); - - appInstall.Execute(); + context << + Workflow::GetManifest << + Workflow::EnsureMinOSVersion << + Workflow::SelectInstaller << + Workflow::EnsureApplicableInstaller << + Workflow::DownloadInstaller << + Workflow::VerifyInstallerHash << + Workflow::ExecuteInstaller; } void InstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - // TODO: Maybe one day implement argument groups - if (!execArgs.Contains(Execution::Args::Type::Query) && !execArgs.Contains(Execution::Args::Type::Manifest)) - { - throw CommandException(LOCME("Required argument not provided"), s_InstallCommand_ArgName_QueryOrManifest); - } - if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) { - throw CommandException(LOCME("More than one install behavior argument provided"), s_InstallCommand_ArgName_QueryOrManifest); + throw CommandException(LOCME("More than one install behavior argument provided"), s_InstallCommand_ArgName_SilentAndInteractive); } } } diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -3,7 +3,7 @@ #include "pch.h" #include "SearchCommand.h" #include "Localization.h" -#include "Workflows/SearchFlow.h" +#include "Workflows/WorkflowBase.h" namespace AppInstaller::CLI { @@ -37,8 +37,10 @@ namespace AppInstaller::CLI void SearchCommand::ExecuteInternal(Context& context) const { - Workflow::SearchFlow appSearch{ context }; - - appSearch.Execute(); + context << + Workflow::OpenSource << + Workflow::SearchSource << + Workflow::EnsureMatchesFromSearchResult << + Workflow::ReportSearchResult; } } diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -3,17 +3,16 @@ #include "pch.h" #include "ShowCommand.h" #include "Localization.h" -#include "Workflows\ShowFlow.h" +#include "Workflows/ShowFlow.h" +#include "Workflows/WorkflowBase.h" namespace AppInstaller::CLI { - using namespace AppInstaller::Workflow; - using namespace std::string_view_literals; - std::vector<Argument> ShowCommand::GetArguments() const { return { Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Manifest), Argument::ForType(Execution::Args::Type::Id), Argument::ForType(Execution::Args::Type::Name), Argument::ForType(Execution::Args::Type::Moniker), @@ -27,18 +26,39 @@ namespace AppInstaller::CLI std::string ShowCommand::ShortDescription() const { - return LOCME("Shows info of the given application"); + return LOCME("Shows info about an application"); } std::string ShowCommand::GetLongDescription() const { - return LOCME("Shows info of the given application"); + return LOCME("Shows information on a specific application."); } void ShowCommand::ExecuteInternal(Execution::Context& context) const { - ShowFlow appShowInfo{ context }; - - appShowInfo.Execute(); + if (context.Args.Contains(Execution::Args::Type::ListVersions)) + { + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + context << + Workflow::GetManifestFromArg << + Workflow::ShowManifestVersion; + } + else + { + context << + Workflow::OpenSource << + Workflow::SearchSource << + Workflow::EnsureOneMatchFromSearchResult << + Workflow::ShowAppVersions; + } + } + else + { + context << + Workflow::GetManifest << + Workflow::SelectInstaller << + Workflow::ShowManifestInfo; + } } } diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -3,18 +3,12 @@ #include "pch.h" #include "SourceCommand.h" #include "Localization.h" +#include "Workflows/SourceFlow.h" +#include "Workflows/WorkflowBase.h" namespace AppInstaller::CLI { using namespace AppInstaller::CLI::Execution; - using namespace std::string_view_literals; - - constexpr std::string_view s_SourceCommand_ArgName_Name = "name"sv; - constexpr char s_SourceCommand_ArgAlias_Name = 'n'; - constexpr std::string_view s_SourceCommand_ArgName_Type = "type"sv; - constexpr char s_SourceCommand_ArgAlias_Type = 't'; - constexpr std::string_view s_SourceCommand_ArgName_Arg = "arg"sv; - constexpr char s_SourceCommand_ArgAlias_Arg = 'a'; std::vector<std::unique_ptr<Command>> SourceCommand::GetCommands() const { @@ -33,10 +27,10 @@ namespace AppInstaller::CLI std::string SourceCommand::GetLongDescription() const { - return LOCME("Manage sources of applications"); + return LOCME("Manage sources with the sub-commands. A source provides the data for you to discover and install applications. Only add a new source if you trust it as a secure location."); } - void SourceCommand::ExecuteInternal(Execution::Context& context) const + void SourceCommand::ExecuteInternal(Context& context) const { OutputHelp(context.Reporter); } @@ -44,9 +38,9 @@ namespace AppInstaller::CLI std::vector<Argument> SourceAddCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source for future reference"), ArgumentType::Positional, true }, - Argument{ s_SourceCommand_ArgName_Arg, s_SourceCommand_ArgAlias_Arg, Args::Type::SourceArg, LOCME("Argument given to the source"), ArgumentType::Positional, true }, - Argument{ s_SourceCommand_ArgName_Type, s_SourceCommand_ArgAlias_Type, Args::Type::SourceType, LOCME("Type of the source"), ArgumentType::Positional }, + Argument::ForType(Args::Type::SourceName).SetRequired(true), + Argument::ForType(Args::Type::SourceArg), + Argument::ForType(Args::Type::SourceType), }; } @@ -57,36 +51,21 @@ namespace AppInstaller::CLI std::string SourceAddCommand::GetLongDescription() const { - return LOCME("Add a new source"); + return LOCME("Add a new source. A source provides the data for you to discover and install applications. Only add a new source if you trust it as a secure location."); } - void SourceAddCommand::ExecuteInternal(Execution::Context& context) const + void SourceAddCommand::ExecuteInternal(Context& context) const { - std::string name(context.Args.GetArg(Execution::Args::Type::SourceName)); - std::string arg(context.Args.GetArg(Execution::Args::Type::SourceArg)); - std::string type; - if (context.Args.Contains(Execution::Args::Type::SourceType)) - { - type = context.Args.GetArg(Execution::Args::Type::SourceType); - } - - context.Reporter.ShowMsg("Adding source:"); - context.Reporter.ShowMsg(" Name: " + name); - context.Reporter.ShowMsg(" Arg: " + arg); - if (!type.empty()) - { - context.Reporter.ShowMsg(" Type: " + type); - } - - context.Reporter.ExecuteWithProgress(std::bind(Repository::AddSource, std::move(name), std::move(type), std::move(arg), std::placeholders::_1)); - - context.Reporter.ShowMsg("Done"); + context << + Workflow::GetSourceList << + Workflow::CheckSourceListAgainstAdd << + Workflow::AddSource; } std::vector<Argument> SourceListCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to list full details for"), ArgumentType::Positional }, + Argument::ForType(Args::Type::SourceName), }; } @@ -97,62 +76,20 @@ namespace AppInstaller::CLI std::string SourceListCommand::GetLongDescription() const { - return LOCME("List current sources"); + return LOCME("List all current sources, or full details of a specific source."); } - void SourceListCommand::ExecuteInternal(Execution::Context& context) const - { - std::vector<Repository::SourceDetails> sources = Repository::GetSources(); - - if (context.Args.Contains(Execution::Args::Type::SourceName)) - { - auto name = context.Args.GetArg(Execution::Args::Type::SourceName); - auto itr = std::find_if(sources.begin(), sources.end(), [name](const Repository::SourceDetails& sd) { return Utility::CaseInsensitiveEquals(sd.Name, name); }); - - if (itr == sources.end()) - { - context.Reporter.Info() << "No source with the given name was found: " << name << std::endl; - } - else - { - context.Reporter.ShowMsg("Name: " + itr->Name); - context.Reporter.ShowMsg("Type: " + itr->Type); - context.Reporter.ShowMsg("Arg: " + itr->Arg); - context.Reporter.ShowMsg("Data: " + itr->Data); - if (itr->LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) - { - context.Reporter.ShowMsg("Last Update: <never>"); - } - else - { - std::stringstream stream; - stream << itr->LastUpdateTime; - context.Reporter.ShowMsg("Last Update: " + stream.str()); - } - } - } - else - { - context.Reporter.ShowMsg("Current sources:"); - - if (sources.empty()) - { - context.Reporter.ShowMsg(" <none>"); - } - else - { - for (const auto& source : sources) - { - context.Reporter.ShowMsg(" " + source.Name + " => " + source.Arg); - } - } - } + void SourceListCommand::ExecuteInternal(Context& context) const + { + context << + Workflow::GetSourceListWithFilter << + Workflow::ListSources; } std::vector<Argument> SourceUpdateCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to update"), ArgumentType::Positional }, + Argument::ForType(Args::Type::SourceName), }; } @@ -163,43 +100,20 @@ namespace AppInstaller::CLI std::string SourceUpdateCommand::GetLongDescription() const { - return LOCME("Update current sources"); + return LOCME("Update all sources, or only a specific source."); } - void SourceUpdateCommand::ExecuteInternal(Execution::Context& context) const - { - if (context.Args.Contains(Execution::Args::Type::SourceName)) - { - auto name = context.Args.GetArg(Execution::Args::Type::SourceName); - context.Reporter.Info() << "Updating source: " << name << "..." << std::endl; - if (!context.Reporter.ExecuteWithProgress(std::bind(Repository::UpdateSource, name, std::placeholders::_1))) - { - context.Reporter.EmptyLine(); - context.Reporter.ShowMsg(" Could not find a source by that name.", Execution::Reporter::Level::Warning); - } - else - { - context.Reporter.ShowMsg("Done"); - } - } - else - { - context.Reporter.ShowMsg("Updating all sources..."); - - std::vector<Repository::SourceDetails> sources = Repository::GetSources(); - for (const auto& sd : sources) - { - context.Reporter.ShowMsg("Updating source: " + sd.Name + "..."); - context.Reporter.ExecuteWithProgress(std::bind(Repository::UpdateSource, sd.Name, std::placeholders::_1)); - context.Reporter.ShowMsg(LOCME("Done.") ); - } - } + void SourceUpdateCommand::ExecuteInternal(Context& context) const + { + context << + Workflow::GetSourceListWithFilter << + Workflow::UpdateSources; } std::vector<Argument> SourceRemoveCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to remove"), ArgumentType::Positional, true }, + Argument::ForType(Args::Type::SourceName).SetRequired(true), }; } @@ -210,20 +124,13 @@ namespace AppInstaller::CLI std::string SourceRemoveCommand::GetLongDescription() const { - return LOCME("Remove current sources"); + return LOCME("Remove a specific source."); } - void SourceRemoveCommand::ExecuteInternal(Execution::Context& context) const + void SourceRemoveCommand::ExecuteInternal(Context& context) const { - auto name = context.Args.GetArg(Execution::Args::Type::SourceName); - context.Reporter.Info() << "Removing source: " << name << "..." << std::endl; - if (!context.Reporter.ExecuteWithProgress(std::bind(Repository::RemoveSource, name, std::placeholders::_1))) - { - context.Reporter.ShowMsg("Could not find a source by that name.", Execution::Reporter::Level::Warning); - } - else - { - context.Reporter.ShowMsg("Done"); - } + context << + Workflow::GetSourceListWithFilter << + Workflow::RemoveSources; } } diff --git a/src/AppInstallerCLICore/Commands/ValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ValidateCommand.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "ValidateCommand.h" #include "Localization.h" +#include "Workflows/WorkflowBase.h" namespace AppInstaller::CLI { @@ -27,24 +28,22 @@ namespace AppInstaller::CLI void ValidateCommand::ExecuteInternal(Execution::Context& context) const { - auto inputFile = context.Args.GetArg(Execution::Args::Type::ValidateManifest); - - if (!std::filesystem::exists(inputFile)) + context << + Workflow::VerifyFile(Execution::Args::Type::ValidateManifest) << + [](Execution::Context& context) { - AICLI_LOG(CLI, Error, << "Input file does not exist. Path: " << inputFile); - context.Reporter.Error() << "The input manifest file does not exist. Path: " << inputFile << std::endl; - return; - } + auto inputFile = context.Args.GetArg(Execution::Args::Type::ValidateManifest); - try - { - Manifest::Manifest::CreateFromPath(inputFile, true); - context.Reporter.Info() << "Manifest validation succeeded." << std::endl; - } - catch (const Manifest::ManifestException& e) - { - context.Reporter.Warn() << "Manifest validation failed." << std::endl; - context.Reporter.Warn() << e.GetManifestErrorMessage() << std::endl; - } + try + { + (void)Manifest::Manifest::CreateFromPath(inputFile, true); + context.Reporter.Info() << "Manifest validation succeeded." << std::endl; + } + catch (const Manifest::ManifestException& e) + { + context.Reporter.Warn() << "Manifest validation failed." << std::endl; + context.Reporter.Warn() << e.GetManifestErrorMessage() << std::endl; + } + }; } } diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -54,7 +54,7 @@ namespace AppInstaller::CLI Logging::Telemetry().LogStartup(); Execution::Context context{ std::cout, std::cin }; - context.Reporter.EnableCtrlHandler(); + context.EnableCtrlHandler(); // Convert incoming wide char args to UTF8 std::vector<std::string> utf8Args; @@ -103,35 +103,50 @@ namespace AppInstaller::CLI command->Execute(context); } // Exceptions that may occur in the process of executing an arbitrary command + catch (const wil::ResultException& re) + { + // Even though they are logged at their source, log again here for completeness. + Logging::Telemetry().LogException(command->FullName(), "wil::ResultException", re.what()); + context.Reporter.Error() << + "An unexpected error occurred while executing the command: " << std::endl << + re.what() << std::endl; + return re.GetErrorCode(); + } catch (const winrt::hresult_error& hre) { - // TODO: Better error output std::string message = Utility::ConvertToUTF8(hre.message()); - context.Reporter.ShowMsg("An error occurred while executing the command: " + message, Execution::Reporter::Level::Error); - AICLI_LOG(CLI, Error, << "Error encountered executing command: " << message); - return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; + Logging::Telemetry().LogException(command->FullName(), "winrt::hresult_error", message); + context.Reporter.Error() << + "An unexpected error occurred while executing the command: " << std::endl << + message << std::endl; + return hre.code(); } catch (const std::exception& e) { - // TODO: Better error output - context.Reporter.ShowMsg("An error occurred while executing the command: " + std::string(e.what()), Execution::Reporter::Level::Error); - AICLI_LOG(CLI, Error, << "Error encountered executing command: " << e.what()); + Logging::Telemetry().LogException(command->FullName(), "std::exception", e.what()); + context.Reporter.Error() << + "An unexpected error occurred while executing the command: " << std::endl << + e.what() << std::endl; + return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + Logging::Telemetry().LogException(command->FullName(), "unknown", {}); + context.Reporter.Error() << + "An unexpected error occurred while executing the command" << std::endl; return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; } - Logging::Telemetry().LogCommandSuccess(command->FullName()); - return 0; + if (SUCCEEDED(context.GetTerminationHR())) + { + Logging::Telemetry().LogCommandSuccess(command->FullName()); + } + + return context.GetTerminationHR(); } // End of the line exceptions that are not ever expected. // Telemetry cannot be reliable beyond this point, so don't let these happen. - catch (const winrt::hresult_error&) - { - return APPINSTALLER_CLI_ERROR_INTERNAL_ERROR; - } - catch (const std::exception&) - { - return APPINSTALLER_CLI_ERROR_INTERNAL_ERROR; - } catch (...) { return APPINSTALLER_CLI_ERROR_INTERNAL_ERROR; diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ExecutionContext.h" + + +namespace AppInstaller::CLI::Execution +{ + namespace + { + // The context that will receive CTRL signals + Context* s_contextForCtrlHandler = nullptr; + + BOOL WINAPI CtrlHandlerForContext(DWORD ctrlType) + { + AICLI_LOG(CLI, Info, << "Got CTRL type: " << ctrlType); + + // Won't save us from every crash, but a few more than direct access. + Context* context = s_contextForCtrlHandler; + if (!context) + { + return FALSE; + } + + switch (ctrlType) + { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + context->Terminate(E_ABORT); + context->Reporter.CancelInProgressTask(false); + return TRUE; + // According to MSDN, we should never receive these due to having gdi32/user32 loaded in our process. + // But handle them as a force terminate anyway. + case CTRL_CLOSE_EVENT: + case CTRL_LOGOFF_EVENT: + case CTRL_SHUTDOWN_EVENT: + context->Terminate(E_ABORT); + context->Reporter.CancelInProgressTask(true); + return TRUE; + default: + return FALSE; + } + } + + void SetCtrlHandlerContext(Context* context) + { + // Only one is allowed right now. + THROW_HR_IF(E_UNEXPECTED, s_contextForCtrlHandler != nullptr && context != nullptr); + + if (context == nullptr) + { + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(CtrlHandlerForContext, FALSE)); + s_contextForCtrlHandler = nullptr; + } + else + { + s_contextForCtrlHandler = context; + LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(CtrlHandlerForContext, TRUE)); + } + } + } + + Context::~Context() + { + if (m_disableCtrlHandlerOnExit) + { + EnableCtrlHandler(false); + } + } + + void Context::EnableCtrlHandler(bool enabled) + { + SetCtrlHandlerContext(enabled ? this : nullptr); + m_disableCtrlHandlerOnExit = enabled; + } +} diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -1,22 +1,189 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerLogging.h> +#include <AppInstallerRepositorySearch.h> +#include <AppInstallerRepositorySource.h> +#include <Manifest/Manifest.h> #include "ExecutionReporter.h" #include "ExecutionArgs.h" +#include <filesystem> +#include <map> +#include <string> +#include <utility> +#include <variant> +#include <vector> + + +// Terminates the Context with some logging to indicate the location. +// Also returns from the current function. +#define AICLI_TERMINATE_CONTEXT_ARGS(_context_,_hr_) \ + do { \ + HRESULT AICLI_TERMINATE_CONTEXT_ARGS_hr = _hr_; \ + ::AppInstaller::Logging::Telemetry().LogCommandTermination(AICLI_TERMINATE_CONTEXT_ARGS_hr, __FILE__, __LINE__); \ + _context_.Terminate(AICLI_TERMINATE_CONTEXT_ARGS_hr); \ + return; \ + } while(0,0) + +// Terminates the Context namd 'context' with some logging to indicate the location. +// Also returns from the current function. +#define AICLI_TERMINATE_CONTEXT(_hr_) AICLI_TERMINATE_CONTEXT_ARGS(context,_hr_) + +namespace AppInstaller::CLI::Workflow +{ + struct WorkflowTask; +} + namespace AppInstaller::CLI::Execution { + // Names a peice of data stored in the context by a workflow step. + // Must start at 0 to enable direct access to variant in Context. + // Max must be last and unused. + enum class Data : size_t + { + Source, + SearchResult, + SourceList, + Manifest, + Installer, + HashPair, + InstallerPath, + LogPath, + InstallerArgs, + Max + }; + + namespace details + { + template <Data D> + struct DataMapping + { + // value_t type specifies the type of this data + }; + + template <> + struct DataMapping<Data::Source> + { + using value_t = std::shared_ptr<Repository::ISource>; + }; + + template <> + struct DataMapping<Data::SearchResult> + { + using value_t = Repository::SearchResult; + }; + + template <> + struct DataMapping<Data::SourceList> + { + using value_t = std::vector<Repository::SourceDetails>; + }; + + template <> + struct DataMapping<Data::Manifest> + { + using value_t = Manifest::Manifest; + }; + + template <> + struct DataMapping<Data::Installer> + { + using value_t = std::optional<Manifest::ManifestInstaller>; + }; + + template <> + struct DataMapping<Data::HashPair> + { + using value_t = std::pair<std::vector<uint8_t>, std::vector<uint8_t>>; + }; + + template <> + struct DataMapping<Data::InstallerPath> + { + using value_t = std::filesystem::path; + }; + + template <> + struct DataMapping<Data::LogPath> + { + using value_t = std::filesystem::path; + }; + + template <> + struct DataMapping<Data::InstallerArgs> + { + using value_t = std::string; + }; + + // Used to deduce the DataVariant type; making a variant that includes std::monostate and all DataMapping types. + template <size_t... I> + inline auto Deduce(std::index_sequence<I...>) { return std::variant<std::monostate, DataMapping<static_cast<Data>(I)>::value_t...>{}; } + + // Holds data of any type listed in a DataMapping. + using DataVariant = decltype(Deduce(std::make_index_sequence<static_cast<size_t>(Data::Max)>())); + + // Gets the index into the variant for the given Data. + constexpr inline size_t DataIndex(Data d) { return static_cast<size_t>(d) + 1; } + } + // The context within which all commands execute. - // Contains inout/output via Execution::Reporter and + // Contains input/output via Execution::Reporter and // arguments via Execution::Args. struct Context { + Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} + + virtual ~Context(); + // The path for console input/output for all functionality. Reporter Reporter; // The arguments given to execute with. Args Args; - Context(std::ostream& out, std::istream& in) : Reporter(out, in) {} + // Enables reception of CTRL signals. + // Only one context can be enabled to handle CTRL signals at a time. + void EnableCtrlHandler(bool enabled = true); + + // Returns a value indicating whether the context is terminated. + bool IsTerminated() const { return m_isTerminated; } + + // Gets the HRESULT reason for the termination. + HRESULT GetTerminationHR() const { return m_terminationHR; } + + // Set the context to the terminated state. + void Terminate(HRESULT hr) { m_isTerminated = true; m_terminationHR = hr; } + + // Adds a value to the context data, or overwrites an existing entry. + // This must be used to create the intial data entry, but Get can be used to modify. + template <Data D> + void Add(typename details::DataMapping<D>::value_t&& v) + { + m_data[D].emplace<details::DataIndex(D)>(std::forward<typename details::DataMapping<D>::value_t>(v)); + } + + // Return a value indicating whether the given data type is stored in the context. + bool Contains(Data d) { return (m_data.find(d) != m_data.end()); } + + // Gets context data; which can be modified in place. + template <Data D> + typename details::DataMapping<D>::value_t& Get() + { + auto itr = m_data.find(D); + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), itr == m_data.end(), "Get(%d)", D); + return std::get<details::DataIndex(D)>(itr->second); + } + +#ifndef AICLI_DISABLE_TEST_HOOKS + // Enable tests to override behavior + virtual bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask&) { return true; } +#endif + + private: + DestructionToken m_disableCtrlHandlerOnExit = false; + bool m_isTerminated = false; + HRESULT m_terminationHR = S_OK; + std::map<Data, details::DataVariant> m_data; }; -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -9,50 +9,6 @@ namespace AppInstaller::CLI::Execution VirtualTerminal::Sequence HelpCommandEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; VirtualTerminal::Sequence HelpArgumentEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; - namespace - { - // The reporter that will receive CTRL signals - Reporter* s_reporterForCtrlHandler = nullptr; - - BOOL WINAPI CtrlHandlerForReporter(DWORD ctrlType) - { - switch (ctrlType) - { - case CTRL_C_EVENT: - case CTRL_BREAK_EVENT: - s_reporterForCtrlHandler->CancelInProgressTask(false); - return TRUE; - // According to MSDN, we should never receive these due to having gdi32/user32 loaded in our process. - // But handle them as a force terminate anyway. - case CTRL_CLOSE_EVENT: - case CTRL_LOGOFF_EVENT: - case CTRL_SHUTDOWN_EVENT: - s_reporterForCtrlHandler->CancelInProgressTask(true); - return TRUE; - default: - AICLI_LOG(CLI, Info, << "Got unrecognized CTRL type: " << ctrlType); - return FALSE; - } - } - - void SetCtrlHandlerReporter(Reporter* reporter) - { - // Only one is allowed right now. - THROW_HR_IF(E_UNEXPECTED, s_reporterForCtrlHandler != nullptr && reporter != nullptr); - - if (reporter == nullptr) - { - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(CtrlHandlerForReporter, FALSE)); - s_reporterForCtrlHandler = nullptr; - } - else - { - s_reporterForCtrlHandler = reporter; - LOG_IF_WIN32_BOOL_FALSE(SetConsoleCtrlHandler(CtrlHandlerForReporter, TRUE)); - } - } - } - namespace details { void IndefiniteSpinner::ShowSpinner() @@ -166,11 +122,6 @@ namespace AppInstaller::CLI::Execution { m_out << VirtualTerminal::TextFormat::Default; } - - if (m_disableCtrlHandlerOnExit) - { - EnableCtrlHandler(false); - } } Reporter::OutputStream Reporter::GetOutputStream(Level level) @@ -210,11 +161,6 @@ namespace AppInstaller::CLI::Execution return tolower(response) == 'y'; } - void Reporter::ShowMsg(const std::string& msg, Level level) - { - GetOutputStream(level) << msg << std::endl; - } - void Reporter::ShowProgress(bool running, uint64_t progress) { m_progressBar.ShowProgress(running, progress); @@ -239,12 +185,6 @@ namespace AppInstaller::CLI::Execution ShowProgress(true, (maximum ? static_cast<uint64_t>((static_cast<double>(current) / maximum) * 100) : current)); } - void Reporter::EnableCtrlHandler(bool enabled) - { - SetCtrlHandlerReporter(enabled ? this : nullptr); - m_disableCtrlHandlerOnExit = enabled; - } - void Reporter::SetProgressCallback(ProgressCallback* callback) { auto lock = m_progressCallbackLock.lock_exclusive(); diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -115,8 +115,6 @@ namespace AppInstaller::CLI::Execution bool PromptForBoolResponse(const std::string& msg, Level level = Level::Info); - void ShowMsg(const std::string& msg, Level level = Level::Info); - // Used to show definite progress. // running: shows progress bar if set to true, dismisses progress bar if set to false void ShowProgress(bool running, uint64_t progress); @@ -146,10 +144,6 @@ namespace AppInstaller::CLI::Execution return f(callback); } - // Enables reception of CTRL signals. - // Only one reporter can be enabled to handle CTRL signals at a time. - void EnableCtrlHandler(bool enabled = true); - // Sets the in progress callback. void SetProgressCallback(ProgressCallback* callback); @@ -162,7 +156,6 @@ namespace AppInstaller::CLI::Execution VirtualTerminal::ConsoleModeRestore m_consoleMode; details::IndefiniteSpinner m_spinner; details::ProgressBar m_progressBar; - DestructionToken m_disableCtrlHandlerOnExit = false; wil::srwlock m_progressCallbackLock; std::atomic<ProgressCallback*> m_progressCallback; }; diff --git a/src/AppInstallerCLICore/Workflows/Common.h b/src/AppInstallerCLICore/Workflows/Common.h @@ -1,13 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#pragma once - -namespace AppInstaller::Workflow -{ - class WorkflowException : public wil::ResultException - { - public: - WorkflowException(HRESULT hr) : wil::ResultException(hr) {} - }; -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -3,62 +3,160 @@ #include "pch.h" #include "InstallFlow.h" #include "ShellExecuteInstallerHandler.h" -#include "MsixInstallerHandler.h" +#include "WorkflowBase.h" -using namespace AppInstaller::CLI; + +using namespace winrt::Windows::Foundation; +using namespace winrt::Windows::Management::Deployment; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - void InstallFlow::Execute() + void EnsureMinOSVersion(Execution::Context& context) { - if (m_argsRef.Contains(Execution::Args::Type::Manifest)) + const auto& manifest = context.Get<Execution::Data::Manifest>(); + + if (!manifest.MinOSVersion.empty() && + !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version(manifest.MinOSVersion))) { - m_manifest = Manifest::Manifest::CreateFromPath(m_argsRef.GetArg(Execution::Args::Type::Manifest)); - Logging::Telemetry().LogManifestFields(m_manifest.Id, m_manifest.Name, m_manifest.Version); + context.Reporter.Error() << "Cannot install application, as it requires a higher OS version: " << manifest.MinOSVersion << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_OLD_WIN_VERSION)); } - else + } + + void EnsureApplicableInstaller(Execution::Context& context) + { + const auto& installer = context.Get<Execution::Data::Installer>(); + + if (!installer.has_value()) + { + context.Reporter.Error() << "No installers are applicable to the current system" << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); + } + } + + void DownloadInstaller(Execution::Context& context) + { + const auto& installer = context.Get<Execution::Data::Installer>().value(); + + switch (installer.InstallerType) { - if (!IndexSearch() || !EnsureOneMatchFromSearchResult() || !GetManifest()) + case ManifestInstaller::InstallerTypeEnum::Exe: + case ManifestInstaller::InstallerTypeEnum::Burn: + case ManifestInstaller::InstallerTypeEnum::Inno: + case ManifestInstaller::InstallerTypeEnum::Msi: + case ManifestInstaller::InstallerTypeEnum::Nullsoft: + case ManifestInstaller::InstallerTypeEnum::Wix: + context << DownloadInstallerFile; + break; + case ManifestInstaller::InstallerTypeEnum::Msix: + if (installer.SignatureSha256.empty()) { - return; + context << DownloadInstallerFile; } - m_reporterRef.ShowMsg("Found app: " + m_searchResult.Matches[0].Application->GetName()); + else + { + // Signature hash provided. No download needed. Just verify signature hash. + context << GetMsixSignatureHash; + } + break; + default: + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } + } + + void DownloadInstallerFile(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + const auto& installer = context.Get<Execution::Data::Installer>().value(); - if (VerifyOSVersion()) + std::filesystem::path tempInstallerPath = Runtime::GetPathToTemp(); + tempInstallerPath /= manifest.Id + '.' + manifest.Version; + + AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); + + auto hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, + installer.Url, + tempInstallerPath, + std::placeholders::_1, + true)); + + if (!hash) { - SelectInstaller(); - InstallInternal(); + context.Reporter.Info() << "Package download canceled." << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); } + + context.Add<Execution::Data::HashPair>(std::make_pair(installer.Sha256, hash.value())); + context.Add<Execution::Data::InstallerPath>(std::move(tempInstallerPath)); } - void InstallFlow::InstallInternal() + void GetMsixSignatureHash(Execution::Context& context) { - auto installerHandler = GetInstallerHandler(); + // We use this when the server won't support streaming install to swap to download. + bool downloadInstead = false; + + try + { + const auto& installer = context.Get<Execution::Data::Installer>().value(); + + Msix::MsixInfo msixInfo(installer.Url); + auto signature = msixInfo.GetSignature(); + + auto signatureHash = SHA256::ComputeHash(signature.data(), static_cast<uint32_t>(signature.size())); - installerHandler->Download(); - installerHandler->Install(); + context.Add<Execution::Data::HashPair>(std::make_pair(installer.SignatureSha256, signatureHash)); + } + catch (const winrt::hresult_error& e) + { + if (e.code() == HRESULT_FROM_WIN32(ERROR_NO_RANGES_PROCESSED)) + { + // Server does not support range request, use download + downloadInstead = true; + } + else + { + throw; + } + } + + if (downloadInstead) + { + context << DownloadInstallerFile; + } } - bool InstallFlow::VerifyOSVersion() + void VerifyInstallerHash(Execution::Context& context) { - if (!m_manifest.MinOSVersion.empty() && - !Runtime::IsCurrentOSVersionGreaterThanOrEqual(Version(m_manifest.MinOSVersion))) + const auto& hashPair = context.Get<Execution::Data::HashPair>(); + + if (!std::equal( + hashPair.first.begin(), + hashPair.first.end(), + hashPair.second.begin())) { - m_reporterRef.Error() << "Cannot install application, as it requires a higher OS version: " << m_manifest.MinOSVersion << std::endl; - return false; + const auto& manifest = context.Get<Execution::Data::Manifest>(); + Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, hashPair.first, hashPair.second); + + if (!context.Reporter.PromptForBoolResponse("Installer hash verification failed. Continue?", Execution::Reporter::Level::Warning)) + { + context.Reporter.Error() << "Canceled. Installer hash mismatch." << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); + } } else { - return true; + AICLI_LOG(CLI, Info, << "Installer hash verified"); + context.Reporter.Info() << "Successfully verified installer hash." << std::endl; } } - std::unique_ptr<InstallerHandlerBase> InstallFlow::GetInstallerHandler() + void ExecuteInstaller(Execution::Context& context) { - switch (m_selectedInstaller.InstallerType) + const auto& installer = context.Get<Execution::Data::Installer>().value(); + + switch (installer.InstallerType) { case ManifestInstaller::InstallerTypeEnum::Exe: case ManifestInstaller::InstallerTypeEnum::Burn: @@ -66,11 +164,54 @@ namespace AppInstaller::Workflow case ManifestInstaller::InstallerTypeEnum::Msi: case ManifestInstaller::InstallerTypeEnum::Nullsoft: case ManifestInstaller::InstallerTypeEnum::Wix: - return std::make_unique<ShellExecuteInstallerHandler>(m_selectedInstaller, m_contextRef); + context << ShellExecuteInstall; + break; case ManifestInstaller::InstallerTypeEnum::Msix: - return std::make_unique<MsixInstallerHandler>(m_selectedInstaller, m_contextRef); + context << MsixInstall; + break; default: THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } } -}- \ No newline at end of file + + void ShellExecuteInstall(Execution::Context& context) + { + context << + GetInstallerArgs << + RenameDownloadedInstaller << + ShellExecuteInstallImpl; + } + + void MsixInstall(Execution::Context& context) + { + Uri uri = nullptr; + if (context.Contains(Execution::Data::InstallerPath)) + { + uri = Uri(context.Get<Execution::Data::InstallerPath>().c_str()); + } + else + { + uri = Uri(Utility::ConvertToUTF16(context.Get<Execution::Data::Installer>()->Url)); + } + + context.Reporter.Info() << "Starting package install..." << std::endl; + + try + { + DeploymentOptions deploymentOptions = + DeploymentOptions::ForceApplicationShutdown | + DeploymentOptions::ForceTargetApplicationShutdown; + context.Reporter.ExecuteWithProgress(std::bind(Deployment::RequestAddPackageAsync, uri, deploymentOptions, std::placeholders::_1)); + } + catch (const wil::ResultException& re) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, "MSIX", re.GetErrorCode()); + + context.Reporter.Error() << Utility::ConvertToUTF8(re.GetFailureInfo().pszMessage) << std::endl; + AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); + } + + context.Reporter.Info() << "Successfully installed." << std::endl; + } +} diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.h b/src/AppInstallerCLICore/Workflows/InstallFlow.h @@ -1,30 +1,67 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #pragma once -#include "Common.h" -#include "WorkflowBase.h" -#include "InstallerHandlerBase.h" #include "ExecutionContext.h" -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - class InstallFlow : public SingleManifestWorkflow - { - public: - InstallFlow(AppInstaller::CLI::Execution::Context& context) : SingleManifestWorkflow(context) {} + using namespace std::string_view_literals; + + // Token specified in installer args will be replaced by proper value. + static constexpr std::string_view ARG_TOKEN_LOGPATH = "<LOGPATH>"sv; + static constexpr std::string_view ARG_TOKEN_INSTALLPATH = "<INSTALLPATH>"sv; + + // Ensures that the current OS version is greater than or equal to the one in the manifest. + // Required Args: None + // Inputs: Manifest + // Outputs: None + void EnsureMinOSVersion(Execution::Context& context); + + // Ensures that there is an applicable installer. + // Required Args: None + // Inputs: Installer + // Outputs: None + void EnsureApplicableInstaller(Execution::Context& context); + + // Composite flow that chooses what to do based on the installer type. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + void DownloadInstaller(Execution::Context& context); + + // Downloads the file referenced by the Installer. + // Required Args: None + // Inputs: Installer + // Outputs: HashPair, InstallerPath + void DownloadInstallerFile(Execution::Context& context); + + // Computes the hash of the MSIX signature file. + // Required Args: None + // Inputs: Installer + // Outputs: HashPair + void GetMsixSignatureHash(Execution::Context& context); - // Execute will perform a query against index and do app install if a target app is found. - // If a manifest is given with /manifest, use the manifest and no index search is performed. - void Execute(); + // Gets the source list, filtering it if SourceName is present. + // Required Args: None + // Inputs: HashPair + // Outputs: SourceList + void VerifyInstallerHash(Execution::Context& context); - protected: - void InstallInternal(); + // Composite flow that chooses what to do based on the installer type. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + void ExecuteInstaller(Execution::Context& context); - // Verifies the OS version is capable of supporting the application. - bool VerifyOSVersion(); + // Runs the installer via ShellExecute. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: None + void ShellExecuteInstall(Execution::Context& context); - // Creates corresponding InstallerHandler according to InstallerType - virtual std::unique_ptr<InstallerHandlerBase> GetInstallerHandler(); - }; -}- \ No newline at end of file + // Deploys the MSIX. + // Required Args: None + // Inputs: Manifest?, Installer || InstallerPath + // Outputs: None + void MsixInstall(Execution::Context& context); +} diff --git a/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.cpp b/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.cpp @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Common.h" -#include "InstallerHandlerBase.h" - -using namespace AppInstaller::CLI; -using namespace AppInstaller::Manifest; - -namespace AppInstaller::Workflow -{ - void InstallerHandlerBase::Download() - { - // Todo: Rework the path logic. The new path logic should work with MOTW. - std::filesystem::path tempInstallerPath = Runtime::GetPathToTemp(); - tempInstallerPath /= Utility::SHA256::ConvertToString(m_manifestInstallerRef.Sha256); - - AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); - - auto hash = m_reporterRef.ExecuteWithProgress(std::bind(Utility::Download, - m_manifestInstallerRef.Url, - tempInstallerPath, - std::placeholders::_1, - true)); - - if (!hash) - { - m_reporterRef.ShowMsg("Package download canceled."); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package download canceled"); - } - - if (!std::equal( - m_manifestInstallerRef.Sha256.begin(), - m_manifestInstallerRef.Sha256.end(), - hash.value().begin())) - { - AICLI_LOG(CLI, Error, - << "Package hash verification failed. SHA256 in manifest: " - << Utility::SHA256::ConvertToString(m_manifestInstallerRef.Sha256) - << " SHA256 from download: " - << Utility::SHA256::ConvertToString(hash.value())); - - if (!m_reporterRef.PromptForBoolResponse("Package hash verification failed. Continue?", Execution::Reporter::Level::Warning)) - { - m_reporterRef.ShowMsg("Canceled. Package hash mismatch.", Execution::Reporter::Level::Error); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package installation canceled"); - } - } - else - { - AICLI_LOG(CLI, Info, << "Downloaded installer hash verified"); - m_reporterRef.ShowMsg("Successfully verified SHA256."); - } - - m_downloadedInstaller = tempInstallerPath; - } -} diff --git a/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.h b/src/AppInstallerCLICore/Workflows/InstallerHandlerBase.h @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#pragma once -#include <string> -#include "Manifest/Manifest.h" -#include "ExecutionContext.h" - -namespace AppInstaller::Workflow -{ - using namespace std::string_view_literals; - - // Token specified in installer args will be replaced by proper value. - static constexpr std::string_view ARG_TOKEN_LOGPATH = "<LOGPATH>"sv; - static constexpr std::string_view ARG_TOKEN_INSTALLPATH = "<INSTALLPATH>"sv; - - // This is the base class for installer handlers. Individual installer handler should override - // member methods to do appropriate work on different installers. - class InstallerHandlerBase - { - public: - - // The Download method downloads installer to local temp folder. - // The downloaded installer does not have any extension appended. - // SHA256 of the downloaded installer is verified during download. - virtual void Download(); - - virtual void Install() { THROW_HR(E_NOTIMPL); } - virtual void Cancel() { THROW_HR(E_NOTIMPL); } - - protected: - InstallerHandlerBase( - const Manifest::ManifestInstaller& manifestInstaller, - AppInstaller::CLI::Execution::Context& context) : - m_manifestInstallerRef(manifestInstaller), m_reporterRef(context.Reporter), m_argsRef(context.Args) {}; - - const Manifest::ManifestInstaller& m_manifestInstallerRef; - const AppInstaller::CLI::Execution::Args& m_argsRef; - AppInstaller::CLI::Execution::Reporter& m_reporterRef; - std::filesystem::path m_downloadedInstaller; - }; -} - diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp b/src/AppInstallerCLICore/Workflows/ManifestComparator.cpp @@ -1,14 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #include "pch.h" -#include "Common.h" #include "ManifestComparator.h" using namespace AppInstaller::CLI; using namespace AppInstaller::Manifest; -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { bool InstallerComparator::operator() (const ManifestInstaller& installer1, const ManifestInstaller& installer2) { @@ -38,48 +36,49 @@ namespace AppInstaller::Workflow return true; } - ManifestInstaller ManifestComparator::GetPreferredInstaller(const Execution::Args&) + std::optional<Manifest::ManifestInstaller> ManifestComparator::GetPreferredInstaller(const Manifest::Manifest& manifest) { AICLI_LOG(CLI, Info, << "Starting installer selection."); // Sorting the list of availlable installers according to rules defined in InstallerComparator. - std::sort(m_manifestRef.Installers.begin(), m_manifestRef.Installers.end(), InstallerComparator()); + auto installers = manifest.Installers; + std::sort(installers.begin(), installers.end(), InstallerComparator()); // If the first one is inapplicable, then no installer is applicable. - if (Utility::IsApplicableArchitecture(m_manifestRef.Installers[0].Arch) == -1) + if (Utility::IsApplicableArchitecture(installers[0].Arch) == -1) { - m_reporterRef.ShowMsg("No applicable installer found.", Execution::Reporter::Level::Error); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_WORKFLOW_FAILED), "No installer with applicable architecture found."); + return {}; } - ManifestInstaller selectedInstaller = m_manifestRef.Installers[0]; + ManifestInstaller& selectedInstaller = installers[0]; Logging::Telemetry().LogSelectedInstaller((int)selectedInstaller.Arch, selectedInstaller.Url, Manifest::ManifestInstaller::InstallerTypeToString(selectedInstaller.InstallerType), selectedInstaller.Scope, selectedInstaller.Language); - return selectedInstaller; + return std::move(selectedInstaller); } - ManifestLocalization ManifestComparator::GetPreferredLocalization(const Execution::Args&) + Manifest::ManifestLocalization ManifestComparator::GetPreferredLocalization(const Manifest::Manifest& manifest) { AICLI_LOG(CLI, Info, << "Starting localization selection."); ManifestLocalization selectedLocalization; // Sorting the list of availlable localizations according to rules defined in LocalizationComparator. - if (!m_manifestRef.Localization.empty()) + if (!manifest.Localization.empty()) { - std::sort(m_manifestRef.Localization.begin(), m_manifestRef.Localization.end(), LocalizationComparator()); + auto localization = manifest.Localization; + std::sort(localization.begin(), localization.end(), LocalizationComparator()); // TODO: needs to check language applicability here - selectedLocalization = m_manifestRef.Localization[0]; + selectedLocalization = localization[0]; } else { // Pupulate default from package manifest - selectedLocalization.Description = m_manifestRef.Description; - selectedLocalization.Homepage = m_manifestRef.Homepage; - selectedLocalization.LicenseUrl = m_manifestRef.LicenseUrl; + selectedLocalization.Description = manifest.Description; + selectedLocalization.Homepage = manifest.Homepage; + selectedLocalization.LicenseUrl = manifest.LicenseUrl; } AICLI_LOG(CLI, Info, << "Completed localization selection. Selected localization language: " << selectedLocalization.Language); diff --git a/src/AppInstallerCLICore/Workflows/ManifestComparator.h b/src/AppInstallerCLICore/Workflows/ManifestComparator.h @@ -1,39 +1,41 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #pragma once -#include "ExecutionContext.h" +#include "ExecutionArgs.h" +#include <Manifest/Manifest.h> + +#include <optional> + -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { // This is used in sorting the list of available installers to get the best match. struct InstallerComparator { bool operator() ( - const AppInstaller::Manifest::ManifestInstaller& installer1, - const AppInstaller::Manifest::ManifestInstaller& installer2); + const Manifest::ManifestInstaller& installer1, + const Manifest::ManifestInstaller& installer2); }; // This is used in sorting the list of available localizations to get the best match. struct LocalizationComparator { bool operator() ( - const AppInstaller::Manifest::ManifestLocalization& loc1, - const AppInstaller::Manifest::ManifestLocalization& loc2); + const Manifest::ManifestLocalization& loc1, + const Manifest::ManifestLocalization& loc2); }; // Class in charge of comparing manifest entries class ManifestComparator { public: - ManifestComparator(AppInstaller::Manifest::Manifest& manifest, AppInstaller::CLI::Execution::Reporter& reporter) : m_manifestRef(manifest), m_reporterRef(reporter) {} + ManifestComparator(const Execution::Args&) {} - AppInstaller::Manifest::ManifestInstaller GetPreferredInstaller(const AppInstaller::CLI::Execution::Args& args); - AppInstaller::Manifest::ManifestLocalization GetPreferredLocalization(const AppInstaller::CLI::Execution::Args& args); + std::optional<Manifest::ManifestInstaller> GetPreferredInstaller(const Manifest::Manifest& manifest); + Manifest::ManifestLocalization GetPreferredLocalization(const Manifest::Manifest& manifest); private: - AppInstaller::Manifest::Manifest& m_manifestRef; - AppInstaller::CLI::Execution::Reporter& m_reporterRef; + // TODO: Handle args to change how we select. }; } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.cpp @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#include "pch.h" -#include "Common.h" -#include "MsixInstallerHandler.h" -#include <AppInstallerDeployment.h> - -using namespace winrt::Windows::Foundation; -using namespace winrt::Windows::Management::Deployment; -using namespace AppInstaller::CLI; -using namespace AppInstaller::Utility; -using namespace AppInstaller::Manifest; - -namespace AppInstaller::Workflow -{ - void MsixInstallerHandler::Download() - { - if (m_manifestInstallerRef.SignatureSha256.empty()) - { - // Signature hash not provided. Go with download flow. - InstallerHandlerBase::Download(); - m_useStreaming = false; - } - else - { - // Signature hash provided. No download needed. Just verify signature hash. - Msix::MsixInfo msixInfo(m_manifestInstallerRef.Url); - auto signature = msixInfo.GetSignature(); - - auto signatureHash = SHA256::ComputeHash(signature.data(), static_cast<uint32_t>(signature.size())); - - if (!std::equal( - m_manifestInstallerRef.SignatureSha256.begin(), - m_manifestInstallerRef.SignatureSha256.end(), - signatureHash.begin())) - { - AICLI_LOG(CLI, Error, - << "Package hash verification failed. Signature SHA256 in manifest: " - << SHA256::ConvertToString(m_manifestInstallerRef.SignatureSha256) - << "Signature SHA256 from download: " - << SHA256::ConvertToString(signatureHash)); - - if (!m_reporterRef.PromptForBoolResponse("Package hash verification failed. Continue?", Execution::Reporter::Level::Warning)) - { - m_reporterRef.ShowMsg("Canceled. Package hash mismatch.", Execution::Reporter::Level::Error); - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Package installation canceled"); - } - } - else - { - AICLI_LOG(CLI, Info, << "Msix package signature hash verified"); - m_reporterRef.ShowMsg("Successfully verified SHA256."); - } - - m_useStreaming = true; - } - } - - void MsixInstallerHandler::Install() - { - if (!m_useStreaming && m_downloadedInstaller.empty()) - { - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer not downloaded yet"); - } - - Uri target = m_useStreaming ? Uri(Utility::ConvertToUTF16(m_manifestInstallerRef.Url)) : Uri(m_downloadedInstaller.c_str()); - - m_reporterRef.ShowMsg("Starting package install..."); - ExecuteInstallerAsync(target); - m_reporterRef.ShowMsg("Successfully installed."); - } - - void MsixInstallerHandler::ExecuteInstallerAsync(const winrt::Windows::Foundation::Uri& uri) - { - DeploymentOptions deploymentOptions = - DeploymentOptions::ForceApplicationShutdown | - DeploymentOptions::ForceTargetApplicationShutdown; - m_reporterRef.ExecuteWithProgress(std::bind(Deployment::RequestAddPackageAsync, uri, deploymentOptions, std::placeholders::_1)); - } -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.h b/src/AppInstallerCLICore/Workflows/MsixInstallerHandler.h @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -#pragma once -#include "InstallerHandlerBase.h" - -namespace AppInstaller::Workflow -{ - // MsixInstallerHandler handles appx/msix installers. - class MsixInstallerHandler : public InstallerHandlerBase - { - public: - MsixInstallerHandler( - const Manifest::ManifestInstaller& manifestInstaller, - AppInstaller::CLI::Execution::Context& context) : - InstallerHandlerBase(manifestInstaller, context) {} - - // Download method just checks installer signature hash if signature hash - // is provided in the manifest. Otherwise, Download will download the whole - // installer to local temp folder. - void Download() override; - - void Install() override; - - protected: - // If use streaming install vs download install. - bool m_useStreaming = true; - - virtual void ExecuteInstallerAsync(const winrt::Windows::Foundation::Uri& uri); - }; -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/SearchFlow.cpp b/src/AppInstallerCLICore/Workflows/SearchFlow.cpp @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#include "pch.h" -#include "SearchFlow.h" - -using namespace AppInstaller::Repository; - -namespace AppInstaller::Workflow -{ - void SearchFlow::Execute() - { - if (WorkflowBase::IndexSearch()) - { - ProcessSearchResult(); - } - } - - void SearchFlow::ProcessSearchResult() - { - if (m_searchResult.Matches.size() == 0) - { - Logging::Telemetry().LogNoAppMatch(); - m_reporterRef.ShowMsg("No app found matching input criteria."); - } - else - { - WorkflowBase::ReportSearchResult(); - } - } -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/SearchFlow.h b/src/AppInstallerCLICore/Workflows/SearchFlow.h @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -#pragma once -#include "ExecutionContext.h" -#include "WorkflowBase.h" - -namespace AppInstaller::Workflow -{ - class SearchFlow : public WorkflowBase - { - public: - SearchFlow(AppInstaller::CLI::Execution::Context& context) : WorkflowBase(context) {} - - void Execute();; - - protected: - - void ProcessSearchResult(); - }; -}- \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp @@ -1,187 +1,198 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" -#include "Common.h" #include "ShellExecuteInstallerHandler.h" using namespace AppInstaller::CLI; using namespace AppInstaller::Utility; using namespace AppInstaller::Manifest; -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - void ShellExecuteInstallerHandler::Install() + namespace { - if (m_downloadedInstaller.empty()) - { - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), "Installer not downloaded yet"); + // ShellExecutes the given path. + std::optional<DWORD> InvokeShellExecute(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) + { + AICLI_LOG(CLI, Info, << "Staring installer. Path: " << filePath); + + SHELLEXECUTEINFOA execInfo = { 0 }; + execInfo.cbSize = sizeof(SHELLEXECUTEINFO); + execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; + std::string filePathUTF8Str = filePath.u8string(); + execInfo.lpFile = filePathUTF8Str.c_str(); + execInfo.lpParameters = args.c_str(); + // Some installer forces UI. Setting to SW_HIDE will hide installer UI and installation will hang forever. + // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. + execInfo.nShow = SW_SHOW; + if (!ShellExecuteExA(&execInfo) || !execInfo.hProcess) + { + return GetLastError(); + } + + wil::unique_process_handle process{ execInfo.hProcess }; + + // Wait for installation to finish + while (!progress.IsCancelled()) + { + DWORD waitResult = WaitForSingleObject(process.get(), 250); + if (waitResult == WAIT_OBJECT_0) + { + break; + } + if (waitResult != WAIT_TIMEOUT) + { + THROW_LAST_ERROR_MSG("Unexpected WaitForSingleObjectResult: %d", waitResult); + } + } + + if (progress.IsCancelled()) + { + return {}; + } + else + { + DWORD exitCode = 0; + GetExitCodeProcess(process.get(), &exitCode); + return exitCode; + } } - m_reporterRef.ShowMsg("Installing package ..."); + // Gets the escaped isntaller args. + std::string GetInstallerArgsTemplate(Execution::Context& context) + { + std::string installerArgs = ""; + const std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString>& installerSwitches = context.Get<Execution::Data::Installer>()->Switches; - std::string installerArgs = GetInstallerArgs(); - AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs); + // Construct install experience arg. + if (context.Args.Contains(Execution::Args::Type::Silent) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Silent) != installerSwitches.end()) + { + installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::Silent); + } + else if (context.Args.Contains(Execution::Args::Type::Interactive) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive) != installerSwitches.end()) + { + installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::Interactive); + } + else if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress) != installerSwitches.end()) + { + installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress); + } - RenameDownloadedInstaller(); + // Construct language arg if necessary. + if (context.Args.Contains(Execution::Args::Type::Language) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Language) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Language); + } - auto installResult = m_reporterRef.ExecuteWithProgress( - std::bind(ExecuteInstaller, - m_downloadedInstaller, - installerArgs, - std::placeholders::_1)); + // Construct install location arg if necessary. + if (context.Args.Contains(Execution::Args::Type::InstallLocation) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); + } - if (!installResult) - { - m_reporterRef.ShowMsg("Installation abandoned", Execution::Reporter::Level::Error); - } - else if (installResult.value() != 0) - { - m_reporterRef.ShowMsg("Install failed. Exit code: " + std::to_string(installResult.value()), Execution::Reporter::Level::Error); + // Construct log path arg. + if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Log) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Log); + } - THROW_EXCEPTION_MSG(WorkflowException(APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED), - "Install failed. Installer task returned: %u", installResult.value()); - } - else - { - m_reporterRef.ShowMsg("Successfully installed!"); - } - } + // Construct custom arg. + if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Custom) != installerSwitches.end()) + { + installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Custom); + } - std::optional<DWORD> ShellExecuteInstallerHandler::ExecuteInstaller(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress) - { - AICLI_LOG(CLI, Info, << "Staring installer. Path: " << filePath); - - SHELLEXECUTEINFOA execInfo = { 0 }; - execInfo.cbSize = sizeof(SHELLEXECUTEINFO); - execInfo.fMask = SEE_MASK_NOCLOSEPROCESS; - std::string filePathUTF8Str = Utility::ConvertToUTF8(filePath.c_str()); - execInfo.lpFile = filePathUTF8Str.c_str(); - execInfo.lpParameters = args.c_str(); - // Some installer forces UI. Setting to SW_HIDE will hide installer UI and installation will hang forever. - // Verified setting to SW_SHOW does not hurt silent mode since no UI will be shown. - execInfo.nShow = SW_SHOW; - if (!ShellExecuteExA(&execInfo) || !execInfo.hProcess) - { - return GetLastError(); + return installerArgs; } - - wil::unique_process_handle process{ execInfo.hProcess }; - // Wait for installation to finish - while (!progress.IsCancelled()) + // Applies values to the template. + void PopulateInstallerArgsTemplate(Execution::Context& context, std::string& installerArgs) { - DWORD waitResult = WaitForSingleObject(process.get(), 250); - if (waitResult == WAIT_OBJECT_0) + // Populate <LogPath> with value from command line or temp path. + std::string logPath; + if (context.Args.Contains(Execution::Args::Type::Log)) { - break; + logPath = context.Args.GetArg(Execution::Args::Type::Log); } - if (waitResult != WAIT_TIMEOUT) + else { - THROW_LAST_ERROR_MSG("Unexpected WaitForSingleObjectResult: %d", waitResult); + logPath = Utility::ConvertToUTF8(context.Get<Execution::Data::InstallerPath>().c_str()) + ".log"; } - } - if (progress.IsCancelled()) - { - return {}; - } - else - { - DWORD exitCode = 0; - GetExitCodeProcess(process.get(), &exitCode); - return exitCode; + if (Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_LOGPATH), logPath)) + { + context.Add<Execution::Data::LogPath>(logPath); + } + + // Populate <InstallPath> with value from command line. + if (context.Args.Contains(Execution::Args::Type::InstallLocation)) + { + Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_INSTALLPATH), context.Args.GetArg(Execution::Args::Type::InstallLocation)); + } + + // Todo: language token support will be implemented later } } - std::string ShellExecuteInstallerHandler::GetInstallerArgsTemplate() + void ShellExecuteInstallImpl(Execution::Context& context) { - std::string installerArgs = ""; - const std::map<ManifestInstaller::InstallerSwitchType, Utility::NormalizedString>& installerSwitches = m_manifestInstallerRef.Switches; - - // Construct install experience arg. - if (m_argsRef.Contains(Execution::Args::Type::Silent) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Silent) != installerSwitches.end()) - { - installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::Silent); - } - else if (m_argsRef.Contains(Execution::Args::Type::Interactive) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Interactive) != installerSwitches.end()) - { - installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::Interactive); - } - else if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::SilentWithProgress) != installerSwitches.end()) - { - installerArgs += installerSwitches.at(ManifestInstaller::InstallerSwitchType::SilentWithProgress); - } + context.Reporter.Info() << "Installing ..." << std::endl; - // Construct language arg if necessary. - if (m_argsRef.Contains(Execution::Args::Type::Language) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::Language) != installerSwitches.end()) - { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Language); - } + const std::string& installerArgs = context.Get<Execution::Data::InstallerArgs>(); - // Construct install location arg if necessary. - if (m_argsRef.Contains(Execution::Args::Type::InstallLocation) && installerSwitches.find(ManifestInstaller::InstallerSwitchType::InstallLocation) != installerSwitches.end()) - { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::InstallLocation); - } + auto installResult = context.Reporter.ExecuteWithProgress( + std::bind(InvokeShellExecute, + context.Get<Execution::Data::InstallerPath>(), + installerArgs, + std::placeholders::_1)); - // Construct log path arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Log) != installerSwitches.end()) + if (!installResult) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Log); + context.Reporter.Warn() << "Installation abandoned" << std::endl; + AICLI_TERMINATE_CONTEXT(E_ABORT); } - - // Construct custom arg. - if (installerSwitches.find(ManifestInstaller::InstallerSwitchType::Custom) != installerSwitches.end()) + else if (installResult.value() != 0) { - installerArgs += ' ' + installerSwitches.at(ManifestInstaller::InstallerSwitchType::Custom); - } + const auto& manifest = context.Get<Execution::Data::Manifest>(); + Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, "ShellExecute", installResult.value()); - return installerArgs; - } + context.Reporter.Error() << "Installer failed with exit code: " << installResult.value() << std::endl; + if (context.Contains(Execution::Data::LogPath)) + { + context.Reporter.Info() << "Installer log is available at: " << context.Get<Execution::Data::LogPath>().u8string() << std::endl; + } - void ShellExecuteInstallerHandler::PopulateInstallerArgsTemplate(std::string& installerArgs) - { - // Populate <LogPath> with value from command line or temp path. - std::string logPath; - if (m_argsRef.Contains(Execution::Args::Type::Log)) - { - logPath = m_argsRef.GetArg(Execution::Args::Type::Log); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SHELLEXEC_INSTALL_FAILED); } else { - logPath = Utility::ConvertToUTF8(m_downloadedInstaller.c_str()) + ".log"; + context.Reporter.Info() << "Successfully installed!" << std::endl; } - Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_LOGPATH), logPath); - - // Populate <InstallPath> with value from command line. - if (m_argsRef.Contains(Execution::Args::Type::InstallLocation)) - { - Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_INSTALLPATH), m_argsRef.GetArg(Execution::Args::Type::InstallLocation)); - } - - // Todo: language token support will be implemented later } - std::string ShellExecuteInstallerHandler::GetInstallerArgs() + void GetInstallerArgs(Execution::Context& context) { // If override switch is specified, use the override value as installer args. - if (m_argsRef.Contains(Execution::Args::Type::Override)) + if (context.Args.Contains(Execution::Args::Type::Override)) { - return std::string{ m_argsRef.GetArg(Execution::Args::Type::Override) }; + context.Add<Execution::Data::InstallerArgs>(std::string{ context.Args.GetArg(Execution::Args::Type::Override) }); + return; } - std::string installerArgs = GetInstallerArgsTemplate(); + std::string installerArgs = GetInstallerArgsTemplate(context); - PopulateInstallerArgsTemplate(installerArgs); + PopulateInstallerArgsTemplate(context, installerArgs); - return installerArgs; + AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs); + context.Add<Execution::Data::InstallerArgs>(std::move(installerArgs)); } - void ShellExecuteInstallerHandler::RenameDownloadedInstaller() + void RenameDownloadedInstaller(Execution::Context& context) { - std::filesystem::path renamedDownloadedInstaller(m_downloadedInstaller); + auto& installerPath = context.Get<Execution::Data::InstallerPath>(); + std::filesystem::path renamedDownloadedInstaller(installerPath); - switch(m_manifestInstallerRef.InstallerType) + switch(context.Get<Execution::Data::Installer>()->InstallerType) { case ManifestInstaller::InstallerTypeEnum::Burn: case ManifestInstaller::InstallerTypeEnum::Exe: @@ -196,9 +207,9 @@ namespace AppInstaller::Workflow } // std::filesystem::rename() handles motw correctly if applicable. - std::filesystem::rename(m_downloadedInstaller, renamedDownloadedInstaller); + std::filesystem::rename(installerPath, renamedDownloadedInstaller); - m_downloadedInstaller.assign(renamedDownloadedInstaller); - AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << m_downloadedInstaller ); + installerPath.assign(renamedDownloadedInstaller); + AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h b/src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.h @@ -1,40 +1,33 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once -#include "InstallerHandlerBase.h" #include <AppInstallerProgress.h> +#include "ExecutionContext.h" +#include <filesystem> #include <optional> -namespace AppInstaller::Workflow +// ShellExecuteInstallerHandler handles installers run through ShellExecute. +// Exe, Wix, Nullsoft, Msi and Inno should be handled by this installer handler. +namespace AppInstaller::CLI::Workflow { - // ShellExecuteInstallerHandler handles installers run through ShellExecute. - // Exe, Wix, Nullsoft, Msi and Inno should be handled by this installer handler. - class ShellExecuteInstallerHandler : public InstallerHandlerBase - { - public: - ShellExecuteInstallerHandler( - const Manifest::ManifestInstaller& manifestInstaller, - AppInstaller::CLI::Execution::Context& context) : - InstallerHandlerBase(manifestInstaller, context) {}; - - // Install is done though invoking SheelExecute on downloaded installer. - void Install() override; - - protected: - static std::optional<DWORD> ExecuteInstaller(const std::filesystem::path& filePath, const std::string& args, IProgressCallback& progress); - - // Construct the installer arg string from appropriate source(known args, manifest) according to command line args. - // Token is not replaced with actual values yet. - std::string GetInstallerArgsTemplate(); - - // Replace tokens in the installer arg string with appropriate values. - void PopulateInstallerArgsTemplate(std::string& installerArgs); - - std::string GetInstallerArgs(); - - // This method appends appropriate extension to the downloaded installer. - // ShellExecute uses file extension to launch the installer appropriately. - virtual void RenameDownloadedInstaller(); - }; + // Install is done through invoking ShellExecute on downloaded installer. + // Required Args: None + // Inputs: Manifest?, InstallerPath, InstallerArgs + // Outputs: None + void ShellExecuteInstallImpl(Execution::Context& context); + + // Gets the installer args from the context. + // Required Args: None + // Inputs: Installer, InstallerPath + // Outputs: InstallerArgs + void GetInstallerArgs(Execution::Context& context); + + // This method appends appropriate extension to the downloaded installer. + // ShellExecute uses file extension to launch the installer appropriately. + // Required Args: None + // Inputs: Installer, InstallerPath + // Modifies: InstallerPath + // Outputs: None + void RenameDownloadedInstaller(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.cpp b/src/AppInstallerCLICore/Workflows/ShowFlow.cpp @@ -5,62 +5,74 @@ #include "ShowFlow.h" #include "ManifestComparator.h" -using namespace AppInstaller::CLI; using namespace AppInstaller::Repository; -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - void ShowFlow::Execute() + namespace { - if (IndexSearch() && EnsureOneMatchFromSearchResult()) + void OutputVersionAndChannel(Execution::Context& context, std::string_view version, std::string_view channel) { - if (m_argsRef.Contains(Execution::Args::Type::ListVersions)) - { - ShowAppVersion(); - } - else + auto out = context.Reporter.Info(); + + out << " " << version; + if (!channel.empty()) { - ShowAppInfo(); + out << '[' << channel << ']'; } + out << std::endl; } } - void ShowFlow::ShowAppInfo() + void ShowManifestInfo(Execution::Context& context) { - if (GetManifest()) - { - SelectInstaller(); - ManifestComparator manifestComparator(m_manifest, m_reporterRef); - auto selectedLocalization = manifestComparator.GetPreferredLocalization(m_argsRef); + const auto& manifest = context.Get<Execution::Data::Manifest>(); + const auto& installer = context.Get<Execution::Data::Installer>(); + + ManifestComparator manifestComparator(context.Args); + auto selectedLocalization = manifestComparator.GetPreferredLocalization(manifest); - m_reporterRef.ShowMsg("Id: " + m_manifest.Id); - m_reporterRef.ShowMsg("Name: " + m_manifest.Name); - m_reporterRef.ShowMsg("Version: " + m_manifest.Version); - m_reporterRef.ShowMsg("Author: " + m_manifest.Author); - m_reporterRef.ShowMsg("AppMoniker: " + m_manifest.AppMoniker); - m_reporterRef.ShowMsg("Description: " + selectedLocalization.Description); - m_reporterRef.ShowMsg("Homepage: " + selectedLocalization.Homepage); - m_reporterRef.ShowMsg("License: " + selectedLocalization.LicenseUrl); + // TODO: Come up with a prettier format + context.Reporter.Info() << "Id: " + manifest.Id << std::endl; + context.Reporter.Info() << "Name: " + manifest.Name << std::endl; + context.Reporter.Info() << "Version: " + manifest.Version << std::endl; + context.Reporter.Info() << "Author: " + manifest.Author << std::endl; + context.Reporter.Info() << "AppMoniker: " + manifest.AppMoniker << std::endl; + context.Reporter.Info() << "Description: " + selectedLocalization.Description << std::endl; + context.Reporter.Info() << "Homepage: " + selectedLocalization.Homepage << std::endl; + context.Reporter.Info() << "License: " + selectedLocalization.LicenseUrl << std::endl; - m_reporterRef.ShowMsg("Installer info:" + m_manifest.Id); - m_reporterRef.ShowMsg("--Installer Language: " + m_selectedInstaller.Language); - m_reporterRef.ShowMsg("--Installer SHA256: " + Utility::SHA256::ConvertToString(m_selectedInstaller.Sha256)); - m_reporterRef.ShowMsg("--Installer Download Url: " + m_selectedInstaller.Url); - m_reporterRef.ShowMsg("--Installer Type: " + Manifest::ManifestInstaller::InstallerTypeToString(m_selectedInstaller.InstallerType)); + context.Reporter.Info() << "Installer:" << std::endl; + if (installer) + { + context.Reporter.Info() << " Language: " + installer->Language << std::endl; + context.Reporter.Info() << " SHA256: " + Utility::SHA256::ConvertToString(installer->Sha256) << std::endl; + context.Reporter.Info() << " Download Url: " + installer->Url << std::endl; + context.Reporter.Info() << " Type: " + Manifest::ManifestInstaller::InstallerTypeToString(installer->InstallerType) << std::endl; } + else + { + context.Reporter.Warn() << " No installers are applicable to the current system" << std::endl; + } + } + + void ShowManifestVersion(Execution::Context& context) + { + const auto& manifest = context.Get<Execution::Data::Manifest>(); + + context.Reporter.Info() << manifest.Id << ", " << manifest.Name << std::endl; + OutputVersionAndChannel(context, manifest.Version, manifest.Channel); } - void ShowFlow::ShowAppVersion() + void ShowAppVersions(Execution::Context& context) { - auto app = m_searchResult.Matches.at(0).Application.get(); + auto app = context.Get<Execution::Data::SearchResult>().Matches.at(0).Application.get(); - m_reporterRef.ShowMsg("Id: " + app->GetId()); - m_reporterRef.ShowMsg("Name: " + app->GetName()); - m_reporterRef.ShowMsg("Versions:"); + context.Reporter.Info() << app->GetId() << ", " << app->GetName() << std::endl; for (auto& version : app->GetVersions()) { - m_reporterRef.ShowMsg(" " + version.ToString()); + OutputVersionAndChannel(context, version.GetVersion().ToString(), version.GetChannel().ToString()); } } } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/ShowFlow.h b/src/AppInstallerCLICore/Workflows/ShowFlow.h @@ -1,22 +1,25 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #pragma once #include "ExecutionContext.h" -#include "WorkflowBase.h" -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - class ShowFlow : public SingleManifestWorkflow - { - public: - ShowFlow(AppInstaller::CLI::Execution::Context& context) : SingleManifestWorkflow(context) {} - - void Execute(); + // Shows information on an application. + // Required Args: None + // Inputs: Manifest, Installer + // Outputs: None + void ShowManifestInfo(Execution::Context& context); - protected: + // Shows the version for the specific manifest. + // Required Args: None + // Inputs: Manifest + // Outputs: None + void ShowManifestVersion(Execution::Context& context); - void ShowAppInfo(); - void ShowAppVersion(); - }; + // Shows all versions for an application. + // Required Args: None + // Inputs: SearchResult [only operates on first match] + // Outputs: None + void ShowAppVersions(Execution::Context& context); } \ No newline at end of file diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.cpp b/src/AppInstallerCLICore/Workflows/SourceFlow.cpp @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "SourceFlow.h" + +namespace AppInstaller::CLI::Workflow +{ + using namespace AppInstaller::CLI::Execution; + + void GetSourceList(Execution::Context& context) + { + context.Add<Execution::Data::SourceList>(Repository::GetSources()); + } + + void GetSourceListWithFilter(Execution::Context& context) + { + if (context.Args.Contains(Args::Type::SourceName)) + { + std::string_view name = context.Args.GetArg(Args::Type::SourceName); + std::optional<Repository::SourceDetails> source = Repository::GetSource(name); + + if (!source) + { + context.Reporter.Error() << "Did not find a source named: " << name << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } + + std::vector<Repository::SourceDetails> sources; + sources.emplace_back(std::move(source.value())); + context.Add<Execution::Data::SourceList>(std::move(sources)); + } + else + { + context.Add<Execution::Data::SourceList>(Repository::GetSources()); + } + } + + void CheckSourceListAgainstAdd(Execution::Context& context) + { + std::string_view name = context.Args.GetArg(Args::Type::SourceName); + std::string_view arg = context.Args.GetArg(Args::Type::SourceArg); + + // First check if this is going to be a name conflict + std::optional<Repository::SourceDetails> source = Repository::GetSource(name); + if (source) + { + if (source->Arg == arg) + { + // Name and arg match, indicate this to the user and bail. + context.Reporter.Info() << "A source with the given name already exists and refers to the same location: " << std::endl << + " " << source->Name << " -> " << source->Arg << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS); + } + else + { + context.Reporter.Error() << "A source with the given name already exists and refers to a different location: " << std::endl << + " " << source->Name << " -> " << source->Arg << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_ALREADY_EXISTS); + } + } + + // Now check if the URL is already in use under a different name + auto sourceList = context.Get<Execution::Data::SourceList>(); + std::string_view type = context.Args.GetArg(Args::Type::SourceType); + + for (const auto& details : sourceList) + { + if (!details.Arg.empty() && details.Arg == arg && details.Type == type) + { + context.Reporter.Error() << "A source with a different name already refers to this location: " << std::endl << + " " << details.Name << " -> " << details.Arg << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_ARG_ALREADY_EXISTS); + } + } + } + + void AddSource(Execution::Context& context) + { + std::string name(context.Args.GetArg(Args::Type::SourceName)); + std::string arg(context.Args.GetArg(Args::Type::SourceArg)); + std::string type; + if (context.Args.Contains(Args::Type::SourceType)) + { + type = context.Args.GetArg(Args::Type::SourceType); + } + + context.Reporter.Info() << + "Adding source:" << std::endl << + " " << name << " -> " << arg << std::endl; + + context.Reporter.ExecuteWithProgress(std::bind(Repository::AddSource, std::move(name), std::move(type), std::move(arg), std::placeholders::_1)); + + context.Reporter.Info() << "Done"; + } + + void ListSources(Execution::Context& context) + { + const std::vector<Repository::SourceDetails>& sources = context.Get<Data::SourceList>(); + + if (context.Args.Contains(Args::Type::SourceName)) + { + // If a source name was specified, list full details of the one and only source. + const Repository::SourceDetails& source = sources[0]; + + context.Reporter.Info() << + "Name : " + source.Name << std::endl << + "Type : " + source.Type << std::endl << + "Arg : " + source.Arg << std::endl << + "Data : " + source.Data << std::endl << + "Updated: "; + + if (source.LastUpdateTime == Utility::ConvertUnixEpochToSystemClock(0)) + { + context.Reporter.Info() << "<never>" << std::endl; + } + else + { + context.Reporter.Info() << source.LastUpdateTime << std::endl; + } + } + else + { + context.Reporter.Info() << "Current sources:" << std::endl; + + if (sources.empty()) + { + context.Reporter.Info() << " <none>" << std::endl; + } + else + { + for (const auto& source : sources) + { + context.Reporter.Info() << " " << source.Name << " -> " << source.Arg << std::endl; + } + } + } + } + + void UpdateSources(Execution::Context& context) + { + if (!context.Args.Contains(Args::Type::SourceName)) + { + context.Reporter.Info() << "Updating all sources..." << std::endl; + } + + const std::vector<Repository::SourceDetails>& sources = context.Get<Data::SourceList>(); + for (const auto& sd : sources) + { + context.Reporter.Info() << "Updating source: " << sd.Name << "..." << std::endl; + context.Reporter.ExecuteWithProgress(std::bind(Repository::UpdateSource, sd.Name, std::placeholders::_1)); + context.Reporter.Info() << "Done." << std::endl; + } + } + + void RemoveSources(Execution::Context& context) + { + if (!context.Args.Contains(Args::Type::SourceName)) + { + context.Reporter.Info() << "Removing all sources..." << std::endl; + } + + const std::vector<Repository::SourceDetails>& sources = context.Get<Data::SourceList>(); + for (const auto& sd : sources) + { + context.Reporter.Info() << "Removing source: " << sd.Name << "..." << std::endl; + context.Reporter.ExecuteWithProgress(std::bind(Repository::RemoveSource, sd.Name, std::placeholders::_1)); + context.Reporter.Info() << "Done." << std::endl; + } + } +} diff --git a/src/AppInstallerCLICore/Workflows/SourceFlow.h b/src/AppInstallerCLICore/Workflows/SourceFlow.h @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Gets the current source list. + // Required Args: None + // Inputs: None + // Outputs: SourceList + void GetSourceList(Execution::Context& context); + + // Gets the source list, filtering it if SourceName is present. + // Required Args: None + // Inputs: None + // Outputs: SourceList + void GetSourceListWithFilter(Execution::Context& context); + + // Checks the source list against the inputs to ensure a successful add after this. + // Required Args: SourceName, SourceArg + // Inputs: SourceList + // Outputs: None + void CheckSourceListAgainstAdd(Execution::Context& context); + + // Adds the source. + // Required Args: SourceName, SourceArg + // Inputs: None + // Outputs: None + void AddSource(Execution::Context& context); + + // Lists the sources in SourceList. + // Required Args: None + // Inputs: SourceList + // Outputs: None + void ListSources(Execution::Context& context); + + // Updates the sources in SourceList. + // Required Args: None + // Inputs: SourceList + // Outputs: None + void UpdateSources(Execution::Context& context); + + // Removes the sources in SourceList. + // Required Args: None + // Inputs: SourceList + // Outputs: None + void RemoveSources(Execution::Context& context); +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -2,195 +2,283 @@ // Licensed under the MIT License. #include "pch.h" #include "WorkflowBase.h" +#include "ExecutionContext.h" #include "ManifestComparator.h" -using namespace AppInstaller::CLI; -using namespace AppInstaller::Repository; -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - void WorkflowBase::OpenIndexSource() + using namespace AppInstaller::Repository; + + bool WorkflowTask::operator==(const WorkflowTask& other) const { - std::string sourceName; - if (m_argsRef.Contains(Execution::Args::Type::Source)) + if (m_isFunc && other.m_isFunc) + { + return m_func == other.m_func; + } + else if (!m_isFunc && !other.m_isFunc) + { + return m_name == other.m_name; + } + else { - sourceName = m_argsRef.GetArg(Execution::Args::Type::Source); + return false; } + } - m_source = m_reporterRef.ExecuteWithProgress(std::bind(OpenSource, sourceName, std::placeholders::_1)); + void WorkflowTask::operator()(Execution::Context& context) const + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_isFunc); + m_func(context); } - bool WorkflowBase::IndexSearch() + void OpenSource(Execution::Context& context) { - OpenIndexSource(); - if (!m_source) + std::string_view sourceName; + if (context.Args.Contains(Execution::Args::Type::Source)) { - bool noSources = true; + sourceName = context.Args.GetArg(Execution::Args::Type::Source); + } + + std::shared_ptr<Repository::ISource> source = context.Reporter.ExecuteWithProgress(std::bind(Repository::OpenSource, sourceName, std::placeholders::_1)); + + if (!source) + { + std::vector<SourceDetails> sources = GetSources(); - if (m_argsRef.Contains(Execution::Args::Type::Source)) + if (context.Args.Contains(Execution::Args::Type::Source) && !sources.empty()) { // A bad name was given, try to help. - std::vector<SourceDetails> sources = GetSources(); - if (!sources.empty()) + context.Reporter.Error() << "No sources match the given value: " << sourceName << std::endl; + context.Reporter.Info() << "The configured sources are:" << std::endl; + for (const auto& details : sources) { - noSources = false; - - m_reporterRef.Warn() << "No sources match the given value '" << m_argsRef.GetArg(Execution::Args::Type::Source) << "'" << std::endl; - m_reporterRef.ShowMsg("The configured sources are:"); - for (const auto& details : sources) - { - m_reporterRef.ShowMsg(" " + details.Name); - } + context.Reporter.Info() << " " << details.Name << std::endl; } - } - if (noSources) + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST); + } + else { - m_reporterRef.ShowMsg("No sources defined; add one with 'source add'", - Execution::Reporter::Level::Warning); + // Even if a name was given, there are no sources + context.Reporter.Error() << "No sources defined; add one with 'source add'" << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED); } - - return false; } + context.Add<Execution::Data::Source>(std::move(source)); + } + + void SearchSource(Execution::Context& context) + { + auto& args = context.Args; + // Construct query MatchType matchType = MatchType::Substring; - if (m_argsRef.Contains(Execution::Args::Type::Exact)) + if (args.Contains(Execution::Args::Type::Exact)) { matchType = MatchType::Exact; } SearchRequest searchRequest; - if (m_argsRef.Contains(Execution::Args::Type::Query)) + if (args.Contains(Execution::Args::Type::Query)) { - searchRequest.Query.emplace(RequestMatch(matchType, m_argsRef.GetArg(Execution::Args::Type::Query))); + searchRequest.Query.emplace(RequestMatch(matchType, args.GetArg(Execution::Args::Type::Query))); } - if (m_argsRef.Contains(Execution::Args::Type::Id)) + if (args.Contains(Execution::Args::Type::Id)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Id, matchType, m_argsRef.GetArg(Execution::Args::Type::Id))); + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Id, matchType, args.GetArg(Execution::Args::Type::Id))); } - if (m_argsRef.Contains(Execution::Args::Type::Name)) + if (args.Contains(Execution::Args::Type::Name)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Name, matchType, m_argsRef.GetArg(Execution::Args::Type::Name))); + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Name, matchType, args.GetArg(Execution::Args::Type::Name))); } - if (m_argsRef.Contains(Execution::Args::Type::Moniker)) + if (args.Contains(Execution::Args::Type::Moniker)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Moniker, matchType, m_argsRef.GetArg(Execution::Args::Type::Moniker))); + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Moniker, matchType, args.GetArg(Execution::Args::Type::Moniker))); } - if (m_argsRef.Contains(Execution::Args::Type::Tag)) + if (args.Contains(Execution::Args::Type::Tag)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Tag, matchType, m_argsRef.GetArg(Execution::Args::Type::Tag))); + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Tag, matchType, args.GetArg(Execution::Args::Type::Tag))); } - if (m_argsRef.Contains(Execution::Args::Type::Command)) + if (args.Contains(Execution::Args::Type::Command)) { - searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Command, matchType, m_argsRef.GetArg(Execution::Args::Type::Command))); + searchRequest.Filters.emplace_back(ApplicationMatchFilter(ApplicationMatchField::Command, matchType, args.GetArg(Execution::Args::Type::Command))); } - if (m_argsRef.Contains(Execution::Args::Type::Count)) + if (args.Contains(Execution::Args::Type::Count)) { - searchRequest.MaximumResults = std::stoi(std::string(m_argsRef.GetArg(Execution::Args::Type::Count))); + searchRequest.MaximumResults = std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); } Logging::Telemetry().LogSearchRequest( - m_argsRef.GetArg(Execution::Args::Type::Query), - m_argsRef.GetArg(Execution::Args::Type::Id), - m_argsRef.GetArg(Execution::Args::Type::Name), - m_argsRef.GetArg(Execution::Args::Type::Moniker), - m_argsRef.GetArg(Execution::Args::Type::Tag), - m_argsRef.GetArg(Execution::Args::Type::Command), + args.GetArg(Execution::Args::Type::Query), + args.GetArg(Execution::Args::Type::Id), + args.GetArg(Execution::Args::Type::Name), + args.GetArg(Execution::Args::Type::Moniker), + args.GetArg(Execution::Args::Type::Tag), + args.GetArg(Execution::Args::Type::Command), searchRequest.MaximumResults, searchRequest.ToString()); - m_searchResult = m_source->Search(searchRequest); - return true; + context.Add<Execution::Data::SearchResult>(context.Get<Execution::Data::Source>()->Search(searchRequest)); } - void WorkflowBase::ReportSearchResult() + void ReportSearchResult(Execution::Context& context) { - for (auto& match : m_searchResult.Matches) + auto& searchResult = context.Get<Execution::Data::SearchResult>(); + Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); + for (auto& match : searchResult.Matches) { auto app = match.Application.get(); auto allVersions = app->GetVersions(); - // Todo: Assume versions are sorted when returned so we'll use the first one as the latest version - // Need to call sort if the above is not the case. - std::string msg = app->GetId() + ", " + app->GetName() + ", " + allVersions.at(0).GetVersion().ToString(); + // Assume versions are sorted when returned so we'll use the first one as the latest version + context.Reporter.Info() << app->GetId() << ", " << app->GetName() << ", " << allVersions.at(0).GetVersion().ToString(); if (match.MatchCriteria.Field != ApplicationMatchField::Id && match.MatchCriteria.Field != ApplicationMatchField::Name) { - msg += ", ["; - msg += ApplicationMatchFieldToString(match.MatchCriteria.Field); - msg += ": " + match.MatchCriteria.Value + "]"; + context.Reporter.Info() << ", [" << ApplicationMatchFieldToString(match.MatchCriteria.Field) << ": " << match.MatchCriteria.Value << "]"; } - Logging::Telemetry().LogSearchResultCount(m_searchResult.Matches.size()); - m_reporterRef.ShowMsg(msg); + context.Reporter.Info() << std::endl; } } - bool SingleManifestWorkflow::EnsureOneMatchFromSearchResult() + void EnsureMatchesFromSearchResult(Execution::Context& context) { - if (m_searchResult.Matches.size() == 0) + auto& searchResult = context.Get<Execution::Data::SearchResult>(); + + if (searchResult.Matches.size() == 0) { Logging::Telemetry().LogNoAppMatch(); - m_reporterRef.ShowMsg("No app found matching input criteria."); - return false; + context.Reporter.Info() << "No app found matching input criteria." << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); } + } - if (m_searchResult.Matches.size() > 1) + void EnsureOneMatchFromSearchResult(Execution::Context& context) + { + context << + EnsureMatchesFromSearchResult << + [](Execution::Context& context) { - Logging::Telemetry().LogMultiAppMatch(); - m_reporterRef.ShowMsg("Multiple apps found matching input criteria. Please refine the input."); - ReportSearchResult(); - return false; - } + auto& searchResult = context.Get<Execution::Data::SearchResult>(); + + if (searchResult.Matches.size() > 1) + { + Logging::Telemetry().LogMultiAppMatch(); + context.Reporter.Warn() << "Multiple apps found matching input criteria. Please refine the input." << std::endl; + context << ReportSearchResult; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); + } - auto app = m_searchResult.Matches.at(0).Application.get(); - Logging::Telemetry().LogAppFound(app->GetName(), app->GetId()); - return true; + auto app = searchResult.Matches.at(0).Application.get(); + Logging::Telemetry().LogAppFound(app->GetName(), app->GetId()); + }; } - bool SingleManifestWorkflow::GetManifest() + void GetManifestFromSearchResult(Execution::Context& context) { - auto app = m_searchResult.Matches.at(0).Application.get(); + auto app = context.Get<Execution::Data::SearchResult>().Matches.at(0).Application.get(); - std::string_view version = m_argsRef.GetArg(Execution::Args::Type::Version); - std::string_view channel = m_argsRef.GetArg(Execution::Args::Type::Channel); + std::string_view version = context.Args.GetArg(Execution::Args::Type::Version); + std::string_view channel = context.Args.GetArg(Execution::Args::Type::Channel); std::optional<Manifest::Manifest> manifest = app->GetManifest(version, channel); if (!manifest) { - std::string message = "No version found matching "; + context.Reporter.Error() << "No version found matching: "; if (!version.empty()) { - message += version; + context.Reporter.Error() << version; } if (!channel.empty()) { - message += '['; - message += channel; - message += ']'; + context.Reporter.Error() << '[' << channel << ']'; } - m_reporterRef.ShowMsg(message, Execution::Reporter::Level::Warning); - return false; + context.Reporter.Error() << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); + } + + Logging::Telemetry().LogManifestFields(manifest->Id, manifest->Name, manifest->Version); + context.Add<Execution::Data::Manifest>(std::move(manifest.value())); + } + + void VerifyFile::operator()(Execution::Context& context) const + { + std::filesystem::path path = context.Args.GetArg(m_arg); + + if (!std::filesystem::exists(path)) + { + context.Reporter.Error() << "File does not exist: " << path.u8string() << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + } + + if (std::filesystem::is_directory(path)) + { + context.Reporter.Error() << "Path is a directory: " << path.u8string() << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED)); } + } + + void GetManifestFromArg(Execution::Context& context) + { + context << + VerifyFile(Execution::Args::Type::Manifest) << + [](Execution::Context& context) + { + Manifest::Manifest manifest = Manifest::Manifest::CreateFromPath(context.Args.GetArg(Execution::Args::Type::Manifest)); + Logging::Telemetry().LogManifestFields(manifest.Id, manifest.Name, manifest.Version); + context.Add<Execution::Data::Manifest>(std::move(manifest)); + }; + } - m_manifest = std::move(manifest.value()); - Logging::Telemetry().LogManifestFields(m_manifest.Id, m_manifest.Name, m_manifest.Version); + void GetManifest(Execution::Context& context) + { + if (context.Args.Contains(Execution::Args::Type::Manifest)) + { + context << GetManifestFromArg; + } + else + { + context << + OpenSource << + SearchSource << + EnsureOneMatchFromSearchResult << + GetManifestFromSearchResult; + } + } - return true; + void SelectInstaller(Execution::Context& context) + { + ManifestComparator manifestComparator(context.Args); + context.Add<Execution::Data::Installer>(manifestComparator.GetPreferredInstaller(context.Get<Execution::Data::Manifest>())); } +} + +AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) +{ + return (context << AppInstaller::CLI::Workflow::WorkflowTask(f)); +} - void SingleManifestWorkflow::SelectInstaller() +AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, const AppInstaller::CLI::Workflow::WorkflowTask& task) +{ + if (!context.IsTerminated()) { - ManifestComparator manifestComparator(m_manifest, m_reporterRef); - m_selectedInstaller = manifestComparator.GetPreferredInstaller(m_argsRef); +#ifndef AICLI_DISABLE_TEST_HOOKS + if (context.ShouldExecuteWorkflowTask(task)) +#endif + { + task(context); + } } -}- \ No newline at end of file + return context; +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -1,44 +1,118 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - #pragma once -#include "ExecutionContext.h" -#include "Public/AppInstallerRepositorySearch.h" -#include "Public/AppInstallerRepositorySource.h" +#include "ExecutionArgs.h" + +#include <string> +#include <string_view> + + +namespace AppInstaller::CLI::Execution +{ + struct Context; +} -namespace AppInstaller::Workflow +namespace AppInstaller::CLI::Workflow { - class WorkflowBase + // A task in the workflow. + struct WorkflowTask { - protected: - WorkflowBase(AppInstaller::CLI::Execution::Context& context) : - m_contextRef(context), m_reporterRef(context.Reporter), m_argsRef(context.Args) {} + using Func = void (*)(Execution::Context&); - AppInstaller::CLI::Execution::Context& m_contextRef; - AppInstaller::CLI::Execution::Reporter& m_reporterRef; - const AppInstaller::CLI::Execution::Args& m_argsRef; + WorkflowTask(Func f) : m_isFunc(true), m_func(f) {} + WorkflowTask(std::string_view name) : m_name(name) {} - virtual void OpenIndexSource(); + virtual ~WorkflowTask() = default; - bool IndexSearch(); + WorkflowTask(const WorkflowTask&) = default; + WorkflowTask& operator=(const WorkflowTask&) = default; - void ReportSearchResult(); + WorkflowTask(WorkflowTask&&) = default; + WorkflowTask& operator=(WorkflowTask&&) = default; - std::shared_ptr<AppInstaller::Repository::ISource> m_source; - AppInstaller::Repository::SearchResult m_searchResult; + bool operator==(const WorkflowTask& other) const; + + virtual void operator()(Execution::Context& context) const; + + const std::string& GetName() const { return m_name; } + + private: + bool m_isFunc = false; + Func m_func = nullptr; + std::string m_name; }; - // A workflow that requires a single manifest to operate properly. - class SingleManifestWorkflow : public WorkflowBase + // Creates the source object. + // Required Args: None + // Inputs: None + // Outputs: Source + void OpenSource(Execution::Context& context); + + // Performs a search on the source. + // Required Args: None + // Inputs: Source + // Outputs: SearchResult + void SearchSource(Execution::Context& context); + + // Outputs the search results. + // Required Args: None + // Inputs: SearchResult + // Outputs: None + void ReportSearchResult(Execution::Context& context); + + // Ensures that there is at least one result in the search. + // Required Args: None + // Inputs: SearchResult + // Outputs: None + void EnsureMatchesFromSearchResult(Execution::Context& context); + + // Ensures that there is only one result in the search. + // Required Args: None + // Inputs: SearchResult + // Outputs: None + void EnsureOneMatchFromSearchResult(Execution::Context& context); + + // Gets the manifest from a search result. + // Required Args: None + // Inputs: SearchResult + // Outputs: Manifest + void GetManifestFromSearchResult(Execution::Context& context); + + // Ensures the the file exists and is not a directory. + // Required Args: the one given + // Inputs: None + // Outputs: None + struct VerifyFile : public WorkflowTask { - protected: - using WorkflowBase::WorkflowBase; + VerifyFile(Execution::Args::Type arg) : WorkflowTask("VerifyFile"), m_arg(arg) {} - bool EnsureOneMatchFromSearchResult(); - bool GetManifest(); - void SelectInstaller(); + void operator()(Execution::Context& context) const override; - AppInstaller::Manifest::Manifest m_manifest; - AppInstaller::Manifest::ManifestInstaller m_selectedInstaller; + private: + Execution::Args::Type m_arg; }; -}- \ No newline at end of file + + // Opens the manifest file provided on the command line. + // Required Args: Manifest + // Inputs: None + // Outputs: Manifest + void GetManifestFromArg(Execution::Context& context); + + // Composite flow that produces a manifest; either from one given on the command line or by searching. + // Required Args: None + // Inputs: None + // Outputs: Manifest + void GetManifest(Execution::Context& context); + + // Selects the installer from the manifest, if one is applicable. + // Required Args: None + // Inputs: Manifest + // Outputs: Installer + void SelectInstaller(Execution::Context& context); +} + +// Passes the context to the function if it has not been terminated; returns the context. +AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f); + +// Passes the context to the task if it has not been terminated; returns the context. +AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, const AppInstaller::CLI::Workflow::WorkflowTask& task); diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -28,6 +28,7 @@ #include <AppxPackaging.h> #include <AppInstallerDateTime.h> +#include <AppInstallerDeployment.h> #include <AppInstallerDownloader.h> #include <AppInstallerErrors.h> #include <AppInstallerLogging.h> diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -2,67 +2,33 @@ // Licensed under the MIT License. #include "pch.h" #include "TestCommon.h" -#include "AppInstallerLogging.h" -#include "Manifest/Manifest.h" -#include "AppInstallerDownloader.h" -#include "AppInstallerStrings.h" -#include "Workflows/InstallFlow.h" -#include "Workflows/ShowFlow.h" -#include "Workflows/ShellExecuteInstallerHandler.h" -#include "Workflows/MsixInstallerHandler.h" -#include "Public/AppInstallerRepositorySource.h" -#include "Public/AppInstallerRepositorySearch.h" +#include <AppInstallerLogging.h> +#include <Manifest/Manifest.h> +#include <AppInstallerDownloader.h> +#include <AppInstallerStrings.h> +#include <Workflows/InstallFlow.h> +#include <Workflows/ShowFlow.h> +#include <Workflows/ShellExecuteInstallerHandler.h> +#include <Workflows/WorkflowBase.h> +#include <Public/AppInstallerRepositorySource.h> +#include <Public/AppInstallerRepositorySearch.h> +#include <Commands/InstallCommand.h> +#include <Commands/ShowCommand.h> using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Management::Deployment; using namespace TestCommon; using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; +using namespace AppInstaller::CLI::Workflow; using namespace AppInstaller::Manifest; using namespace AppInstaller::Repository; using namespace AppInstaller::Utility; -using namespace AppInstaller::Workflow; -class MsixInstallerHandlerTest : public MsixInstallerHandler -{ -public: - MsixInstallerHandlerTest( - const ManifestInstaller& manifestInstaller, - Execution::Context& context) : MsixInstallerHandler(manifestInstaller, context) {}; - -protected: - - void ExecuteInstallerAsync(const Uri& uri) override - { - std::filesystem::path temp = std::filesystem::temp_directory_path(); - temp /= "TestMsixInstalled.txt"; - std::ofstream file(temp, std::ofstream::out); - - file << AppInstaller::Utility::ConvertToUTF8(uri.ToString()); - - file.close(); - } -}; - -class ShellExecuteInstallerHandlerTest : public ShellExecuteInstallerHandler -{ -public: - ShellExecuteInstallerHandlerTest( - const ManifestInstaller& manifestInstaller, - Execution::Context& context) : ShellExecuteInstallerHandler(manifestInstaller, context) {}; - - void Download() override - { - this->m_downloadedInstaller = TestDataFile("AppInstallerTestExeInstaller.exe"); - } - - void RenameDownloadedInstaller() override {}; - std::string TestInstallerArgs() - { - Download(); - return ShellExecuteInstallerHandler::GetInstallerArgs(); - } -}; +#define REQUIRE_TERMINATED_WITH(_context_,_hr_) \ + REQUIRE(_context_.IsTerminated()); \ + REQUIRE(_hr_ == _context_.GetTerminationHR()) struct TestSource : public ISource { @@ -126,56 +92,121 @@ struct TestSource : public ISource return result; } - virtual const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } + const SourceDetails& GetDetails() const override { THROW_HR(E_NOTIMPL); } +}; + +struct TestContext; + +struct WorkflowTaskOverride +{ + WorkflowTaskOverride(WorkflowTask::Func f, const std::function<void(TestContext&)>& o) : + Target(f), Override(o) {} + + WorkflowTaskOverride(std::string_view n, const std::function<void(TestContext&)>& o) : + Target(n), Override(o) {} + + WorkflowTaskOverride(const WorkflowTask& t, const std::function<void(TestContext&)>& o) : + Target(t), Override(o) {} + + bool Used = false; + WorkflowTask Target; + std::function<void(TestContext&)> Override; }; -class InstallFlowTest : public InstallFlow +// Enables overriding the behavior of specific workflow tasks. +struct TestContext : public Context { -public: - InstallFlowTest(Execution::Context& context) : InstallFlow(context) {} + TestContext(std::ostream& out, std::istream& in) : Context(out, in) {} -protected: - std::unique_ptr<InstallerHandlerBase> GetInstallerHandler() override + ~TestContext() { - switch (m_selectedInstaller.InstallerType) + for (const auto& wto : m_overrides) { - case ManifestInstaller::InstallerTypeEnum::Exe: - return std::make_unique<ShellExecuteInstallerHandlerTest>(m_selectedInstaller, m_contextRef); - case ManifestInstaller::InstallerTypeEnum::Msix: - return std::make_unique<MsixInstallerHandlerTest>(m_selectedInstaller, m_contextRef); - default: - THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + if (!wto.Used) + { + FAIL("Unused override"); + } } } - void OpenIndexSource() override + bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task) override { - m_source = std::make_unique<TestSource>(); + auto itr = std::find_if(m_overrides.begin(), m_overrides.end(), [&](const WorkflowTaskOverride& wto) { return wto.Target == task; }); + + if (itr == m_overrides.end()) + { + return true; + } + else + { + itr->Used = true; + itr->Override(*this); + return false; + } } + + void Override(const WorkflowTaskOverride& wto) + { + m_overrides.emplace_back(wto); + } + +private: + std::vector<WorkflowTaskOverride> m_overrides; }; -class ShowFlowTest : public ShowFlow +void OverrideForOpenSource(TestContext& context) { -public: - ShowFlowTest(Execution::Context& context) : ShowFlow(context) {} + context.Override({ Workflow::OpenSource, [](TestContext& context) + { + context.Add<Execution::Data::Source>(std::make_shared<TestSource>()); + } }); +} -protected: +void OverrideForShellExecute(TestContext& context) +{ + context.Override({ DownloadInstallerFile, [](TestContext& context) + { + context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); + } }); - void OpenIndexSource() override + context.Override({ RenameDownloadedInstaller, [](TestContext&) { - m_source = std::make_unique<TestSource>(); - } -}; + } }); +} + +void OverrideForMSIX(TestContext& context) +{ + context.Override({ MsixInstall, [](TestContext& context) + { + std::filesystem::path temp = std::filesystem::temp_directory_path(); + temp /= "TestMsixInstalled.txt"; + std::ofstream file(temp, std::ofstream::out); + + if (context.Contains(Execution::Data::InstallerPath)) + { + file << context.Get<Execution::Data::InstallerPath>().u8string(); + } + else + { + file << context.Get<Execution::Data::Installer>()->Url; + } + + file.close(); + } }); +} TEST_CASE("ExeInstallFlowWithTestManifest", "[InstallFlow]") { TestCommon::TempFile installResultPath("TestExeInstalled.txt"); std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForShellExecute(context); context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string()); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify Installer is called and parameters are passed in. @@ -193,13 +224,16 @@ TEST_CASE("InstallFlowWithNonApplicableArchitecture", "[InstallFlow]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_NoApplicableArchitecture.yaml").GetPath().u8string()); - InstallFlowTest testFlow(context); - REQUIRE_THROWS_WITH(testFlow.Execute(), Catch::Contains("No installer with applicable architecture found.")); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); - // Verify Installer is called and parameters are passed in. + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER); + + // Verify Installer was not called REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); } @@ -208,11 +242,13 @@ TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForMSIX(context); // Todo: point to files from our repo when the repo goes public context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Msix_DownloadFlow.yaml").GetPath().u8string()); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify Installer is called and a local file is used as package Uri. @@ -221,7 +257,8 @@ TEST_CASE("MsixInstallFlow_DownloadFlow", "[InstallFlow]") REQUIRE(installResultFile.is_open()); std::string installResultStr; std::getline(installResultFile, installResultStr); - REQUIRE(installResultStr.find("file://") != std::string::npos); + Uri uri = Uri(ConvertToUTF16(installResultStr)); + REQUIRE(uri.SchemeName() == L"file"); } TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") @@ -229,11 +266,13 @@ TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") TestCommon::TempFile installResultPath("TestMsixInstalled.txt"); std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForMSIX(context); // Todo: point to files from our repo when the repo goes public context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_Msix_StreamingFlow.yaml").GetPath().u8string()); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify Installer is called and a http address is used as package Uri. @@ -242,32 +281,36 @@ TEST_CASE("MsixInstallFlow_StreamingFlow", "[InstallFlow]") REQUIRE(installResultFile.is_open()); std::string installResultStr; std::getline(installResultFile, installResultStr); - REQUIRE(installResultStr.find("https://") != std::string::npos); + Uri uri = Uri(ConvertToUTF16(installResultStr)); + REQUIRE(uri.SchemeName() == L"https"); } TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") { { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Default Msi type with no args passed in, no switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/passive") != std::string::npos); REQUIRE(installerArgs.find("AppInstallerTestExeInstaller.exe.log") != std::string::npos); } { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Msi type with /silent and /log and /custom and /installlocation, no switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_NoSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/quiet") != std::string::npos); REQUIRE(installerArgs.find("/log \"MyLog.log\"") != std::string::npos); REQUIRE(installerArgs.find("TARGETDIR=\"MyDir\"") != std::string::npos); @@ -275,14 +318,15 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Msi type with /silent and /log and /custom and /installlocation, switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Msi_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/mysilent") != std::string::npos); // Use declaration in manifest REQUIRE(installerArgs.find("/mylog=\"MyLog.log\"") != std::string::npos); // Use declaration in manifest REQUIRE(installerArgs.find("/mycustom") != std::string::npos); // Use declaration in manifest @@ -291,25 +335,28 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Default Inno type with no args passed in, no switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/SILENT") != std::string::npos); REQUIRE(installerArgs.find("AppInstallerTestExeInstaller.exe.log") != std::string::npos); } { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Inno type with /silent and /log and /custom and /installlocation, no switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_NoSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/VERYSILENT") != std::string::npos); REQUIRE(installerArgs.find("/LOG=\"MyLog.log\"") != std::string::npos); REQUIRE(installerArgs.find("/DIR=\"MyDir\"") != std::string::npos); @@ -317,14 +364,15 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Inno type with /silent and /log and /custom and /installlocation, switches specified in manifest auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs.find("/mysilent") != std::string::npos); // Use declaration in manifest REQUIRE(installerArgs.find("/mylog=\"MyLog.log\"") != std::string::npos); // Use declaration in manifest REQUIRE(installerArgs.find("/mycustom") != std::string::npos); // Use declaration in manifest @@ -333,15 +381,16 @@ TEST_CASE("ShellExecuteHandlerInstallerArgs", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; // Override switch specified. The whole arg passed to installer is overrided. auto manifest = Manifest::CreateFromPath(TestDataFile("InstallerArgTest_Inno_WithSwitches.yaml")); context.Args.AddArg(Execution::Args::Type::Silent); context.Args.AddArg(Execution::Args::Type::Log, "MyLog.log"); context.Args.AddArg(Execution::Args::Type::InstallLocation, "MyDir"); context.Args.AddArg(Execution::Args::Type::Override, "/OverrideEverything"); - ShellExecuteInstallerHandlerTest testhandler(manifest.Installers.at(0), context); - std::string installerArgs = testhandler.TestInstallerArgs(); + context.Add<Data::Installer>(manifest.Installers.at(0)); + context << GetInstallerArgs; + std::string installerArgs = context.Get<Data::InstallerArgs>(); REQUIRE(installerArgs == "/OverrideEverything"); // Use value specified in override switch } } @@ -351,10 +400,13 @@ TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow]") TestCommon::TempFile installResultPath("TestExeInstalled.txt"); std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForOpenSource(context); + OverrideForShellExecute(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnOne"); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify Installer is called and parameters are passed in. @@ -370,10 +422,12 @@ TEST_CASE("InstallFlow_SearchAndInstall", "[InstallFlow]") TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForOpenSource(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnZero"); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify proper message is printed @@ -383,10 +437,12 @@ TEST_CASE("InstallFlow_SearchFoundNoApp", "[InstallFlow]") TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") { std::ostringstream installOutput; - Execution::Context context{ installOutput, std::cin }; + TestContext context{ installOutput, std::cin }; + OverrideForOpenSource(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnTwo"); - InstallFlowTest testFlow(context); - testFlow.Execute(); + + InstallCommand install({}); + install.Execute(context); INFO(installOutput.str()); // Verify proper message is printed @@ -396,31 +452,35 @@ TEST_CASE("InstallFlow_SearchFoundMultipleApp", "[InstallFlow]") TEST_CASE("InstallFlow_SearchAndShowAppInfo", "[ShowFlow]") { std::ostringstream showOutput; - Execution::Context context{ showOutput, std::cin }; + TestContext context{ showOutput, std::cin }; + OverrideForOpenSource(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnOne"); - ShowFlowTest testFlow(context); - testFlow.Execute(); + + ShowCommand show({}); + show.Execute(context); INFO(showOutput.str()); // Verify AppInfo is printed REQUIRE(showOutput.str().find("Id: AppInstallerCliTest.TestInstaller") != std::string::npos); REQUIRE(showOutput.str().find("Name: AppInstaller Test Installer") != std::string::npos); REQUIRE(showOutput.str().find("Version: 1.0.0.0") != std::string::npos); - REQUIRE(showOutput.str().find("--Installer Download Url: https://ThisIsNotUsed") != std::string::npos); + REQUIRE(showOutput.str().find(" Download Url: https://ThisIsNotUsed") != std::string::npos); } TEST_CASE("InstallFlow_SearchAndShowAppVersion", "[ShowFlow]") { std::ostringstream showOutput; - Execution::Context context{ showOutput, std::cin }; + TestContext context{ showOutput, std::cin }; + OverrideForOpenSource(context); context.Args.AddArg(Execution::Args::Type::Query, "TestQueryReturnOne"); context.Args.AddArg(Execution::Args::Type::ListVersions); - ShowFlowTest testFlow(context); - testFlow.Execute(); + + ShowCommand show({}); + show.Execute(context); INFO(showOutput.str()); // Verify App version is printed REQUIRE(showOutput.str().find("1.0.0.0") != std::string::npos); // No manifest info is printed - REQUIRE(showOutput.str().find("--Installer Download Url: https://ThisIsNotUsed") == std::string::npos); + REQUIRE(showOutput.str().find(" Download Url: https://ThisIsNotUsed") == std::string::npos); } \ No newline at end of file diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -123,14 +123,17 @@ namespace AppInstaller::Utility return nonWhitespaceNotFound; } - void FindAndReplace(std::string& inputStr, std::string_view token, std::string_view value) + bool FindAndReplace(std::string& inputStr, std::string_view token, std::string_view value) { + bool result = false; std::string::size_type pos = 0u; while ((pos = inputStr.find(token, pos)) != std::string::npos) { + result = true; inputStr.replace(pos, token.length(), value); pos += value.length(); } + return result; } std::string ReadEntireStream(std::istream& stream) diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -2,9 +2,9 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/AppInstallerTelemetry.h" - #include "Public/AppInstallerLogging.h" #include "Public/AppInstallerRuntime.h" +#include "Public/AppInstallerSHA256.h" #include "Public/AppInstallerStrings.h" #define AICLI_TraceLoggingStringView(_sv_,_name_) TraceLoggingCountedString(_sv_.data(), static_cast<ULONG>(_sv_.size()), _name_) @@ -143,6 +143,42 @@ namespace AppInstaller::Logging AICLI_LOG(CLI, Info, << "Leaf command succeeded: " << commandName); } + void TelemetryTraceLogger::LogCommandTermination(HRESULT hr, std::string_view file, size_t line) noexcept + { + if (g_IsTelemetryProviderEnabled) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "CommandTermination", + GetActivityId(), + nullptr, + TraceLoggingHResult(hr, "hr"), + AICLI_TraceLoggingStringView(file, "File"), + TraceLoggingUInt64(static_cast<UINT64>(line), "Line"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << "Terminating context: 0x" << std::hex << std::setw(8) << std::setfill('0') << hr << " at " << file << ":" << line); + } + + void TelemetryTraceLogger::LogException(std::string_view commandName, std::string_view type, std::string_view message) noexcept + { + if (g_IsTelemetryProviderEnabled) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "Exception", + GetActivityId(), + nullptr, + AICLI_TraceLoggingStringView(commandName, "Command"), + AICLI_TraceLoggingStringView(type, "Type"), + AICLI_TraceLoggingStringView(message, "Message"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << "Caught " << type << ": " << message); + } + void TelemetryTraceLogger::LogManifestFields(std::string_view id, std::string_view name, std::string_view version) noexcept { if (g_IsTelemetryProviderEnabled) @@ -276,6 +312,51 @@ namespace AppInstaller::Logging } } + void TelemetryTraceLogger::LogInstallerHashMismatch(std::string_view id, std::string_view version, std::string_view channel, const std::vector<uint8_t>& expected, const std::vector<uint8_t>& actual) + { + if (g_IsTelemetryProviderEnabled) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "HashMismatch", + GetActivityId(), + nullptr, + AICLI_TraceLoggingStringView(id, "Id"), + AICLI_TraceLoggingStringView(version, "Version"), + AICLI_TraceLoggingStringView(channel, "Channel"), + TraceLoggingBinary(expected.data(), static_cast<ULONG>(expected.size()), "Expected"), + TraceLoggingBinary(actual.data(), static_cast<ULONG>(actual.size()), "Actual"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, + << "Package hash verification failed. SHA256 in manifest [" + << Utility::SHA256::ConvertToString(expected) + << "] does not match download [" + << Utility::SHA256::ConvertToString(actual) + << ']'); + } + + void TelemetryTraceLogger::LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) + { + if (g_IsTelemetryProviderEnabled) + { + TraceLoggingWriteActivity(g_hTelemetryProvider, + "InstallerFailure", + GetActivityId(), + nullptr, + AICLI_TraceLoggingStringView(id, "Id"), + AICLI_TraceLoggingStringView(version, "Version"), + AICLI_TraceLoggingStringView(channel, "Channel"), + AICLI_TraceLoggingStringView(type, "Type"), + TraceLoggingUInt32(errorCode, "ErrorCode"), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance | PDT_ProductAndServiceUsage), + TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); + } + + AICLI_LOG(CLI, Error, << type << " installer failed: " << errorCode); + } + void EnableWilFailureTelemetry() { wil::SetResultLoggingCallback(wilResultLoggingCallback); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerErrors.h b/src/AppInstallerCommonCore/Public/AppInstallerErrors.h @@ -10,7 +10,7 @@ #define APPINSTALLER_CLI_ERROR_COMMAND_FAILED ((HRESULT)0x8A150003) #define APPINSTALLER_CLI_ERROR_MANIFEST_FAILED ((HRESULT)0x8A150004) #define APPINSTALLER_CLI_ERROR_WORKFLOW_FAILED ((HRESULT)0x8A150005) -#define APPINSTALLER_CLI_ERROR_INSTALLFLOW_FAILED ((HRESULT)0x8A150006) +#define APPINSTALLER_CLI_ERROR_SHELLEXEC_INSTALL_FAILED ((HRESULT)0x8A150006) #define APPINSTALLER_CLI_ERROR_RUNTIME_ERROR ((HRESULT)0x8A150007) #define APPINSTALLER_CLI_ERROR_DOWNLOAD_FAILED ((HRESULT)0x8A150008) #define APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX ((HRESULT)0x8A150009) @@ -20,3 +20,11 @@ #define APPINSTALLER_CLI_ERROR_INVALID_SOURCE_TYPE ((HRESULT)0x8A15000D) #define APPINSTALLER_CLI_ERROR_PACKAGE_IS_BUNDLE ((HRESULT)0x8A15000E) #define APPINSTALLER_CLI_ERROR_SOURCE_DATA_MISSING ((HRESULT)0x8A15000F) +#define APPINSTALLER_CLI_ERROR_NO_APPLICABLE_INSTALLER ((HRESULT)0x8A150010) +#define APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH ((HRESULT)0x8A150011) +#define APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST ((HRESULT)0x8A150012) +#define APPINSTALLER_CLI_ERROR_SOURCE_ARG_ALREADY_EXISTS ((HRESULT)0x8A150013) +#define APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND ((HRESULT)0x8A150014) +#define APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED ((HRESULT)0x8A150015) +#define APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND ((HRESULT)0x8A150016) +#define APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND ((HRESULT)0x8A150017) diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -83,7 +83,8 @@ namespace AppInstaller::Utility bool IsEmptyOrWhitespace(std::wstring_view str); // Find token in the input string and replace with value. - void FindAndReplace(std::string& inputStr, std::string_view token, std::string_view value); + // Returns a value indicating whether a replacement occurred. + bool FindAndReplace(std::string& inputStr, std::string_view token, std::string_view value); // Reads the entire stream into a string. std::string ReadEntireStream(std::istream& stream); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -4,6 +4,7 @@ #include <wil/result_macros.h> #include <string_view> +#include <vector> namespace AppInstaller::Logging { @@ -36,6 +37,12 @@ namespace AppInstaller::Logging // Logs the invoked command success. void LogCommandSuccess(std::string_view commandName) noexcept; + // Logs the invoked command termination. + void LogCommandTermination(HRESULT hr, std::string_view file, size_t line) noexcept; + + // Logs the invoked command termination. + void LogException(std::string_view commandName, std::string_view type, std::string_view message) noexcept; + // Logs the Manifest fields. void LogManifestFields(std::string_view id, std::string_view name, std::string_view version) noexcept; @@ -65,6 +72,12 @@ namespace AppInstaller::Logging // Logs the Search Result void LogSearchResultCount(uint64_t resultCount) noexcept; + // Logs a mismatch between the expected and actual hash values. + void LogInstallerHashMismatch(std::string_view id, std::string_view version, std::string_view channel, const std::vector<uint8_t>& expected, const std::vector<uint8_t>& actual); + + // Logs a faild installation attempt. + void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode); + private: TelemetryTraceLogger(); }; diff --git a/src/AppInstallerCommonCore/Versions.cpp b/src/AppInstallerCommonCore/Versions.cpp @@ -110,8 +110,9 @@ namespace AppInstaller::Utility result = m_version.ToString(); if (!m_channel.ToString().empty()) { - result += ", "; + result += '['; result += m_channel.ToString(); + result += ']'; } return result; } diff --git a/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h b/src/AppInstallerRepositoryCore/Public/AppInstallerRepositorySource.h @@ -6,6 +6,7 @@ #include <chrono> #include <memory> +#include <optional> #include <string> #include <string_view> #include <vector> @@ -47,6 +48,9 @@ namespace AppInstaller::Repository // Gets the details for all sources. std::vector<SourceDetails> GetSources(); + // Gets the details for a single source. + std::optional<SourceDetails> GetSource(std::string_view name); + // Adds a new source for the user. void AddSource(std::string name, std::string type, std::string arg, IProgressCallback& progress); diff --git a/src/AppInstallerRepositoryCore/RepositorySource.cpp b/src/AppInstallerRepositoryCore/RepositorySource.cpp @@ -255,6 +255,22 @@ namespace AppInstaller::Repository return GetSourcesFromSetting(s_RepositorySettings_UserSources); } + std::optional<SourceDetails> GetSource(std::string_view name) + { + // Check all sources for the given name. + std::vector<SourceDetails> currentSources = GetSources(); + + auto itr = FindSourceByName(currentSources, name); + if (itr == currentSources.end()) + { + return {}; + } + else + { + return *itr; + } + } + void AddSource(std::string name, std::string type, std::string arg, IProgressCallback& progress) { THROW_HR_IF(E_INVALIDARG, name.empty());