winget-cli

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

commit 5a1631facc9b419d5ae96bd00dfafccc62f4bdeb
parent 457f84bf5ac7947a12c8b65879001cce1cdbfc43
Author: Ruben Guerrero <rubengu@microsoft.com>
Date:   Thu,  2 May 2024 13:36:13 -0700

Configure export command (#4434)

This PR introduces the `configure export` command as an exprimental
feature. This is mostly a proof of concept and should not be considered
a full feature.

#### Scenario A - Create a configuration to install a winget package.
Use `--pacakgeId` with the package identifier of an application in the
winget source to produce a configuration file that uses the
`Microsoft.WinGet.DSC/WinGetPackage` resource to install the package via
winget. Right now, we don't validate if the package id exists and will
just copy what the user sets into the settings of the resource.

#### Scenario B - 'Export' the configuration from the specified
resource.
Use both `--module` and `--resource` to get the configuration of a
resource and add it to the configuration file. Internally, configuration
will install the module (if not installed already) and call Get on the
resource. We will then try to serialize its property into the
configuration file. Current limitation is that if a resource has a
required setting (like WinGetPackage required Id) it won't work. If the
resource is not found in the gallery a retry will be performed allowing
prereleased modules in the case it exists.

#### Scenario C - Mix of A and B
If `--packageId`, `--module` and `--resource` are used, configure export
will produce two resources. The first one is the `WinGetPackage` for the
specified package. The second one is the same as in B, with the
difference that it includes a dependency of the previously created
`WinGetPackage` resource.

#### Scenario D - Configuration file already exists.
If the file passed to the `--output` parameters already exists and is a
valid configuration file, the resources will be appended. There is
currently not validation into the correctness of this, so it can result
in a configuration with resources with the same id.

For example `winget configure export --packageId Microsoft.AppInstaller
--module Microsoft.WinGet.DSC --resource WinGetUserSettings -o
test_export.yml` would produce the following file

```
# Created using winget configure export 1.8.0-preview
# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/0.2
properties:
  configurationVersion: 0.2
  resources:
  - resource: Microsoft.WinGet.DSC/WinGetPackage
    id: Microsoft.AppInstaller
    directives:
      description: Install Microsoft.AppInstaller
      allowPrerelease: true
    settings:
      id: Microsoft.AppInstaller
      source: winget
  - resource: Microsoft.WinGet.DSC/WinGetUserSettings
    dependsOn:
    - Microsoft.AppInstaller
    directives:
      description: Configure Microsoft.AppInstaller
    settings:
      Settings:
        experimentalFeatures:
          configureSelfElevate: true
        installBehavior:
          preferences:
            locale:
            - en-US
            - fr-FR
        $schema: https://aka.ms/winget-settings.schema.json
      Action: Full
```


###### Microsoft Reviewers: [Open in
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/microsoft/winget-cli/pull/4434)
Diffstat:
M.github/actions/spelling/expect.txt | 2++
Mdoc/Settings.md | 17++++++++++++++---
Mdoc/windows/package-manager/winget/returnCodes.md | 4++++
Mschemas/JSON/settings/settings.schema.0.2.json | 5+++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 2++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Msrc/AppInstallerCLICore/Argument.cpp | 10++++++++--
Msrc/AppInstallerCLICore/Command.h | 2++
Msrc/AppInstallerCLICore/Commands/ConfigureCommand.cpp | 3++-
Msrc/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp | 2+-
Msrc/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp | 1-
Msrc/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp | 1-
Asrc/AppInstallerCLICore/ConfigureExportCommand.cpp | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/ConfigureExportCommand.h | 23+++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionArgs.h | 11+++++++++--
Msrc/AppInstallerCLICore/Resources.h | 16++++++++++++++--
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 458++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.h | 18++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Constants.cs | 1+
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 8++++++--
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 3++-
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
Msrc/AppInstallerSharedLib/Errors.cpp | 1+
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 1+
Msrc/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs | 7++++++-
Asrc/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msrc/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs | 1+
Asrc/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs | 14+++++++-------
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer.h | 7++++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp | 34+++++++++++++++++++++++++++++++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h | 4++++
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters | 8++++++--
39 files changed, 1106 insertions(+), 158 deletions(-)

diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -161,6 +161,7 @@ GRPICONDIR GRPICONDIRENTRY guiddef Hackathon +hashtables helplib helplibrary hhx @@ -217,6 +218,7 @@ JToken JValue Kaido KNOWNFOLDERID +kool ktf ldcase learnxinyminutes diff --git a/doc/Settings.md b/doc/Settings.md @@ -332,11 +332,11 @@ Currently, this means that properly attributed configuration units (and only tho "experimentalFeatures": { "configureSelfElevate": true }, -``` - +``` + ### storeDownload -This feature enables packages to be downloaded from the Microsoft Store. +This feature enables packages to be downloaded from the Microsoft Store. You can enable the feature as shown below. ```json @@ -344,3 +344,14 @@ You can enable the feature as shown below. "storeDownload": true }, ``` + +### configureExport + +This feature enables exporting a configuration file. +You can enable the feature as shown below. + +```json + "experimentalFeatures": { + "configureExport": true + }, +``` diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -195,6 +195,9 @@ Installation failed. Restart your PC then try again. | | 0x8A15C00C | -1978286068 | WINGET_CONFIG_ERROR_SET_DEPENDENCY_CYCLE | The dependency graph contains a cycle which cannot be resolved. | | 0x8A15C00D | -1978286067 | WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE | The configuration has an invalid field value. | | 0x8A15C00E | -1978286066 | WINGET_CONFIG_ERROR_MISSING_FIELD | The configuration is missing a field. | +| 0x8A15C00F | -1978286065 | WINGET_CONFIG_ERROR_TEST_FAILED | Some of the configuration units failed while testing their state. | +| 0x8A15C010 | -1978286064 | WINGET_CONFIG_ERROR_TEST_NOT_RUN | Configuration state was not tested. | +| 0x8A15C011 | -1978286063 | WINGET_CONFIG_ERROR_GET_FAILED | The configuration unit failed getting its properties. | ## Configuration Processor Errors @@ -211,3 +214,4 @@ Installation failed. Restart your PC then try again. | | 0x8A15C109 | -1978285815 | WINGET_CONFIG_ERROR_UNIT_INVOKE_INVALID_RESULT | The configuration unit returned an unexpected result during execution. | | 0x8A15C110 | -1978285814 | WINGET_CONFIG_ERROR_UNIT_SETTING_CONFIG_ROOT | A unit contains a setting that requires the config root. | | 0x8A15C111 | -1978285813 | WINGET_CONFIG_ERROR_UNIT_IMPORT_MODULE_ADMIN | Loading the module for the configuration unit failed because it requires administrator privileges to run. | +| 0x8A15C112 | -1978285812 | WINGET_CONFIG_ERROR_NOT_SUPPORTED_BY_PROCESSOR | Operation is not supported by the configuration processor. | diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -290,6 +290,11 @@ "description": "Enable support for downloading packages from the Microsoft Store", "type": "boolean", "default": false + }, + "configureExport": { + "description": "Enable support for the configure export command", + "type": "boolean", + "default": false } } } diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -384,6 +384,7 @@ <ClInclude Include="ConfigurationCommon.h" /> <ClInclude Include="ConfigurationContext.h" /> <ClInclude Include="ConfigurationWingetDscModuleUnitValidation.h" /> + <ClInclude Include="ConfigureExportCommand.h" /> <ClInclude Include="ContextOrchestrator.h" /> <ClInclude Include="COMContext.h" /> <ClInclude Include="Public\ConfigurationSetProcessorFactoryRemoting.h" /> @@ -446,6 +447,7 @@ <ClCompile Include="ConfigurationDynamicRuntimeFactory.cpp" /> <ClCompile Include="ConfigurationSetProcessorFactoryRemoting.cpp" /> <ClCompile Include="ConfigurationWingetDscModuleUnitValidation.cpp" /> + <ClCompile Include="ConfigureExportCommand.cpp" /> <ClCompile Include="ContextOrchestrator.cpp" /> <ClCompile Include="Workflows\ConfigurationFlow.cpp" /> <ClCompile Include="Workflows\DependenciesFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -251,6 +251,9 @@ <ClInclude Include="ConfigurationWingetDscModuleUnitValidation.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="ConfigureExportCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -472,6 +475,9 @@ <ClCompile Include="ConfigurationDynamicRuntimeFactory.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="ConfigureExportCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -141,8 +141,6 @@ namespace AppInstaller::CLI return { type, "position"_liv }; // Export Command - case Execution::Args::Type::OutputFile: - return { type, "output"_liv, 'o' }; case Execution::Args::Type::IncludeVersions: return { type, "include-versions"_liv }; @@ -211,6 +209,12 @@ namespace AppInstaller::CLI return { type, "disable"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::StubType }; case Execution::Args::Type::ConfigurationModulePath: return { type, "module-path"_liv }; + case Execution::Args::Type::ConfigurationExportPackageId: + return { type, "package-id"_liv }; + case Execution::Args::Type::ConfigurationExportModule: + return { type, "module"_liv }; + case Execution::Args::Type::ConfigurationExportResource: + return { type, "resource"_liv }; // Download command case Execution::Args::Type::DownloadDirectory: @@ -237,6 +241,8 @@ namespace AppInstaller::CLI return { type, "open-logs"_liv, "logs"_liv }; case Execution::Args::Type::Force: return { type, "force"_liv, ArgTypeCategory::CopyFlagToSubContext }; + case Execution::Args::Type::OutputFile: + return { type, "output"_liv, 'o' }; case Execution::Args::Type::DependencySource: return { type, "dependency-source"_liv, ArgTypeCategory::ExtendedSource }; diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -57,6 +57,8 @@ namespace AppInstaller::CLI Command(name, {}, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, Settings::TogglePolicy::Policy::None, outputFlags) {} Command(std::string_view name, std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility) : Command(name, aliases, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} + Command(std::string_view name, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : + Command(name, {}, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector<std::string_view> aliases, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : Command(name, aliases, parent, Command::Visibility::Show, feature) {} Command(std::string_view name, std::vector<std::string_view> aliases, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -5,6 +5,7 @@ #include "ConfigureShowCommand.h" #include "ConfigureTestCommand.h" #include "ConfigureValidateCommand.h" +#include "ConfigureExportCommand.h" #include "Workflows/ConfigurationFlow.h" #include "Workflows/MSStoreInstallerHandler.h" #include "ConfigurationCommon.h" @@ -25,6 +26,7 @@ namespace AppInstaller::CLI std::make_unique<ConfigureShowCommand>(FullName()), std::make_unique<ConfigureTestCommand>(FullName()), std::make_unique<ConfigureValidateCommand>(FullName()), + std::make_unique<ConfigureExportCommand>(FullName()), }); } @@ -51,7 +53,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -30,13 +30,13 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureShowCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#show"_liv; } void ConfigureShowCommand::ExecuteInternal(Execution::Context& context) const { context << + VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << CreateConfigurationProcessor << OpenConfigurationSet << diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -30,7 +30,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureTestCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#test"_liv; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -29,7 +29,6 @@ namespace AppInstaller::CLI Utility::LocIndView ConfigureValidateCommand::HelpLink() const { - // TODO: Make this exist return "https://aka.ms/winget-command-configure#validate"_liv; } diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigureExportCommand.h" +#include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" + +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + std::vector<Argument> ConfigureExportCommand::GetArguments() const + { + return { + Argument{ Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, true }, + Argument{ Execution::Args::Type::ConfigurationExportPackageId, Resource::String::ConfigureExportPackageId }, + Argument{ Execution::Args::Type::ConfigurationExportModule, Resource::String::ConfigureExportModule }, + Argument{ Execution::Args::Type::ConfigurationExportResource, Resource::String::ConfigureExportResource }, + Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath }, + }; + } + + Resource::LocString ConfigureExportCommand::ShortDescription() const + { + return { Resource::String::ConfigureExportCommandShortDescription }; + } + + Resource::LocString ConfigureExportCommand::LongDescription() const + { + return { Resource::String::ConfigureExportCommandLongDescription }; + } + + Utility::LocIndView ConfigureExportCommand::HelpLink() const + { + return "https://aka.ms/winget-command-configure#export"_liv; + } + + void ConfigureExportCommand::ExecuteInternal(Execution::Context& context) const + { + context << + VerifyIsFullPackage << + CreateConfigurationProcessor << + CreateOrOpenConfigurationSet << + AddWinGetPackageAndResource << + WriteConfigFile; + } + + void ConfigureExportCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + Configuration::ValidateCommonArguments(execArgs); + + bool validInputArgs = false; + if (execArgs.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource) || + execArgs.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + validInputArgs = true; + } + + if (!validInputArgs) + { + throw CommandException(Resource::String::ConfigureExportArgumentError); + } + } +} diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.h b/src/AppInstallerCLICore/ConfigureExportCommand.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct ConfigureExportCommand final : public Command + { + ConfigureExportCommand(std::string_view parent) : Command("export", parent, Settings::ExperimentalFeature::Feature::ConfigureExport) {} + + std::vector<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + }; +} diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -79,7 +79,6 @@ namespace AppInstaller::CLI::Execution Position, // Export Command - OutputFile, IncludeVersions, // Import Command @@ -126,6 +125,9 @@ namespace AppInstaller::CLI::Execution ConfigurationEnable, ConfigurationDisable, ConfigurationModulePath, + ConfigurationExportPackageId, + ConfigurationExportModule, + ConfigurationExportResource, // Common arguments NoVT, // Disable VirtualTerminal outputs @@ -138,6 +140,7 @@ namespace AppInstaller::CLI::Execution Wait, // Prompts the user to press any key before exiting OpenLogs, // Opens the default logs directory after executing the command Force, // Forces the execution of the workflow with non security related issues + OutputFile, DependencySource, // Index source to be queried against for finding dependencies CustomHeader, // Optional Rest source header @@ -159,7 +162,11 @@ namespace AppInstaller::CLI::Execution Max }; - bool Contains(Type arg) const { return (m_parsedArgs.count(arg) != 0); } + template<typename... T, std::enable_if_t<(... && std::is_same_v<T, Args::Type>), bool> = true> + bool Contains(T... arg) const + { + return (... && (m_parsedArgs.count(arg) != 0)); + } const std::vector<std::string>* GetArgs(Type arg) const { diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -67,6 +67,9 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnableMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationEnablingMessage); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportAddingToFile); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportFailed); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationExportSuccessful); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToApply); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToGetDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFailedToTest); @@ -79,6 +82,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileInvalidYAML); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileVersionUnknown); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingDetails); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingResourceSettings); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInform); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInitializing); @@ -132,6 +136,14 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationWarningValueTruncated); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportArgumentError); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportModule); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportPackageId); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportResource); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitInstallDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureTestCommandLongDescription); @@ -535,6 +547,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceExportCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListAdditionalSource); @@ -543,12 +556,12 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceListData); + WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListField); WINGET_DEFINE_RESOURCE_STRINGID(SourceListIdentifier); WINGET_DEFINE_RESOURCE_STRINGID(SourceListName); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoneFound); WINGET_DEFINE_RESOURCE_STRINGID(SourceListNoSources); - WINGET_DEFINE_RESOURCE_STRINGID(SourceListExplicit); WINGET_DEFINE_RESOURCE_STRINGID(SourceListTrustLevel); WINGET_DEFINE_RESOURCE_STRINGID(SourceListType); WINGET_DEFINE_RESOURCE_STRINGID(SourceListUpdated); @@ -563,7 +576,6 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceRemoveOne); WINGET_DEFINE_RESOURCE_STRINGID(SourceRequiresAuthentication); - WINGET_DEFINE_RESOURCE_STRINGID(SourceExplicitArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetAll); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SourceResetCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -33,12 +33,21 @@ namespace AppInstaller::CLI::Workflow } #endif - namespace + namespace anon { constexpr std::wstring_view s_Directive_Description = L"description"; constexpr std::wstring_view s_Directive_Module = L"module"; constexpr std::wstring_view s_Directive_AllowPrerelease = L"allowPrerelease"; + constexpr std::wstring_view s_Unit_WinGetPackage = L"WinGetPackage"; + + constexpr std::wstring_view s_Module_WinGetClient = L"Microsoft.WinGet.DSC"; + + constexpr std::wstring_view s_Setting_Id = L"id"; + constexpr std::wstring_view s_Setting_Source = L"source"; + + constexpr std::wstring_view s_WinGetSource = L"winget"; + Logging::Level ConvertLevel(DiagnosticLevel level) { switch (level) @@ -918,6 +927,254 @@ namespace AppInstaller::CLI::Workflow return validationOrder; } + + void SetNameAndOrigin(ConfigurationSet& set, std::filesystem::path& absolutePath) + { + // TODO: Consider how to properly determine a good value for name and origin. + set.Name(absolutePath.filename().wstring()); + set.Origin(absolutePath.parent_path().wstring()); + set.Path(absolutePath.wstring()); + } + + void OpenConfigurationSet(Execution::Context& context, const std::string& argPath, bool allowRemote) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); + + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + bool isRemote = Utility::IsUrlRemote(argPath); + std::filesystem::path absolutePath; + Streams::IInputStream inputStream = nullptr; + + if (isRemote) + { + if (!allowRemote) + { + AICLI_LOG(Config, Error, << "Remote files are not supported"); + AICLI_TERMINATE_CONTEXT(ERROR_NOT_SUPPORTED); + } + + std::ostringstream stringStream; + ProgressCallback emptyCallback; + Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); + + auto strContent = stringStream.str(); + std::vector<BYTE> byteContent{ strContent.begin(), strContent.end() }; + + Streams::InMemoryRandomAccessStream memoryStream; + Streams::DataWriter streamWriter{ memoryStream }; + streamWriter.WriteBytes(byteContent); + streamWriter.StoreAsync().get(); + streamWriter.DetachStream(); + memoryStream.Seek(0); + inputStream = memoryStream; + } + else + { + absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + inputStream = openAction.get(); + } + + OpenConfigurationSetResult openResult = nullptr; + { + auto openAction = context.Get<Data::ConfigurationContext>().Processor().OpenConfigurationSetAsync(inputStream); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + openResult = openAction.get(); + } + + progressScope.reset(); + + if (FAILED_LOG(static_cast<HRESULT>(openResult.ResultCode().value))) + { + AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast<HRESULT>(openResult.ResultCode().value)); + + switch (openResult.ResultCode()) + { + case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: + context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_MISSING_FIELD: + context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: + context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; + break; + case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: + case WINGET_CONFIG_ERROR_INVALID_YAML: + default: + context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; + break; + } + + if (openResult.Line() != 0) + { + context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; + } + + AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); + } + + ConfigurationSet result = openResult.Set(); + + // Temporary block on using schema 0.3 while experimental + if (result.SchemaVersion() == L"0.3") + { + AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); + } + + // Fill out the information about the set based on it coming from a file. + if (isRemote) + { + result.Name(Utility::GetFileNameFromURI(argPath).wstring()); + result.Origin(argPathWide); + // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. + } + else + { + SetNameAndOrigin(result, absolutePath); + } + + context.Get<Data::ConfigurationContext>().Set(result); + } + + std::optional<ConfigurationUnit> CreateWinGetUnit(const Execution::Context& context) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportPackageId)) + { + // Maybe we can add some checks to validate the package id exists. + std::string packageId{ context.Args.GetArg(Args::Type::ConfigurationExportPackageId) }; + std::wstring packageIdWide = Utility::ConvertToUTF16(packageId); + + ConfigurationUnit unit; + unit.Type(s_Unit_WinGetPackage); + unit.Identifier(packageIdWide); + unit.Intent(ConfigurationUnitIntent::Apply); + + auto description = Resource::String::ConfigureExportUnitInstallDescription(Utility::LocIndView{ packageId }); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(s_Module_WinGetClient)); + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + ValueSet settings; + settings.Insert(s_Setting_Id, PropertyValue::CreateString(packageIdWide)); + settings.Insert(s_Setting_Source, PropertyValue::CreateString(s_WinGetSource)); + unit.Settings(settings); + + return unit; + } + + return {}; + } + + GetConfigurationUnitSettingsResult GetUnitSettings(Execution::Context& context, ConfigurationUnit& unit) + { + // This assumes there are no required properties for Get, but for example WinGetPackage requires the Id. + // It is obviously wrong and will be wrong until Export is implemented for DSC v2 and a proper way to inform + // about input to winget configure export is implemented. Drink the kool-aid and transcend. + unit.Intent(ConfigurationUnitIntent::Inform); + + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationGettingResourceSettings()); + + GetConfigurationUnitSettingsResult getResult = nullptr; + { + auto getAction = context.Get<Data::ConfigurationContext>().Processor().GetUnitSettingsAsync(unit); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { getAction.Cancel(); }); + getResult = getAction.get(); + } + + progressScope.reset(); + return getResult; + } + + std::optional<ConfigurationUnit> CreateConfigurationUnit(Execution::Context& context, const std::optional<ConfigurationUnit> dependantUnit) + { + if (context.Args.Contains(Execution::Args::Type::ConfigurationExportModule, Execution::Args::Type::ConfigurationExportResource)) + { + std::string moduleName{ context.Args.GetArg(Args::Type::ConfigurationExportModule) }; + std::wstring moduleNameWide = Utility::ConvertToUTF16(moduleName); + + std::string resourceName{ context.Args.GetArg(Args::Type::ConfigurationExportResource) }; + std::wstring resourceNameWide = Utility::ConvertToUTF16(resourceName); + + ConfigurationUnit unit; + unit.Type(resourceNameWide); + + ValueSet directives; + directives.Insert(s_Directive_Module, PropertyValue::CreateString(moduleNameWide)); + + Utility::LocIndString description; + if (dependantUnit.has_value()) + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ Utility::ConvertToUTF8(dependantUnit.value().Identifier()) }); + } + else + { + description = Resource::String::ConfigureExportUnitDescription(Utility::LocIndView{ resourceName }); + } + + directives.Insert(s_Directive_Description, PropertyValue::CreateString(winrt::to_hstring(description.get()))); + unit.Metadata(directives); + + // Call processor to get settings for the unit. + auto getResult = GetUnitSettings(context, unit); + winrt::hresult resultCode = getResult.ResultInformation().ResultCode(); + if (FAILED(resultCode)) + { + // Retry if it fails with not found in the case the module is a pre-released one. + bool isPreRelease = false; + if (resultCode == WINGET_CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY) + { + directives.Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + unit.Metadata(directives); + + auto preReleaseResult = GetUnitSettings(context, unit); + if (SUCCEEDED(preReleaseResult.ResultInformation().ResultCode())) + { + isPreRelease = true; + getResult = preReleaseResult; + } + else + { + AICLI_LOG(Config, Error, << "Failed Get allowing prerelease modules"); + LogFailedGetConfigurationUnitDetails(unit, preReleaseResult.ResultInformation()); + } + } + + if (!isPreRelease) + { + OutputUnitRunFailure(context, unit, getResult.ResultInformation()); + THROW_HR(WINGET_CONFIG_ERROR_GET_FAILED); + } + } + + unit.Settings(getResult.Settings()); + + // GetUnitSettings will set it to Inform. + unit.Intent(ConfigurationUnitIntent::Apply); + + // Add dependency if needed. + if (dependantUnit.has_value()) + { + auto dependencies = winrt::single_threaded_vector<winrt::hstring>(); + dependencies.Append(dependantUnit.value().Identifier()); + unit.Dependencies(std::move(dependencies)); + } + + return unit; + } + + return {}; + } } void CreateConfigurationProcessor(Context& context) @@ -925,10 +1182,10 @@ namespace AppInstaller::CLI::Workflow auto progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationInitializing()); - ConfigurationProcessor processor{ CreateConfigurationSetProcessorFactory(context)}; + ConfigurationProcessor processor{ anon::CreateConfigurationSetProcessorFactory(context)}; // Set the processor to the current level of the logging. - processor.MinimumLevel(ConvertLevel(Logging::Log().GetLevel())); + processor.MinimumLevel(anon::ConvertLevel(Logging::Log().GetLevel())); processor.Caller(L"winget"); // Use same activity as the overall winget command processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); @@ -938,7 +1195,7 @@ namespace AppInstaller::CLI::Workflow // Route the configuration diagnostics into the context's diagnostics logging processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const IDiagnosticInformation& diagnostics) { - context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); + context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); }); ConfigurationContext configurationContext; @@ -949,106 +1206,30 @@ namespace AppInstaller::CLI::Workflow void OpenConfigurationSet(Context& context) { - auto progressScope = context.Reporter.BeginAsyncProgress(true); - progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationReadingConfigFile()); - std::string argPath{ context.Args.GetArg(Args::Type::ConfigurationFile) }; - std::wstring argPathWide = Utility::ConvertToUTF16(argPath); - bool isRemote = Utility::IsUrlRemote(argPath); - std::filesystem::path absolutePath; - Streams::IInputStream inputStream = nullptr; + anon::OpenConfigurationSet(context, argPath, true); + } - if (isRemote) - { - std::ostringstream stringStream; - ProgressCallback emptyCallback; - Utility::DownloadToStream(argPath, stringStream, Utility::DownloadType::ConfigurationFile, emptyCallback); - - auto strContent = stringStream.str(); - std::vector<BYTE> byteContent{ strContent.begin(), strContent.end() }; - - Streams::InMemoryRandomAccessStream memoryStream; - Streams::DataWriter streamWriter{ memoryStream }; - streamWriter.WriteBytes(byteContent); - streamWriter.StoreAsync().get(); - streamWriter.DetachStream(); - memoryStream.Seek(0); - inputStream = memoryStream; - } - else - { - absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); - auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::Read); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - inputStream = openAction.get(); - } + void CreateOrOpenConfigurationSet(Context& context) + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; - OpenConfigurationSetResult openResult = nullptr; + if (std::filesystem::exists(argPath)) { - auto openAction = context.Get<Data::ConfigurationContext>().Processor().OpenConfigurationSetAsync(inputStream); - auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); - openResult = openAction.get(); + anon::OpenConfigurationSet(context, argPath, false); } - - progressScope.reset(); - - if (FAILED_LOG(static_cast<HRESULT>(openResult.ResultCode().value))) + else { - AICLI_LOG(Config, Error, << "Failed to open configuration set at " << (isRemote ? argPath : absolutePath.u8string()) << " with error 0x" << Logging::SetHRFormat << static_cast<HRESULT>(openResult.ResultCode().value)); - - switch (openResult.ResultCode()) - { - case WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidType(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE: - context.Reporter.Error() << Resource::String::ConfigurationFieldInvalidValue(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }, Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_MISSING_FIELD: - context.Reporter.Error() << Resource::String::ConfigurationFieldMissing(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Field()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION: - context.Reporter.Error() << Resource::String::ConfigurationFileVersionUnknown(Utility::LocIndString{ Utility::ConvertToUTF8(openResult.Value()) }) << std::endl; - break; - case WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE: - case WINGET_CONFIG_ERROR_INVALID_YAML: - default: - context.Reporter.Error() << Resource::String::ConfigurationFileInvalidYAML << std::endl; - break; - } - - if (openResult.Line() != 0) - { - context.Reporter.Error() << Resource::String::SeeLineAndColumn(openResult.Line(), openResult.Column()) << std::endl; - } - - AICLI_TERMINATE_CONTEXT(openResult.ResultCode()); - } + // TODO: support other schema versions or pick up latest. + ConfigurationSet set; + set.SchemaVersion(L"0.2"); - ConfigurationSet result = openResult.Set(); + std::wstring argPathWide = Utility::ConvertToUTF16(argPath); + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPathWide }); + anon::SetNameAndOrigin(set, absolutePath); - // Temporary block on using schema 0.3 while experimental - if (result.SchemaVersion() == L"0.3") - { - AICLI_RETURN_IF_TERMINATED(context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::Configuration03)); - } - - // Fill out the information about the set based on it coming from a file. - if (isRemote) - { - result.Name(Utility::GetFileNameFromURI(argPath).wstring()); - result.Origin(argPathWide); - // Do not set path. This means ${WinGetConfigRoot} not supported in remote configs. - } - else - { - // TODO: Consider how to properly determine a good value for name and origin. - result.Name(absolutePath.filename().wstring()); - result.Origin(absolutePath.parent_path().wstring()); - result.Path(absolutePath.wstring()); + context.Get<Data::ConfigurationContext>().Set(set); } - - context.Get<Data::ConfigurationContext>().Set(result); } void ShowConfigurationSet(Context& context) @@ -1067,9 +1248,9 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::ReadOnly); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getDetailsOperation); - OutputHelper outputHelper{ context }; + anon::OutputHelper outputHelper{ context }; uint32_t unitsShown = 0; getDetailsOperation.Progress([&](const IAsyncOperationWithProgress<GetConfigurationSetDetailsResult, GetConfigurationUnitDetailsResult>& operation, const GetConfigurationUnitDetailsResult&) @@ -1082,7 +1263,7 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } @@ -1127,7 +1308,7 @@ namespace AppInstaller::CLI::Workflow for (unitsShown; unitsShown < unitResults.Size(); ++unitsShown) { GetConfigurationUnitDetailsResult unitResult = unitResults.GetAt(unitsShown); - LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); + anon::LogFailedGetConfigurationUnitDetails(unitResult.Unit(), unitResult.ResultInformation()); outputHelper.OutputConfigurationUnitInformation(unitResult.Unit()); } } @@ -1180,7 +1361,7 @@ namespace AppInstaller::CLI::Workflow { auto applyOperation = configContext.Processor().ApplySetAsync(configContext.Set(), ApplyConfigurationSetFlags::None); - ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; + anon::ApplyConfigurationSetProgressOutput progress{ context, applyOperation }; result = applyOperation.get(); progress.HandleUnreportedProgress(result); @@ -1207,7 +1388,7 @@ namespace AppInstaller::CLI::Workflow { auto testOperation = configContext.Processor().TestSetAsync(configContext.Set()); - TestConfigurationSetProgressOutput progress{ context, testOperation }; + anon::TestConfigurationSetProgressOutput progress{ context, testOperation }; result = testOperation.get(); progress.HandleUnreportedProgress(result); @@ -1270,7 +1451,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationUnit unit = unitResult.Unit(); - OutputConfigurationUnitHeader(context, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); switch (resultCode) { @@ -1306,7 +1487,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getLocalDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Local); - auto unification = CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); + auto unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getLocalDetailsOperation); HRESULT getLocalHR = S_OK; GetConfigurationSetDetailsResult getLocalResult = nullptr; @@ -1340,7 +1521,7 @@ namespace AppInstaller::CLI::Workflow progressScope->Callback().SetProgressMessage(gettingDetailString); auto getCatalogDetailsOperation = configContext.Processor().GetSetDetailsAsync(configContext.Set(), ConfigurationUnitDetailFlags::Catalog); - unification = CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); + unification = anon::CreateProgressCancellationUnification(std::move(progressScope), getCatalogDetailsOperation); HRESULT getCatalogHR = S_OK; GetConfigurationSetDetailsResult getCatalogResult = nullptr; @@ -1400,13 +1581,14 @@ namespace AppInstaller::CLI::Workflow { if (needsHeader) { - OutputConfigurationUnitHeader(context, unit, unit.Type()); + anon::OutputConfigurationUnitHeader(context, unit, unit.Type()); + needsHeader = false; foundIssue = true; } }; - if (GetValueSetString(unit.Metadata(), s_Directive_Module).empty()) + if (anon::GetValueSetString(unit.Metadata(), anon::s_Directive_Module).empty()) { outputHeaderIfNeeded(); context.Reporter.Warn() << " "_liv << Resource::String::ConfigurationUnitModuleNotProvidedWarning << std::endl; @@ -1429,23 +1611,23 @@ namespace AppInstaller::CLI::Workflow if (FAILED(catalogUnitResult.ResultInformation().ResultCode())) { outputHeaderIfNeeded(); - OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); + anon::OutputUnitRunFailure(context, unit, catalogUnitResult.ResultInformation()); continue; } // If not already prerelease, try with prerelease and warn if found - std::optional<bool> allowPrereleaseDirective = GetValueSetBool(unit.Metadata(), s_Directive_AllowPrerelease); + std::optional<bool> allowPrereleaseDirective = anon::GetValueSetBool(unit.Metadata(), anon::s_Directive_AllowPrerelease); if (!allowPrereleaseDirective || !allowPrereleaseDirective.value()) { // Check if the configuration unit is prerelease but the author forgot it ConfigurationUnit clone = unit.Copy(); - clone.Metadata().Insert(s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); + clone.Metadata().Insert(anon::s_Directive_AllowPrerelease, PropertyValue::CreateBoolean(true)); progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(gettingDetailString); auto getUnitDetailsOperation = configContext.Processor().GetUnitDetailsAsync(clone, ConfigurationUnitDetailFlags::Catalog); - auto unitUnification = CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); + auto unitUnification = anon::CreateProgressCancellationUnification(std::move(progressScope), getUnitDetailsOperation); IConfigurationUnitProcessorDetails prereleaseDetails; @@ -1489,7 +1671,7 @@ namespace AppInstaller::CLI::Workflow { ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); auto units = configContext.Set().Units(); - auto validationOrder = GetConfigurationSetUnitValidationOrder(units.GetView()); + auto validationOrder = anon::GetConfigurationSetUnitValidationOrder(units.GetView()); Configuration::WingetDscModuleUnitValidator wingetUnitValidator; @@ -1519,4 +1701,58 @@ namespace AppInstaller::CLI::Workflow { context.Reporter.Info() << Resource::String::ConfigurationValidationFoundNoIssues << std::endl; } + + void AddWinGetPackageAndResource(Execution::Context& context) + { + auto wingetUnit = anon::CreateWinGetUnit(context); + auto configUnit = anon::CreateConfigurationUnit(context, wingetUnit); + + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + if (wingetUnit.has_value()) + { + configContext.Set().Units().Append(wingetUnit.value()); + } + + if (configUnit.has_value()) + { + configContext.Set().Units().Append(configUnit.value()); + } + } + + void WriteConfigFile(Execution::Context& context) + { + try + { + std::string argPath{ context.Args.GetArg(Args::Type::OutputFile) }; + + context.Reporter.Info() << Resource::String::ConfigurationExportAddingToFile(Utility::LocIndView{ argPath }) << std::endl; + + auto tempFilePath = Runtime::GetNewTempFilePath(); + + { + std::ofstream tempStream{ tempFilePath }; + tempStream << "# Created using winget configure export " << Runtime::GetClientVersion().get() << std::endl; + } + + auto openAction = Streams::FileRandomAccessStream::OpenAsync( + tempFilePath.wstring(), + FileAccessMode::ReadWrite); + + auto stream = openAction.get(); + stream.Seek(stream.Size()); + + ConfigurationContext& configContext = context.Get<Data::ConfigurationContext>(); + configContext.Set().Serialize(openAction.get()); + + auto absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ argPath }); + std::filesystem::rename(tempFilePath, absolutePath); + + context.Reporter.Info() << Resource::String::ConfigurationExportSuccessful << std::endl; + } + catch (...) + { + context.Reporter.Error() << Resource::String::ConfigurationExportFailed << std::endl; + throw; + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -17,6 +17,12 @@ namespace AppInstaller::CLI::Workflow // Outputs: ConfigurationSet void OpenConfigurationSet(Execution::Context& context); + // Creates or opens the configuration set. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor + // Outputs: ConfigurationSet + void CreateOrOpenConfigurationSet(Execution::Context& context); + // Outputs the configuration set. // Required Args: None // Inputs: ConfigurationSet @@ -84,4 +90,16 @@ namespace AppInstaller::CLI::Workflow // Inputs: None // Outputs: None void ValidateAllGoodMessage(Execution::Context& context); + + // Adds a configuration unit with the winget package and/or exports resource given. + // Required Args: None + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void AddWinGetPackageAndResource(Execution::Context& context); + + // Write the configuration file. + // Required Args: OutputFile + // Inputs: ConfigurationProcessor, ConfigurationSet + // Outputs: None + void WriteConfigFile(Execution::Context& context); } diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -307,6 +307,7 @@ namespace AppInstallerCLIE2ETests public const int CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public const int CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public const int CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public const int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); public const int CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); public const int CONFIG_ERROR_UNIT_NOT_FOUND_REPOSITORY = unchecked((int)0x8A15C102); diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2889,6 +2889,50 @@ Please specify one of them using the --source option to proceed.</value> <data name="MSStoreDownloadPackageNotFound" xml:space="preserve"> <value>The MSStore package could not be found.</value> </data> + <data name="ConfigurationExportAddingToFile" xml:space="preserve"> + <value>Adding configuration file: {0}</value> + <comment>{Locked="{0}"}</comment> + </data> + <data name="ConfigurationExportSuccessful" xml:space="preserve"> + <value>Successfully exported</value> + </data> + <data name="ConfigurationGettingResourceSettings" xml:space="preserve"> + <value>Getting configuration settings...</value> + </data> + <data name="ConfigureExportArgumentError" xml:space="preserve"> + <value>At least --packageId and/or --module with --resource must be provided</value> + <comment>{Locked="--packageId,--module, --resource"}</comment> + </data> + <data name="ConfigureExportCommandLongDescription" xml:space="preserve"> + <value>Exports configuration resources to a configuration file. When used with --packageId, exports a WinGetPackage resource of the given package id. When used with --module and --resource, gets the settings of the resource and exports it to the configuration file. If the output configuration file already exists, appends the exported configuration resources.</value> + <comment>{Locked="WinGetPackage,--packageId,--module, --resource"}</comment> + </data> + <data name="ConfigureExportCommandShortDescription" xml:space="preserve"> + <value>Exports configuration resources to a configuration file.</value> + </data> + <data name="ConfigureExportModule" xml:space="preserve"> + <value>The module of the resource to export.</value> + </data> + <data name="ConfigureExportPackageId" xml:space="preserve"> + <value>The package identifier to export.</value> + </data> + <data name="ConfigureExportResource" xml:space="preserve"> + <value>The configuration resource to export.</value> + </data> + <data name="WINGET_CONFIG_ERROR_GET_FAILED" xml:space="preserve"> + <value>The configuration unit failed getting its properties.</value> + </data> + <data name="ConfigurationExportFailed" xml:space="preserve"> + <value>Failed exporting configuration.</value> + </data> + <data name="ConfigureExportUnitDescription" xml:space="preserve"> + <value>Configure {0}</value> + <comment>{Locked="{0}"}</comment> + </data> + <data name="ConfigureExportUnitInstallDescription" xml:space="preserve"> + <value>Install {0}</value> + <comment>{Locked="{0}"}</comment> + </data> <data name="ConfigurationWarningSetViewTruncated" xml:space="preserve"> <value>Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content.</value> </data> diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -49,9 +49,11 @@ namespace AppInstaller::Settings case ExperimentalFeature::Feature::Proxy: return userSettings.Get<Setting::EFProxy>(); case ExperimentalFeature::Feature::ConfigureSelfElevation: - return userSettings.Get<Setting::EFConfigureSelfElevation>(); + return userSettings.Get<Setting::EFConfigureSelfElevation>(); case ExperimentalFeature::Feature::StoreDownload: return userSettings.Get<Setting::EFStoreDownload>(); + case ExperimentalFeature::Feature::ConfigureExport: + return userSettings.Get<Setting::EFConfigureExport>(); default: THROW_HR(E_UNEXPECTED); } @@ -92,7 +94,9 @@ namespace AppInstaller::Settings case Feature::ConfigureSelfElevation: return ExperimentalFeature{ "Configure Self Elevation", "configureSelfElevate", "https://aka.ms/winget-settings", Feature::ConfigureSelfElevation }; case Feature::StoreDownload: - return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + return ExperimentalFeature{ "Store Download", "storeDownload", "https://aka.ms/winget-settings", Feature::StoreDownload }; + case Feature::ConfigureExport: + return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -27,8 +27,9 @@ namespace AppInstaller::Settings Configuration03 = 0x4, Proxy = 0x8, SideBySide = 0x10, - ConfigureSelfElevation = 0x20, + ConfigureSelfElevation = 0x20, StoreDownload = 0x40, + ConfigureExport = 0x80, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -76,6 +76,7 @@ namespace AppInstaller::Settings EFProxy, EFConfigureSelfElevation, EFStoreDownload, + EFConfigureExport, // Telemetry TelemetryDisable, // Install behavior @@ -159,6 +160,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFProxy, bool, bool, false, ".experimentalFeatures.proxy"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureSelfElevation, bool, bool, false, ".experimentalFeatures.configureSelfElevate"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFStoreDownload, bool, bool, false, ".experimentalFeatures.storeDownload"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -265,6 +265,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFProxy) WINGET_VALIDATE_PASS_THROUGH(EFConfigureSelfElevation) WINGET_VALIDATE_PASS_THROUGH(EFStoreDownload) + WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -263,6 +263,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_MISSING_FIELD, "The configuration is missing a field."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_FAILED, "Some of the configuration units failed while testing their state."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_NOT_RUN, "Configuration state was not tested."), + WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_GET_FAILED, "The configuration unit failed getting its properties."), // Configuration Processor Errors WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED, "The configuration unit was not installed."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -198,6 +198,7 @@ #define WINGET_CONFIG_ERROR_MISSING_FIELD ((HRESULT)0x8A15C00E) #define WINGET_CONFIG_ERROR_TEST_FAILED ((HRESULT)0x8A15C00F) #define WINGET_CONFIG_ERROR_TEST_NOT_RUN ((HRESULT)0x8A15C010) +#define WINGET_CONFIG_ERROR_GET_FAILED ((HRESULT)0x8A15C011) // Configuration Processor Errors #define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/ErrorCodes.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ErrorCodes.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -70,5 +70,10 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions /// The module where the DSC resource is implemented requires admin. /// </summary> internal const int WinGetConfigUnitImportModuleAdmin = unchecked((int)0x8A15C111); + + /// <summary> + /// The property type of a unit is not supported. + /// </summary> + internal const int WinGetConfigUnitUnsupportedType = unchecked((int)0x8A15C112); } } diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/UnitPropertyUnsupportedException.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// <copyright file="UnitPropertyUnsupportedException.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Exceptions +{ + using System; + + /// <summary> + /// The property type of a unit is not supported. + /// </summary> + internal class UnitPropertyUnsupportedException : Exception + { + /// <summary> + /// Initializes a new instance of the <see cref="UnitPropertyUnsupportedException"/> class. + /// </summary> + /// <param name="name">Name.</param> + /// <param name="type">Type.</param> + /// <param name="inner">Inner exception.</param> + public UnitPropertyUnsupportedException(string name, Type type, Exception inner) + : base($"Property {name} of type {type.FullName} is not supported.", inner) + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Name = name; + this.Type = type; + } + + /// <summary> + /// Initializes a new instance of the <see cref="UnitPropertyUnsupportedException"/> class. + /// </summary> + /// <param name="type">Type.</param> + public UnitPropertyUnsupportedException(Type type) + : base($"Type {type.FullName} is not supported.") + { + this.HResult = ErrorCodes.WinGetConfigUnitUnsupportedType; + this.Type = type; + } + + /// <summary> + /// Gets the name. + /// </summary> + public string? Name { get; } + + /// <summary> + /// Gets the type. + /// </summary> + public Type Type { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/HashtableExtensions.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// <copyright file="HashtableExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Windows.Foundation.Collections; + + /// <summary> + /// Extensions for Hashtable. + /// </summary> + internal static class HashtableExtensions + { + /// <summary> + /// Convert a hashtable to a value set. + /// </summary> + /// <param name="hashtable">hashtable.</param> + /// <returns>Value set.</returns> + public static ValueSet ToValueSet(this Hashtable hashtable) + { + var valueSet = new ValueSet(); + + foreach (DictionaryEntry entry in hashtable) + { + if (entry.Key is string key) + { + if (entry.Value is null) + { + valueSet.Add(key, null); + } + else + { + var value = TypeHelpers.GetCompatibleValueSetValueOfProperty(entry.Value.GetType(), entry.Value); + if (value != null) + { + valueSet.Add(key, value); + } + } + } + else + { + throw new UnitPropertyUnsupportedException(entry.Key.GetType()); + } + } + + return valueSet; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs b/src/Microsoft.Management.Configuration.Processor/Helpers/TypeHelpers.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="TypeHelpers.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -6,8 +6,12 @@ namespace Microsoft.Management.Configuration.Processor.Helpers { + using System; + using System.Collections; using System.Collections.Generic; using System.Reflection; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; using Windows.Foundation.Collections; /// <summary> @@ -74,18 +78,78 @@ namespace Microsoft.Management.Configuration.Processor.Helpers var result = new ValueSet(); foreach (PropertyInfo property in obj.GetType().GetProperties()) { - // Specialize here. - if (property.PropertyType.IsEnum) + var key = property.Name; + var value = GetCompatibleValueSetValueOfProperty(property.PropertyType, property.GetValue(obj)); + result.Add(key, value); + } + + return result; + } + + /// <summary> + /// Gets a compatible type for a ValueSet value. + /// </summary> + /// <param name="type">Type.</param> + /// <param name="value">Value.</param> + /// <returns>Value converted to a compatible type.</returns> + public static object? GetCompatibleValueSetValueOfProperty(Type type, object? value) + { + if (value == null) + { + return null; + } + + // Specialize here. + if (type.IsEnum) + { + return value.ToString(); + } + else if (type == typeof(Hashtable)) + { + Hashtable hashtable = (Hashtable)value; + return hashtable.ToValueSet(); + } + else if (type.IsArray) + { + var valueSetArray = new ValueSet(); + int index = 0; + foreach (object arrayObj in (Array)value) + { + var arrayValue = GetCompatibleValueSetValueOfProperty(arrayObj.GetType(), arrayObj); + if (arrayValue != null) + { + valueSetArray.Add(index.ToString(), arrayValue); + index++; + } + } + + if (valueSetArray.Count > 0) + { + valueSetArray.Add("treatAsArray", true); + } + + return valueSetArray; + } + else if (type == typeof(string)) + { + // Ignore empty strings. + string propertyString = (string)value; + if (!string.IsNullOrEmpty(propertyString)) { - result.Add(property.Name, property.GetValue(obj)?.ToString()); + return propertyString; } else { - result.Add(property.Name, property.GetValue(obj)); + return null; } } + else if (type.IsValueType) + { + return value; + } - return result; + // This might be too restrictive but anything else is going to be some object that we don't support anyway. + throw new UnitPropertyUnsupportedException(value.GetType()); } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/Errors.cs @@ -31,6 +31,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers public static readonly int WINGET_CONFIG_ERROR_MISSING_FIELD = unchecked((int)0x8A15C00E); public static readonly int WINGET_CONFIG_ERROR_TEST_FAILED = unchecked((int)0x8A15C00F); public static readonly int WINGET_CONFIG_ERROR_TEST_NOT_RUN = unchecked((int)0x8A15C010); + public static readonly int WINGET_CONFIG_ERROR_GET_FAILED = unchecked((int)0x8A15C011); // Configuration Processor Errors public static readonly int WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED = unchecked((int)0x8A15C101); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/HashtableExtensionsTests.cs @@ -0,0 +1,134 @@ +// ----------------------------------------------------------------------------- +// <copyright file="HashtableExtensionsTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System.Collections; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Windows.Foundation.Collections; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Hashtable extension tests. + /// </summary> + [Collection("UnitTestCollection")] + public class HashtableExtensionsTests + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="HashtableExtensionsTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public HashtableExtensionsTests(UnitTestFixture fixture, ITestOutputHelper log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests ToValueSet with simple types. + /// </summary> + [Fact] + public void ToValueSet_Test() + { + var ht = new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)valueSet["key1"]); + + Assert.True(valueSet.ContainsKey("key2")); + Assert.Equal(2, (int)valueSet["key2"]); + + Assert.True(valueSet.ContainsKey("key3")); + Assert.True((bool)valueSet["key3"]); + } + + /// <summary> + /// Test for inner hashtables. + /// </summary> + [Fact] + public void ToValueSet_InnerHashtable() + { + var ht = new Hashtable() + { + { "hashtableKey", new Hashtable() + { + { "key1", "value1" }, + { "key2", 2 }, + { "key3", true }, + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("hashtableKey")); + var resultValueSet = (ValueSet)valueSet["hashtableKey"]; + + Assert.True(resultValueSet.ContainsKey("key1")); + Assert.Equal("value1", (string)resultValueSet["key1"]); + + Assert.True(resultValueSet.ContainsKey("key2")); + Assert.Equal(2, (int)resultValueSet["key2"]); + + Assert.True(resultValueSet.ContainsKey("key3")); + Assert.True((bool)resultValueSet["key3"]); + } + + /// <summary> + /// Test for inner arrays. + /// </summary> + [Fact] + public void ToValueSet_InnerArray() + { + var ht = new Hashtable() + { + { + "arrayKey", new string[] + { + "s1", + "s2", + "s3", + } + }, + }; + + var valueSet = ht.ToValueSet(); + + Assert.True(valueSet.ContainsKey("arrayKey")); + var resultValueSet = (ValueSet)valueSet["arrayKey"]; + Assert.True(resultValueSet.ContainsKey("treatAsArray")); + Assert.Equal(4, resultValueSet.Count); + } + + /// <summary> + /// Test when a key is not a string. + /// </summary> + [Fact] + public void ToValueSet_KeyNotString() + { + var ht = new Hashtable() + { + { 1, "value" }, + }; + + Assert.Throws<UnitPropertyUnsupportedException>(() => ht.ToValueSet()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/OpenConfigurationSetTests.cs @@ -482,7 +482,7 @@ properties: properties: configurationVersion: 0.2 assertions: - - resource: FakeModule + - resource: FakeModule/FakeResource id: TestId directives: description: FakeDescription @@ -493,7 +493,7 @@ properties: TestBool: false TestInt: 1234 resources: - - resource: FakeModule2 + - resource: FakeModule2/FakeResource2 id: TestId2 dependsOn: - TestId @@ -526,17 +526,17 @@ properties: Assert.Equal("0.2", set.SchemaVersion); Assert.Equal(2, set.Units.Count); - Assert.Equal("FakeModule", set.Units[0].Type); + Assert.Equal("FakeResource", set.Units[0].Type); Assert.Equal(ConfigurationUnitIntent.Assert, set.Units[0].Intent); Assert.Equal("TestId", set.Units[0].Identifier); - this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated")); + this.VerifyValueSet(set.Units[0].Metadata, new ("description", "FakeDescription"), new ("allowPrerelease", true), new ("securityContext", "elevated"), new ("module", "FakeModule")); this.VerifyValueSet(set.Units[0].Settings, new ("TestString", "Hello"), new ("TestBool", false), new ("TestInt", 1234)); - Assert.Equal("FakeModule2", set.Units[1].Type); + Assert.Equal("FakeResource2", set.Units[1].Type); Assert.Equal(ConfigurationUnitIntent.Apply, set.Units[1].Intent); Assert.Equal("TestId2", set.Units[1].Identifier); this.VerifyStringArray(set.Units[1].Dependencies, "TestId", "dependency2", "dependency3"); - this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated")); + this.VerifyValueSet(set.Units[1].Metadata, new ("description", "FakeDescription2"), new ("securityContext", "elevated"), new ("module", "FakeModule2")); ValueSet mapping = new ValueSet(); mapping.Add("Key", "TestValue"); @@ -757,7 +757,7 @@ parameters: foreach (var expectation in expected) { - Assert.True(values.ContainsKey(expectation.Key)); + Assert.True(values.ContainsKey(expectation.Key), $"Not Found {expectation.Key}"); object value = values[expectation.Key]; switch (expectation.Value) diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/TypeHelpersTests.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="TypeHelpersTests.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -6,6 +6,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests { + using System.Collections; using System.Collections.Generic; using Microsoft.Management.Configuration.Processor.Helpers; using Microsoft.Management.Configuration.UnitTests.Fixtures; @@ -136,5 +137,75 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Assert.True(set.TryGetValue("Property3", out object v3)); Assert.Equal(e.ToString(), v3); } + + /// <summary> + /// Verifies when a property is a Hashtable. It must be converted to a ValueSet. + /// </summary> + [Fact] + public void GetAllPropertiesValuesTest_Hashtable() + { + string k1 = "key1"; + string k2 = "key2"; + int v1 = 7; + string v2 = "value2"; + dynamic obj = new + { + Property1 = new Hashtable + { + { k1, v1 }, + { k2, v2 }, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(2, valueSetResult.Count); + Assert.True(valueSetResult.ContainsKey(k1)); + Assert.Equal(v1, (int)valueSetResult[k1]); + Assert.True(valueSetResult.ContainsKey(k2)); + Assert.Equal(v2, (string)valueSetResult[k2]); + } + + /// <summary> + /// Verifies when a property is an array. It must generate a ValueSet + /// where the keys are the index and a key treatAsArray means the value + /// must be treated an array. + /// </summary> + [Fact] + public void GetAllPropertiesValuesTest_Array() + { + dynamic obj = new + { + Property1 = new int[] + { + 1, + 2, + 3, + 4, + }, + }; + + ValueSet set = TypeHelpers.GetAllPropertiesValues(obj); + Assert.Single(set); + + Assert.True(set.ContainsKey("Property1")); + Assert.True(set.TryGetValue("Property1", out object valueSetResultObj)); + + ValueSet? valueSetResult = valueSetResultObj as ValueSet; + Assert.NotNull(valueSetResult); + Assert.Equal(5, valueSetResult.Count); + + Assert.True(valueSetResult.ContainsKey("treatAsArray")); + Assert.True(valueSetResult.ContainsKey("0")); + Assert.True(valueSetResult.ContainsKey("1")); + Assert.True(valueSetResult.ContainsKey("2")); + Assert.True(valueSetResult.ContainsKey("3")); + } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -15,6 +15,11 @@ using namespace winrt::Windows::Foundation; namespace winrt::Microsoft::Management::Configuration::implementation { + namespace anon + { + static constexpr std::string_view s_nullValue = "null"; + } + std::unique_ptr<ConfigurationSetSerializer> ConfigurationSetSerializer::CreateSerializer(hstring version) { // Create the parser based on the version selected @@ -46,13 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation for (const auto& [key, value] : valueSet) { - std::string keyName = winrt::to_string(key); - const auto& currentValueSet = value.try_as<Windows::Foundation::Collections::ValueSet>(); + if (value != nullptr) + { + std::string keyName = winrt::to_string(key); + emitter << Key << keyName << Value; + WriteYamlValue(emitter, value); + } + } + + emitter << EndMap; + } + + void ConfigurationSetSerializer::WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value) + { + if (value == nullptr) + { + emitter << anon::s_nullValue; + } + else + { + const auto& currentValueSet = value.try_as<Windows::Foundation::Collections::ValueSet>(); if (currentValueSet) { - emitter << AppInstaller::YAML::Key << keyName; - WriteYamlValueSet(emitter, currentValueSet); + if (currentValueSet.HasKey(L"treatAsArray")) + { + WriteYamlValueSetAsArray(emitter, currentValueSet); + } + else + { + WriteYamlValueSet(emitter, currentValueSet); + } } else { @@ -61,15 +90,15 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (type == PropertyType::Boolean) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetBoolean(); + emitter << property.GetBoolean(); } else if (type == PropertyType::String) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << AppInstaller::Utility::ConvertToUTF8(property.GetString()); + emitter << AppInstaller::Utility::ConvertToUTF8(property.GetString()); } else if (type == PropertyType::Int64) { - emitter << AppInstaller::YAML::Key << keyName << AppInstaller::YAML::Value << property.GetInt64(); + emitter << property.GetInt64(); } else { @@ -77,8 +106,35 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } } + } - emitter << EndMap; + void ConfigurationSetSerializer::WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray) + { + std::vector<std::pair<int, winrt::Windows::Foundation::IInspectable>> arrayValues; + for (const auto& arrayValue : valueSetArray) + { + if (arrayValue.Key() != L"treatAsArray") + { + arrayValues.emplace_back(std::make_pair(std::stoi(arrayValue.Key().c_str()), arrayValue.Value())); + } + } + + std::sort( + arrayValues.begin(), + arrayValues.end(), + [](const std::pair<int, winrt::Windows::Foundation::IInspectable>& a, const std::pair<int, winrt::Windows::Foundation::IInspectable>& b) + { + return a.first < b.first; + }); + + emitter << BeginSeq; + + for (const auto& arrayValue : arrayValues) + { + WriteYamlValue(emitter, arrayValue.second); + } + + emitter << EndSeq; } void ConfigurationSetSerializer::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units) @@ -89,7 +145,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { // Resource emitter << BeginMap; - emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(unit.Type()); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Resource) << Value << AppInstaller::Utility::ConvertToUTF8(GetResourceName(unit)); // Id if (!unit.Identifier().empty()) @@ -112,9 +168,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } // Directives - const auto& metadata = unit.Metadata(); - emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); - WriteYamlValueSet(emitter, metadata); + WriteResourceDirectives(emitter, unit); // Settings const auto& settings = unit.Settings(); @@ -126,4 +180,21 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndSeq; } + + winrt::hstring ConfigurationSetSerializer::GetResourceName(const ConfigurationUnit& unit) + { + return unit.Type(); + } + + void ConfigurationSetSerializer::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); + } + + winrt::hstring ConfigurationSetSerializer::GetSchemaVersionComment(winrt::hstring version) + { + return winrt::to_hstring(L"# yaml-language-server: $schema=https://aka.ms/configuration-dsc-schema/") + version; + } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -25,8 +25,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation protected: ConfigurationSetSerializer() = default; + void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet); + void WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value); + void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); + winrt::hstring GetSchemaVersionComment(winrt::hstring version); - void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); + virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit) = 0; + virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) = 0; }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp @@ -9,6 +9,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { using namespace AppInstaller::YAML; + using namespace winrt::Windows::Foundation; hstring ConfigurationSetSerializer_0_2::Serialize(ConfigurationSet* configurationSet) { @@ -50,6 +51,37 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndMap; emitter << EndMap; - return winrt::to_hstring(emitter.str()); + return GetSchemaVersionComment(configurationSet->SchemaVersion()) + winrt::to_hstring(L"\n") + winrt::to_hstring(emitter.str()); + } + + winrt::hstring ConfigurationSetSerializer_0_2::GetResourceName(const ConfigurationUnit& unit) + { + const auto& metadata = unit.Metadata(); + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + auto object = metadata.Lookup(moduleKey); + auto property = object.try_as<IPropertyValue>(); + if (property && property.Type() == PropertyType::String) + { + return property.GetString() + '/' + unit.Type(); + } + } + + return unit.Type(); + } + + void ConfigurationSetSerializer_0_2::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) + { + auto metadata = unit.Metadata(); + + const auto moduleKey = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); + if (metadata.HasKey(moduleKey)) + { + metadata.Remove(moduleKey); + } + + emitter << Key << GetConfigurationFieldName(ConfigurationField::Directives); + WriteYamlValueSet(emitter, metadata); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h @@ -18,5 +18,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation ConfigurationSetSerializer_0_2& operator=(ConfigurationSetSerializer_0_2&&) = default; hstring Serialize(ConfigurationSet* configurationSet) override; + + protected: + winrt::hstring GetResourceName(const ConfigurationUnit& unit) override; + void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) override; }; } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -222,11 +222,15 @@ <ClInclude Include="ApplyGroupSettingsResult.h"> <Filter>Internals</Filter> </ClInclude> - <ClInclude Include="ConfigurationSetSerializer.h" /> - <ClInclude Include="ConfigurationSetSerializer_0_2.h" /> <ClInclude Include="ConfigurationSetUtilities.h"> <Filter>Parser</Filter> </ClInclude> + <ClInclude Include="ConfigurationSetSerializer.h"> + <Filter>Parser</Filter> + </ClInclude> + <ClInclude Include="ConfigurationSetSerializer_0_2.h"> + <Filter>Parser</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" />