winget-cli

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

DscPackageResource.cpp (20828B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "DscPackageResource.h"
      5 #include "DscComposableObject.h"
      6 #include "Resources.h"
      7 #include "Workflows/WorkflowBase.h"
      8 #include "Workflows/ConfigurationFlow.h"
      9 #include "Workflows/InstallFlow.h"
     10 #include "Workflows/UninstallFlow.h"
     11 #include "Workflows/UpdateFlow.h"
     12 #include <winget/PackageVersionSelection.h>
     13 
     14 using namespace AppInstaller::Utility::literals;
     15 using namespace AppInstaller::Repository;
     16 
     17 namespace AppInstaller::CLI
     18 {
     19     namespace
     20     {
     21         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(IdProperty, std::string, Identifier, "id", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageId);
     22         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(SourceProperty, std::string, Source, "source", DscComposablePropertyFlag::CopyToOutput, Resource::String::DscResourcePropertyDescriptionPackageSource);
     23         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(VersionProperty, std::string, Version, "version", Resource::String::DscResourcePropertyDescriptionPackageVersion);
     24         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(MatchOptionProperty, std::string, MatchOption, "matchOption", Resource::String::DscResourcePropertyDescriptionPackageMatchOption, ({ "equals", "equalsCaseInsensitive", "startsWithCaseInsensitive", "containsCaseInsensitive" }), "equalsCaseInsensitive");
     25         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_DEFAULT(UseLatestProperty, bool, UseLatest, "useLatest", Resource::String::DscResourcePropertyDescriptionPackageUseLatest, "false");
     26         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_ENUM(InstallModeProperty, std::string, InstallMode, "installMode", Resource::String::DscResourcePropertyDescriptionPackageInstallMode, ({ "default", "silent", "interactive" }), "silent");
     27         WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(AcceptAgreementsProperty, bool, AcceptAgreements, "acceptAgreements", Resource::String::DscResourcePropertyDescriptionAcceptAgreements);
     28 
     29         // TODO: To support Scope on this resource:
     30         //  1. Change the installed source to pull in all package info for both scopes by default
     31         //  2. Change the installed source open in workflows to always open for everything, regardless of scope
     32         //  3. Improve correlation handling if needed for cross-scope package installations
     33         //  4. Update the test EXE installer to handle being installed for both scopes
     34         using PackageResourceObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, IdProperty, SourceProperty, VersionProperty, MatchOptionProperty, UseLatestProperty, InstallModeProperty, AcceptAgreementsProperty>;
     35 
     36         std::optional<MatchType> ToMatchType(const std::optional<std::string>& value)
     37         {
     38             if (!value)
     39             {
     40                 return std::nullopt;
     41             }
     42 
     43             std::string lowerValue = Utility::ToLower(value.value());
     44 
     45             if (lowerValue == "equals")
     46             {
     47                 return MatchType::Exact;
     48             }
     49             else if (lowerValue == "equals""case""insensitive")
     50             {
     51                 return MatchType::CaseInsensitive;
     52             }
     53             else if (lowerValue == "starts""with""case""insensitive")
     54             {
     55                 return MatchType::StartsWith;
     56             }
     57             else if (lowerValue == "contains""case""insensitive")
     58             {
     59                 return MatchType::Substring;
     60             }
     61 
     62             THROW_HR(E_INVALIDARG);
     63         }
     64 
     65         struct PackageFunctionData
     66         {
     67             PackageFunctionData(Execution::Context& context, const std::optional<Json::Value>& json, bool ignoreFieldRequirements = false) :
     68                 Input(json, ignoreFieldRequirements),
     69                 ParentContext(context)
     70             {
     71                 Reset();
     72             }
     73 
     74             const PackageResourceObject Input;
     75             PackageResourceObject Output;
     76             Execution::Context& ParentContext;
     77             std::unique_ptr<Execution::Context> SubContext;
     78 
     79             // Reset the state that is modified by Get
     80             void Reset()
     81             {
     82                 Output = Input.CopyForOutput();
     83 
     84                 SubContext = ParentContext.CreateSubContext();
     85                 SubContext->SetFlags(Execution::ContextFlag::DisableInteractivity);
     86 
     87                 if (Input.AcceptAgreements().value_or(false))
     88                 {
     89                     SubContext->Args.AddArg(Execution::Args::Type::AcceptSourceAgreements);
     90                     SubContext->Args.AddArg(Execution::Args::Type::AcceptPackageAgreements);
     91                 }
     92             }
     93 
     94             void PrepareSubContextInputs()
     95             {
     96                 if (Input.Source())
     97                 {
     98                     SubContext->Args.AddArg(Execution::Args::Type::Source, Input.Source().value());
     99                 }
    100             }
    101 
    102             // Fills the Output object with the current state
    103             bool Get()
    104             {
    105                 PrepareSubContextInputs();
    106 
    107                 *SubContext <<
    108                     Workflow::ReportExecutionStage(Workflow::ExecutionStage::Discovery) <<
    109                     Workflow::OpenSource() <<
    110                     Workflow::OpenCompositeSource(Workflow::DetermineInstalledSource(*SubContext), false, CompositeSearchBehavior::AllPackages);
    111 
    112                 if (SubContext->IsTerminated())
    113                 {
    114                     ParentContext.Terminate(SubContext->GetTerminationHR());
    115                     return false;
    116                 }
    117 
    118                 // Do a manual search of the now opened source
    119                 Source& source = SubContext->Get<Execution::Data::Source>();
    120                 MatchType matchType = ToMatchType(Input.MatchOption()).value_or(MatchType::CaseInsensitive);
    121 
    122                 SearchRequest request;
    123                 request.Filters.emplace_back(PackageMatchFilter(PackageMatchField::Id, matchType, Input.Identifier().value()));
    124 
    125                 SearchResult result = source.Search(request);
    126                 SubContext->Add<Execution::Data::SearchResult>(result);
    127 
    128                 if (result.Matches.empty())
    129                 {
    130                     Output.Exist(false);
    131                 }
    132                 else if (result.Matches.size() > 1)
    133                 {
    134                     AICLI_LOG(Config, Warning, << "Found " << result.Matches.size() << " matches when searching for '" << Input.Identifier().value() << "'");
    135                     Output.Exist(false);
    136                 }
    137                 else
    138                 {
    139                     auto& package = result.Matches.front().Package;
    140                     SubContext->Add<Execution::Data::Package>(package);
    141 
    142                     auto installedPackage = package->GetInstalled();
    143 
    144                     // Fill Output and SubContext
    145                     Output.Exist(static_cast<bool>(installedPackage));
    146                     Output.Identifier(package->GetProperty(PackageProperty::Id));
    147 
    148                     if (installedPackage)
    149                     {
    150                         auto versionKeys = installedPackage->GetVersionKeys();
    151                         AICLI_LOG(CLI, Verbose, << "Package::Get found " << versionKeys.size() << " installed versions");
    152 
    153                         std::shared_ptr<Repository::IPackageVersion> installedVersion;
    154 
    155                         // Find the specific version provided if possible
    156                         if (Input.Version())
    157                         {
    158                             Utility::Version inputVersion{ Input.Version().value() };
    159 
    160                             for (const auto& key : versionKeys)
    161                             {
    162                                 if (inputVersion == Utility::Version{ key.Version })
    163                                 {
    164                                     installedVersion = installedPackage->GetVersion(key);
    165                                     break;
    166                                 }
    167                             }
    168                         }
    169 
    170                         if (!installedVersion)
    171                         {
    172                             installedVersion = installedPackage->GetLatestVersion();
    173                         }
    174 
    175                         if (installedVersion)
    176                         {
    177                             Output.Version(installedVersion->GetProperty(PackageVersionProperty::Version));
    178                         }
    179 
    180                         auto data = Repository::GetLatestApplicableVersion(package);
    181                         Output.UseLatest(!data.UpdateAvailable);
    182                     }
    183                 }
    184 
    185                 AICLI_LOG(CLI, Verbose, << "Package::Get found:\n" << Json::writeString(Json::StreamWriterBuilder{}, Output.ToJson()));
    186                 return true;
    187             }
    188 
    189             void Uninstall()
    190             {
    191                 AICLI_LOG(CLI, Verbose, << "Package::Uninstall invoked");
    192 
    193                 if (Input.Version())
    194                 {
    195                     SubContext->Args.AddArg(Execution::Args::Type::TargetVersion, Input.Version().value());
    196                 }
    197                 else
    198                 {
    199                     SubContext->Args.AddArg(Execution::Args::Type::AllVersions);
    200                 }
    201 
    202                 *SubContext <<
    203                     Workflow::UninstallSinglePackage;
    204 
    205                 if (SubContext->IsTerminated())
    206                 {
    207                     ParentContext.Terminate(SubContext->GetTerminationHR());
    208                     return;
    209                 }
    210 
    211                 Output.Exist(false);
    212                 Output.Version(std::nullopt);
    213                 Output.UseLatest(std::nullopt);
    214             }
    215 
    216             void Install(bool allowDowngrade = false)
    217             {
    218                 AICLI_LOG(CLI, Verbose, << "Package::Install invoked");
    219 
    220                 if (Input.Version())
    221                 {
    222                     SubContext->Args.AddArg(Execution::Args::Type::Version, Input.Version().value());
    223                 }
    224 
    225                 *SubContext <<
    226                     Workflow::SelectSinglePackageVersionForInstallOrUpgrade(Workflow::OperationType::Install, allowDowngrade) <<
    227                     Workflow::InstallSinglePackage;
    228 
    229                 if (SubContext->IsTerminated())
    230                 {
    231                     ParentContext.Terminate(SubContext->GetTerminationHR());
    232                     return;
    233                 }
    234 
    235                 Output.Exist(true);
    236 
    237                 Output.Version(std::nullopt);
    238                 if (SubContext->Contains(Execution::Data::PackageVersion))
    239                 {
    240                     const auto& packageVersion = SubContext->Get<Execution::Data::PackageVersion>();
    241                     if (packageVersion)
    242                     {
    243                         Output.Version(packageVersion->GetProperty(Repository::PackageVersionProperty::Version));
    244                     }
    245                 }
    246 
    247                 Output.UseLatest(std::nullopt);
    248             }
    249 
    250             void Reinstall()
    251             {
    252                 AICLI_LOG(CLI, Verbose, << "Package::Reinstall invoked");
    253 
    254                 SubContext->Args.AddArg(Execution::Args::Type::UninstallPrevious);
    255 
    256                 Install(true);
    257             }
    258 
    259             // Determines if the current Output values match the Input values state.
    260             bool Test()
    261             {
    262                 // Need to populate Output before calling
    263                 THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value());
    264 
    265                 if (Input.ShouldExist())
    266                 {
    267                     if (Output.Exist().value())
    268                     {
    269                         AICLI_LOG(CLI, Verbose, << "Package::Test needed to inspect these properties: Version(" << TestVersion() << "), Latest(" << TestLatest() << ")");
    270                         return TestVersion() && TestLatest();
    271                     }
    272                     else
    273                     {
    274                         AICLI_LOG(CLI, Verbose, << "Package::Test was false because the package was not installed");
    275                         return false;
    276                     }
    277                 }
    278                 else
    279                 {
    280                     AICLI_LOG(CLI, Verbose, << "Package::Test desired the package to not exist, and it " << (Output.Exist().value() ? "did" : "did not"));
    281                     return !Output.Exist().value();
    282                 }
    283             }
    284 
    285             Json::Value DiffJson()
    286             {
    287                 // Need to populate Output before calling
    288                 THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value());
    289 
    290                 Json::Value result{ Json::ValueType::arrayValue };
    291 
    292                 if (Input.ShouldExist() != Output.Exist().value())
    293                 {
    294                     result.append(std::string{ StandardExistProperty::Name() });
    295                 }
    296                 else
    297                 {
    298                     if (!TestVersion())
    299                     {
    300                         result.append(std::string{ VersionProperty::Name() });
    301                     }
    302 
    303                     if (!TestLatest())
    304                     {
    305                         result.append(std::string{ UseLatestProperty::Name() });
    306                     }
    307                 }
    308 
    309                 return result;
    310             }
    311 
    312             bool TestVersion()
    313             {
    314                 if (Input.Version())
    315                 {
    316                     if (Output.Version())
    317                     {
    318                         return Utility::Version{ Input.Version().value() } == Utility::Version{ Output.Version().value() };
    319                     }
    320                     else
    321                     {
    322                         return false;
    323                     }
    324                 }
    325                 else
    326                 {
    327                     return true;
    328                 }
    329             }
    330 
    331             bool TestLatest()
    332             {
    333                 if (Input.UseLatest() && Input.UseLatest().value())
    334                 {
    335                     if (Output.UseLatest())
    336                     {
    337                         return Output.UseLatest().value();
    338                     }
    339                     else
    340                     {
    341                         return false;
    342                     }
    343                 }
    344                 else
    345                 {
    346                     return true;
    347                 }
    348             }
    349         };
    350     }
    351 
    352     DscPackageResource::DscPackageResource(std::string_view parent) :
    353         DscCommandBase(parent, "package", DscResourceKind::Resource,
    354             DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema,
    355             DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist | DscFunctionModifiers::ReturnsStateAndDiff)
    356     {
    357     }
    358 
    359     Resource::LocString DscPackageResource::ShortDescription() const
    360     {
    361         return Resource::String::DscPackageResourceShortDescription;
    362     }
    363 
    364     Resource::LocString DscPackageResource::LongDescription() const
    365     {
    366         return Resource::String::DscPackageResourceLongDescription;
    367     }
    368 
    369     std::string DscPackageResource::ResourceType() const
    370     {
    371         return "Package";
    372     }
    373 
    374     void DscPackageResource::ResourceFunctionGet(Execution::Context& context) const
    375     {
    376         if (auto json = GetJsonFromInput(context))
    377         {
    378             PackageFunctionData data{ context, json };
    379 
    380             if (!data.Get())
    381             {
    382                 return;
    383             }
    384 
    385             WriteJsonOutputLine(context, data.Output.ToJson());
    386         }
    387     }
    388 
    389     void DscPackageResource::ResourceFunctionSet(Execution::Context& context) const
    390     {
    391         if (auto json = GetJsonFromInput(context))
    392         {
    393             PackageFunctionData data{ context, json };
    394 
    395             if (!data.Get())
    396             {
    397                 return;
    398             }
    399 
    400             // Capture the diff before updating the output
    401             auto diff = data.DiffJson();
    402 
    403             if (!data.Test())
    404             {
    405                 if (data.Input.ShouldExist())
    406                 {
    407                     if (data.Output.Exist().value())
    408                     {
    409                         if (!data.TestLatest())
    410                         {
    411                             // Install will swap to update flow
    412                             AICLI_LOG(CLI, Info, << "Installing package to update to latest");
    413                             data.Install();
    414                         }
    415                         else // (!data.TestVersion())
    416                         {
    417                             Utility::Version inputVersion{ data.Input.Version().value() };
    418                             Utility::Version outputVersion{ data.Output.Version().value() };
    419 
    420                             if (outputVersion < inputVersion)
    421                             {
    422                                 // Install will swap to update flow
    423                                 AICLI_LOG(CLI, Info, << "Installing package to update to desired version");
    424                                 data.Install();
    425                             }
    426                             else
    427                             {
    428                                 AICLI_LOG(CLI, Info, << "Reinstalling package to downgrade to desired version");
    429                                 data.Reinstall();
    430                             }
    431                         }
    432                     }
    433                     else
    434                     {
    435                         AICLI_LOG(CLI, Info, << "Installing package as it was not found");
    436                         data.Install();
    437                     }
    438                 }
    439                 else
    440                 {
    441                     AICLI_LOG(CLI, Info, << "Uninstalling package as desired");
    442                     data.Uninstall();
    443                 }
    444 
    445                 if (data.SubContext->IsTerminated())
    446                 {
    447                     return;
    448                 }
    449             }
    450 
    451             WriteJsonOutputLine(context, data.Output.ToJson());
    452             WriteJsonOutputLine(context, diff);
    453         }
    454     }
    455 
    456     void DscPackageResource::ResourceFunctionTest(Execution::Context& context) const
    457     {
    458         if (auto json = GetJsonFromInput(context))
    459         {
    460             PackageFunctionData data{ context, json };
    461 
    462             if (!data.Get())
    463             {
    464                 return;
    465             }
    466 
    467             data.Output.InDesiredState(data.Test());
    468 
    469             WriteJsonOutputLine(context, data.Output.ToJson());
    470             WriteJsonOutputLine(context, data.DiffJson());
    471         }
    472     }
    473 
    474     void DscPackageResource::ResourceFunctionExport(Execution::Context& context) const
    475     {
    476         auto json = GetJsonFromInput(context, false);
    477         PackageFunctionData data{ context, json, true };
    478 
    479         data.PrepareSubContextInputs();
    480 
    481         if (!data.Input.UseLatest().value_or(true))
    482         {
    483             data.SubContext->Args.AddArg(Execution::Args::Type::IncludeVersions);
    484         }
    485 
    486         data.SubContext->Args.AddArg(Execution::Args::Type::ConfigurationExportAll);
    487 
    488         *data.SubContext <<
    489             Workflow::SearchSourceForPackageExport;
    490 
    491         if (data.SubContext->IsTerminated())
    492         {
    493             context.Terminate(data.SubContext->GetTerminationHR());
    494             return;
    495         }
    496 
    497         const auto& packageCollection = data.SubContext->Get<Execution::Data::PackageCollection>();
    498 
    499         for (const auto& source : packageCollection.Sources)
    500         {
    501             for (const auto& package : source.Packages)
    502             {
    503                 PackageResourceObject output;
    504 
    505                 output.Identifier(package.Id);
    506                 output.Source(source.Details.Name);
    507 
    508                 if (!package.VersionAndChannel.GetVersion().IsEmpty())
    509                 {
    510                     output.Version(package.VersionAndChannel.GetVersion().ToString());
    511                 }
    512 
    513                 // TODO: Exporting scope requires one or more of the following:
    514                 //  1. Support for "OrUnknown" scope variants during Set (and workflows)
    515                 //  2. Tracking scope intent as we do for some other installer properties
    516                 //  3. Checking for the availability of the current scope in the package
    517 
    518                 WriteJsonOutputLine(context, output.ToJson());
    519             }
    520         }
    521     }
    522 
    523     void DscPackageResource::ResourceFunctionSchema(Execution::Context& context) const
    524     {
    525         WriteJsonOutputLine(context, PackageResourceObject::Schema(ResourceType()));
    526     }
    527 }