diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index 4641d99359..48e72bf84c 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#include "pch.h" #include "WorkflowBase.h" #include "ExecutionContext.h" #include "PackageTableSortHelper.h" @@ -8,17 +7,18 @@ #include "ShowFlow.h" #include "Sixel.h" #include "TableOutput.h" -#include -#include -#include +#include "pch.h" +#include #include +#include +#include +#include #include +#include #include #include -#include #include -#include -#include +#include EXTERN_C IMAGE_DOS_HEADER __ImageBase; @@ -29,1828 +29,1872 @@ using namespace AppInstaller::Repository; using namespace AppInstaller::Settings; using namespace winrt::Windows::Foundation; -namespace AppInstaller::CLI::Workflow -{ - namespace - { - std::string GetMatchCriteriaDescriptor(const ResultMatch& match) - { - if (match.MatchCriteria.Field != PackageMatchField::Id && match.MatchCriteria.Field != PackageMatchField::Name) - { - std::string result{ ToString(match.MatchCriteria.Field) }; - result += ": "; - result += match.MatchCriteria.Value; - return result; - } - else - { - return {}; - } - } +namespace AppInstaller::CLI::Workflow { +namespace { +std::string GetMatchCriteriaDescriptor(const ResultMatch &match) { + if (match.MatchCriteria.Field != PackageMatchField::Id && + match.MatchCriteria.Field != PackageMatchField::Name) { + std::string result{ToString(match.MatchCriteria.Field)}; + result += ": "; + result += match.MatchCriteria.Value; + return result; + } else { + return {}; + } +} - void ReportIdentity( - Execution::Context& context, - Utility::LocIndView prefix, - std::optional label, - std::string_view name, - std::string_view id, - std::string_view version = {}, - Execution::Reporter::Level level = Execution::Reporter::Level::Info) - { - auto out = context.Reporter.GetOutputStream(level); - out << prefix; - if (label) - { - out << *label << ' '; - } - out << Execution::NameEmphasis << name << " ["_liv << Execution::IdEmphasis << id << ']'; - - if (!version.empty()) - { - out << ' ' << Resource::String::ShowVersion << ' ' << version; - } - - out << std::endl; - } +void ReportIdentity( + Execution::Context &context, Utility::LocIndView prefix, + std::optional label, std::string_view name, + std::string_view id, std::string_view version = {}, + Execution::Reporter::Level level = Execution::Reporter::Level::Info) { + auto out = context.Reporter.GetOutputStream(level); + out << prefix; + if (label) { + out << *label << ' '; + } + out << Execution::NameEmphasis << name << " ["_liv << Execution::IdEmphasis + << id << ']'; + + if (!version.empty()) { + out << ' ' << Resource::String::ShowVersion << ' ' << version; + } + + out << std::endl; +} - bool IsSecondIconResolutionBetter(Manifest::IconResolutionEnum current, Manifest::IconResolutionEnum alternative) - { - static constexpr std::array s_iconResolutionOrder - { - 9, // Unknown - 8, // Custom - 15, // Square16 - 14, // Square20 - 13, // Square24 - 12, // Square30 - 11, // Square32 - 10, // Square36 - 6, // Square40 - 5, // Square48 - 4, // Square60 - 3, // Square64 - 2, // Square72 - 0, // Square80 - 1, // Square96 - 7, // Square256 - }; - - return s_iconResolutionOrder.at(ToIntegral(alternative)) < s_iconResolutionOrder.at(ToIntegral(current)); - } +bool IsSecondIconResolutionBetter(Manifest::IconResolutionEnum current, + Manifest::IconResolutionEnum alternative) { + static constexpr std::array< + uint8_t, ToIntegral(Manifest::IconResolutionEnum::Square256) + 1> + s_iconResolutionOrder{ + 9, // Unknown + 8, // Custom + 15, // Square16 + 14, // Square20 + 13, // Square24 + 12, // Square30 + 11, // Square32 + 10, // Square36 + 6, // Square40 + 5, // Square48 + 4, // Square60 + 3, // Square64 + 2, // Square72 + 0, // Square80 + 1, // Square96 + 7, // Square256 + }; + + return s_iconResolutionOrder.at(ToIntegral(alternative)) < + s_iconResolutionOrder.at(ToIntegral(current)); +} - // Determines icon fit given two options. - // Targets an 80x80 icon as the best resolution for this use case. - // TODO: Consider theme based on current background color. - bool IsSecondIconBetter(const Manifest::Icon& current, const Manifest::Icon& alternative) - { - return IsSecondIconResolutionBetter(current.Resolution, alternative.Resolution); - } +// Determines icon fit given two options. +// Targets an 80x80 icon as the best resolution for this use case. +// TODO: Consider theme based on current background color. +bool IsSecondIconBetter(const Manifest::Icon ¤t, + const Manifest::Icon &alternative) { + return IsSecondIconResolutionBetter(current.Resolution, + alternative.Resolution); +} - bool IsSecondIconBetter(const ExtractedIconInfo& current, const ExtractedIconInfo& alternative) - { - return IsSecondIconResolutionBetter(current.IconResolution, alternative.IconResolution); - } +bool IsSecondIconBetter(const ExtractedIconInfo ¤t, + const ExtractedIconInfo &alternative) { + return IsSecondIconResolutionBetter(current.IconResolution, + alternative.IconResolution); +} - void ShowIcon(Execution::OutputStream& outputStream, VirtualTerminal::Sixel::Image& icon) - { - // Using a height of 4 arbitrarily; allow width up to the entire console. - UINT imageHeightCells = 4; - UINT imageWidthCells = static_cast(Execution::GetConsoleWidth().value_or(120)); +void ShowIcon(Execution::OutputStream &outputStream, + VirtualTerminal::Sixel::Image &icon) { + // Using a height of 4 arbitrarily; allow width up to the entire console. + UINT imageHeightCells = 4; + UINT imageWidthCells = + static_cast(Execution::GetConsoleWidth().value_or(120)); - icon.RenderSizeInCells(imageWidthCells, imageHeightCells); - icon.RenderTo(outputStream); + icon.RenderSizeInCells(imageWidthCells, imageHeightCells); + icon.RenderTo(outputStream); - // Force the final sixel line to not be overwritten - outputStream << std::endl; - } + // Force the final sixel line to not be overwritten + outputStream << std::endl; +} - void ShowManifestIcon(Execution::Context& context, const Manifest::Manifest& manifest) try - { - if (!context.Reporter.SixelsEnabled()) - { - return; - } - - auto icons = manifest.CurrentLocalization.Get(); - const Manifest::Icon* bestFitIcon = nullptr; - - for (const auto& icon : icons) - { - if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) - { - bestFitIcon = &icon; - } - } - - if (!bestFitIcon) - { - return; - } - - // Use a cache to hold the icons - auto splitUri = Utility::SplitFileNameFromURI(bestFitIcon->Url); - Caching::FileCache fileCache{ Caching::FileCache::Type::Icon, Utility::SHA256::ConvertToString(bestFitIcon->Sha256), { splitUri.first } }; - auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); - - VirtualTerminal::Sixel::Image sixelIcon{ *iconStream, bestFitIcon->FileType }; - auto infoOut = context.Reporter.Info(); - - ShowIcon(infoOut, sixelIcon); - } - CATCH_LOG(); - - void ShowExtractedIcon(Execution::OutputStream& outputStream, const std::vector& icons) try - { - const ExtractedIconInfo* bestFitIcon = nullptr; - - for (const auto& icon : icons) - { - if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) - { - bestFitIcon = &icon; - } - } - - if (!bestFitIcon) - { - return; - } - - VirtualTerminal::Sixel::Image sixelIcon{ bestFitIcon->IconContent, bestFitIcon->IconFileType }; - ShowIcon(outputStream, sixelIcon); - } - CATCH_LOG(); - - Repository::Source OpenNamedSource(Execution::Context& context, Utility::LocIndView sourceName) - { - Repository::Source source; - - try - { - source = Source{ sourceName }; - - if (!source) - { - std::vector sources = Source::GetCurrentSources(); - - if (!sourceName.empty() && !sources.empty()) - { - // A bad name was given, try to help. - context.Reporter.Error() << Resource::String::OpenSourceFailedNoMatch(sourceName) << std::endl; - context.Reporter.Info() << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; - for (const auto& details : sources) - { - context.Reporter.Info() << " "_liv << details.Name << std::endl; - } - - AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, {}); - } - else - { - // Even if a name was given, there are no sources - context.Reporter.Error() << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; - AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED, {}); - } - } - - if (context.Args.Contains(Execution::Args::Type::CustomHeader)) - { - std::string customHeader{ context.Args.GetArg(Execution::Args::Type::CustomHeader) }; - if (!source.SetCustomHeader(customHeader)) - { - context.Reporter.Warn() << Resource::String::HeaderArgumentNotApplicableForNonRestSourceWarning << std::endl; - } - } - - auto openFunction = [&](IProgressCallback& progress)->std::vector - { - source.SetCaller("winget-cli"); - source.SetAuthenticationArguments(GetAuthenticationArguments(context)); - source.SetThreadGlobals(context.GetSharedThreadGlobals()); - return source.Open(progress); - }; - auto updateFailures = context.Reporter.ExecuteWithProgress(openFunction, true); - - // We'll only report the source update failure as warning and continue - for (const auto& s : updateFailures) - { - context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate(Utility::LocIndView{ s.Name }) << std::endl; - } - - // Report sources that may need authentication - if (source.IsComposite()) - { - for (const auto& s : source.GetAvailableSources()) - { - if (s.GetInformation().Authentication.Type != Authentication::AuthenticationType::None) - { - context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::SourceRequiresAuthentication(Utility::LocIndView{ s.GetDetails().Name }) << std::endl; - } - } - } - else if (source.GetInformation().Authentication.Type != Authentication::AuthenticationType::None) - { - context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::SourceRequiresAuthentication(Utility::LocIndView{ source.GetDetails().Name }) << std::endl; - } - } - catch (const wil::ResultException& re) - { - context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; - if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_FAILED_TO_OPEN_ALL_SOURCES) - { - // Since we know there must have been multiple errors here, just fail the context rather - // than trying to get one of the exceptions back out. - AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_FAILED_TO_OPEN_ALL_SOURCES, {}); - } - else - { - throw; - } - } - catch (...) - { - context.Reporter.Error() << Resource::String::SourceOpenFailedSuggestion << std::endl; - throw; - } - - return source; - } +void ShowManifestIcon(Execution::Context &context, + const Manifest::Manifest &manifest) try { + if (!context.Reporter.SixelsEnabled()) { + return; + } - void SearchSourceApplyFilters(Execution::Context& context, SearchRequest& searchRequest, MatchType matchType) - { - const auto& args = context.Args; - - if (args.Contains(Execution::Args::Type::Id)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, args.GetArg(Execution::Args::Type::Id))); - } - - if (args.Contains(Execution::Args::Type::Name)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, matchType, args.GetArg(Execution::Args::Type::Name))); - } - - if (args.Contains(Execution::Args::Type::Moniker)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, args.GetArg(Execution::Args::Type::Moniker))); - } - - if (args.Contains(Execution::Args::Type::ProductCode)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, matchType, args.GetArg(Execution::Args::Type::ProductCode))); - } - - if (args.Contains(Execution::Args::Type::Tag)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Tag, matchType, args.GetArg(Execution::Args::Type::Tag))); - } - - if (args.Contains(Execution::Args::Type::Command)) - { - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Command, matchType, args.GetArg(Execution::Args::Type::Command))); - } - - if (args.Contains(Execution::Args::Type::Count)) - { - searchRequest.MaximumResults = std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); - } - } + auto icons = + manifest.CurrentLocalization.Get(); + const Manifest::Icon *bestFitIcon = nullptr; - // Data shown on a line of a table displaying installed packages - struct InstalledPackagesTableLine - { - InstalledPackagesTableLine( - std::shared_ptr package, - std::shared_ptr installedVersion, - const Utility::LocIndString& availableVersion, - const Utility::LocIndString& source) - : Package(std::move(package)), InstalledPackageVersion(std::move(installedVersion)), AvailableVersion(availableVersion), Source(source) - { - Name = InstalledPackageVersion->GetProperty(PackageVersionProperty::Name); - Id = Package->GetProperty(PackageProperty::Id); - InstalledVersion = InstalledPackageVersion->GetProperty(PackageVersionProperty::Version); - } - - std::shared_ptr Package; - std::shared_ptr InstalledPackageVersion; - - Utility::LocIndString Name; - Utility::LocIndString Id; - Utility::LocIndString InstalledVersion; - Utility::LocIndString AvailableVersion; - Utility::LocIndString Source; - }; - - void OutputInstalledPackagesTable(Execution::Context& context, std::vector& lines) - { - Execution::TableOutput<5> table(context.Reporter, - { - Resource::String::SearchName, - Resource::String::SearchId, - Resource::String::SearchVersion, - Resource::String::AvailableHeader, - Resource::String::SearchSource - }); - - for (auto& line : lines) - { - table.OutputLine({ - line.Name, - line.Id, - line.InstalledVersion, - line.AvailableVersion, - line.Source - }); - } - - table.Complete(); - } + for (const auto &icon : icons) { + if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) { + bestFitIcon = &icon; + } + } - void ShowMetadataField( - Execution::OutputStream& outputStream, - StringResource::StringId label, - const IPackageVersion::Metadata& metadata, - PackageVersionMetadata field) - { - auto itr = metadata.find(field); - if (itr != metadata.end()) - { - ShowSingleLineField(outputStream, label, Utility::LocIndView{ itr->second }); - } - } + if (!bestFitIcon) { + return; + } - // Outputs every package "line" with many details, with a format similar to the `show` command. - void OutputInstalledPackagesDetails(Execution::Context& context, std::vector& lines) - { - auto info = context.Reporter.Info(); - size_t packageIndex = 0; - size_t totalLines = lines.size(); - bool shouldGetIcon = context.Reporter.SixelsEnabled(); - for (const auto& line : lines) - { - // Identity header including package count indicator if multiple lines provided - if (totalLines > 1) - { - info << '(' << ++packageIndex << '/' << totalLines << ") "_liv; - } - - ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); - - auto metadata = line.InstalledPackageVersion->GetMetadata(); - auto productCodes = line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::ProductCode); - - if (shouldGetIcon && !productCodes.empty()) - { - Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; - - auto itr = metadata.find(PackageVersionMetadata::InstalledScope); - if (itr != metadata.end()) - { - scope = Manifest::ConvertToScopeEnum(itr->second); - } - - auto icons = ExtractIconFromArpEntry(productCodes[0], scope); - ShowExtractedIcon(info, icons); - } - - ShowSingleLineField(info, Resource::String::ShowLabelVersion, line.InstalledVersion); - ShowSingleLineField(info, Resource::String::ShowLabelChannel, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Channel)); - ShowSingleLineField(info, Resource::String::ShowLabelPublisher, line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Publisher)); - auto localIdentifier = line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Id); - if (line.Id != localIdentifier) - { - ShowSingleLineField(info, Resource::String::ShowListLocalIdentifier, localIdentifier); - } - - ShowMultiValueField(info, Resource::String::ShowListPackageFamilyName, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::PackageFamilyName)); - ShowMultiValueField(info, Resource::String::ShowListProductCode, productCodes); - ShowMultiValueField(info, Resource::String::ShowListUpgradeCode, line.InstalledPackageVersion->GetMultiProperty(PackageVersionMultiProperty::UpgradeCode)); - - ShowMetadataField(info, Resource::String::ShowListInstallerCategory, metadata, PackageVersionMetadata::InstalledType); - ShowMetadataField(info, Resource::String::ShowListInstalledScope, metadata, PackageVersionMetadata::InstalledScope); - ShowMetadataField(info, Resource::String::ShowListInstalledArchitecture, metadata, PackageVersionMetadata::InstalledArchitecture); - ShowMetadataField(info, Resource::String::ShowListInstalledLocale, metadata, PackageVersionMetadata::InstalledLocale); - ShowMetadataField(info, Resource::String::ShowListInstalledLocation, metadata, PackageVersionMetadata::InstalledLocation); - - auto source = line.InstalledPackageVersion->GetSource(); - if (source.ContainsAvailablePackages()) - { - ShowSingleLineField(info, Resource::String::ShowListInstalledSource, Utility::LocIndView{ source.GetDetails().Name }); - } - - Utility::Version currentVersion{ line.InstalledVersion }; - bool hasUpgradeVersion = false; - for (const auto& available : line.Package->GetAvailable()) - { - auto latestAvailable = available->GetLatestVersion(); - auto availableVersion = latestAvailable->GetProperty(PackageVersionProperty::Version); - if (Utility::Version{ availableVersion } > currentVersion) - { - if (!hasUpgradeVersion) - { - hasUpgradeVersion = true; - info << details::GetIndentFor(0) << Execution::ManifestInfoEmphasis << Resource::String::ShowListAvailableUpgrades << '\n'; - } - - info << details::GetIndentFor(1) << Utility::LocIndView{ available->GetSource().GetDetails().Name } << - " ["_liv << availableVersion << "]\n"_liv; - } - } - - // FUTURE: We could also pull data from the tracking database to show some things that we store there specifically. - } - } + // Use a cache to hold the icons + auto splitUri = Utility::SplitFileNameFromURI(bestFitIcon->Url); + Caching::FileCache fileCache{ + Caching::FileCache::Type::Icon, + Utility::SHA256::ConvertToString(bestFitIcon->Sha256), + {splitUri.first}}; + auto iconStream = fileCache.GetFile(splitUri.second, bestFitIcon->Sha256); - // Sorts a vector of InstalledPackagesTableLine according to the user's sort preferences. - // Resolution order: CLI args (--sort) > settings (output.sortOrder) > query-aware default. - void SortInstalledPackagesTableLines(Execution::Context& context, std::vector& lines) - { - if (lines.size() <= 1) - { - return; - } - - const SortParameters params(context); - - // Not strictly required — SortBy handles this internally — but avoids - // constructing the SortablePackageEntry vector when no sorting is needed. - if (!params.ShouldSort) - { - return; - } - - const SortField mask = ComputeSortFieldMask(params.Fields); - SortBy(lines, - [mask](const InstalledPackagesTableLine& line, size_t index) { - return SortablePackageEntry( - index, - line.Name.get(), - line.Id.get(), - line.InstalledVersion.get(), - line.AvailableVersion.get(), - line.Source.get(), - mask); - }, - params); - } + VirtualTerminal::Sixel::Image sixelIcon{*iconStream, bestFitIcon->FileType}; + auto infoOut = context.Reporter.Info(); - void OutputInstalledPackages(Execution::Context& context, std::vector& lines) - { - SortInstalledPackagesTableLines(context, lines); - - if (context.Args.Contains(Execution::Args::Type::ListDetails)) - { - OutputInstalledPackagesDetails(context, lines); - } - else - { - OutputInstalledPackagesTable(context, lines); - } - } - } + ShowIcon(infoOut, sixelIcon); +} +CATCH_LOG(); - bool WorkflowTask::operator==(const WorkflowTask& other) const - { - 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 - { - return false; - } - } +void ShowExtractedIcon(Execution::OutputStream &outputStream, + const std::vector &icons) try { + const ExtractedIconInfo *bestFitIcon = nullptr; - void WorkflowTask::operator()(Execution::Context& context) const - { - THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_isFunc); - m_func(context); + for (const auto &icon : icons) { + if (!bestFitIcon || IsSecondIconBetter(*bestFitIcon, icon)) { + bestFitIcon = &icon; } + } - void WorkflowTask::Log() const - { - if (m_isFunc) - { - // Using `00000001`80000000` as base address default when loading dll into windbg as dump file. - AICLI_LOG(Workflow, Verbose, << "Running task: 0x" << m_func << " [ln 00000001`80000000+" << std::hex << (reinterpret_cast(m_func) - reinterpret_cast(&__ImageBase)) << "]"); - } - else - { - AICLI_LOG(Workflow, Verbose, << "Running task: " << m_name); - } - } + if (!bestFitIcon) { + return; + } - Repository::PredefinedSource DetermineInstalledSource(const Execution::Context& context) - { - Repository::PredefinedSource installedSource = Repository::PredefinedSource::Installed; - Manifest::ScopeEnum scope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - if (scope == Manifest::ScopeEnum::Machine) - { - installedSource = Repository::PredefinedSource::InstalledMachine; - } - else if (scope == Manifest::ScopeEnum::User) - { - installedSource = Repository::PredefinedSource::InstalledUser; - } + VirtualTerminal::Sixel::Image sixelIcon{bestFitIcon->IconContent, + bestFitIcon->IconFileType}; + ShowIcon(outputStream, sixelIcon); +} +CATCH_LOG(); + +Repository::Source OpenNamedSource(Execution::Context &context, + Utility::LocIndView sourceName) { + Repository::Source source; + + try { + source = Source{sourceName}; + + if (!source) { + std::vector sources = Source::GetCurrentSources(); + + if (!sourceName.empty() && !sources.empty()) { + // A bad name was given, try to help. + context.Reporter.Error() + << Resource::String::OpenSourceFailedNoMatch(sourceName) + << std::endl; + context.Reporter.Info() + << Resource::String::OpenSourceFailedNoMatchHelp << std::endl; + for (const auto &details : sources) { + context.Reporter.Info() << " "_liv << details.Name << std::endl; + } + + AICLI_TERMINATE_CONTEXT_RETURN( + APPINSTALLER_CLI_ERROR_SOURCE_NAME_DOES_NOT_EXIST, {}); + } else { + // Even if a name was given, there are no sources + context.Reporter.Error() + << Resource::String::OpenSourceFailedNoSourceDefined << std::endl; + AICLI_TERMINATE_CONTEXT_RETURN( + APPINSTALLER_CLI_ERROR_NO_SOURCES_DEFINED, {}); + } + } - return installedSource; + if (context.Args.Contains(Execution::Args::Type::CustomHeader)) { + std::string customHeader{ + context.Args.GetArg(Execution::Args::Type::CustomHeader)}; + if (!source.SetCustomHeader(customHeader)) { + context.Reporter.Warn() + << Resource::String:: + HeaderArgumentNotApplicableForNonRestSourceWarning + << std::endl; + } } - Authentication::AuthenticationArguments GetAuthenticationArguments(const Execution::Context& context) - { - AppInstaller::Authentication::AuthenticationArguments authArgs; + auto openFunction = [&](IProgressCallback &progress) + -> std::vector { + source.SetCaller("winget-cli"); + source.SetAuthenticationArguments(GetAuthenticationArguments(context)); + source.SetThreadGlobals(context.GetSharedThreadGlobals()); + return source.Open(progress); + }; + auto updateFailures = + context.Reporter.ExecuteWithProgress(openFunction, true); + + // We'll only report the source update failure as warning and continue + for (const auto &s : updateFailures) { + context.Reporter.Warn() << Resource::String::SourceOpenWithFailedUpdate( + Utility::LocIndView{s.Name}) + << std::endl; + } - if (context.Args.Contains(Execution::Args::Type::AuthenticationMode)) - { - authArgs.Mode = Authentication::ConvertToAuthenticationMode(context.Args.GetArg(Execution::Args::Type::AuthenticationMode)); - } - else - { - // If user did not specify authentication mode, determine based on if disable interactivity flag exists. - authArgs.Mode = context.Args.Contains(Execution::Args::Type::DisableInteractivity) ? Authentication::AuthenticationMode::Silent : Authentication::AuthenticationMode::SilentPreferred; - } + // Report sources that may need authentication + if (source.IsComposite()) { + for (const auto &s : source.GetAvailableSources()) { + if (s.GetInformation().Authentication.Type != + Authentication::AuthenticationType::None) { + context.Reporter.Info() + << Execution::AuthenticationEmphasis + << Resource::String::SourceRequiresAuthentication( + Utility::LocIndView{s.GetDetails().Name}) + << std::endl; + } + } + } else if (source.GetInformation().Authentication.Type != + Authentication::AuthenticationType::None) { + context.Reporter.Info() + << Execution::AuthenticationEmphasis + << Resource::String::SourceRequiresAuthentication( + Utility::LocIndView{source.GetDetails().Name}) + << std::endl; + } + } catch (const wil::ResultException &re) { + context.Reporter.Error() + << Resource::String::SourceOpenFailedSuggestion << std::endl; + if (re.GetErrorCode() == + APPINSTALLER_CLI_ERROR_FAILED_TO_OPEN_ALL_SOURCES) { + // Since we know there must have been multiple errors here, just fail the + // context rather than trying to get one of the exceptions back out. + AICLI_TERMINATE_CONTEXT_RETURN( + APPINSTALLER_CLI_ERROR_FAILED_TO_OPEN_ALL_SOURCES, {}); + } else { + throw; + } + } catch (...) { + context.Reporter.Error() + << Resource::String::SourceOpenFailedSuggestion << std::endl; + throw; + } - if (context.Args.Contains(Execution::Args::Type::AuthenticationAccount)) - { - authArgs.AuthenticationAccount = context.Args.GetArg(Execution::Args::Type::AuthenticationAccount); - } + return source; +} - AICLI_LOG(CLI, Info, << "Created authentication arguments. Mode: " << Authentication::AuthenticationModeToString(authArgs.Mode) << ", Account: " << authArgs.AuthenticationAccount); +void SearchSourceApplyFilters(Execution::Context &context, + SearchRequest &searchRequest, + MatchType matchType) { + const auto &args = context.Args; + + if (args.Contains(Execution::Args::Type::Id)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::Id, matchType, + args.GetArg(Execution::Args::Type::Id))); + } + + if (args.Contains(Execution::Args::Type::Name)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::Name, matchType, + args.GetArg(Execution::Args::Type::Name))); + } + + if (args.Contains(Execution::Args::Type::Moniker)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::Moniker, matchType, + args.GetArg(Execution::Args::Type::Moniker))); + } + + if (args.Contains(Execution::Args::Type::ProductCode)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::ProductCode, matchType, + args.GetArg(Execution::Args::Type::ProductCode))); + } + + if (args.Contains(Execution::Args::Type::Tag)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::Tag, matchType, + args.GetArg(Execution::Args::Type::Tag))); + } + + if (args.Contains(Execution::Args::Type::Command)) { + searchRequest.Filters.emplace_back( + PackageMatchFilter(PackageMatchField::Command, matchType, + args.GetArg(Execution::Args::Type::Command))); + } + + if (args.Contains(Execution::Args::Type::Count)) { + searchRequest.MaximumResults = + std::stoi(std::string(args.GetArg(Execution::Args::Type::Count))); + } +} +// Splits a version string on '.' and '-' into segments. +// E.g. "1.2.3-beta" -> {"1", "2", "3", "beta"} +std::vector SplitVersionSegments(std::string_view version) { + std::vector segments; + size_t start = 0; + for (size_t i = 0; i <= version.size(); ++i) { + if (i == version.size() || version[i] == '.' || version[i] == '-') { + segments.push_back(version.substr(start, i - start)); + start = i + 1; + } + } + return segments; +} - return authArgs; +// Returns the char offset where oldVer and newVer first diverge, on a segment +// boundary. +size_t FindVersionDiffOffset(std::string_view oldVer, std::string_view newVer) { + auto oldSegs = SplitVersionSegments(oldVer); + auto newSegs = SplitVersionSegments(newVer); + size_t offset = 0; + size_t count = std::min(oldSegs.size(), newSegs.size()); + for (size_t i = 0; i < count; ++i) { + if (oldSegs[i] != newSegs[i]) { + break; } + offset += oldSegs[i].size() + 1; + } + return std::min(offset, std::min(oldVer.size(), newVer.size())); +} - HRESULT HandleException(Execution::Context* context, std::exception_ptr exception) - { - try - { - std::rethrow_exception(exception); - } - // 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(Logging::FailureTypeEnum::ResultException, re.what()); - if (context) - { - context->Reporter.Error() << - Resource::String::UnexpectedErrorExecutingCommand << ' ' << std::endl << - GetUserPresentableMessage(re) << std::endl; - } - return re.GetErrorCode(); - } - catch (const winrt::hresult_error& hre) - { - std::string message = GetUserPresentableMessage(hre); - Logging::Telemetry().LogException(Logging::FailureTypeEnum::WinrtHResultError, message); - if (context) - { - context->Reporter.Error() << - Resource::String::UnexpectedErrorExecutingCommand << ' ' << std::endl << - message << std::endl; - } - return hre.code(); - } - catch (const Settings::GroupPolicyException& e) - { - if (context) - { - auto policy = Settings::TogglePolicy::GetPolicy(e.Policy()); - auto policyNameId = policy.PolicyName(); - context->Reporter.Error() << Resource::String::DisabledByGroupPolicy(policyNameId) << std::endl; - } - return APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY; - } - catch (const std::exception& e) - { - Logging::Telemetry().LogException(Logging::FailureTypeEnum::StdException, e.what()); - if (context) - { - context->Reporter.Error() << - Resource::String::UnexpectedErrorExecutingCommand << ' ' << std::endl << - GetUserPresentableMessage(e) << std::endl; - } - return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - Logging::Telemetry().LogException(Logging::FailureTypeEnum::Unknown, {}); - if (context) - { - context->Reporter.Error() << - Resource::String::UnexpectedErrorExecutingCommand << " ???"_liv << std::endl; - } - return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; - } +// Builds a colorized LocIndString: plain prefix + colored suffix + reset. +Utility::LocIndString +ColorizeVersionDiff(std::string_view version, size_t commonPrefixLen, + const VirtualTerminal::Sequence &diffColor) { + std::string result; + result += version.substr(0, commonPrefixLen); + result += diffColor.Get(); + result += version.substr(commonPrefixLen); + result += VirtualTerminal::TextFormat::Default.Get(); + return Utility::LocIndString{result}; +} - return E_UNEXPECTED; +// Data shown on a line of a table displaying installed packages +struct InstalledPackagesTableLine { + InstalledPackagesTableLine(std::shared_ptr package, + std::shared_ptr installedVersion, + const Utility::LocIndString &availableVersion, + const Utility::LocIndString &source) + : Package(std::move(package)), + InstalledPackageVersion(std::move(installedVersion)), + AvailableVersion(availableVersion), Source(source) { + Name = InstalledPackageVersion->GetProperty(PackageVersionProperty::Name); + Id = Package->GetProperty(PackageProperty::Id); + InstalledVersion = + InstalledPackageVersion->GetProperty(PackageVersionProperty::Version); + } + + std::shared_ptr Package; + std::shared_ptr InstalledPackageVersion; + + Utility::LocIndString Name; + Utility::LocIndString Id; + Utility::LocIndString InstalledVersion; + Utility::LocIndString AvailableVersion; + Utility::LocIndString Source; +}; + +void OutputInstalledPackagesTable( + Execution::Context &context, + std::vector &lines) { + const bool useColor = ConsoleModeRestore::Instance().IsVTEnabled() && + !context.Args.Contains(Execution::Args::Type::NoVT); + + Execution::TableOutput<5> table( + context.Reporter, + {Resource::String::SearchName, Resource::String::SearchId, + Resource::String::SearchVersion, Resource::String::AvailableHeader, + Resource::String::SearchSource}); + + for (auto &line : lines) { + if (useColor && !line.InstalledVersion.empty() && + !line.AvailableVersion.empty()) { + size_t offset = + FindVersionDiffOffset(line.InstalledVersion, line.AvailableVersion); + table.OutputLine( + {line.Name, line.Id, + ColorizeVersionDiff( + line.InstalledVersion, offset, + VirtualTerminal::TextFormat::Foreground::BrightRed), + ColorizeVersionDiff( + line.AvailableVersion, offset, + VirtualTerminal::TextFormat::Foreground::BrightGreen), + line.Source}); + } else { + table.OutputLine({line.Name, line.Id, line.InstalledVersion, + line.AvailableVersion, line.Source}); } + } - HRESULT HandleException(Execution::Context& context, std::exception_ptr exception) - { - return HandleException(&context, exception); + table.Complete(); +} +void ShowMetadataField(Execution::OutputStream &outputStream, + StringResource::StringId label, + const IPackageVersion::Metadata &metadata, + PackageVersionMetadata field) { + auto itr = metadata.find(field); + if (itr != metadata.end()) { + ShowSingleLineField(outputStream, label, Utility::LocIndView{itr->second}); + } +} + +// Outputs every package "line" with many details, with a format similar to the +// `show` command. +void OutputInstalledPackagesDetails( + Execution::Context &context, + std::vector &lines) { + auto info = context.Reporter.Info(); + size_t packageIndex = 0; + size_t totalLines = lines.size(); + bool shouldGetIcon = context.Reporter.SixelsEnabled(); + for (const auto &line : lines) { + // Identity header including package count indicator if multiple lines + // provided + if (totalLines > 1) { + info << '(' << ++packageIndex << '/' << totalLines << ") "_liv; } - AppInstaller::Manifest::ManifestComparator::Options GetManifestComparatorOptions(const Execution::Context& context, const IPackageVersion::Metadata& metadata) - { - AppInstaller::Manifest::ManifestComparator::Options options; - bool getAllowedArchitecturesFromMetadata = false; + ReportIdentity(context, {}, std::nullopt, line.Name, line.Id); - if (context.Contains(Execution::Data::AllowedArchitectures)) - { - // Com caller can directly set allowed architectures - options.AllowedArchitectures = context.Get(); - } - else if (context.Args.Contains(Execution::Args::Type::InstallArchitecture)) - { - // Arguments provided in command line - options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallArchitecture))); - } - else if (context.Args.Contains(Execution::Args::Type::InstallerArchitecture)) - { - // Arguments provided in command line. Also skips applicability check. - options.AllowedArchitectures.emplace_back(Utility::ConvertToArchitectureEnum(context.Args.GetArg(Execution::Args::Type::InstallerArchitecture))); - options.SkipApplicabilityCheck = true; - } - else - { - getAllowedArchitecturesFromMetadata = true; - } + auto metadata = line.InstalledPackageVersion->GetMetadata(); + auto productCodes = line.InstalledPackageVersion->GetMultiProperty( + PackageVersionMultiProperty::ProductCode); - if (context.Args.Contains(Execution::Args::Type::InstallerType)) - { - options.RequestedInstallerType = Manifest::ConvertToInstallerTypeEnum(std::string(context.Args.GetArg(Execution::Args::Type::InstallerType))); - } + if (shouldGetIcon && !productCodes.empty()) { + Manifest::ScopeEnum scope = Manifest::ScopeEnum::Unknown; - if (context.Args.Contains(Execution::Args::Type::InstallScope)) - { - options.RequestedInstallerScope = Manifest::ConvertToScopeEnum(context.Args.GetArg(Execution::Args::Type::InstallScope)); - } + auto itr = metadata.find(PackageVersionMetadata::InstalledScope); + if (itr != metadata.end()) { + scope = Manifest::ConvertToScopeEnum(itr->second); + } - if (context.Contains(Execution::Data::AllowUnknownScope)) - { - options.AllowUnknownScope = context.Get(); - } + auto icons = ExtractIconFromArpEntry(productCodes[0], scope); + ShowExtractedIcon(info, icons); + } - if (context.Args.Contains(Execution::Args::Type::Locale)) - { - options.RequestedInstallerLocale = context.Args.GetArg(Execution::Args::Type::Locale); - } + ShowSingleLineField(info, Resource::String::ShowLabelVersion, + line.InstalledVersion); + ShowSingleLineField(info, Resource::String::ShowLabelChannel, + line.InstalledPackageVersion->GetProperty( + PackageVersionProperty::Channel)); + ShowSingleLineField(info, Resource::String::ShowLabelPublisher, + line.InstalledPackageVersion->GetProperty( + PackageVersionProperty::Publisher)); + auto localIdentifier = + line.InstalledPackageVersion->GetProperty(PackageVersionProperty::Id); + if (line.Id != localIdentifier) { + ShowSingleLineField(info, Resource::String::ShowListLocalIdentifier, + localIdentifier); + } - Repository::GetManifestComparatorOptionsFromMetadata(options, metadata, getAllowedArchitecturesFromMetadata); + ShowMultiValueField(info, Resource::String::ShowListPackageFamilyName, + line.InstalledPackageVersion->GetMultiProperty( + PackageVersionMultiProperty::PackageFamilyName)); + ShowMultiValueField(info, Resource::String::ShowListProductCode, + productCodes); + ShowMultiValueField(info, Resource::String::ShowListUpgradeCode, + line.InstalledPackageVersion->GetMultiProperty( + PackageVersionMultiProperty::UpgradeCode)); + + ShowMetadataField(info, Resource::String::ShowListInstallerCategory, + metadata, PackageVersionMetadata::InstalledType); + ShowMetadataField(info, Resource::String::ShowListInstalledScope, metadata, + PackageVersionMetadata::InstalledScope); + ShowMetadataField(info, Resource::String::ShowListInstalledArchitecture, + metadata, PackageVersionMetadata::InstalledArchitecture); + ShowMetadataField(info, Resource::String::ShowListInstalledLocale, metadata, + PackageVersionMetadata::InstalledLocale); + ShowMetadataField(info, Resource::String::ShowListInstalledLocation, + metadata, PackageVersionMetadata::InstalledLocation); + + auto source = line.InstalledPackageVersion->GetSource(); + if (source.ContainsAvailablePackages()) { + ShowSingleLineField(info, Resource::String::ShowListInstalledSource, + Utility::LocIndView{source.GetDetails().Name}); + } - return options; + Utility::Version currentVersion{line.InstalledVersion}; + bool hasUpgradeVersion = false; + for (const auto &available : line.Package->GetAvailable()) { + auto latestAvailable = available->GetLatestVersion(); + auto availableVersion = + latestAvailable->GetProperty(PackageVersionProperty::Version); + if (Utility::Version{availableVersion} > currentVersion) { + if (!hasUpgradeVersion) { + hasUpgradeVersion = true; + info << details::GetIndentFor(0) << Execution::ManifestInfoEmphasis + << Resource::String::ShowListAvailableUpgrades << '\n'; + } + + info << details::GetIndentFor(1) + << Utility::LocIndView{available->GetSource().GetDetails().Name} + << " ["_liv << availableVersion << "]\n"_liv; + } } - void OpenSource::operator()(Execution::Context& context) const - { - std::string_view sourceName; - if (m_forDependencies) - { - if (context.Args.Contains(Execution::Args::Type::DependencySource)) - { - sourceName = context.Args.GetArg(Execution::Args::Type::DependencySource); - } - } - else - { - if (context.Args.Contains(Execution::Args::Type::Source)) - { - sourceName = context.Args.GetArg(Execution::Args::Type::Source); - } - } + // FUTURE: We could also pull data from the tracking database to show some + // things that we store there specifically. + } +} - auto source = OpenNamedSource(context, Utility::LocIndView{ sourceName }); - if (context.IsTerminated()) - { - return; - } +// Sorts a vector of InstalledPackagesTableLine according to the user's sort +// preferences. Resolution order: CLI args (--sort) > settings +// (output.sortOrder) > query-aware default. +void SortInstalledPackagesTableLines( + Execution::Context &context, + std::vector &lines) { + if (lines.size() <= 1) { + return; + } + + const SortParameters params(context); + + // Not strictly required — SortBy handles this internally — but avoids + // constructing the SortablePackageEntry vector when no sorting is needed. + if (!params.ShouldSort) { + return; + } + + const SortField mask = ComputeSortFieldMask(params.Fields); + SortBy( + lines, + [mask](const InstalledPackagesTableLine &line, size_t index) { + return SortablePackageEntry( + index, line.Name.get(), line.Id.get(), line.InstalledVersion.get(), + line.AvailableVersion.get(), line.Source.get(), mask); + }, + params); +} - context << HandleSourceAgreements(source); - if (context.IsTerminated()) - { - return; - } +void OutputInstalledPackages(Execution::Context &context, + std::vector &lines) { + SortInstalledPackagesTableLines(context, lines); - if (m_forDependencies) - { - context.Add(std::move(source)); - } - else - { - context.Add(std::move(source)); - } - } + if (context.Args.Contains(Execution::Args::Type::ListDetails)) { + OutputInstalledPackagesDetails(context, lines); + } else { + OutputInstalledPackagesTable(context, lines); + } +} +} // namespace + +bool WorkflowTask::operator==(const WorkflowTask &other) const { + 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 { + return false; + } +} - void OpenNamedSourceForSources::operator()(Execution::Context& context) const - { - auto source = OpenNamedSource(context, m_sourceName); - if (context.IsTerminated()) - { - return; - } +void WorkflowTask::operator()(Execution::Context &context) const { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_isFunc); + m_func(context); +} - context << HandleSourceAgreements(source); - if (context.IsTerminated()) - { - return; - } +void WorkflowTask::Log() const { + if (m_isFunc) { + // Using `00000001`80000000` as base address default when loading dll into + // windbg as dump file. + AICLI_LOG(Workflow, Verbose, << "Running task: 0x" << m_func + << " [ln 00000001`80000000+" << std::hex + << (reinterpret_cast(m_func) - + reinterpret_cast(&__ImageBase)) + << "]"); + } else { + AICLI_LOG(Workflow, Verbose, << "Running task: " << m_name); + } +} - if (!context.Contains(Execution::Data::Sources)) - { - context.Add({ std::move(source) }); - } - else - { - context.Get().emplace_back(std::move(source)); - } - } +Repository::PredefinedSource +DetermineInstalledSource(const Execution::Context &context) { + Repository::PredefinedSource installedSource = + Repository::PredefinedSource::Installed; + Manifest::ScopeEnum scope = Manifest::ConvertToScopeEnum( + context.Args.GetArg(Execution::Args::Type::InstallScope)); + if (scope == Manifest::ScopeEnum::Machine) { + installedSource = Repository::PredefinedSource::InstalledMachine; + } else if (scope == Manifest::ScopeEnum::User) { + installedSource = Repository::PredefinedSource::InstalledUser; + } + + return installedSource; +} - void OpenPredefinedSource::operator()(Execution::Context& context) const - { - Repository::Source source; - try - { - source = Source{ m_predefinedSource }; - - // A well known predefined source should return a value. - THROW_HR_IF(E_UNEXPECTED, !source); - - auto openFunction = [&](IProgressCallback& progress)->std::vector - { - return source.Open(progress); - }; - context.Reporter.ExecuteWithProgress(openFunction, true); - } - catch (...) - { - context.Reporter.Error() << Resource::String::SourceOpenPredefinedFailedSuggestion << std::endl; - throw; - } +Authentication::AuthenticationArguments +GetAuthenticationArguments(const Execution::Context &context) { + AppInstaller::Authentication::AuthenticationArguments authArgs; + + if (context.Args.Contains(Execution::Args::Type::AuthenticationMode)) { + authArgs.Mode = Authentication::ConvertToAuthenticationMode( + context.Args.GetArg(Execution::Args::Type::AuthenticationMode)); + } else { + // If user did not specify authentication mode, determine based on if + // disable interactivity flag exists. + authArgs.Mode = + context.Args.Contains(Execution::Args::Type::DisableInteractivity) + ? Authentication::AuthenticationMode::Silent + : Authentication::AuthenticationMode::SilentPreferred; + } + + if (context.Args.Contains(Execution::Args::Type::AuthenticationAccount)) { + authArgs.AuthenticationAccount = + context.Args.GetArg(Execution::Args::Type::AuthenticationAccount); + } + + AICLI_LOG(CLI, + Info, << "Created authentication arguments. Mode: " + << Authentication::AuthenticationModeToString(authArgs.Mode) + << ", Account: " << authArgs.AuthenticationAccount); + + return authArgs; +} - if (m_forDependencies) - { - context.Add(std::move(source)); - } - else - { - context.Add(std::move(source)); - } +HRESULT HandleException(Execution::Context *context, + std::exception_ptr exception) { + try { + std::rethrow_exception(exception); + } + // 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(Logging::FailureTypeEnum::ResultException, + re.what()); + if (context) { + context->Reporter.Error() + << Resource::String::UnexpectedErrorExecutingCommand << ' ' + << std::endl + << GetUserPresentableMessage(re) << std::endl; } + return re.GetErrorCode(); + } catch (const winrt::hresult_error &hre) { + std::string message = GetUserPresentableMessage(hre); + Logging::Telemetry().LogException( + Logging::FailureTypeEnum::WinrtHResultError, message); + if (context) { + context->Reporter.Error() + << Resource::String::UnexpectedErrorExecutingCommand << ' ' + << std::endl + << message << std::endl; + } + return hre.code(); + } catch (const Settings::GroupPolicyException &e) { + if (context) { + auto policy = Settings::TogglePolicy::GetPolicy(e.Policy()); + auto policyNameId = policy.PolicyName(); + context->Reporter.Error() + << Resource::String::DisabledByGroupPolicy(policyNameId) << std::endl; + } + return APPINSTALLER_CLI_ERROR_BLOCKED_BY_POLICY; + } catch (const std::exception &e) { + Logging::Telemetry().LogException(Logging::FailureTypeEnum::StdException, + e.what()); + if (context) { + context->Reporter.Error() + << Resource::String::UnexpectedErrorExecutingCommand << ' ' + << std::endl + << GetUserPresentableMessage(e) << std::endl; + } + return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; + } catch (...) { + LOG_CAUGHT_EXCEPTION(); + Logging::Telemetry().LogException(Logging::FailureTypeEnum::Unknown, {}); + if (context) { + context->Reporter.Error() + << Resource::String::UnexpectedErrorExecutingCommand << " ???"_liv + << std::endl; + } + return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; + } - void OpenCompositeSource::operator()(Execution::Context& context) const - { - // Get the already open source for use as the available. - Repository::Source availableSource; - if (m_forDependencies) - { - availableSource = context.Get(); - } - else - { - availableSource = context.Get(); - } - - // Open the predefined source. - context << OpenPredefinedSource(m_predefinedSource, m_forDependencies); + return E_UNEXPECTED; +} - // Create the composite source from the two. - Repository::Source source; - if (m_forDependencies) - { - source = context.Get(); - } - else - { - source = context.Get(); - } +HRESULT HandleException(Execution::Context &context, + std::exception_ptr exception) { + return HandleException(&context, exception); +} - Repository::Source compositeSource{ source, availableSource, m_searchBehavior }; +AppInstaller::Manifest::ManifestComparator::Options +GetManifestComparatorOptions(const Execution::Context &context, + const IPackageVersion::Metadata &metadata) { + AppInstaller::Manifest::ManifestComparator::Options options; + bool getAllowedArchitecturesFromMetadata = false; + + if (context.Contains(Execution::Data::AllowedArchitectures)) { + // Com caller can directly set allowed architectures + options.AllowedArchitectures = + context.Get(); + } else if (context.Args.Contains( + Execution::Args::Type::InstallArchitecture)) { + // Arguments provided in command line + options.AllowedArchitectures.emplace_back( + Utility::ConvertToArchitectureEnum( + context.Args.GetArg(Execution::Args::Type::InstallArchitecture))); + } else if (context.Args.Contains( + Execution::Args::Type::InstallerArchitecture)) { + // Arguments provided in command line. Also skips applicability check. + options.AllowedArchitectures.emplace_back( + Utility::ConvertToArchitectureEnum( + context.Args.GetArg(Execution::Args::Type::InstallerArchitecture))); + options.SkipApplicabilityCheck = true; + } else { + getAllowedArchitecturesFromMetadata = true; + } + + if (context.Args.Contains(Execution::Args::Type::InstallerType)) { + options.RequestedInstallerType = Manifest::ConvertToInstallerTypeEnum( + std::string(context.Args.GetArg(Execution::Args::Type::InstallerType))); + } + + if (context.Args.Contains(Execution::Args::Type::InstallScope)) { + options.RequestedInstallerScope = Manifest::ConvertToScopeEnum( + context.Args.GetArg(Execution::Args::Type::InstallScope)); + } + + if (context.Contains(Execution::Data::AllowUnknownScope)) { + options.AllowUnknownScope = + context.Get(); + } + + if (context.Args.Contains(Execution::Args::Type::Locale)) { + options.RequestedInstallerLocale = + context.Args.GetArg(Execution::Args::Type::Locale); + } + + Repository::GetManifestComparatorOptionsFromMetadata( + options, metadata, getAllowedArchitecturesFromMetadata); + + return options; +} - // Overwrite the source with the composite. - if (m_forDependencies) - { - context.Add(std::move(compositeSource)); - } - else - { - context.Add(std::move(compositeSource)); - } +void OpenSource::operator()(Execution::Context &context) const { + std::string_view sourceName; + if (m_forDependencies) { + if (context.Args.Contains(Execution::Args::Type::DependencySource)) { + sourceName = context.Args.GetArg(Execution::Args::Type::DependencySource); } + } else { + if (context.Args.Contains(Execution::Args::Type::Source)) { + sourceName = context.Args.GetArg(Execution::Args::Type::Source); + } + } + + auto source = OpenNamedSource(context, Utility::LocIndView{sourceName}); + if (context.IsTerminated()) { + return; + } + + context << HandleSourceAgreements(source); + if (context.IsTerminated()) { + return; + } + + if (m_forDependencies) { + context.Add(std::move(source)); + } else { + context.Add(std::move(source)); + } +} - void SearchSourceForMany(Execution::Context& context) - { - const auto& args = context.Args; - - MatchType matchType = MatchType::Substring; - if (args.Contains(Execution::Args::Type::Exact)) - { - matchType = MatchType::Exact; - } - - SearchRequest searchRequest; - - if (args.Contains(Execution::Args::Type::Query)) - { - searchRequest.Query.emplace(RequestMatch(matchType, args.GetArg(Execution::Args::Type::Query))); - } +void OpenNamedSourceForSources::operator()(Execution::Context &context) const { + auto source = OpenNamedSource(context, m_sourceName); + if (context.IsTerminated()) { + return; + } + + context << HandleSourceAgreements(source); + if (context.IsTerminated()) { + return; + } + + if (!context.Contains(Execution::Data::Sources)) { + context.Add({std::move(source)}); + } else { + context.Get().emplace_back(std::move(source)); + } +} - SearchSourceApplyFilters(context, searchRequest, matchType); +void OpenPredefinedSource::operator()(Execution::Context &context) const { + Repository::Source source; + try { + source = Source{m_predefinedSource}; + + // A well known predefined source should return a value. + THROW_HR_IF(E_UNEXPECTED, !source); + + auto openFunction = [&](IProgressCallback &progress) + -> std::vector { + return source.Open(progress); + }; + context.Reporter.ExecuteWithProgress(openFunction, true); + } catch (...) { + context.Reporter.Error() + << Resource::String::SourceOpenPredefinedFailedSuggestion << std::endl; + throw; + } + + if (m_forDependencies) { + context.Add(std::move(source)); + } else { + context.Add(std::move(source)); + } +} - Logging::Telemetry().LogSearchRequest( - "many", - 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()); +void OpenCompositeSource::operator()(Execution::Context &context) const { + // Get the already open source for use as the available. + Repository::Source availableSource; + if (m_forDependencies) { + availableSource = context.Get(); + } else { + availableSource = context.Get(); + } + + // Open the predefined source. + context << OpenPredefinedSource(m_predefinedSource, m_forDependencies); + + // Create the composite source from the two. + Repository::Source source; + if (m_forDependencies) { + source = context.Get(); + } else { + source = context.Get(); + } + + Repository::Source compositeSource{source, availableSource, m_searchBehavior}; + + // Overwrite the source with the composite. + if (m_forDependencies) { + context.Add(std::move(compositeSource)); + } else { + context.Add(std::move(compositeSource)); + } +} - context.Add(context.Get().Search(searchRequest)); - } +void SearchSourceForMany(Execution::Context &context) { + const auto &args = context.Args; - void GetSearchRequestForSingle(Execution::Context& context) - { - const auto& args = context.Args; + MatchType matchType = MatchType::Substring; + if (args.Contains(Execution::Args::Type::Exact)) { + matchType = MatchType::Exact; + } - MatchType matchType = MatchType::CaseInsensitive; - if (args.Contains(Execution::Args::Type::Exact)) - { - matchType = MatchType::Exact; - } + SearchRequest searchRequest; - SearchRequest searchRequest; - // Note: MultiQuery when we need search for single is handled with one sub-context per query. - if (args.Contains(Execution::Args::Type::Query)) - { - std::string_view query = args.GetArg(Execution::Args::Type::Query); - - // Regardless of match type, always use an exact match for the system reference strings. - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, matchType, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); - } + if (args.Contains(Execution::Args::Type::Query)) { + searchRequest.Query.emplace( + RequestMatch(matchType, args.GetArg(Execution::Args::Type::Query))); + } - SearchSourceApplyFilters(context, searchRequest, matchType); + SearchSourceApplyFilters(context, searchRequest, matchType); - context.Add(std::move(searchRequest)); - } + Logging::Telemetry().LogSearchRequest( + "many", 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()); - void SearchSourceForSingle(Execution::Context& context) - { - const auto& args = context.Args; - context << GetSearchRequestForSingle; - if (!context.IsTerminated()) - { - const auto& searchRequest = context.Get(); - - Logging::Telemetry().LogSearchRequest( - "single", - 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()); - - context.Add(context.Get().Search(searchRequest)); - } - } + context.Add( + context.Get().Search(searchRequest)); +} - void SearchSourceForManyCompletion(Execution::Context& context) - { - MatchType matchType = MatchType::StartsWith; +void GetSearchRequestForSingle(Execution::Context &context) { + const auto &args = context.Args; + + MatchType matchType = MatchType::CaseInsensitive; + if (args.Contains(Execution::Args::Type::Exact)) { + matchType = MatchType::Exact; + } + + SearchRequest searchRequest; + // Note: MultiQuery when we need search for single is handled with one + // sub-context per query. + if (args.Contains(Execution::Args::Type::Query)) { + std::string_view query = args.GetArg(Execution::Args::Type::Query); + + // Regardless of match type, always use an exact match for the system + // reference strings. + searchRequest.Inclusions.emplace_back(PackageMatchFilter( + PackageMatchField::PackageFamilyName, MatchType::Exact, query)); + searchRequest.Inclusions.emplace_back(PackageMatchFilter( + PackageMatchField::ProductCode, MatchType::Exact, query)); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Id, matchType, query)); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Name, matchType, query)); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); + } + + SearchSourceApplyFilters(context, searchRequest, matchType); + + context.Add(std::move(searchRequest)); +} - SearchRequest searchRequest; - std::string_view query = context.Get().Word(); - searchRequest.Query.emplace(RequestMatch(matchType, query)); +void SearchSourceForSingle(Execution::Context &context) { + const auto &args = context.Args; + context << GetSearchRequestForSingle; + if (!context.IsTerminated()) { + const auto &searchRequest = context.Get(); + + Logging::Telemetry().LogSearchRequest( + "single", 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()); + + context.Add( + context.Get().Search(searchRequest)); + } +} - SearchSourceApplyFilters(context, searchRequest, matchType); +void SearchSourceForManyCompletion(Execution::Context &context) { + MatchType matchType = MatchType::StartsWith; - context.Add(context.Get().Search(searchRequest)); - } + SearchRequest searchRequest; + std::string_view query = + context.Get().Word(); + searchRequest.Query.emplace(RequestMatch(matchType, query)); - void SearchSourceForSingleCompletion(Execution::Context& context) - { - MatchType matchType = MatchType::StartsWith; + SearchSourceApplyFilters(context, searchRequest, matchType); - SearchRequest searchRequest; - std::string_view query = context.Get().Word(); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Name, matchType, query)); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); + context.Add( + context.Get().Search(searchRequest)); +} - SearchSourceApplyFilters(context, searchRequest, matchType); +void SearchSourceForSingleCompletion(Execution::Context &context) { + MatchType matchType = MatchType::StartsWith; - context.Add(context.Get().Search(searchRequest)); - } + SearchRequest searchRequest; + std::string_view query = + context.Get().Word(); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Id, matchType, query)); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Name, matchType, query)); + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::Moniker, matchType, query)); - void SearchSourceForCompletionField::operator()(Execution::Context& context) const - { - const std::string& word = context.Get().Word(); + SearchSourceApplyFilters(context, searchRequest, matchType); - SearchRequest searchRequest; - searchRequest.Inclusions.emplace_back(PackageMatchFilter(m_field, MatchType::StartsWith, word)); + context.Add( + context.Get().Search(searchRequest)); +} - // If filters are provided, be generous with the search no matter the intended result. - SearchSourceApplyFilters(context, searchRequest, MatchType::Substring); +void SearchSourceForCompletionField::operator()( + Execution::Context &context) const { + const std::string &word = + context.Get().Word(); - context.Add(context.Get().Search(searchRequest)); - } + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(m_field, MatchType::StartsWith, word)); - void ReportSearchResult(Execution::Context& context) - { - auto& searchResult = context.Get(); - - bool sourceIsComposite = context.Get().IsComposite(); - Execution::TableOutput<5> table(context.Reporter, - { - Resource::String::SearchName, - Resource::String::SearchId, - Resource::String::SearchVersion, - Resource::String::SearchMatch, - Resource::String::SearchSource - }); - - for (size_t i = 0; i < searchResult.Matches.size(); ++i) - { - auto latestVersion = GetAllAvailableVersions(searchResult.Matches[i].Package)->GetLatestVersion(); - - table.OutputLine({ - latestVersion->GetProperty(PackageVersionProperty::Name), - latestVersion->GetProperty(PackageVersionProperty::Id), - latestVersion->GetProperty(PackageVersionProperty::Version), - GetMatchCriteriaDescriptor(searchResult.Matches[i]), - sourceIsComposite ? static_cast(latestVersion->GetProperty(PackageVersionProperty::SourceName)) : ""s - }); - } + // If filters are provided, be generous with the search no matter the intended + // result. + SearchSourceApplyFilters(context, searchRequest, MatchType::Substring); - table.Complete(); + context.Add( + context.Get().Search(searchRequest)); +} - if (searchResult.Truncated) - { - context.Reporter.Info() << '<' << Resource::String::SearchTruncated << '>' << std::endl; - } - } +void ReportSearchResult(Execution::Context &context) { + auto &searchResult = context.Get(); + + bool sourceIsComposite = context.Get().IsComposite(); + Execution::TableOutput<5> table( + context.Reporter, + {Resource::String::SearchName, Resource::String::SearchId, + Resource::String::SearchVersion, Resource::String::SearchMatch, + Resource::String::SearchSource}); + + for (size_t i = 0; i < searchResult.Matches.size(); ++i) { + auto latestVersion = + GetAllAvailableVersions(searchResult.Matches[i].Package) + ->GetLatestVersion(); + + table.OutputLine( + {latestVersion->GetProperty(PackageVersionProperty::Name), + latestVersion->GetProperty(PackageVersionProperty::Id), + latestVersion->GetProperty(PackageVersionProperty::Version), + GetMatchCriteriaDescriptor(searchResult.Matches[i]), + sourceIsComposite + ? static_cast(latestVersion->GetProperty( + PackageVersionProperty::SourceName)) + : ""s}); + } + + table.Complete(); + + if (searchResult.Truncated) { + context.Reporter.Info() + << '<' << Resource::String::SearchTruncated << '>' << std::endl; + } +} - void HandleSearchResultFailures(Execution::Context& context) - { - const auto& searchResult = context.Get(); - - if (!searchResult.Failures.empty()) - { - if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::TreatSourceFailuresAsWarning)) - { - auto warn = context.Reporter.Warn(); - for (const auto& failure : searchResult.Failures) - { - warn << Resource::String::SearchFailureWarning(Utility::LocIndView{ failure.SourceName }) << std::endl; - } - } - else - { - HRESULT overallHR = S_OK; - auto error = context.Reporter.Error(); - for (const auto& failure : searchResult.Failures) - { - error << Resource::String::SearchFailureError(Utility::LocIndView{ failure.SourceName }) << std::endl; - HRESULT failureHR = HandleException(context, failure.Exception); - - // Just take first failure for now - if (overallHR == S_OK) - { - overallHR = failureHR; - } - } - - if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::ShowSearchResultsOnPartialFailure)) - { - if (searchResult.Matches.empty()) - { - context.Reporter.Info() << std::endl << Resource::String::SearchFailureErrorNoMatches << std::endl; - } - else - { - context.Reporter.Info() << std::endl << Resource::String::SearchFailureErrorListMatches << std::endl; - context << ReportMultiplePackageFoundResultWithSource; - } - } - - context.SetTerminationHR(overallHR); - } - } +void HandleSearchResultFailures(Execution::Context &context) { + const auto &searchResult = context.Get(); + + if (!searchResult.Failures.empty()) { + if (WI_IsFlagSet(context.GetFlags(), + Execution::ContextFlag::TreatSourceFailuresAsWarning)) { + auto warn = context.Reporter.Warn(); + for (const auto &failure : searchResult.Failures) { + warn << Resource::String::SearchFailureWarning( + Utility::LocIndView{failure.SourceName}) + << std::endl; + } + } else { + HRESULT overallHR = S_OK; + auto error = context.Reporter.Error(); + for (const auto &failure : searchResult.Failures) { + error << Resource::String::SearchFailureError( + Utility::LocIndView{failure.SourceName}) + << std::endl; + HRESULT failureHR = HandleException(context, failure.Exception); + + // Just take first failure for now + if (overallHR == S_OK) { + overallHR = failureHR; + } + } + + if (WI_IsFlagSet( + context.GetFlags(), + Execution::ContextFlag::ShowSearchResultsOnPartialFailure)) { + if (searchResult.Matches.empty()) { + context.Reporter.Info() + << std::endl + << Resource::String::SearchFailureErrorNoMatches << std::endl; + } else { + context.Reporter.Info() + << std::endl + << Resource::String::SearchFailureErrorListMatches << std::endl; + context << ReportMultiplePackageFoundResultWithSource; + } + } + + context.SetTerminationHR(overallHR); } + } +} - void ReportMultiplePackageFoundResult(Execution::Context& context) - { - auto& searchResult = context.Get(); - - Execution::TableOutput<2> table(context.Reporter, - { - Resource::String::SearchName, - Resource::String::SearchId - }); - - for (size_t i = 0; i < searchResult.Matches.size(); ++i) - { - auto package = searchResult.Matches[i].Package; - - table.OutputLine({ - package->GetProperty(PackageProperty::Name), - package->GetProperty(PackageProperty::Id) - }); - } +void ReportMultiplePackageFoundResult(Execution::Context &context) { + auto &searchResult = context.Get(); - table.Complete(); + Execution::TableOutput<2> table( + context.Reporter, + {Resource::String::SearchName, Resource::String::SearchId}); - if (searchResult.Truncated) - { - context.Reporter.Info() << '<' << Resource::String::SearchTruncated << '>' << std::endl; - } - } + for (size_t i = 0; i < searchResult.Matches.size(); ++i) { + auto package = searchResult.Matches[i].Package; - void ReportMultiplePackageFoundResultWithSource(Execution::Context& context) - { - auto& searchResult = context.Get(); - - Execution::TableOutput<3> table(context.Reporter, - { - Resource::String::SearchName, - Resource::String::SearchId, - Resource::String::SearchSource - }); - - for (size_t i = 0; i < searchResult.Matches.size(); ++i) - { - auto package = searchResult.Matches[i].Package; - - std::string sourceName; - auto available = package->GetAvailable(); - if (!available.empty()) - { - auto source = available[0]->GetSource(); - if (source) - { - sourceName = source.GetDetails().Name; - } - } - - table.OutputLine({ - package->GetProperty(PackageProperty::Name), - package->GetProperty(PackageProperty::Id), - std::move(sourceName) - }); - } + table.OutputLine({package->GetProperty(PackageProperty::Name), + package->GetProperty(PackageProperty::Id)}); + } - table.Complete(); + table.Complete(); - if (searchResult.Truncated) - { - context.Reporter.Info() << '<' << Resource::String::SearchTruncated << '>' << std::endl; - } - } + if (searchResult.Truncated) { + context.Reporter.Info() + << '<' << Resource::String::SearchTruncated << '>' << std::endl; + } +} - void ReportListResult::operator()(Execution::Context& context) const - { - auto& searchResult = context.Get(); +void ReportMultiplePackageFoundResultWithSource(Execution::Context &context) { + auto &searchResult = context.Get(); - std::vector lines; - std::vector linesForExplicitUpgrade; - std::vector linesForPins; + Execution::TableOutput<3> table(context.Reporter, + {Resource::String::SearchName, + Resource::String::SearchId, + Resource::String::SearchSource}); - int availableUpgradesCount = 0; + for (size_t i = 0; i < searchResult.Matches.size(); ++i) { + auto package = searchResult.Matches[i].Package; - // We will show a line with a summary for skipped and pinned packages at the end. - // The strings suggest using a --include-unknown/pinned argument, so we should - // ensure that the count is 0 when using the arguments. - int packagesWithUnknownVersionSkipped = 0; - int packagesWithUserPinsSkipped = 0; + std::string sourceName; + auto available = package->GetAvailable(); + if (!available.empty()) { + auto source = available[0]->GetSource(); + if (source) { + sourceName = source.GetDetails().Name; + } + } - auto &source = context.Get(); - bool shouldShowSource = source.IsComposite() && source.GetAvailableSources().size() > 1; - bool sourceFilterProvided = context.Args.Contains(Execution::Args::Type::Source); + table.OutputLine({package->GetProperty(PackageProperty::Name), + package->GetProperty(PackageProperty::Id), + std::move(sourceName)}); + } - PinBehavior pinBehavior; - if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::Force)) - { - // For listing upgrades, show the version we would upgrade to with the given pins. - pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) ? PinBehavior::IncludePinned : PinBehavior::ConsiderPins; - } - else - { - // For listing installed apps or if we are ignoring pins due to --force, show the latest available. - pinBehavior = PinBehavior::IgnorePins; - } - - PinningData pinningData{ PinningData::Disposition::ReadOnly }; - - for (const auto& match : searchResult.Matches) - { - auto installedPackage = match.Package->GetInstalled(); - if (!installedPackage) - { - continue; - } - - // We only want to evaluate update availability for the latest version. - bool isFirstInstalledVersion = true; - - for (const auto& installedVersionKey : installedPackage->GetVersionKeys()) - { - bool isFirstInstalledVersionLocal = isFirstInstalledVersion; - isFirstInstalledVersion = false; - - auto installedVersion = installedPackage->GetVersion(installedVersionKey); - - auto evaluator = pinningData.CreatePinStateEvaluator(pinBehavior, installedVersion); - auto availableVersions = GetAvailableVersionsForInstalledVersion(match.Package, installedVersion); - - auto latestVersion = evaluator.GetLatestAvailableVersionForPins(availableVersions); - bool updateAvailable = isFirstInstalledVersionLocal && evaluator.IsUpdate(latestVersion); - bool updateIsPinned = false; - - if (m_onlyShowUpgrades && !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && Utility::Version(installedVersion->GetProperty(PackageVersionProperty::Version)).IsUnknown() && updateAvailable) - { - // We are only showing upgrades, and the user did not request to include packages with unknown versions. - ++packagesWithUnknownVersionSkipped; - continue; - } - - if (m_onlyShowUpgrades && !updateAvailable && isFirstInstalledVersionLocal) - { - // Reuse the evaluator to check if there is an update outside of the pinning - auto unpinnedLatestVersion = availableVersions->GetLatestVersion(); - bool updateAvailableWithoutPins = evaluator.IsUpdate(unpinnedLatestVersion); - - if (updateAvailableWithoutPins) - { - // When given the --include-pinned argument, report blocking and gating pins in a separate table. - // Otherwise, simply show a count of them - if (context.Args.Contains(Execution::Args::Type::IncludePinned)) - { - updateIsPinned = true; - - // Override these so we generate the table line below. - latestVersion = std::move(unpinnedLatestVersion); - updateAvailable = true; - } - else - { - ++packagesWithUserPinsSkipped; - continue; - } - } - } - - // When --source is given, only show packages that have a correlation (available version) - // in the specified source. Packages with no available match are not from that source. - if (sourceFilterProvided && !latestVersion) - { - continue; - } - - if (updateAvailable || !m_onlyShowUpgrades) - { - Utility::LocIndString availableVersion, sourceName; - - if (latestVersion) - { - // Always show the source for correlated packages - sourceName = latestVersion->GetProperty(PackageVersionProperty::SourceName); - - if (updateAvailable) - { - availableVersion = latestVersion->GetProperty(PackageVersionProperty::Version); - availableUpgradesCount++; - } - } - - // Output using the local PackageName instead of the name in the manifest, to prevent confusion for packages that add multiple - // Add/Remove Programs entries. - // TODO: De-duplicate this list, and only show (by default) one entry per matched package. - InstalledPackagesTableLine line( - match.Package, - installedVersion, - availableVersion, - shouldShowSource ? sourceName : Utility::LocIndString() - ); - - auto pinnedState = ConvertToPinTypeEnum(installedVersion->GetMetadata()[PackageVersionMetadata::PinnedState]); - if (updateIsPinned) - { - linesForPins.push_back(std::move(line)); - } - else if (m_onlyShowUpgrades && pinnedState == PinType::PinnedByManifest) - { - linesForExplicitUpgrade.push_back(std::move(line)); - } - else - { - lines.push_back(std::move(line)); - } - } - } - } + table.Complete(); - OutputInstalledPackages(context, lines); + if (searchResult.Truncated) { + context.Reporter.Info() + << '<' << Resource::String::SearchTruncated << '>' << std::endl; + } +} - if (lines.empty()) - { - context.Reporter.Info() << Resource::String::NoInstalledPackageFound << std::endl; - } - else - { - if (searchResult.Truncated) - { - context.Reporter.Info() << '<' << Resource::String::SearchTruncated << '>' << std::endl; - } - - if (m_onlyShowUpgrades) - { - context.Reporter.Info() << Resource::String::AvailableUpgrades(availableUpgradesCount) << std::endl; - } - } +void ReportListResult::operator()(Execution::Context &context) const { + auto &searchResult = context.Get(); + + std::vector lines; + std::vector linesForExplicitUpgrade; + std::vector linesForPins; + + int availableUpgradesCount = 0; + + // We will show a line with a summary for skipped and pinned packages at the + // end. The strings suggest using a --include-unknown/pinned argument, so we + // should ensure that the count is 0 when using the arguments. + int packagesWithUnknownVersionSkipped = 0; + int packagesWithUserPinsSkipped = 0; + + auto &source = context.Get(); + bool shouldShowSource = + source.IsComposite() && source.GetAvailableSources().size() > 1; + bool sourceFilterProvided = + context.Args.Contains(Execution::Args::Type::Source); + + PinBehavior pinBehavior; + if (m_onlyShowUpgrades && + !context.Args.Contains(Execution::Args::Type::Force)) { + // For listing upgrades, show the version we would upgrade to with the given + // pins. + pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) + ? PinBehavior::IncludePinned + : PinBehavior::ConsiderPins; + } else { + // For listing installed apps or if we are ignoring pins due to --force, + // show the latest available. + pinBehavior = PinBehavior::IgnorePins; + } + + PinningData pinningData{PinningData::Disposition::ReadOnly}; + + for (const auto &match : searchResult.Matches) { + auto installedPackage = match.Package->GetInstalled(); + if (!installedPackage) { + continue; + } - if (!linesForExplicitUpgrade.empty()) - { - context.Reporter.Info() << std::endl << Resource::String::UpgradeAvailableForPinned << std::endl; - OutputInstalledPackages(context, linesForExplicitUpgrade); - } + // We only want to evaluate update availability for the latest version. + bool isFirstInstalledVersion = true; + + for (const auto &installedVersionKey : installedPackage->GetVersionKeys()) { + bool isFirstInstalledVersionLocal = isFirstInstalledVersion; + isFirstInstalledVersion = false; + + auto installedVersion = installedPackage->GetVersion(installedVersionKey); + + auto evaluator = + pinningData.CreatePinStateEvaluator(pinBehavior, installedVersion); + auto availableVersions = GetAvailableVersionsForInstalledVersion( + match.Package, installedVersion); + + auto latestVersion = + evaluator.GetLatestAvailableVersionForPins(availableVersions); + bool updateAvailable = + isFirstInstalledVersionLocal && evaluator.IsUpdate(latestVersion); + bool updateIsPinned = false; + + if (m_onlyShowUpgrades && + !context.Args.Contains(Execution::Args::Type::IncludeUnknown) && + Utility::Version( + installedVersion->GetProperty(PackageVersionProperty::Version)) + .IsUnknown() && + updateAvailable) { + // We are only showing upgrades, and the user did not request to include + // packages with unknown versions. + ++packagesWithUnknownVersionSkipped; + continue; + } + + if (m_onlyShowUpgrades && !updateAvailable && + isFirstInstalledVersionLocal) { + // Reuse the evaluator to check if there is an update outside of the + // pinning + auto unpinnedLatestVersion = availableVersions->GetLatestVersion(); + bool updateAvailableWithoutPins = + evaluator.IsUpdate(unpinnedLatestVersion); + + if (updateAvailableWithoutPins) { + // When given the --include-pinned argument, report blocking and + // gating pins in a separate table. Otherwise, simply show a count of + // them + if (context.Args.Contains(Execution::Args::Type::IncludePinned)) { + updateIsPinned = true; + + // Override these so we generate the table line below. + latestVersion = std::move(unpinnedLatestVersion); + updateAvailable = true; + } else { + ++packagesWithUserPinsSkipped; + continue; + } + } + } + + // When --source is given, only show packages that have a correlation + // (available version) in the specified source. Packages with no available + // match are not from that source. + if (sourceFilterProvided && !latestVersion) { + continue; + } + + if (updateAvailable || !m_onlyShowUpgrades) { + Utility::LocIndString availableVersion, sourceName; + + if (latestVersion) { + // Always show the source for correlated packages + sourceName = + latestVersion->GetProperty(PackageVersionProperty::SourceName); + + if (updateAvailable) { + availableVersion = + latestVersion->GetProperty(PackageVersionProperty::Version); + availableUpgradesCount++; + } + } + + // Output using the local PackageName instead of the name in the + // manifest, to prevent confusion for packages that add multiple + // Add/Remove Programs entries. + // TODO: De-duplicate this list, and only show (by default) one entry + // per matched package. + InstalledPackagesTableLine line( + match.Package, installedVersion, availableVersion, + shouldShowSource ? sourceName : Utility::LocIndString()); + + auto pinnedState = ConvertToPinTypeEnum( + installedVersion + ->GetMetadata()[PackageVersionMetadata::PinnedState]); + if (updateIsPinned) { + linesForPins.push_back(std::move(line)); + } else if (m_onlyShowUpgrades && + pinnedState == PinType::PinnedByManifest) { + linesForExplicitUpgrade.push_back(std::move(line)); + } else { + lines.push_back(std::move(line)); + } + } + } + } - if (!linesForPins.empty()) - { - context.Reporter.Info() << std::endl << Resource::String::UpgradeBlockedByPinCount(linesForPins.size()) << std::endl; - OutputInstalledPackages(context, linesForPins); - } + OutputInstalledPackages(context, lines); - if (m_onlyShowUpgrades) - { - if (packagesWithUnknownVersionSkipped > 0) - { - AICLI_LOG(CLI, Info, << packagesWithUnknownVersionSkipped << " package(s) skipped due to unknown installed version"); - context.Reporter.Info() << Resource::String::UpgradeUnknownVersionCount(packagesWithUnknownVersionSkipped) << std::endl; - } - - if (packagesWithUserPinsSkipped > 0) - { - AICLI_LOG(CLI, Info, << packagesWithUserPinsSkipped << " package(s) skipped due to user pins"); - context.Reporter.Info() << Resource::String::UpgradePinnedByUserCount(packagesWithUserPinsSkipped) << std::endl; - } - } + if (lines.empty()) { + context.Reporter.Info() + << Resource::String::NoInstalledPackageFound << std::endl; + } else { + if (searchResult.Truncated) { + context.Reporter.Info() + << '<' << Resource::String::SearchTruncated << '>' << std::endl; } - void EnsureMatchesFromSearchResult::operator()(Execution::Context& context) const - { - auto& searchResult = context.Get(); - - Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); - - if (searchResult.Matches.size() == 0) - { - Logging::Telemetry().LogNoAppMatch(); - - switch (m_operationType) - { - // These search purposes require a package to be found in the Installed Packages - case OperationType::Export: - case OperationType::List: - case OperationType::Uninstall: - case OperationType::Pin: - case OperationType::Upgrade: - case OperationType::Repair: - context.Reporter.Info() << Resource::String::NoInstalledPackageFound << std::endl; - break; - case OperationType::Completion: - case OperationType::Install: - case OperationType::Search: - case OperationType::Show: - case OperationType::Download: - default: - context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; - break; - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); - } + if (m_onlyShowUpgrades) { + context.Reporter.Info() + << Resource::String::AvailableUpgrades(availableUpgradesCount) + << std::endl; } - - void EnsureOneMatchFromSearchResult::operator()(Execution::Context& context) const - { - context << - EnsureMatchesFromSearchResult(m_operationType); - - if (!context.IsTerminated()) - { - auto& searchResult = context.Get(); - - bool operationTargetsInstalled = m_operationType == OperationType::Upgrade || m_operationType == OperationType::Uninstall || - m_operationType == OperationType::Repair || m_operationType == OperationType::Export; - - // Try limiting results to highest priority sources - if (searchResult.Matches.size() > 1 && !operationTargetsInstalled && - ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::SourcePriority)) - { - // Find the set of matches that have the highest priority - std::vector highestPriorityMatches; - std::optional highestPriority; - - for (const auto& match : searchResult.Matches) - { - std::optional priority = GetSourcePriority(match.Package); - - // Optional provides overloads that make empty less than valued and empties equal. - if (highestPriority < priority) - { - // Current priority is higher; reset. - highestPriority = priority; - highestPriorityMatches.clear(); - } - else if (highestPriority == priority) - { - // Priority is equal, add to the list. - } - else - { - // Current priority is lower, ignore the match. - continue; - } - - highestPriorityMatches.emplace_back(match); - } - - if (highestPriorityMatches.size() < searchResult.Matches.size()) - { - AICLI_LOG(CLI, Info, << "Replacing search results with only those from the highest priority [" << (highestPriority ? std::to_string(highestPriority.value()) : "none"s) << "]."); - searchResult.Matches = std::move(highestPriorityMatches); - context.Reporter.Warn() << Resource::String::MultiplePackagesFoundFilteredBySourcePriority << std::endl; - } - } - - if (searchResult.Matches.size() > 1) - { - Logging::Telemetry().LogMultiAppMatch(); - - if (operationTargetsInstalled) - { - context.Reporter.Warn() << Resource::String::MultipleInstalledPackagesFound << std::endl; - context << ReportMultiplePackageFoundResult; - } - else - { - context.Reporter.Warn() << Resource::String::MultiplePackagesFound << std::endl; - context << ReportMultiplePackageFoundResultWithSource; - } - - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); - } - - std::shared_ptr package = searchResult.Matches.at(0).Package; - Logging::Telemetry().LogAppFound(package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); - - context.Add(std::move(package)); - } + } + + if (!linesForExplicitUpgrade.empty()) { + context.Reporter.Info() + << std::endl + << Resource::String::UpgradeAvailableForPinned << std::endl; + OutputInstalledPackages(context, linesForExplicitUpgrade); + } + + if (!linesForPins.empty()) { + context.Reporter.Info() + << std::endl + << Resource::String::UpgradeBlockedByPinCount(linesForPins.size()) + << std::endl; + OutputInstalledPackages(context, linesForPins); + } + + if (m_onlyShowUpgrades) { + if (packagesWithUnknownVersionSkipped > 0) { + AICLI_LOG( + CLI, Info, << packagesWithUnknownVersionSkipped + << " package(s) skipped due to unknown installed version"); + context.Reporter.Info() << Resource::String::UpgradeUnknownVersionCount( + packagesWithUnknownVersionSkipped) + << std::endl; } - void GetManifestWithVersionFromPackage::operator()(Execution::Context& context) const - { - PackageVersionKey key("", m_version, m_channel); - - std::shared_ptr package = context.Get(); - std::shared_ptr requestedVersion; - auto availableVersions = GetAvailableVersionsForInstalledVersion(package); - - if (m_considerPins) - { - bool isPinned = false; - - PinBehavior pinBehavior; - if (context.Args.Contains(Execution::Args::Type::Force)) - { - // --force ignores any pins - pinBehavior = PinBehavior::IgnorePins; - } - else - { - pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) ? PinBehavior::IncludePinned : PinBehavior::ConsiderPins; - } - - PinningData pinningData{ PinningData::Disposition::ReadOnly }; - auto evaluator = pinningData.CreatePinStateEvaluator(pinBehavior, GetInstalledVersion(package)); - - // TODO: The logic here will probably have to get more difficult once we support channels - if (Utility::IsEmptyOrWhitespace(m_version) && Utility::IsEmptyOrWhitespace(m_channel)) - { - requestedVersion = evaluator.GetLatestAvailableVersionForPins(availableVersions); - - if (!requestedVersion) - { - // Check whether we didn't find the latest version because it was pinned or because there wasn't one - auto latestVersion = availableVersions->GetLatestVersion(); - if (latestVersion) - { - isPinned = true; - } - } - } - else - { - requestedVersion = availableVersions->GetVersion(key); - isPinned = evaluator.EvaluatePinType(requestedVersion) != PinType::Unknown; - } - - if (isPinned) - { - if (context.Args.Contains(Execution::Args::Type::Force)) - { - AICLI_LOG(CLI, Info, << "Ignoring pin on package due to --force argument"); - } - else - { - AICLI_LOG(CLI, Error, << "The requested package version is unavailable because of a pin"); - context.Reporter.Error() << Resource::String::PackageIsPinned << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED); - } - } - } - else - { - // The simple case: Just look up the requested version - requestedVersion = availableVersions->GetVersion(key); - } - - std::optional manifest; - if (requestedVersion) - { - manifest = requestedVersion->GetManifest(); - } - - if (!manifest) - { - std::ostringstream ssVersionInfo; - if (!m_version.empty()) - { - ssVersionInfo << m_version; - } - if (!m_channel.empty()) - { - ssVersionInfo << '[' << m_channel << ']'; - } - - context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound(Utility::LocIndView{ ssVersionInfo.str()}) << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); - } + if (packagesWithUserPinsSkipped > 0) { + AICLI_LOG(CLI, Info, << packagesWithUserPinsSkipped + << " package(s) skipped due to user pins"); + context.Reporter.Info() << Resource::String::UpgradePinnedByUserCount( + packagesWithUserPinsSkipped) + << std::endl; + } + } +} - Logging::Telemetry().LogManifestFields(manifest->Id, manifest->DefaultLocalization.Get(), manifest->Version); +void EnsureMatchesFromSearchResult::operator()( + Execution::Context &context) const { + auto &searchResult = context.Get(); + + Logging::Telemetry().LogSearchResultCount(searchResult.Matches.size()); + + if (searchResult.Matches.size() == 0) { + Logging::Telemetry().LogNoAppMatch(); + + switch (m_operationType) { + // These search purposes require a package to be found in the Installed + // Packages + case OperationType::Export: + case OperationType::List: + case OperationType::Uninstall: + case OperationType::Pin: + case OperationType::Upgrade: + case OperationType::Repair: + context.Reporter.Info() + << Resource::String::NoInstalledPackageFound << std::endl; + break; + case OperationType::Completion: + case OperationType::Install: + case OperationType::Search: + case OperationType::Show: + case OperationType::Download: + default: + context.Reporter.Info() << Resource::String::NoPackageFound << std::endl; + break; + } - std::string targetLocale; - if (context.Args.Contains(Execution::Args::Type::Locale)) - { - targetLocale = context.Args.GetArg(Execution::Args::Type::Locale); - } - manifest->ApplyLocale(targetLocale); + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_APPLICATIONS_FOUND); + } +} - context.Add(std::move(manifest.value())); - context.Add(std::move(requestedVersion)); +void EnsureOneMatchFromSearchResult::operator()( + Execution::Context &context) const { + context << EnsureMatchesFromSearchResult(m_operationType); + + if (!context.IsTerminated()) { + auto &searchResult = context.Get(); + + bool operationTargetsInstalled = + m_operationType == OperationType::Upgrade || + m_operationType == OperationType::Uninstall || + m_operationType == OperationType::Repair || + m_operationType == OperationType::Export; + + // Try limiting results to highest priority sources + if (searchResult.Matches.size() > 1 && !operationTargetsInstalled && + ExperimentalFeature::IsEnabled( + ExperimentalFeature::Feature::SourcePriority)) { + // Find the set of matches that have the highest priority + std::vector highestPriorityMatches; + std::optional highestPriority; + + for (const auto &match : searchResult.Matches) { + std::optional priority = GetSourcePriority(match.Package); + + // Optional provides overloads that make empty less than valued and + // empties equal. + if (highestPriority < priority) { + // Current priority is higher; reset. + highestPriority = priority; + highestPriorityMatches.clear(); + } else if (highestPriority == priority) { + // Priority is equal, add to the list. + } else { + // Current priority is lower, ignore the match. + continue; + } + + highestPriorityMatches.emplace_back(match); + } + + if (highestPriorityMatches.size() < searchResult.Matches.size()) { + AICLI_LOG(CLI, Info, + << "Replacing search results with only those from the " + "highest priority [" + << (highestPriority ? std::to_string(highestPriority.value()) + : "none"s) + << "]."); + searchResult.Matches = std::move(highestPriorityMatches); + context.Reporter.Warn() + << Resource::String::MultiplePackagesFoundFilteredBySourcePriority + << std::endl; + } } - void GetManifestFromPackage::operator()(Execution::Context& context) const - { - context << GetManifestWithVersionFromPackage( - context.Args.GetArg(Execution::Args::Type::Version), - context.Args.GetArg(Execution::Args::Type::Channel), - m_considerPins); + if (searchResult.Matches.size() > 1) { + Logging::Telemetry().LogMultiAppMatch(); + + if (operationTargetsInstalled) { + context.Reporter.Warn() + << Resource::String::MultipleInstalledPackagesFound << std::endl; + context << ReportMultiplePackageFoundResult; + } else { + context.Reporter.Warn() + << Resource::String::MultiplePackagesFound << std::endl; + context << ReportMultiplePackageFoundResultWithSource; + } + + AICLI_TERMINATE_CONTEXT( + APPINSTALLER_CLI_ERROR_MULTIPLE_APPLICATIONS_FOUND); } - void VerifyFile::operator()(Execution::Context& context) const - { - std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); + std::shared_ptr package = + searchResult.Matches.at(0).Package; + Logging::Telemetry().LogAppFound( + package->GetProperty(PackageProperty::Name), + package->GetProperty(PackageProperty::Id)); - if (!std::filesystem::exists(path)) - { - context.Reporter.Error() << Resource::String::VerifyFileFailedNotExist(Utility::LocIndView{ path.u8string() }) << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); - } + context.Add(std::move(package)); + } +} - if (std::filesystem::is_directory(path)) - { - context.Reporter.Error() << Resource::String::VerifyFileFailedIsDirectory(Utility::LocIndView{ path.u8string() }) << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED)); - } +void GetManifestWithVersionFromPackage::operator()( + Execution::Context &context) const { + PackageVersionKey key("", m_version, m_channel); + + std::shared_ptr package = + context.Get(); + std::shared_ptr requestedVersion; + auto availableVersions = GetAvailableVersionsForInstalledVersion(package); + + if (m_considerPins) { + bool isPinned = false; + + PinBehavior pinBehavior; + if (context.Args.Contains(Execution::Args::Type::Force)) { + // --force ignores any pins + pinBehavior = PinBehavior::IgnorePins; + } else { + pinBehavior = context.Args.Contains(Execution::Args::Type::IncludePinned) + ? PinBehavior::IncludePinned + : PinBehavior::ConsiderPins; } - void VerifyPath::operator()(Execution::Context& context) const - { - std::filesystem::path path = Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); - - if (!std::filesystem::exists(path)) - { - context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist(Utility::LocIndView{ path.u8string() }) << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); - } + PinningData pinningData{PinningData::Disposition::ReadOnly}; + auto evaluator = pinningData.CreatePinStateEvaluator( + pinBehavior, GetInstalledVersion(package)); + + // TODO: The logic here will probably have to get more difficult once we + // support channels + if (Utility::IsEmptyOrWhitespace(m_version) && + Utility::IsEmptyOrWhitespace(m_channel)) { + requestedVersion = + evaluator.GetLatestAvailableVersionForPins(availableVersions); + + if (!requestedVersion) { + // Check whether we didn't find the latest version because it was pinned + // or because there wasn't one + auto latestVersion = availableVersions->GetLatestVersion(); + if (latestVersion) { + isPinned = true; + } + } + } else { + requestedVersion = availableVersions->GetVersion(key); + isPinned = + evaluator.EvaluatePinType(requestedVersion) != PinType::Unknown; } - void VerifyFileOrUri::operator()(Execution::Context& context) const - { - // Argument requirement is handled elsewhere. - if (!context.Args.Contains(m_arg)) - { - return; - } + if (isPinned) { + if (context.Args.Contains(Execution::Args::Type::Force)) { + AICLI_LOG(CLI, Info, + << "Ignoring pin on package due to --force argument"); + } else { + AICLI_LOG( + CLI, Error, + << "The requested package version is unavailable because of a pin"); + context.Reporter.Error() + << Resource::String::PackageIsPinned << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_PACKAGE_IS_PINNED); + } + } + } else { + // The simple case: Just look up the requested version + requestedVersion = availableVersions->GetVersion(key); + } + + std::optional manifest; + if (requestedVersion) { + manifest = requestedVersion->GetManifest(); + } + + if (!manifest) { + std::ostringstream ssVersionInfo; + if (!m_version.empty()) { + ssVersionInfo << m_version; + } + if (!m_channel.empty()) { + ssVersionInfo << '[' << m_channel << ']'; + } - auto path = context.Args.GetArg(m_arg); + context.Reporter.Error() + << Resource::String::GetManifestResultVersionNotFound( + Utility::LocIndView{ssVersionInfo.str()}) + << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); + } + + Logging::Telemetry().LogManifestFields( + manifest->Id, + manifest->DefaultLocalization.Get(), + manifest->Version); + + std::string targetLocale; + if (context.Args.Contains(Execution::Args::Type::Locale)) { + targetLocale = context.Args.GetArg(Execution::Args::Type::Locale); + } + manifest->ApplyLocale(targetLocale); + + context.Add(std::move(manifest.value())); + context.Add(std::move(requestedVersion)); +} - // try uri first - Uri pathAsUri = nullptr; - try - { - pathAsUri = Uri{ Utility::ConvertToUTF16(path) }; - } - catch (...) {} - - if (pathAsUri) - { - if (pathAsUri.Suspicious()) - { - context.Reporter.Error() << Resource::String::UriNotWellFormed(Utility::LocIndView{ path }) << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); - } - // SchemeName() always returns lower case - else if (L"file" == pathAsUri.SchemeName() && !Utility::CaseInsensitiveStartsWith(path, "file:")) - { - // Uri constructor is smart enough to parse an absolute local file path to file uri. - // In this case, we should continue with VerifyFile. - context << VerifyFile(m_arg); - } - else if (std::find(m_supportedSchemes.begin(), m_supportedSchemes.end(), pathAsUri.SchemeName()) != m_supportedSchemes.end()) - { - // Scheme supported. - return; - } - else - { - context.Reporter.Error() << Resource::String::UriSchemeNotSupported(Utility::LocIndView{ path }) << std::endl; - AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); - } - } - else - { - context << VerifyFile(m_arg); - } - } +void GetManifestFromPackage::operator()(Execution::Context &context) const { + context << GetManifestWithVersionFromPackage( + context.Args.GetArg(Execution::Args::Type::Version), + context.Args.GetArg(Execution::Args::Type::Channel), m_considerPins); +} - void GetManifestFromArg(Execution::Context& context) - { - Logging::Telemetry().LogIsManifestLocal(true); - - context << - VerifyPath(Execution::Args::Type::Manifest) << - [](Execution::Context& context) - { - Manifest::Manifest manifest = Manifest::YamlParser::CreateFromPath(Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Manifest))); - Logging::Telemetry().LogManifestFields(manifest.Id, manifest.DefaultLocalization.Get(), manifest.Version); - - std::string targetLocale; - if (context.Args.Contains(Execution::Args::Type::Locale)) - { - targetLocale = context.Args.GetArg(Execution::Args::Type::Locale); - } - manifest.ApplyLocale(targetLocale); - - context.Add(std::move(manifest)); - }; - } +void VerifyFile::operator()(Execution::Context &context) const { + std::filesystem::path path = + Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); + + if (!std::filesystem::exists(path)) { + context.Reporter.Error() << Resource::String::VerifyFileFailedNotExist( + Utility::LocIndView{path.u8string()}) + << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + } + + if (std::filesystem::is_directory(path)) { + context.Reporter.Error() << Resource::String::VerifyFileFailedIsDirectory( + Utility::LocIndView{path.u8string()}) + << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_DIRECTORY_NOT_SUPPORTED)); + } +} - void ReportPackageIdentity(Execution::Context& context) - { - auto package = context.Get(); - ReportIdentity(context, {}, Resource::String::ReportIdentityFound, package->GetProperty(PackageProperty::Name), package->GetProperty(PackageProperty::Id)); - } +void VerifyPath::operator()(Execution::Context &context) const { + std::filesystem::path path = + Utility::ConvertToUTF16(context.Args.GetArg(m_arg)); - void ReportInstalledPackageVersionIdentity(Execution::Context& context) - { - auto package = context.Get(); - auto version = context.Get(); - ReportIdentity(context, {}, Resource::String::ReportIdentityFound, version->GetProperty(PackageVersionProperty::Name), package ? package->GetProperty(PackageProperty::Id) : version->GetProperty(PackageVersionProperty::Id)); - } + if (!std::filesystem::exists(path)) { + context.Reporter.Error() << Resource::String::VerifyPathFailedNotExist( + Utility::LocIndView{path.u8string()}) + << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND)); + } +} - void ReportManifestIdentity(Execution::Context& context) - { - const auto& manifest = context.Get(); - ReportIdentity(context, {}, Resource::String::ReportIdentityFound, manifest.CurrentLocalization.Get(), manifest.Id); - ShowManifestIcon(context, manifest); +void VerifyFileOrUri::operator()(Execution::Context &context) const { + // Argument requirement is handled elsewhere. + if (!context.Args.Contains(m_arg)) { + return; + } + + auto path = context.Args.GetArg(m_arg); + + // try uri first + Uri pathAsUri = nullptr; + try { + pathAsUri = Uri{Utility::ConvertToUTF16(path)}; + } catch (...) { + } + + if (pathAsUri) { + if (pathAsUri.Suspicious()) { + context.Reporter.Error() + << Resource::String::UriNotWellFormed(Utility::LocIndView{path}) + << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } - - void ReportManifestIdentityWithVersion::operator()(Execution::Context& context) const - { - const auto& manifest = context.Get(); - ReportIdentity(context, m_prefix, m_label, manifest.CurrentLocalization.Get(), manifest.Id, manifest.Version, m_level); - ShowManifestIcon(context, manifest); + // SchemeName() always returns lower case + else if (L"file" == pathAsUri.SchemeName() && + !Utility::CaseInsensitiveStartsWith(path, "file:")) { + // Uri constructor is smart enough to parse an absolute local file path to + // file uri. In this case, we should continue with VerifyFile. + context << VerifyFile(m_arg); + } else if (std::find(m_supportedSchemes.begin(), m_supportedSchemes.end(), + pathAsUri.SchemeName()) != m_supportedSchemes.end()) { + // Scheme supported. + return; + } else { + context.Reporter.Error() + << Resource::String::UriSchemeNotSupported(Utility::LocIndView{path}) + << std::endl; + AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); } + } else { + context << VerifyFile(m_arg); + } +} - void SelectInstaller(Execution::Context& context) - { - bool isUpdate = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); - bool isRepair = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseRepair); +void GetManifestFromArg(Execution::Context &context) { + Logging::Telemetry().LogIsManifestLocal(true); - IPackageVersion::Metadata installationMetadata; + context << VerifyPath(Execution::Args::Type::Manifest) << + [](Execution::Context &context) { + Manifest::Manifest manifest = + Manifest::YamlParser::CreateFromPath(Utility::ConvertToUTF16( + context.Args.GetArg(Execution::Args::Type::Manifest))); + Logging::Telemetry().LogManifestFields( + manifest.Id, + manifest.DefaultLocalization + .Get(), + manifest.Version); - if (isUpdate || isRepair) - { - installationMetadata = context.Get()->GetMetadata(); + std::string targetLocale; + if (context.Args.Contains(Execution::Args::Type::Locale)) { + targetLocale = context.Args.GetArg(Execution::Args::Type::Locale); } + manifest.ApplyLocale(targetLocale); - Manifest::ManifestComparator manifestComparator(GetManifestComparatorOptions(context, installationMetadata)); - auto [installer, inapplicabilities] = manifestComparator.GetPreferredInstaller(context.Get()); - - if (!installer.has_value()) - { - auto onlyInstalledType = std::find(inapplicabilities.begin(), inapplicabilities.end(), Manifest::InapplicabilityFlags::InstalledType); - if (onlyInstalledType != inapplicabilities.end()) - { - if (isRepair) - { - context.Reporter.Info() << Resource::String::RepairDifferentInstallTechnology << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE); - } - else - { - context.Reporter.Info() << Resource::String::UpgradeDifferentInstallTechnology << std::endl; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); - } - } - } + context.Add(std::move(manifest)); + }; +} - if (installer.has_value()) - { - Logging::Telemetry().LogSelectedInstaller( - static_cast(installer->Arch), - installer->Url, - Manifest::InstallerTypeToString(installer->EffectiveInstallerType()), - Manifest::ScopeToString(installer->Scope), - installer->Locale); - } +void ReportPackageIdentity(Execution::Context &context) { + auto package = context.Get(); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, + package->GetProperty(PackageProperty::Name), + package->GetProperty(PackageProperty::Id)); +} - context.Add(installer); - } +void ReportInstalledPackageVersionIdentity(Execution::Context &context) { + auto package = context.Get(); + auto version = context.Get(); + ReportIdentity(context, {}, Resource::String::ReportIdentityFound, + version->GetProperty(PackageVersionProperty::Name), + package ? package->GetProperty(PackageProperty::Id) + : version->GetProperty(PackageVersionProperty::Id)); +} - void EnsureRunningAsAdmin(Execution::Context& context) - { - if (!Runtime::IsRunningAsAdmin()) - { - context.Reporter.Error() << Resource::String::CommandRequiresAdmin; - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN); - } - } +void ReportManifestIdentity(Execution::Context &context) { + const auto &manifest = context.Get(); + ReportIdentity( + context, {}, Resource::String::ReportIdentityFound, + manifest.CurrentLocalization.Get(), + manifest.Id); + ShowManifestIcon(context, manifest); +} - void EnsureFeatureEnabled::operator()(Execution::Context& context) const - { - if (!Settings::ExperimentalFeature::IsEnabled(m_feature)) - { - context.Reporter.Error() - << Resource::String::FeatureDisabledMessage(Utility::LocIndView{ Settings::ExperimentalFeature::GetFeature(m_feature).JsonName() }) - << std::endl; - AICLI_LOG(CLI, Error, << Settings::ExperimentalFeature::GetFeature(m_feature).Name() << " feature is disabled. Execution cancelled."); - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); - } +void ReportManifestIdentityWithVersion::operator()( + Execution::Context &context) const { + const auto &manifest = context.Get(); + ReportIdentity( + context, m_prefix, m_label, + manifest.CurrentLocalization.Get(), + manifest.Id, manifest.Version, m_level); + ShowManifestIcon(context, manifest); +} + +void SelectInstaller(Execution::Context &context) { + bool isUpdate = WI_IsFlagSet( + context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseUpdate); + bool isRepair = WI_IsFlagSet( + context.GetFlags(), Execution::ContextFlag::InstallerExecutionUseRepair); + + IPackageVersion::Metadata installationMetadata; + + if (isUpdate || isRepair) { + installationMetadata = + context.Get()->GetMetadata(); + } + + Manifest::ManifestComparator manifestComparator( + GetManifestComparatorOptions(context, installationMetadata)); + auto [installer, inapplicabilities] = + manifestComparator.GetPreferredInstaller( + context.Get()); + + if (!installer.has_value()) { + auto onlyInstalledType = + std::find(inapplicabilities.begin(), inapplicabilities.end(), + Manifest::InapplicabilityFlags::InstalledType); + if (onlyInstalledType != inapplicabilities.end()) { + if (isRepair) { + context.Reporter.Info() + << Resource::String::RepairDifferentInstallTechnology << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_REPAIR_NOT_APPLICABLE); + } else { + context.Reporter.Info() + << Resource::String::UpgradeDifferentInstallTechnology << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_UPDATE_NOT_APPLICABLE); + } } + } - void SearchSourceUsingManifest(Execution::Context& context) - { - const auto& manifest = context.Get(); - auto source = context.Get(); - - // First try search using ProductId or PackageFamilyName - for (const auto& installer : manifest.Installers) - { - SearchRequest searchRequest; - if (!installer.PackageFamilyName.empty()) - { - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::PackageFamilyName, MatchType::Exact, installer.PackageFamilyName)); - } - else if (!installer.ProductCode.empty()) - { - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, installer.ProductCode)); - } - else if (installer.EffectiveInstallerType() == Manifest::InstallerTypeEnum::Portable) - { - const auto& productCode = Utility::MakeSuitablePathPart(manifest.Id + '_' + source.GetIdentifier()); - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::ProductCode, MatchType::CaseInsensitive, Utility::Normalize(productCode))); - } - else if (installer.EffectiveInstallerType() == Manifest::InstallerTypeEnum::Font) - { - // Font Packages match by Package Id first. - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); - } - - if (!searchRequest.Inclusions.empty()) - { - auto searchResult = source.Search(searchRequest); - - if (!searchResult.Matches.empty()) - { - context.Add(std::move(searchResult)); - return; - } - } - } + if (installer.has_value()) { + Logging::Telemetry().LogSelectedInstaller( + static_cast(installer->Arch), installer->Url, + Manifest::InstallerTypeToString(installer->EffectiveInstallerType()), + Manifest::ScopeToString(installer->Scope), installer->Locale); + } - // If we cannot find a package using PackageFamilyName or ProductId, try manifest Id and Name pair - SearchRequest searchRequest; - searchRequest.Inclusions.emplace_back(PackageMatchFilter(PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); - - // In case there are same Ids from different sources, filter the result using package name - for (const auto& localization : manifest.Localizations) - { - const auto& localizedPackageName = localization.Get(); - if (!localizedPackageName.empty()) - { - searchRequest.Filters.emplace_back(PackageMatchField::Name, MatchType::CaseInsensitive, localizedPackageName); - } - } + context.Add(installer); +} - searchRequest.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Name, MatchType::CaseInsensitive, manifest.DefaultLocalization.Get())); +void EnsureRunningAsAdmin(Execution::Context &context) { + if (!Runtime::IsRunningAsAdmin()) { + context.Reporter.Error() << Resource::String::CommandRequiresAdmin; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_COMMAND_REQUIRES_ADMIN); + } +} - context.Add(source.Search(searchRequest)); - } +void EnsureFeatureEnabled::operator()(Execution::Context &context) const { + if (!Settings::ExperimentalFeature::IsEnabled(m_feature)) { + context.Reporter.Error() + << Resource::String::FeatureDisabledMessage(Utility::LocIndView{ + Settings::ExperimentalFeature::GetFeature(m_feature).JsonName()}) + << std::endl; + AICLI_LOG( + CLI, + Error, << Settings::ExperimentalFeature::GetFeature(m_feature).Name() + << " feature is disabled. Execution cancelled."); + AICLI_TERMINATE_CONTEXT( + APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } +} - void GetInstalledPackageVersion(Execution::Context& context) - { - std::shared_ptr installed = context.Get()->GetInstalled(); - - if (installed) - { - // TODO: This may need to be expanded dramatically to enable targeting across a variety of dimensions (architecture, etc.) - // Alternatively, if we make it easier to see the fully unique package identifiers, we may avoid that need. - if (context.Args.Contains(Execution::Args::Type::TargetVersion)) - { - Repository::PackageVersionKey versionKey{ "", context.Args.GetArg(Execution::Args::Type::TargetVersion) , "" }; - std::shared_ptr installedVersion = installed->GetVersion(versionKey); - - if (!installedVersion) - { - context.Reporter.Error() << Resource::String::GetManifestResultVersionNotFound(Utility::LocIndView{ versionKey.Version }) << std::endl; - // This error maintains consistency with passing an available version to commands - AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); - } - - context.Add(std::move(installedVersion)); - } - else - { - context.Add(installed->GetLatestVersion()); - } - } - else - { - context.Add(nullptr); - } +void SearchSourceUsingManifest(Execution::Context &context) { + const auto &manifest = context.Get(); + auto source = context.Get(); + + // First try search using ProductId or PackageFamilyName + for (const auto &installer : manifest.Installers) { + SearchRequest searchRequest; + if (!installer.PackageFamilyName.empty()) { + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::PackageFamilyName, + MatchType::Exact, installer.PackageFamilyName)); + } else if (!installer.ProductCode.empty()) { + searchRequest.Inclusions.emplace_back( + PackageMatchFilter(PackageMatchField::ProductCode, MatchType::Exact, + installer.ProductCode)); + } else if (installer.EffectiveInstallerType() == + Manifest::InstallerTypeEnum::Portable) { + const auto &productCode = Utility::MakeSuitablePathPart( + manifest.Id + '_' + source.GetIdentifier()); + searchRequest.Inclusions.emplace_back(PackageMatchFilter( + PackageMatchField::ProductCode, MatchType::CaseInsensitive, + Utility::Normalize(productCode))); + } else if (installer.EffectiveInstallerType() == + Manifest::InstallerTypeEnum::Font) { + // Font Packages match by Package Id first. + searchRequest.Inclusions.emplace_back(PackageMatchFilter( + PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); } - void ReportExecutionStage::operator()(Execution::Context& context) const - { - context.SetExecutionStage(m_stage); + if (!searchRequest.Inclusions.empty()) { + auto searchResult = source.Search(searchRequest); + + if (!searchResult.Matches.empty()) { + context.Add(std::move(searchResult)); + return; + } } + } + + // If we cannot find a package using PackageFamilyName or ProductId, try + // manifest Id and Name pair + SearchRequest searchRequest; + searchRequest.Inclusions.emplace_back(PackageMatchFilter( + PackageMatchField::Id, MatchType::CaseInsensitive, manifest.Id)); + + // In case there are same Ids from different sources, filter the result using + // package name + for (const auto &localization : manifest.Localizations) { + const auto &localizedPackageName = + localization.Get(); + if (!localizedPackageName.empty()) { + searchRequest.Filters.emplace_back(PackageMatchField::Name, + MatchType::CaseInsensitive, + localizedPackageName); + } + } - void ShowAppVersions(Execution::Context& context) - { - auto versions = GetAllAvailableVersions(context.Get())->GetVersionKeys(); + searchRequest.Filters.emplace_back(PackageMatchFilter( + PackageMatchField::Name, MatchType::CaseInsensitive, + manifest.DefaultLocalization.Get())); - Execution::TableOutput<2> table(context.Reporter, { Resource::String::ShowVersion, Resource::String::ShowChannel }); - for (const auto& version : versions) - { - table.OutputLine({ version.Version, version.Channel }); - } - table.Complete(); + context.Add(source.Search(searchRequest)); +} + +void GetInstalledPackageVersion(Execution::Context &context) { + std::shared_ptr installed = + context.Get()->GetInstalled(); + + if (installed) { + // TODO: This may need to be expanded dramatically to enable targeting + // across a variety of dimensions (architecture, etc.) + // Alternatively, if we make it easier to see the fully unique package + // identifiers, we may avoid that need. + if (context.Args.Contains(Execution::Args::Type::TargetVersion)) { + Repository::PackageVersionKey versionKey{ + "", context.Args.GetArg(Execution::Args::Type::TargetVersion), ""}; + std::shared_ptr installedVersion = + installed->GetVersion(versionKey); + + if (!installedVersion) { + context.Reporter.Error() + << Resource::String::GetManifestResultVersionNotFound( + Utility::LocIndView{versionKey.Version}) + << std::endl; + // This error maintains consistency with passing an available version to + // commands + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_NO_MANIFEST_FOUND); + } + + context.Add( + std::move(installedVersion)); + } else { + context.Add( + installed->GetLatestVersion()); } + } else { + context.Add(nullptr); + } } -AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, AppInstaller::CLI::Workflow::WorkflowTask::Func f) -{ - return (context << AppInstaller::CLI::Workflow::WorkflowTask(f)); +void ReportExecutionStage::operator()(Execution::Context &context) const { + context.SetExecutionStage(m_stage); } -AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, const AppInstaller::CLI::Workflow::WorkflowTask& task) -{ - if (!context.IsTerminated() || task.ExecuteAlways()) - { +void ShowAppVersions(Execution::Context &context) { + auto versions = + GetAllAvailableVersions(context.Get()) + ->GetVersionKeys(); + + Execution::TableOutput<2> table( + context.Reporter, + {Resource::String::ShowVersion, Resource::String::ShowChannel}); + for (const auto &version : versions) { + table.OutputLine({version.Version, version.Channel}); + } + table.Complete(); +} +} // namespace AppInstaller::CLI::Workflow + +AppInstaller::CLI::Execution::Context & +operator<<(AppInstaller::CLI::Execution::Context &context, + AppInstaller::CLI::Workflow::WorkflowTask::Func f) { + return (context << AppInstaller::CLI::Workflow::WorkflowTask(f)); +} + +AppInstaller::CLI::Execution::Context & +operator<<(AppInstaller::CLI::Execution::Context &context, + const AppInstaller::CLI::Workflow::WorkflowTask &task) { + if (!context.IsTerminated() || task.ExecuteAlways()) { #ifndef AICLI_DISABLE_TEST_HOOKS - if (context.ShouldExecuteWorkflowTask(task)) + if (context.ShouldExecuteWorkflowTask(task)) #endif - { - task.Log(); - task(context); - } + { + task.Log(); + task(context); } - return context; + } + return context; }