commit b57977b9fb74b61f4b5a26847732af23fe6f2268 parent c20c7177c496c0c06c049b0d8f13c4cf85d131fd Author: JohnMcPMS <johnmcp@microsoft.com> Date: Mon, 3 Mar 2025 10:19:48 -0800 Experimental support for DSC v3 processing (#5252) ## Change Adds experimental support for DSC v3 processing of configurations. When enabled, one can use DSC v3 instead of PowerShell DSC v2 by setting their processor to `dscv3` for the configuration like: ```yaml $schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json metadata: winget: processor: dscv3 resources: <continue with schema 0.3 resource definitions> ``` This is currently on-par with DSC v2 in terms of functionality, except for 2 things: 1. It does not attempt to ensure dsc.exe is present. It will find and use the preview MSIX packaged version of dsc.exe (Store ID: `9PCX3HX4HZ0Z`) if installed. Otherwise, you can specify the `--processor-path` to dsc.exe. 2. Resources for DSC v3 must be present on the system. There is not currently any mechanism to find and install new resources, as the paradigm is that they are part of the configurable item. There is no special handling of DSC v2 resources in the configuration; that could potentially come later. Also makes 0.3 schema not experimental and fixes configuration history for environments. Diffstat:
94 files changed, 3068 insertions(+), 215 deletions(-)
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -167,6 +167,7 @@ MAKEINTRESOURCE makemsix MANIFESTSCHEMA MANIFESTVERSION +Memberwise meme metadatas Minimatch diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -219,6 +219,7 @@ ICONDIRENTRY ICONIMAGE icu idl +IDSC idx IFACEMETHODIMP iid diff --git a/azure-pipelines.yml b/azure-pipelines.yml @@ -374,6 +374,12 @@ jobs: displayName: Clean up Sysinternals PsTools condition: succeededOrFailed() + # Install DSC v3 preview until the DSC v3 processor handles that on its own + - powershell: | + Install-WinGetPackage -Id Microsoft.DSC.Preview -Source winget + displayName: Install DSC v3 + condition: succeededOrFailed() + - task: PowerShell@2 displayName: Run Unit Tests Packaged inputs: diff --git a/doc/Settings.md b/doc/Settings.md @@ -355,25 +355,14 @@ You can enable the feature as shown below. }, ``` -### configuration03 +### dsc3 -This feature enables the configuration schema 0.3. +This feature enables support for DSC v3 integration. You can enable the feature as shown below. ```json "experimentalFeatures": { - "configuration03": true - }, -``` - -### configureSelfElevate - -This feature enables configure commands to request elevation as needed. -Currently, this means that properly attributed configuration units (and only those) will be run through an elevated process while the rest are run from the current context. - -```json - "experimentalFeatures": { - "configureSelfElevate": true + "dsc3": true }, ``` diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -217,6 +217,8 @@ 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::ConfigurationProcessorPath: + return { type, "processor-path"_liv }; case Execution::Args::Type::ConfigurationExportPackageId: return { type, "package-id"_liv }; case Execution::Args::Type::ConfigurationExportModule: diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -37,6 +37,7 @@ namespace AppInstaller::CLI return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationProcessorPath, Resource::String::ConfigurationProcessorPath, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::ConfigurationSuppressPrologue, Resource::String::ConfigurationSuppressPrologueArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }, @@ -77,8 +78,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << OpenConfigurationSet << + CreateConfigurationProcessor << ShowConfigurationSet << ShowConfigurationSetConflicts << ConfirmConfigurationProcessing(true) << diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -15,6 +15,7 @@ namespace AppInstaller::CLI // Required for now, make exclusive when history implemented Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationProcessorPath, Resource::String::ConfigurationProcessorPath, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, }; } @@ -39,8 +40,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << OpenConfigurationSet << + CreateConfigurationProcessor << ShowConfigurationSet; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -14,6 +14,7 @@ namespace AppInstaller::CLI return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationProcessorPath, Resource::String::ConfigurationProcessorPath, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, }; @@ -39,8 +40,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << OpenConfigurationSet << + CreateConfigurationProcessor << ShowConfigurationSet << ShowConfigurationSetConflicts << ConfirmConfigurationProcessing(false) << diff --git a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -14,6 +14,7 @@ namespace AppInstaller::CLI return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationProcessorPath, Resource::String::ConfigurationProcessorPath, ArgumentType::Standard, Argument::Visibility::Help }, }; } @@ -37,8 +38,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << VerifyFileOrUri(Execution::Args::Type::ConfigurationFile) << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << OpenConfigurationSet << + CreateConfigurationProcessor << ValidateConfigurationSetSemantics << ValidateConfigurationSetUnitProcessors << ValidateConfigurationSetUnitContents << diff --git a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -4,6 +4,7 @@ #include "Public/ConfigurationSetProcessorFactoryRemoting.h" #include <AppInstallerErrors.h> #include <AppInstallerLanguageUtilities.h> +#include <AppInstallerLogging.h> #include <AppInstallerStrings.h> #include <winget/ILifetimeWatcher.h> #include <winget/Security.h> @@ -43,9 +44,9 @@ namespace AppInstaller::CLI::ConfigurationRemoting // have this implementation leverage that one with an event handler for the packaged specifics. // TODO: Add SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties and pass values along to sets on creation // In turn, any properties must only be set via the command line (or eventual UI requests to the user). - struct DynamicFactory : winrt::implements<DynamicFactory, IConfigurationSetProcessorFactory, SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase + struct DynamicFactory : winrt::implements<DynamicFactory, IConfigurationSetProcessorFactory, SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties, Collections::IMap<winrt::hstring, winrt::hstring>, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase { - DynamicFactory(); + DynamicFactory(ProcessorEngine processorEngine); IConfigurationSetProcessor CreateSetProcessor(const ConfigurationSet& configurationSet); @@ -105,6 +106,36 @@ namespace AppInstaller::CLI::ConfigurationRemoting m_customLocation = value; } + // Implement a subset of IMap to enable property bag semantics + uint32_t Size() { THROW_HR(E_NOTIMPL); } + void Clear() { THROW_HR(E_NOTIMPL); } + Collections::IMapView<winrt::hstring, winrt::hstring> GetView() { THROW_HR(E_NOTIMPL); } + bool HasKey(winrt::hstring) { THROW_HR(E_NOTIMPL); } + void Remove(winrt::hstring) { THROW_HR(E_NOTIMPL); } + + bool Insert(winrt::hstring key, winrt::hstring value) + { + auto result = m_defaultRemoteFactory.as<Collections::IMap<winrt::hstring, winrt::hstring>>().Insert(key, value); + m_factoryMapValues[key] = value; + return result; + } + + winrt::hstring Lookup(winrt::hstring key) + { + return m_defaultRemoteFactory.as<Collections::IMap<winrt::hstring, winrt::hstring>>().Lookup(key); + } + + ProcessorEngine Engine() const + { + return m_processorEngine; + } + + winrt::hstring GetFactoryMapValue(winrt::hstring key) + { + auto itr = m_factoryMapValues.find(key); + return itr != m_factoryMapValues.end() ? itr->second : winrt::hstring{}; + } + private: IConfigurationSetProcessorFactory m_defaultRemoteFactory; winrt::event<EventHandler<IDiagnosticInformation>> m_diagnostics; @@ -113,6 +144,8 @@ namespace AppInstaller::CLI::ConfigurationRemoting DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational; SetProcessorFactory::PwshConfigurationProcessorLocation m_location = SetProcessorFactory::PwshConfigurationProcessorLocation::Default; winrt::hstring m_customLocation; + ProcessorEngine m_processorEngine; + std::map<winrt::hstring, winrt::hstring> m_factoryMapValues; }; struct DynamicProcessorInfo @@ -277,6 +310,27 @@ namespace AppInstaller::CLI::ConfigurationRemoting json["modulePath"] = locationString; } + // Ensure that we always pass a path to the executable + if (m_dynamicFactory->Engine() == ProcessorEngine::DSCv3) + { + winrt::hstring dscExecutablePathPropertyName = ToHString(PropertyName::DscExecutablePath); + winrt::hstring dscExecutablePath = m_dynamicFactory->GetFactoryMapValue(dscExecutablePathPropertyName); + + if (dscExecutablePath.empty()) + { + dscExecutablePath = m_dynamicFactory->Lookup(ToHString(PropertyName::FoundDscExecutablePath)); + } + + if (dscExecutablePath.empty()) + { + // This is backstop to prevent a case where dsc.exe not found. + AICLI_LOG(Config, Error, << "Could not find dsc.exe, it must be provided by the user."); + THROW_WIN32(ERROR_FILE_NOT_FOUND); + } + + json["processorPath"] = Utility::ConvertToUTF8(dscExecutablePath); + } + Json::StreamWriterBuilder writerBuilder; writerBuilder.settings_["indentation"] = "\t"; return Json::writeString(writerBuilder, json); @@ -337,7 +391,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting useRunAs = !m_enableTestMode; #endif - factory = CreateOutOfProcessFactory(useRunAs, SerializeSetProperties(), SerializeHighIntegrityLevelSet()); + factory = CreateOutOfProcessFactory(m_dynamicFactory->Engine(), useRunAs, SerializeSetProperties(), SerializeHighIntegrityLevelSet()); } else { @@ -346,6 +400,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting if (factory) { + factory.MinimumLevel(m_dynamicFactory->MinimumLevel()); factoryDiagnosticsEventRevoker = factory.Diagnostics(winrt::auto_revoke, [weak_this{ get_weak() }](const IInspectable&, const IDiagnosticInformation& information) { @@ -373,9 +428,10 @@ namespace AppInstaller::CLI::ConfigurationRemoting #endif }; - DynamicFactory::DynamicFactory() + DynamicFactory::DynamicFactory(ProcessorEngine processorEngine) { - m_defaultRemoteFactory = CreateOutOfProcessFactory(); + m_processorEngine = processorEngine; + m_defaultRemoteFactory = CreateOutOfProcessFactory(processorEngine); if (m_defaultRemoteFactory) { @@ -413,6 +469,11 @@ namespace AppInstaller::CLI::ConfigurationRemoting void DynamicFactory::MinimumLevel(DiagnosticLevel value) { m_minimumLevel = value; + + if (m_defaultRemoteFactory) + { + m_defaultRemoteFactory.MinimumLevel(value); + } } HRESULT STDMETHODCALLTYPE DynamicFactory::SetLifetimeWatcher(IUnknown* watcher) @@ -437,8 +498,8 @@ namespace AppInstaller::CLI::ConfigurationRemoting catch (...) {} } - winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateDynamicRuntimeFactory() + winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateDynamicRuntimeFactory(ProcessorEngine processorEngine) { - return winrt::make<anonymous::DynamicFactory>(); + return winrt::make<anonymous::DynamicFactory>(processorEngine); } } diff --git a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp @@ -2,25 +2,44 @@ // Licensed under the MIT License. #include "pch.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" +#include <AppInstallerErrors.h> #include <AppInstallerLanguageUtilities.h> #include <AppInstallerLogging.h> #include <AppInstallerRuntime.h> #include <AppInstallerStrings.h> +#include <winget/ExperimentalFeature.h> #include <winget/ILifetimeWatcher.h> #include <winrt/Microsoft.Management.Configuration.SetProcessorFactory.h> using namespace winrt::Windows::Foundation; using namespace winrt::Microsoft::Management::Configuration; +using namespace std::string_view_literals; namespace AppInstaller::CLI::ConfigurationRemoting { namespace { // The executable file name for the remote server process. - constexpr std::wstring_view s_RemoteServerFileName = L"ConfigurationRemotingServer\\ConfigurationRemotingServer.exe"; + constexpr std::wstring_view s_RemoteServerFileName = L"ConfigurationRemotingServer\\ConfigurationRemotingServer.exe"sv; + + constexpr std::wstring_view s_ProcessorEngine_PowerShell = L"pwsh"sv; + constexpr std::wstring_view s_ProcessorEngine_DSCv3 = L"dscv3"sv; // The string used to divide the arguments sent to the remote server - constexpr std::wstring_view s_ArgumentsDivider = L"\n~~~~~~\n"; + constexpr std::wstring_view s_ArgumentsDivider = L"\n~~~~~~\n"sv; + + std::wstring_view ToString(ProcessorEngine value) + { + switch (value) + { + case ProcessorEngine::PowerShell: + return s_ProcessorEngine_PowerShell; + case ProcessorEngine::DSCv3: + return s_ProcessorEngine_DSCv3; + default: + THROW_HR(E_UNEXPECTED); + } + } // A helper with a convenient function that we use to receive the remote factory object. struct RemoteFactoryCallback : winrt::implements<RemoteFactoryCallback, IConfigurationStatics> @@ -119,9 +138,9 @@ namespace AppInstaller::CLI::ConfigurationRemoting }; // Represents a remote factory object that was created from a specific process. - struct RemoteFactory : winrt::implements<RemoteFactory, IConfigurationSetProcessorFactory, SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase + struct RemoteFactory : winrt::implements<RemoteFactory, IConfigurationSetProcessorFactory, SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties, Collections::IMap<winrt::hstring, winrt::hstring>, winrt::cloaked<WinRT::ILifetimeWatcher>>, WinRT::LifetimeWatcherBase { - RemoteFactory(bool useRunAs, const std::string& properties, const std::string& restrictions) + RemoteFactory(ProcessorEngine processorEngine, bool useRunAs, const std::string& properties, const std::string& restrictions) { AICLI_LOG(Config, Verbose, << "Launching process for configuration processing..."); @@ -162,7 +181,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // ~~~~~~ // YAML configuration set definition std::wostringstream argumentsStream; - argumentsStream << s_RemoteServerFileName << L' ' << marshalledCallback << L' ' << completionEventName << L' ' << GetCurrentProcessId(); + argumentsStream << s_RemoteServerFileName << L' ' << marshalledCallback << L' ' << completionEventName << L' ' << GetCurrentProcessId() << L' ' << ToString(processorEngine); if (!properties.empty() && !restrictions.empty()) { @@ -285,6 +304,23 @@ namespace AppInstaller::CLI::ConfigurationRemoting m_remoteFactory.as<SetProcessorFactory::IPwshConfigurationSetProcessorFactoryProperties>().CustomLocation(value); } + // Implement a subset of IMap to enable property bag semantics + uint32_t Size() { THROW_HR(E_NOTIMPL); } + void Clear() { THROW_HR(E_NOTIMPL); } + Collections::IMapView<winrt::hstring, winrt::hstring> GetView() { THROW_HR(E_NOTIMPL); } + bool HasKey(winrt::hstring) { THROW_HR(E_NOTIMPL); } + void Remove(winrt::hstring) { THROW_HR(E_NOTIMPL); } + + bool Insert(winrt::hstring key, winrt::hstring value) + { + return m_remoteFactory.as<Collections::IMap<winrt::hstring, winrt::hstring>>().Insert(key, value); + } + + winrt::hstring Lookup(winrt::hstring key) + { + return m_remoteFactory.as<Collections::IMap<winrt::hstring, winrt::hstring>>().Lookup(key); + } + HRESULT STDMETHODCALLTYPE SetLifetimeWatcher(IUnknown* watcher) { return WinRT::LifetimeWatcherBase::SetLifetimeWatcher(watcher); @@ -298,9 +334,54 @@ namespace AppInstaller::CLI::ConfigurationRemoting }; } - IConfigurationSetProcessorFactory CreateOutOfProcessFactory(bool useRunAs, const std::string& properties, const std::string& restrictions) + IConfigurationSetProcessorFactory CreateOutOfProcessFactory(ProcessorEngine processorEngine, bool useRunAs, const std::string& properties, const std::string& restrictions) + { + THROW_HR_IF(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED, processorEngine == ProcessorEngine::DSCv3 && !Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::ConfigurationDSCv3)); + + return winrt::make<RemoteFactory>(processorEngine, useRunAs, properties, restrictions); + } + + ProcessorEngine DetermineProcessorEngine(ConfigurationSet set) { - return winrt::make<RemoteFactory>(useRunAs, properties, restrictions); + Utility::Version schemaVersion{ Utility::ConvertToUTF8(set.SchemaVersion()) }; + + if (schemaVersion <= Utility::Version{ "0.3" }) + { + ProcessorEngine result = ProcessorEngine::Unknown; + + std::wstring processorIdentifier = Utility::ToLower(set.Environment().ProcessorIdentifier()); + if (processorIdentifier.empty() || processorIdentifier == s_ProcessorEngine_PowerShell) + { + // Default to PowerShell + result = ProcessorEngine::PowerShell; + } + else if (processorIdentifier == s_ProcessorEngine_DSCv3) + { + result = ProcessorEngine::DSCv3; + } + else + { + AICLI_LOG(Config, Warning, << "Unknown processor: " << Utility::ConvertToUTF8(processorIdentifier)); + } + + return result; + } + else + { + // Intentionally fail out here until a decision is made. + THROW_HR(E_NOTIMPL); + } + } + + winrt::hstring ToHString(PropertyName name) + { + switch (name) + { + case PropertyName::DscExecutablePath: return L"DscExecutablePath"; + case PropertyName::FoundDscExecutablePath: return L"FoundDscExecutablePath"; + } + + THROW_HR(E_UNEXPECTED); } } diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -17,6 +17,7 @@ namespace AppInstaller::CLI Argument{ Execution::Args::Type::ConfigurationExportModule, Resource::String::ConfigureExportModule }, Argument{ Execution::Args::Type::ConfigurationExportResource, Resource::String::ConfigureExportResource }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath }, + Argument{ Execution::Args::Type::ConfigurationProcessorPath, Resource::String::ConfigurationProcessorPath, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::Source, Resource::String::ExportSourceArgumentDescription, ArgumentType::Standard }, Argument{ Execution::Args::Type::IncludeVersions, Resource::String::ExportIncludeVersionsArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::ConfigurationExportAll, Resource::String::ConfigureExportAll, ArgumentType::Flag }, @@ -44,8 +45,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << SearchSourceForPackageExport << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << CreateOrOpenConfigurationSet << + CreateConfigurationProcessor << PopulateConfigurationSetForExport << WriteConfigFile; } diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -131,6 +131,7 @@ namespace AppInstaller::CLI::Execution ConfigurationSuppressPrologue, ConfigurationEnable, ConfigurationDisable, + ConfigurationProcessorPath, ConfigurationModulePath, ConfigurationExportPackageId, ConfigurationExportModule, diff --git a/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h b/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h @@ -6,11 +6,39 @@ namespace AppInstaller::CLI::ConfigurationRemoting { + // The processor engine being used by the factory. + enum class ProcessorEngine + { + // An unknown processor. + Unknown, + // Uses PowerShell DSC v2. + PowerShell, + // Uses DSC v3. + DSCv3, + }; + + // Determines the appropriate processor engine to use for the given configuration set. + ProcessorEngine DetermineProcessorEngine(winrt::Microsoft::Management::Configuration::ConfigurationSet set); + // Creates a factory in another process - winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateOutOfProcessFactory(bool useRunAs = false, const std::string& properties = {}, const std::string& restrictions = {}); + winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateOutOfProcessFactory(ProcessorEngine processorEngine, bool useRunAs = false, const std::string& properties = {}, const std::string& restrictions = {}); // Creates a factory that can route configurations to the appropriate internal factory. - winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateDynamicRuntimeFactory(); + winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory CreateDynamicRuntimeFactory(ProcessorEngine processorEngine); + + // The property names used with IMap property semantics of remote factories. + enum class PropertyName + { + // The path to the dsc.exe executable. + // Read / Write + DscExecutablePath, + // The path to the dsc.exe executable, as discovered. + // Read only. + FoundDscExecutablePath, + }; + + // Gets the string for a property name. + winrt::hstring ToHString(PropertyName name); } // Export for use by the out of process factory server to report its initialization. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -99,6 +99,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNotEnabledMessage); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNoTestRun); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationNotInDesiredState); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationProcessorPath); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationReadingConfigFile); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationSetStateCompleted); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationSetStateInProgress); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -104,19 +104,48 @@ namespace AppInstaller::CLI::Workflow } #endif + // The configuration set must have already been opened to create the proper factory. + THROW_WIN32_IF(ERROR_INVALID_STATE, !context.Contains(Data::ConfigurationContext)); + const auto& configurationContext = context.Get<Data::ConfigurationContext>(); + THROW_WIN32_IF(ERROR_INVALID_STATE, !configurationContext.Set()); + IConfigurationSetProcessorFactory factory; + ConfigurationRemoting::ProcessorEngine processorEngine = ConfigurationRemoting::DetermineProcessorEngine(configurationContext.Set()); + + THROW_HR_IF(WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, processorEngine == ConfigurationRemoting::ProcessorEngine::Unknown); + + if (processorEngine == ConfigurationRemoting::ProcessorEngine::DSCv3) + { + context << EnsureFeatureEnabled(Settings::ExperimentalFeature::Feature::ConfigurationDSCv3); + if (context.IsTerminated()) + { + THROW_HR(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); + } + } // Since downgrading is not currently supported, only use dynamic if running limited. if (Runtime::IsRunningWithLimitedToken()) { - factory = ConfigurationRemoting::CreateDynamicRuntimeFactory(); + factory = ConfigurationRemoting::CreateDynamicRuntimeFactory(processorEngine); } else { - factory = ConfigurationRemoting::CreateOutOfProcessFactory(); + factory = ConfigurationRemoting::CreateOutOfProcessFactory(processorEngine); + } + + if (processorEngine == ConfigurationRemoting::ProcessorEngine::PowerShell) + { + Configuration::SetModulePath(context, factory); + } + else if (processorEngine == ConfigurationRemoting::ProcessorEngine::DSCv3) + { + if (context.Args.Contains(Args::Type::ConfigurationProcessorPath)) + { + auto factoryMap = factory.as<IMap<winrt::hstring, winrt::hstring>>(); + factoryMap.Insert(ConfigurationRemoting::ToHString(ConfigurationRemoting::PropertyName::DscExecutablePath), Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::ConfigurationProcessorPath))); + } } - Configuration::SetModulePath(context, factory); return factory; } @@ -136,10 +165,17 @@ namespace AppInstaller::CLI::Workflow context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); }); - ConfigurationContext configurationContext; - configurationContext.Processor(std::move(processor)); + if (context.Contains(Data::ConfigurationContext)) + { + context.Get<Data::ConfigurationContext>().Processor(std::move(processor)); + } + else + { + ConfigurationContext configurationContext; + configurationContext.Processor(std::move(processor)); - context.Add<Data::ConfigurationContext>(std::move(configurationContext)); + context.Add<Data::ConfigurationContext>(std::move(configurationContext)); + } } winrt::hstring GetValueSetString(const ValueSet& valueSet, std::wstring_view value) @@ -373,7 +409,13 @@ namespace AppInstaller::CLI::Workflow void OutputConfigurationUnitHeader(const ConfigurationUnit& unit, const winrt::hstring& name) { - m_context.Reporter.Info() << ConfigurationIntentEmphasis << ToResource(unit.Intent()) << " :: "_liv << ConfigurationUnitEmphasis << ConvertIdentifier(name); + m_context.Reporter.Info() << ConfigurationUnitEmphasis << ConvertIdentifier(name); + + if (unit.Environment().Context() == SecurityContext::Elevated) + { + // Shield + m_context.Reporter.Info() << "\xF0\x9F\x9B\xA1 "_liv; + } winrt::hstring identifier = unit.Identifier(); if (!identifier.empty()) @@ -392,7 +434,7 @@ namespace AppInstaller::CLI::Workflow if (details) { // -- Sample output when IConfigurationUnitProcessorDetails present -- - // Intent :: UnitType <from details> [Identifier] + // UnitType <from details> [Identifier] // UnitDocumentationUri <if present> // Description <from details first, directives second> // "Module": ModuleName "by" Author / Publisher (IsLocal / ModuleSource) @@ -412,14 +454,12 @@ namespace AppInstaller::CLI::Workflow { m_context.Reporter.Info() << " "_liv << ConvertDetailsValue(unitDescriptionFromDetails) << '\n'; } - else + + auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); + if (!unitDescriptionFromDirectives.empty()) { - auto unitDescriptionFromDirectives = GetValueSetString(metadata, s_Directive_Description); - if (!unitDescriptionFromDirectives.empty()) - { - m_context.Reporter.Info() << " "_liv; - OutputValueWithTruncationWarningIfNeeded(unitDescriptionFromDirectives); - } + m_context.Reporter.Info() << " "_liv; + OutputValueWithTruncationWarningIfNeeded(unitDescriptionFromDirectives); } auto author = ConvertDetailsIdentifier(details.Author()); @@ -427,13 +467,18 @@ namespace AppInstaller::CLI::Workflow { author = ConvertDetailsIdentifier(details.Publisher()); } - if (details.IsLocal()) - { - m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, Resource::String::ConfigurationLocal) << '\n'; - } - else + + auto moduleName = ConvertDetailsIdentifier(details.ModuleName()); + if (!moduleName.empty()) { - m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(ConvertDetailsIdentifier(details.ModuleName()), author, ConvertDetailsIdentifier(details.ModuleSource())) << '\n'; + if (details.IsLocal()) + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(moduleName, author, Resource::String::ConfigurationLocal) << '\n'; + } + else + { + m_context.Reporter.Info() << " "_liv << Resource::String::ConfigurationModuleWithDetails(moduleName, author, ConvertDetailsIdentifier(details.ModuleSource())) << '\n'; + } } // TODO: Currently the signature information is only for the top files. Maybe each item should be tagged? @@ -460,7 +505,7 @@ namespace AppInstaller::CLI::Workflow else { // -- Sample output when no IConfigurationUnitProcessorDetails present -- - // Intent :: Type <from unit> [identifier] + // Type <from unit> [identifier] // Description (from directives) // "Module": module <directive> OutputConfigurationUnitHeader(unit, unit.Type()); @@ -1048,12 +1093,6 @@ namespace AppInstaller::CLI::Workflow 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) { diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -8,6 +8,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; using AppInstallerCLIE2ETests.Helpers; + using Microsoft.Win32; using NUnit.Framework; /// <summary> @@ -23,9 +24,9 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("configuration03", true); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", true); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); } /// <summary> @@ -34,9 +35,9 @@ namespace AppInstallerCLIE2ETests [OneTimeTearDown] public void OneTimeTeardown() { - WinGetSettingsHelper.ConfigureFeature("configuration03", false); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", false); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); } /// <summary> @@ -254,13 +255,51 @@ namespace AppInstallerCLIE2ETests Assert.True(testFileContents.StartsWith(testDirectory)); } - private void DeleteTxtFiles() + /// <summary> + /// Runs a DSCv3 configuration, then changes the state and runs it again from history. + /// </summary> + [Test] + [Ignore("The registry resource is failing for unknown and undiagnosable reasons in the ADO pipeline. Replace these with test resources when we implement them next.")] + public void ConfigureThroughHistory_DSCv3() + { + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")); + Assert.AreEqual(0, result.ExitCode); + + // The configuration creates a file next to itself with the given contents + string valueName = "TestVal"; + var registryKey = Registry.CurrentUser.OpenSubKey(Constants.TestRegistryPath, true); + Assert.NotNull(registryKey); + var registryValue = (string)registryKey.GetValue(valueName); + Assert.NotNull(registryValue); + Assert.AreEqual("Value!", registryValue); + + registryKey.SetValue(valueName, "New Value!", RegistryValueKind.String); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor("ShowDetails_DSCv3.yml"); + result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, $"-h {guid}"); + Assert.AreEqual(0, result.ExitCode); + + registryValue = (string)registryKey.GetValue(valueName); + Assert.NotNull(registryValue); + Assert.AreEqual("Value!", registryValue); + } + + private void DeleteResourceArtifacts() { // Delete all .txt files in the test directory; they are placed there by the tests foreach (string file in Directory.GetFiles(TestCommon.GetTestDataFile("Configuration"), "*.txt")) { File.Delete(file); } + + var registryKey = Registry.CurrentUser.OpenSubKey(Constants.TestRegistryPath, true); + if (registryKey != null) + { + foreach (string valueName in registryKey.GetValueNames()) + { + registryKey.DeleteValue(valueName, false); + } + } } } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -8,6 +8,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; using AppInstallerCLIE2ETests.Helpers; + using Microsoft.Win32; using NUnit.Framework; /// <summary> @@ -16,13 +17,23 @@ namespace AppInstallerCLIE2ETests public class ConfigureShowCommand { /// <summary> + /// Setup done once before all the tests here. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); + } + + /// <summary> /// One time teardown. /// </summary> [OneTimeTearDown] public void OneTimeTearDown() { - WinGetSettingsHelper.ConfigureFeature("configuration03", false); - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + this.DeleteResourceArtifacts(); } /// <summary> @@ -76,25 +87,12 @@ namespace AppInstallerCLIE2ETests } /// <summary> - /// A schema 0.3 config file is not allowed without the experimental feature. - /// </summary> - [Test] - public void ShowDetails_Schema0_3_Fails() - { - WinGetSettingsHelper.ConfigureFeature("configuration03", false); - - var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo_0_3.yml")); - Assert.AreEqual(Constants.ErrorCode.ERROR_EXPERIMENTAL_FEATURE_DISABLED, result.ExitCode); - } - - /// <summary> /// A schema 0.3 config file is allowed with the experimental feature. /// </summary> [Test] public void ShowDetails_Schema0_3_Succeeds() { TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); - WinGetSettingsHelper.ConfigureFeature("configuration03", true); var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\ShowDetails_TestRepo_0_3.yml")} --verbose"); Assert.AreEqual(0, result.ExitCode); @@ -107,8 +105,6 @@ namespace AppInstallerCLIE2ETests [Test] public void ShowDetails_Schema0_3_Parameters() { - WinGetSettingsHelper.ConfigureFeature("configuration03", true); - var result = TestCommon.RunAICLICommand("configure show", TestCommon.GetTestDataFile("Configuration\\WithParameters_0_3.yml")); Assert.AreEqual(0, result.ExitCode); Assert.True(result.StdOut.Contains("Failed to get detailed information about the configuration.")); @@ -152,13 +148,89 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(0, result.ExitCode); } - private void DeleteTxtFiles() + /// <summary> + /// Runs a configuration, then shows it from history. + /// </summary> + [Test] + public void ShowWithBadProcessorIdentifier() + { + var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\Unknown_Processor.yml")} --verbose"); + Assert.AreEqual(Constants.ErrorCode.CONFIG_ERROR_INVALID_FIELD_VALUE, result.ExitCode); + } + + /// <summary> + /// Simple test to confirm that a resource is discoverable with DSC v3. + /// </summary> + [Test] + public void ShowDetails_DSCv3() + { + var result = TestCommon.RunAICLICommand("configure show", $"{TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")} --verbose"); + Assert.AreEqual(0, result.ExitCode); + + var outputLines = result.StdOut.Split('\n'); + int startLine = -1; + for (int i = 0; i < outputLines.Length; ++i) + { + if (outputLines[i].Trim() == "Microsoft.Windows/Registry [RegVal]") + { + startLine = i; + } + } + + Assert.AreNotEqual(-1, startLine); + Assert.LessOrEqual(3, outputLines.Length - startLine); + + // outputLines[1] should contain the discovered resource string if working properly. + Assert.AreEqual("Description 1.", outputLines[startLine + 2].Trim()); + } + + /// <summary> + /// Runs a DSCv3 configuration, then shows it from history. + /// </summary> + [Test] + [Ignore("The registry resource is failing for unknown and undiagnosable reasons in the ADO pipeline. Replace these with test resources when we implement them next.")] + public void ShowFromHistory_DSCv3() + { + var result = TestCommon.RunAICLICommand("configure --accept-configuration-agreements --verbose", TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")); + Assert.AreEqual(0, result.ExitCode); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor("ShowDetails_DSCv3.yml"); + result = TestCommon.RunAICLICommand("configure show", $"-h {guid} --"); + Assert.AreEqual(0, result.ExitCode); + + var outputLines = result.StdOut.Split('\n'); + int startLine = -1; + for (int i = 0; i < outputLines.Length; ++i) + { + if (outputLines[i].Trim() == "Microsoft.Windows/Registry [RegVal]") + { + startLine = i; + } + } + + Assert.AreNotEqual(-1, startLine); + Assert.LessOrEqual(3, outputLines.Length - startLine); + + // outputLines[1] should contain the discovered resource string if working properly. + Assert.AreEqual("Description 1.", outputLines[startLine + 2].Trim()); + } + + private void DeleteResourceArtifacts() { // Delete all .txt files in the test directory; they are placed there by the tests foreach (string file in Directory.GetFiles(TestCommon.GetTestDataFile("Configuration"), "*.txt")) { File.Delete(file); } + + var registryKey = Registry.CurrentUser.OpenSubKey(Constants.TestRegistryPath, true); + if (registryKey != null) + { + foreach (string valueName in registryKey.GetValueNames()) + { + registryKey.DeleteValue(valueName, false); + } + } } } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs @@ -8,6 +8,7 @@ namespace AppInstallerCLIE2ETests { using System.IO; using AppInstallerCLIE2ETests.Helpers; + using Microsoft.Win32; using NUnit.Framework; /// <summary> @@ -23,7 +24,8 @@ namespace AppInstallerCLIE2ETests [OneTimeSetUp] public void OneTimeSetup() { - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); } /// <summary> @@ -32,7 +34,8 @@ namespace AppInstallerCLIE2ETests [OneTimeTearDown] public void OneTimeTeardown() { - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); } /// <summary> @@ -42,7 +45,7 @@ namespace AppInstallerCLIE2ETests public void ConfigureTest_NotInDesiredState() { TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); var result = TestCommon.RunAICLICommand(CommandAndAgreements, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); @@ -56,7 +59,7 @@ namespace AppInstallerCLIE2ETests public void ConfigureTest_InDesiredState() { TestCommon.EnsureModuleState(Constants.SimpleTestModuleName, present: false); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); // Set up the expected state File.WriteAllText(TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.txt"), "Contents!"); @@ -113,13 +116,33 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); } - private void DeleteTxtFiles() + /// <summary> + /// Simple test to confirm that a resource is testable with DSC v3. + /// </summary> + [Test] + public void ConfigureTest_DSCv3() + { + var result = TestCommon.RunAICLICommand(CommandAndAgreements, $"{TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")} --verbose"); + Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); + Assert.True(result.StdOut.Contains("System is not in the described configuration state.")); + } + + private void DeleteResourceArtifacts() { // Delete all .txt files in the test directory; they are placed there by the tests foreach (string file in Directory.GetFiles(TestCommon.GetTestDataFile("Configuration"), "*.txt")) { File.Delete(file); } + + var registryKey = Registry.CurrentUser.OpenSubKey(Constants.TestRegistryPath, true); + if (registryKey != null) + { + foreach (string valueName in registryKey.GetValueNames()) + { + registryKey.DeleteValue(valueName, false); + } + } } } } diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -133,6 +133,7 @@ namespace AppInstallerCLIE2ETests public const string GalleryTestModuleName = "XmlContentDsc"; public const string SimpleTestModuleName = "xE2ETestResource"; public const string LocalModuleDescriptor = "[Local]"; + public const string TestRegistryPath = "Software\\Microsoft\\WinGet\\Tests"; // Group Policy Error Message public const string BlockByWinGetPolicyErrorMessage = "This operation is disabled by Group Policy : Enable Windows Package Manager"; diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -973,7 +973,7 @@ namespace AppInstallerCLIE2ETests.Helpers /// Gets the instance identifier of the first configuration history item with name in its output line. /// </summary> /// <param name="name">The string to search for.</param> - /// <returns>The instance identifier of a configuration that matched the search, or any empty string if none did.</returns> + /// <returns>The instance identifier of a configuration that matched the search, or an empty string if none did.</returns> public static string GetConfigurationInstanceIdentifierFor(string name) { var result = TestCommon.RunAICLICommand("configure list", string.Empty); diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/ShowDetails_DSCv3.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/ShowDetails_DSCv3.yml @@ -0,0 +1,14 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + winget: + processor: dscv3 +resources: + - name: RegVal + type: Microsoft.Windows/Registry + metadata: + description: Description 1. + properties: + keyPath: HKEY_CURRENT_USER\Software\Microsoft\WinGet\Tests + valueName: TestVal + valueData: + String: Value! diff --git a/src/AppInstallerCLIE2ETests/TestData/Configuration/Unknown_Processor.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/Unknown_Processor.yml @@ -0,0 +1,12 @@ +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + winget: + processor: unknown +resources: + - name: Name1 + type: xE2ETestResource/E2EFileResource + metadata: + repository: AppInstallerCLIE2ETestsRepo + properties: + prop1: 3 + prop2: '4' diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3204,4 +3204,7 @@ Please specify one of them using the --source option to proceed.</value> <data name="InstallerDownloadAuthenticationFailed" xml:space="preserve"> <value>Failed to download installer. Authentication failed.</value> </data> -</root> + <data name="ConfigurationProcessorPath" xml:space="preserve"> + <value>Specify the path to the configuration processor</value> + </data> +</root>+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -42,8 +42,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFDirectMSI>(); case ExperimentalFeature::Feature::Resume: return userSettings.Get<Setting::EFResume>(); - case ExperimentalFeature::Feature::Configuration03: - return userSettings.Get<Setting::EFConfiguration03>(); + case ExperimentalFeature::Feature::ConfigurationDSCv3: + return userSettings.Get<Setting::EFConfigurationDSCv3>(); case ExperimentalFeature::Feature::ConfigureExport: return userSettings.Get<Setting::EFConfigureExport>(); case ExperimentalFeature::Feature::Font: @@ -79,8 +79,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Direct MSI Installation", "directMSI", "https://aka.ms/winget-settings", Feature::DirectMSI }; case Feature::Resume: return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; - case Feature::Configuration03: - return ExperimentalFeature{ "Configuration Schema 0.3", "configuration03", "https://aka.ms/winget-settings", Feature::Configuration03 }; + case Feature::ConfigurationDSCv3: + return ExperimentalFeature{ "Support for DSC v3", "dsc3", "https://aka.ms/winget-settings", Feature::ConfigurationDSCv3 }; case Feature::ConfigureExport: return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport }; case Feature::Font: diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -24,7 +24,7 @@ namespace AppInstaller::Settings // Before making DirectMSI non-experimental, it should be part of manifest validation. DirectMSI = 0x1, Resume = 0x2, - Configuration03 = 0x4, + ConfigurationDSCv3 = 0x4, ConfigureExport = 0x8, Font = 0x10, Max, // This MUST always be after all experimental features diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -75,7 +75,7 @@ namespace AppInstaller::Settings EFExperimentalArg, EFDirectMSI, EFResume, - EFConfiguration03, + EFConfigurationDSCv3, EFConfigureExport, EFFonts, // Telemetry @@ -160,7 +160,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); - SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigurationDSCv3, bool, bool, false, ".experimentalFeatures.dsc3"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv); // Telemetry diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -266,7 +266,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFResume) - WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03) + WINGET_VALIDATE_PASS_THROUGH(EFConfigurationDSCv3) WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport) WINGET_VALIDATE_PASS_THROUGH(EFFonts) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) diff --git a/src/AppInstallerSharedLib/Public/winget/ConfigurationSetProcessorHandlers.h b/src/AppInstallerSharedLib/Public/winget/ConfigurationSetProcessorHandlers.h @@ -9,4 +9,6 @@ namespace AppInstaller::Configuration { constexpr std::wstring_view PowerShellHandlerIdentifier = L"pwsh"; constexpr std::wstring_view DynamicRuntimeHandlerIdentifier = L"{73fea39f-6f4a-41c9-ba94-6fd14d633e40}"; + constexpr std::wstring_view DSCv3HandlerIdentifier = L"{dbb2ac6d-1b58-4b05-9c50-b463cc434771}"; + constexpr std::wstring_view DSCv3DynamicRuntimeHandlerIdentifier = L"{5f83e564-ca26-41ca-89db-36f5f0517ffd}"; } diff --git a/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h b/src/AppInstallerSharedLib/Public/winget/IConfigurationStaticsInternals.h @@ -10,8 +10,7 @@ namespace AppInstaller::WinRT enum class ConfigurationStaticsInternalsStateFlags : UINT32 { None = 0, - Configuration03 = 0x1, - All = Configuration03 + All = None }; DEFINE_ENUM_FLAG_OPERATORS(ConfigurationStaticsInternalsStateFlags); diff --git a/src/ConfigurationRemotingServer/Program.cs b/src/ConfigurationRemotingServer/Program.cs @@ -9,6 +9,7 @@ using System.Text.Json.Serialization; using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; using WinRT; +using IConfigurationSetProcessorFactory = global::Microsoft.Management.Configuration.IConfigurationSetProcessorFactory; namespace ConfigurationRemotingServer { @@ -82,19 +83,10 @@ namespace ConfigurationRemotingServer { string completionEventName = args[2]; uint parentProcessId = uint.Parse(args[3]); + string processorEngine = args[4]; - PowerShellConfigurationSetProcessorFactory factory = new PowerShellConfigurationSetProcessorFactory(); - - // Set default properties. - var externalModulesPath = GetExternalModulesPath(); - if (string.IsNullOrWhiteSpace(externalModulesPath)) - { - throw new DirectoryNotFoundException("Failed to get ExternalModules."); - } - - // Set as implicit module paths so it will be always included in AdditionalModulePaths - factory.ImplicitModulePaths = new List<string>() { externalModulesPath }; - factory.ProcessorType = PowerShellConfigurationProcessorType.Hosted; + ConfigurationSet? limitationSet = null; + LimitationSetMetadata? limitationSetMetadata = null; // Parse limitation set if applicable. // The format will be: @@ -124,7 +116,7 @@ namespace ConfigurationRemotingServer memoryStream.Write(limitationSetBytes); memoryStream.Flush(); memoryStream.Seek(0, SeekOrigin.Begin); - ConfigurationProcessor processor = new ConfigurationProcessor(factory); + ConfigurationProcessor processor = new ConfigurationProcessor((IConfigurationSetProcessorFactory?)null); var limitationSetResult = processor.OpenConfigurationSet(memoryStream.AsInputStream()); memoryStream.Close(); @@ -133,41 +125,25 @@ namespace ConfigurationRemotingServer throw limitationSetResult.ResultCode; } - var limitationSet = limitationSetResult.Set; + limitationSet = limitationSetResult.Set; if (limitationSet == null) { throw new ArgumentException("The limitation set cannot be parsed."); } // Now parse metadata json and update the limitation set - var metadataJson = JsonSerializer.Deserialize<LimitationSetMetadata>(commandStr.Substring( + limitationSetMetadata = JsonSerializer.Deserialize<LimitationSetMetadata>(commandStr.Substring( firstSeparatorIndex + CommandLineSectionSeparator.Length, secondSeparatorIndex - firstSeparatorIndex - CommandLineSectionSeparator.Length)); - if (metadataJson != null) + if (limitationSetMetadata != null) { - limitationSet.Path = metadataJson.Path; - - if (metadataJson.ModulePath != null) - { - PowerShellConfigurationProcessorLocation parsedLocation = PowerShellConfigurationProcessorLocation.Default; - if (Enum.TryParse<PowerShellConfigurationProcessorLocation>(metadataJson.ModulePath, out parsedLocation)) - { - factory.Location = parsedLocation; - } - else - { - factory.Location = PowerShellConfigurationProcessorLocation.Custom; - factory.CustomLocation = metadataJson.ModulePath; - } - } + limitationSet.Path = limitationSetMetadata.Path; } - - // Set the limitation set in factory. - factory.LimitationSet = limitationSet; } - IObjectReference factoryInterface = MarshalInterface<global::Microsoft.Management.Configuration.IConfigurationSetProcessorFactory>.CreateMarshaler(factory); + IConfigurationSetProcessorFactory factory = CreateFactory(processorEngine, limitationSet, limitationSetMetadata); + IObjectReference factoryInterface = MarshalInterface<IConfigurationSetProcessorFactory>.CreateMarshaler(factory); return WindowsPackageManagerConfigurationCompleteOutOfProcessFactoryInitialization(0, factoryInterface.ThisPtr, staticsCallback, completionEventName, parentProcessId); } @@ -185,6 +161,90 @@ namespace ConfigurationRemotingServer [JsonPropertyName("modulePath")] public string? ModulePath { get; set; } = null; + + [JsonPropertyName("processorPath")] + public string? ProcessorPath { get; set; } = null; + } + + private static IConfigurationSetProcessorFactory CreateFactory(string processorEngine, ConfigurationSet? limitationSet, LimitationSetMetadata? limitationSetMetadata) + { + switch (processorEngine) + { + case "pwsh": + return CreatePowerShellFactory(limitationSet, limitationSetMetadata); + case "dscv3": + return CreateDSCv3Factory(limitationSet, limitationSetMetadata); + } + + throw new NotImplementedException($"Processor engine unknown: {processorEngine}"); + } + + private static IConfigurationSetProcessorFactory CreatePowerShellFactory(ConfigurationSet? limitationSet, LimitationSetMetadata? limitationSetMetadata) + { + PowerShellConfigurationSetProcessorFactory factory = new PowerShellConfigurationSetProcessorFactory(); + + // Set default properties. + var externalModulesPath = GetExternalModulesPath(); + if (string.IsNullOrWhiteSpace(externalModulesPath)) + { + throw new DirectoryNotFoundException("Failed to get ExternalModules."); + } + + // Set as implicit module paths so it will be always included in AdditionalModulePaths + factory.ImplicitModulePaths = new List<string>() { externalModulesPath }; + factory.ProcessorType = PowerShellConfigurationProcessorType.Hosted; + + if (limitationSetMetadata != null) + { + if (limitationSetMetadata.ModulePath != null) + { + PowerShellConfigurationProcessorLocation parsedLocation = PowerShellConfigurationProcessorLocation.Default; + if (Enum.TryParse(limitationSetMetadata.ModulePath, out parsedLocation)) + { + factory.Location = parsedLocation; + } + else + { + factory.Location = PowerShellConfigurationProcessorLocation.Custom; + factory.CustomLocation = limitationSetMetadata.ModulePath; + } + } + } + + // Apply limitation set and thereby disable changing properties. + if (limitationSet != null) + { + factory.LimitationSet = limitationSet; + } + + return factory; + } + + private static IConfigurationSetProcessorFactory CreateDSCv3Factory(ConfigurationSet? limitationSet, LimitationSetMetadata? limitationSetMetadata) + { + DSCv3ConfigurationSetProcessorFactory factory = new DSCv3ConfigurationSetProcessorFactory(); + + if (limitationSetMetadata != null) + { + if (limitationSetMetadata.ProcessorPath != null) + { + factory.DscExecutablePath = limitationSetMetadata.ProcessorPath; + } + else + { + // Require that the path to the DSC executable be presented to the user in limitation mode. + // This helps prevent path attacks against an elevated process (as long as the user checks the value). + throw new ArgumentNullException("The path to the DSC executable must be supplied in limitation mode."); + } + } + + // Apply limitation set and thereby disable changing properties. + if (limitationSet != null) + { + factory.LimitationSet = limitationSet; + } + + return factory; } private static string GetExternalModulesPath() diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessExecution.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessExecution.cs @@ -0,0 +1,242 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ProcessExecution.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + using System.Threading; + + /// <summary> + /// Wrapper for a single process execution and its output. + /// </summary> + internal class ProcessExecution + { + private List<string> outputLines = new List<string>(); + private List<string> errorLines = new List<string>(); + + /// <summary> + /// Initializes a new instance of the <see cref="ProcessExecution"/> class. + /// </summary> + public ProcessExecution() + { + } + + /// <summary> + /// An event that receives the output lines as they are delivered. + /// </summary> + public event EventHandler<string>? OutputLineReceived; + + /// <summary> + /// An event that receives the error lines as they are delivered. + /// </summary> + public event EventHandler<string>? ErrorLineReceived; + + /// <summary> + /// Gets the executable path. + /// </summary> + required public string ExecutablePath { get; init; } + + /// <summary> + /// Gets the arguments to use for the process. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public IEnumerable<string> Arguments { get; init; } = []; + + /// <summary> + /// Gets the data to write to standard input of the process. + /// </summary> + public string? Input { get; init; } = null; + + /// <summary> + /// Gets the argument string passed to the process. + /// </summary> + public string SerializedArguments + { + get + { + StringBuilder processArguments = new StringBuilder(); + + foreach (string arg in this.Arguments) + { + if (processArguments.Length != 0) + { + processArguments.Append(' '); + } + + processArguments.Append(arg); + } + + return processArguments.ToString(); + } + } + + /// <summary> + /// Gets the full command line that the process should see. + /// </summary> + public string CommandLine + { + get + { + return $"{this.ExecutablePath} {this.SerializedArguments}"; + } + } + + /// <summary> + /// Gets the current set of output lines. + /// Not thread safe, use OutputLineReceived for async flows. + /// </summary> + public IReadOnlyCollection<string> Output + { + get { return this.outputLines; } + } + + /// <summary> + /// Gets the current set of error lines. + /// Not thread safe, use ErrorLineReceived for async flows. + /// </summary> + public IReadOnlyCollection<string> Error + { + get { return this.errorLines; } + } + + /// <summary> + /// Gets the exit code of the process. + /// Will be null until the process exits. + /// </summary> + public int? ExitCode { get; private set; } = null; + + /// <summary> + /// Gets or sets the process object; null until Start called. + /// </summary> + private Process? Process { get; set; } + + /// <summary> + /// Starts the process. + /// </summary> + /// <returns>This object.</returns> + /// <exception cref="InvalidOperationException">Thrown if Start has already been called.</exception> + public ProcessExecution Start() + { + if (this.Process != null) + { + throw new InvalidOperationException("Process has already been started."); + } + + ProcessStartInfo startInfo = new ProcessStartInfo(this.ExecutablePath, this.SerializedArguments); + this.Process = new Process() { StartInfo = startInfo }; + + startInfo.UseShellExecute = false; + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + + startInfo.StandardOutputEncoding = Encoding.UTF8; + startInfo.RedirectStandardOutput = true; + this.Process.OutputDataReceived += (sender, args) => + { + string? output = args.Data; + + if (output != null) + { + this.outputLines.Add(output); + + this.OutputLineReceived?.Invoke(this, output); + } + }; + + startInfo.StandardErrorEncoding = Encoding.UTF8; + startInfo.RedirectStandardError = true; + this.Process.ErrorDataReceived += (sender, args) => + { + string? error = args.Data; + + if (error != null) + { + this.errorLines.Add(error); + + this.ErrorLineReceived?.Invoke(this, error); + } + }; + + if (this.Input != null) + { + startInfo.StandardInputEncoding = Encoding.UTF8; + startInfo.RedirectStandardInput = true; + } + + this.Process.Start(); + this.Process.BeginOutputReadLine(); + this.Process.BeginErrorReadLine(); + + if (this.Input != null) + { + this.Process.StandardInput.Write(this.Input); + this.Process.StandardInput.Close(); + } + + return this; + } + + /// <summary> + /// Waits for the process to exit. + /// </summary> + /// <param name="milliseconds">The minimum amount of time to wait for the process to exit, in milliseconds.</param> + /// <returns>True if the process exited; false if not.</returns> + /// <exception cref="InvalidOperationException">Thrown if Start has not been called.</exception> + public bool WaitForExit(int milliseconds = Timeout.Infinite) + { + if (this.Process == null) + { + throw new InvalidOperationException("Process has not been started."); + } + + if (this.Process.WaitForExit(milliseconds)) + { + // According to documentation, this extra call will ensure that the redirected streams have finished reading all of the data. + this.Process.WaitForExit(); + + this.ExitCode = this.Process.ExitCode; + + return true; + } + else + { + return false; + } + } + + /// <summary> + /// Gets all of the output lines as a single string. + /// </summary> + /// <returns>The output lines as a string.</returns> + public string GetAllOutputLines() + { + return GetAllLines(this.outputLines); + } + + /// <summary> + /// Gets all of the error lines as a single string. + /// </summary> + /// <returns>The error lines as a string.</returns> + public string GetAllErrorLines() + { + return GetAllLines(this.errorLines); + } + + private static string GetAllLines(List<string> lines) + { + StringBuilder stringBuilder = new StringBuilder(); + + foreach (string line in lines) + { + stringBuilder.AppendLine(line); + } + + return stringBuilder.ToString(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs @@ -0,0 +1,161 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ProcessorSettings.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// Contains settings for the DSC v3 processor components to share. + /// </summary> + internal class ProcessorSettings + { + private const string DscExecutableFileName = "dsc.exe"; + + private readonly object dscV3Lock = new (); + private readonly object defaultPathLock = new (); + + private IDSCv3? dscV3 = null; + private string? defaultPath = null; + + /// <summary> + /// Gets or sets the path to the DSC v3 executable. + /// </summary> + public string? DscExecutablePath { get; set; } + + /// <summary> + /// Gets the path to the DSC v3 executable. + /// </summary> + public string EffectiveDscExecutablePath + { + get + { + if (this.DscExecutablePath != null) + { + return this.DscExecutablePath; + } + + lock (this.defaultPathLock) + { + if (this.defaultPath != null) + { + return this.defaultPath; + } + } + + string? localDefaultPath = FindDscExecutablePath(); + + if (localDefaultPath == null) + { + throw new FileNotFoundException("Could not find DSC v3 executable path."); + } + + lock (this.defaultPathLock) + { + if (this.defaultPath == null) + { + this.defaultPath = localDefaultPath; + } + + return this.defaultPath; + } + } + } + + /// <summary> + /// Gets an object for interacting with the DSC executable at EffectiveDscExecutablePath. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1623:Property summary documentation should match accessors", Justification = "Set is only provided for tests.")] + public IDSCv3 DSCv3 + { + get + { + lock (this.dscV3Lock) + { + if (this.dscV3 == null) + { + this.dscV3 = IDSCv3.Create(this); + } + + return this.dscV3; + } + } + +#if !AICLI_DISABLE_TEST_HOOKS + set + { + lock (this.dscV3Lock) + { + this.dscV3 = value; + } + } +#endif + } + + /// <summary> + /// Find the DSC v3 executable. + /// </summary> + /// <returns>The full path to the dsc.exe executable, or null if not found.</returns> + public static string? FindDscExecutablePath() + { + // To start, only attempt to find the package and launch it via the app execution fallback handler. + // In the future, discover it through %PATH% searching, but probably don't allow that from an elevated process. + // That probably means creating another read property for finding the secure path. + Windows.Management.Deployment.PackageManager packageManager = new Windows.Management.Deployment.PackageManager(); + + // Until there is a non-preview of this package, use the preview version. + var packages = packageManager.FindPackagesForUser(null, "Microsoft.DesiredStateConfiguration-Preview_8wekyb3d8bbwe"); + + if (packages == null) + { + return null; + } + + string packageInstallLocation = packages.First().InstalledLocation.Path; + string result = Path.Combine(packageInstallLocation, DscExecutableFileName); + + if (!Path.Exists(result)) + { + return null; + } + + return result; + } + + /// <summary> + /// Create a deep copy of this settings object. + /// </summary> + /// <returns>A deep copy of this object.</returns> + public ProcessorSettings Clone() + { + ProcessorSettings result = new ProcessorSettings(); + + result.DscExecutablePath = this.DscExecutablePath; +#if !AICLI_DISABLE_TEST_HOOKS + result.dscV3 = this.DSCv3; +#endif + + return result; + } + + /// <summary> + /// Gets a string representation of this object. + /// </summary> + /// <returns>A string representation of this object.</returns> + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + sb.Append("EffectiveDscExecutablePath: "); + sb.Append(this.EffectiveDscExecutablePath); + + return sb.ToString(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs @@ -0,0 +1,164 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceDetails.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + using System; + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.Unit; + + /// <summary> + /// Cached data about a resource. + /// </summary> + internal class ResourceDetails + { + private readonly ConfigurationUnitInternal configurationUnitInternal; + + private object detailsUpdateLock = new object(); + private IResourceListItem? resourceListItem = null; + + /// <summary> + /// The current level of detail stored by this object. + /// + /// The method of discovery for each of the levels in ConfigurationUnitDetailFlags: + /// None: No details, either because the resource was not found or EnsureDetails has not been called. + /// Local: `resource list` is used to determine the details. An embedded schema may enable "Load" details level. + /// Property information may not be available at this level. + /// Catalog: Same as local; there is currently no catalog to query against. + /// Download: Same as local; there is currently no catalog to find anything to download. + /// Load: `resource schema` is used to get the full schema for the resource. + /// This ensures that property information is available. + /// </summary> + private ConfigurationUnitDetailFlags currentDetailLevel = ConfigurationUnitDetailFlags.None; + + /// <summary> + /// Initializes a new instance of the <see cref="ResourceDetails"/> class. + /// </summary> + /// <param name="configurationUnitInternal">The internal configuration unit data.</param> + public ResourceDetails(ConfigurationUnitInternal configurationUnitInternal) + { + this.configurationUnitInternal = configurationUnitInternal; + } + + /// <summary> + /// Gets a value indicating whether this resource exists. + /// Will be false until EnsureDetails is called and the resource is found. + /// </summary> + public bool Exists + { + get + { + lock (this.detailsUpdateLock) + { + return this.currentDetailLevel != ConfigurationUnitDetailFlags.None; + } + } + } + + /// <summary> + /// Ensures that the given detail level is present. + /// </summary> + /// <param name="processorSettings">The processor settings to use when getting details.</param> + /// <param name="detailFlags">The detail level flags.</param> + public void EnsureDetails(ProcessorSettings processorSettings, ConfigurationUnitDetailFlags detailFlags) + { + if (this.DetailsNeededFor(detailFlags, ConfigurationUnitDetailFlags.Local)) + { + // If we can't get local details, then exit until we have more options. + if (!this.GetLocalDetails(processorSettings)) + { + return; + } + } + + if (this.DetailsNeededFor(detailFlags, ConfigurationUnitDetailFlags.Load)) + { + this.GetLoadDetails(processorSettings); + } + } + + /// <summary> + /// Gets a ConfigurationUnitProcessorDetails populated with all available data. + /// </summary> + /// <returns>A ConfigurationUnitProcessorDetails populated with all available data.</returns> + public ConfigurationUnitProcessorDetails? GetConfigurationUnitProcessorDetails() + { + if (!this.Exists) + { + return null; + } + + ConfigurationUnitProcessorDetails result = new ConfigurationUnitProcessorDetails() { UnitType = this.configurationUnitInternal.QualifiedName }; + + lock (this.detailsUpdateLock) + { + if (this.resourceListItem != null) + { + // TODO: Expose the Directory; requires adding a new property to the public interface + result.UnitType = this.resourceListItem.Type; + result.IsGroup = IsGroup(this.resourceListItem.Kind); + result.Version = this.resourceListItem.Version; + result.UnitDescription = this.resourceListItem.Description; + result.Author = this.resourceListItem.Author; + + result.IsLocal = true; + } + } + + return result; + } + + private static bool IsGroup(ResourceKind kind) => kind switch + { + ResourceKind.Adapter => true, + ResourceKind.Group => true, + _ => false, + }; + + private bool DetailsNeededFor(ConfigurationUnitDetailFlags detailFlags, ConfigurationUnitDetailFlags targetLevel) + { + if (!detailFlags.HasFlag(targetLevel)) + { + return false; + } + + lock (this.detailsUpdateLock) + { + return !this.currentDetailLevel.HasFlag(targetLevel); + } + } + + private bool GetLocalDetails(ProcessorSettings processorSettings) + { + IResourceListItem? resourceListItem = processorSettings.DSCv3.GetResourceByType(this.configurationUnitInternal.QualifiedName); + + if (resourceListItem != null) + { + // TODO: Attempt to extract embedded schema to avoid the need for Load. + lock (this.detailsUpdateLock) + { + if (!this.currentDetailLevel.HasFlag(ConfigurationUnitDetailFlags.Local)) + { + this.resourceListItem = resourceListItem; + this.currentDetailLevel |= ConfigurationUnitDetailFlags.Local; + } + } + + return true; + } + else + { + return false; + } + } + + private void GetLoadDetails(ProcessorSettings processorSettings) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IDSCv3.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IDSCv3.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IDSCv3.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.Helpers; + + /// <summary> + /// Interface for interacting with DSC v3. + /// </summary> + internal interface IDSCv3 + { + /// <summary> + /// Creates the appropriate instance of the DSCv3 interface for the given executable. + /// </summary> + /// <param name="processorSettings">The processor settings.</param> + /// <returns>An object that properly interacts with the specific version of DSC v3.</returns> + public static IDSCv3 Create(ProcessorSettings processorSettings) + { + // Expand as needed to detect the version of dsc.exe and/or its schemas in use. + return new Schema_2024_04.DSCv3(processorSettings); + } + + /// <summary> + /// Gets a single resource by its type name. + /// </summary> + /// <param name="resourceType">The type name of the resource.</param> + /// <returns>A single resource item.</returns> + public IResourceListItem? GetResourceByType(string resourceType); + + /// <summary> + /// Tests a configuration unit. + /// </summary> + /// <param name="unitInternal">The unit to test.</param> + /// <returns>A test result.</returns> + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// Gets a configuration unit settings. + /// </summary> + /// <param name="unitInternal">The unit to get.</param> + /// <returns>A get result.</returns> + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// Sets a configuration unit settings. + /// </summary> + /// <param name="unitInternal">The unit to set.</param> + /// <returns>A set result.</returns> + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceGetItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceGetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IResourceGetItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using Windows.Foundation.Collections; + + /// <summary> + /// The interface to a `resource get` command result. + /// </summary> + internal interface IResourceGetItem + { + /// <summary> + /// Gets the settings for this item. + /// </summary> + public ValueSet Settings { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceListItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceListItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IResourceListItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// <summary> + /// The interface to a single JSON line output by the `resource list` command. + /// </summary> + internal interface IResourceListItem + { + /// <summary> + /// Gets the type of the resource. + /// Should match the regex "^\\w+(\\.\\w+){0,2}\\/\\w+$". + /// </summary> + public string Type { get; } + + /// <summary> + /// Gets the kind of the resource. + /// </summary> + public ResourceKind Kind { get; } + + /// <summary> + /// Gets the version of the resource. + /// This is a semver version. + /// </summary> + public string? Version { get; } + + /// <summary> + /// Gets the description of the resource. + /// </summary> + public string? Description { get; } + + /// <summary> + /// Gets the path to the directory containing the resource. + /// </summary> + public string? Directory { get; } + + /// <summary> + /// Gets the author of the resource. + /// </summary> + public string? Author { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceSetItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceSetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IResourceSetItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using System.Collections.Generic; + + /// <summary> + /// The interface to a `resource set` command result. + /// </summary> + internal interface IResourceSetItem + { + /// <summary> + /// Gets a value indicating whether a reboot is required. + /// </summary> + public bool RebootRequired { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceTestItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceTestItem.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IResourceTestItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// <summary> + /// The interface to a `resource test` command result. + /// </summary> + internal interface IResourceTestItem + { + /// <summary> + /// Gets a value indicating whether the resource is in the desired state. + /// </summary> + public bool InDesiredState { get; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/ResourceKind.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/ResourceKind.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceKind.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// <summary> + /// https://learn.microsoft.com/en-us/powershell/dsc/reference/schemas/definitions/resourcekind?view=dsc-3.0 + /// The kind of resource. + /// </summary> + internal enum ResourceKind + { + /// <summary> + /// The kind is unknown. + /// </summary> + Unknown, + + /// <summary> + /// A standard resource. + /// </summary> + Resource, + + /// <summary> + /// An adapter resource. + /// </summary> + Adapter, + + /// <summary> + /// A group resource. + /// </summary> + Group, + + /// <summary> + /// An import resource. + /// </summary> + Import, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs @@ -0,0 +1,182 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 +{ + using System; + using System.Linq; + using System.Text.Json; + using System.Text.Json.Serialization; + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.Processor.Helpers; + using Windows.Foundation.Collections; + + /// <summary> + /// An instance of IDSCv3 for interacting with 1.0. + /// </summary> + internal class DSCv3 : IDSCv3 + { + private const string PlainTextTraces = "-t plaintext"; + private const string ResourceCommand = "resource"; + private const string ListCommand = "list"; + private const string TestCommand = "test"; + private const string GetCommand = "get"; + private const string SetCommand = "set"; + private const string ResourceParameter = "-r"; + private const string FileParameter = "-f"; + private const string StdInputIdentifier = "-"; + + private readonly ProcessorSettings processorSettings; + + /// <summary> + /// Initializes a new instance of the <see cref="DSCv3"/> class. + /// </summary> + /// <param name="processorSettings">The processor settings.</param> + public DSCv3(ProcessorSettings processorSettings) + { + this.processorSettings = processorSettings; + } + + /// <inheritdoc /> + public IResourceListItem? GetResourceByType(string resourceType) + { + ProcessExecution processExecution = new ProcessExecution() + { + ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, + Arguments = new[] { PlainTextTraces, ResourceCommand, ListCommand, resourceType }, + }; + + RunSynchronously(processExecution); + + if (processExecution.Output.Count > 1) + { + throw new Exceptions.GetDscResourceMultipleMatches(resourceType, null); + } + + return GetOptionalSingleOutputLineAs<ResourceListItem>(processExecution); + } + + /// <inheritdoc /> + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal) + { + ProcessExecution processExecution = new ProcessExecution() + { + ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, + Arguments = new[] { PlainTextTraces, ResourceCommand, TestCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), + }; + + if (RunSynchronously(processExecution)) + { + throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Test, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); + } + + return TestFullItem.CreateFrom(GetRequiredSingleOutputLineAsJSON(processExecution, Exceptions.InvokeDscResourceException.Test, unitInternal.QualifiedName), GetDefaultJsonOptions()); + } + + /// <inheritdoc /> + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal) + { + ProcessExecution processExecution = new ProcessExecution() + { + ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, + Arguments = new[] { PlainTextTraces, ResourceCommand, GetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), + }; + + if (RunSynchronously(processExecution)) + { + throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Get, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); + } + + return GetFullItem.CreateFrom(GetRequiredSingleOutputLineAsJSON(processExecution, Exceptions.InvokeDscResourceException.Get, unitInternal.QualifiedName), GetDefaultJsonOptions()); + } + + /// <inheritdoc /> + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal) + { + ProcessExecution processExecution = new ProcessExecution() + { + ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, + Arguments = new[] { PlainTextTraces, ResourceCommand, SetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), + }; + + if (RunSynchronously(processExecution)) + { + throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Set, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); + } + + return SetFullItem.CreateFrom(GetRequiredSingleOutputLineAsJSON(processExecution, Exceptions.InvokeDscResourceException.Set, unitInternal.QualifiedName), GetDefaultJsonOptions()); + } + + /// <summary> + /// Runs the process, waiting until it completes. + /// </summary> + /// <param name="processExecution">The process to run.</param> + /// <returns>True if the exit code was not 0.</returns> + private static bool RunSynchronously(ProcessExecution processExecution) + { + processExecution.Start().WaitForExit(); + + return processExecution.ExitCode != 0; + } + + private static void ThrowOnMultipleOutputLines(ProcessExecution processExecution, string method, string resourceName) + { + if (processExecution.Output.Count > 1) + { + throw new Exceptions.InvokeDscResourceException(method, resourceName, processExecution.GetAllOutputLines()); + } + } + + private static void ThrowOnZeroOutputLines(ProcessExecution processExecution, string method, string resourceName) + { + if (processExecution.Output.Count == 0) + { + throw new Exceptions.InvokeDscResourceException(method, resourceName); + } + } + + private static T? GetOptionalSingleOutputLineAs<T>(ProcessExecution processExecution) + { + if (processExecution.Output.Count == 0) + { + return default; + } + + return JsonSerializer.Deserialize<T>(processExecution.Output.First(), GetDefaultJsonOptions()); + } + + private static JsonDocument GetRequiredSingleOutputLineAsJSON(ProcessExecution processExecution, string method, string resourceName) + { + ThrowOnMultipleOutputLines(processExecution, method, resourceName); + ThrowOnZeroOutputLines(processExecution, method, resourceName); + + return JsonDocument.Parse(processExecution.Output.First()); + } + + private static JsonSerializerOptions GetDefaultJsonOptions() + { + return new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(), + }, + }; + } + + private static string ConvertValueSetToJSON(ValueSet valueSet) + { + return JsonSerializer.Serialize(valueSet.ToHashtable()); + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Definitions/ResourceKind.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Definitions/ResourceKind.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceKind.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Definitions +{ + /// <summary> + /// https://learn.microsoft.com/en-us/powershell/dsc/reference/schemas/definitions/resourcekind?view=dsc-3.0 + /// The kind of resource. + /// </summary> + internal enum ResourceKind + { + /// <summary> + /// The kind is unknown. + /// </summary> + Unknown, + + /// <summary> + /// A standard resource. + /// </summary> + Resource, + + /// <summary> + /// An adapter resource. + /// </summary> + Adapter, + + /// <summary> + /// A group resource. + /// </summary> + Group, + + /// <summary> + /// An import resource. + /// </summary> + Import, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Metadata/ResourceInstanceResult.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Metadata/ResourceInstanceResult.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceInstanceResult.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Metadata +{ + using System; + using System.Text.Json.Serialization; + + /// <summary> + /// Defines metadata DSC returns for a DSC configuration operation against a resource instance. + /// </summary> + internal class ResourceInstanceResult + { + /// <summary> + /// Gets or sets the context metadata for this instance result. + /// </summary> + [JsonRequired] + [JsonPropertyName("Microsoft.DSC")] + public ContextMetadata? MicrosoftDSC { get; set; } + + /// <summary> + /// Contains properties generated by the DSC v3 platform. + /// </summary> + public class ContextMetadata + { + /// <summary> + /// Gets or sets the duration of a resource instance execution. + /// </summary> + [JsonRequired] + public TimeSpan Duration { get; set; } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/FullItemBase.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/FullItemBase.cs @@ -0,0 +1,127 @@ +// ----------------------------------------------------------------------------- +// <copyright file="FullItemBase.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.IO; + using System.Text.Json; + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + using Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Metadata; + + /// <summary> + /// The base implementation of the full form output. + /// When the retrieved instance is for group resource, adapter resource, or nested inside a group or adapter resource, DSC returns a full result, which also includes the resource type and instance name. + /// </summary> + /// <typeparam name="TSimple">The simple item type.</typeparam> + /// <typeparam name="TFull">The full item type.</typeparam> + internal class FullItemBase<TSimple, TFull> + where TFull : FullItemBase<TSimple, TFull>, new() + { + private const string NameProperty = "name"; + + /// <summary> + /// Initializes a new instance of the <see cref="FullItemBase{TSimple,TFull}"/> class. + /// </summary> + public FullItemBase() + { + } + + /// <summary> + /// Gets or sets the metadata for this test result. + /// </summary> + [JsonRequired] + public ResourceInstanceResult? Metadata { get; set; } + + /// <summary> + /// Gets or sets the name for this test result. + /// </summary> + [JsonRequired] + public string? Name { get; set; } + + /// <summary> + /// Gets or sets the type for the resource. + /// </summary> + [JsonRequired] + public string? Type { get; set; } + + /// <summary> + /// Gets or sets the result of the test. + /// </summary> + [JsonRequired] + public JsonNode? Result { get; set; } + + /// <summary> + /// Gets or sets the simple result. + /// </summary> + [JsonIgnore] + public TSimple? SimpleResult { get; set; } + + /// <summary> + /// Gets or sets the full results. + /// </summary> + [JsonIgnore] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Opening square brackets should be spaced correctly", Justification = "Pending SC 1.2 release")] + protected TFull[]? FullResults { get; set; } + + /// <summary> + /// Initializes a new instance of the TFull class. + /// </summary> + /// <param name="document">The document to construct from.</param> + /// <param name="options">The options to use.</param> + /// <returns>The item created.</returns> + public static TFull CreateFrom(JsonDocument document, JsonSerializerOptions options) + { + if (!document.RootElement.TryGetProperty(NameProperty, out JsonElement jsonElement)) + { + return new () { SimpleResult = JsonSerializer.Deserialize<TSimple>(document, options) }; + } + else + { + TFull? result = JsonSerializer.Deserialize<TFull>(document, options); + + if (result == null) + { + throw new InvalidDataException("Unable to deserialize full result."); + } + + result.ProcessResult(options); + return result; + } + } + + /// <summary> + /// Converts the Result property into the appropriate simple or full results. + /// </summary> + /// <param name="options">The options to use.</param> + public void ProcessResult(JsonSerializerOptions options) + { + if (this.Result == null) + { + throw new System.InvalidOperationException("JSON result has not been initialized."); + } + + if (this.Result is JsonObject jsonObject) + { + this.SimpleResult = JsonSerializer.Deserialize<TSimple>(this.Result, options); + } + else + { + this.FullResults = JsonSerializer.Deserialize<TFull[]>(this.Result, options); + + if (this.FullResults == null) + { + throw new InvalidDataException("Unable to deserialize full results."); + } + + foreach (TFull result in this.FullResults) + { + result.ProcessResult(options); + } + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetFullItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetFullItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetFullItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Microsoft.Management.Configuration.Processor.Extensions; + using Windows.Foundation.Collections; + + /// <summary> + /// The full form of the get output. + /// When the retrieved instance is for group resource, adapter resource, or nested inside a group or adapter resource, DSC returns a full get result, which also includes the resource type and instance name. + /// </summary> + internal class GetFullItem : FullItemBase<GetSimpleItem, GetFullItem>, IResourceGetItem + { + /// <summary> + /// Initializes a new instance of the <see cref="GetFullItem"/> class. + /// </summary> + public GetFullItem() + { + } + + /// <inheritdoc /> + public ValueSet Settings + { + get + { + if (this.SimpleResult != null) + { + return this.SimpleResult.ActualState?.ToValueSet() ?? throw new System.InvalidOperationException("Get result has not been initialized."); + } + else if (this.FullResults != null) + { + throw new System.NotImplementedException("Requires constructing the entire group as the settings."); + } + else + { + throw new System.InvalidOperationException("Get result has not been initialized."); + } + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetSimpleItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetSimpleItem.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// <copyright file="GetSimpleItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// <summary> + /// The simple form of the get output. + /// DSC returns a simple get response when the instance isn't a group resource, adapter resource, or nested inside a group or adapter resource. + /// </summary> + internal class GetSimpleItem + { + /// <summary> + /// Gets or sets the state of the resource properties. + /// </summary> + [JsonRequired] + public JsonObject? ActualState { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceCapability.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceCapability.cs @@ -0,0 +1,60 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceCapability.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + /// <summary> + /// https://learn.microsoft.com/en-us/powershell/dsc/reference/schemas/outputs/resource/list?view=dsc-3.0#capabilities + /// The capabilities that a resource can have. + /// </summary> + internal enum ResourceCapability + { + /// <summary> + /// Can call get on the resource. + /// Required. + /// </summary> + Get, + + /// <summary> + /// Can call set on the resource. + /// </summary> + Set, + + /// <summary> + /// The resource operates properly in the presence of the `_exist` property. + /// If not present, DSC will use `delete` when `_exist == false`. + /// </summary> + SetHandlesExist, + + /// <summary> + /// The resource can handle a "what if" query directly. + /// Otherwise, DSC will handle it synthetically. + /// </summary> + WhatIf, + + /// <summary> + /// The resource can handle a "test" query directly. + /// Otherwise, DSC will handle it synthetically. + /// </summary> + Test, + + /// <summary> + /// Can call delete on the resource. + /// </summary> + Delete, + + /// <summary> + /// Can call export on the resource. + /// </summary> + Export, + + /// <summary> + /// Can call resolve on the resource. + /// This can produce new resources, such as importing another configuration document. + /// </summary> + Resolve, + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceListItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceListItem.cs @@ -0,0 +1,95 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResourceListItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// The object type from a single JSON line output by the `resource list` command. + /// </summary> + internal class ResourceListItem : IResourceListItem + { + /// <summary> + /// Gets or sets the type of the resource. + /// Should match the regex "^\\w+(\\.\\w+){0,2}\\/\\w+$". + /// </summary> + [JsonRequired] + required public string Type { get; set; } + + /// <summary> + /// Gets or sets the kind of the resource. + /// </summary> + public Definitions.ResourceKind Kind { get; set; } = Definitions.ResourceKind.Unknown; + + /// <inheritdoc /> + [JsonIgnore] + Model.ResourceKind IResourceListItem.Kind => this.Kind switch + { + Definitions.ResourceKind.Unknown => Model.ResourceKind.Unknown, + Definitions.ResourceKind.Resource => Model.ResourceKind.Resource, + Definitions.ResourceKind.Adapter => Model.ResourceKind.Adapter, + Definitions.ResourceKind.Group => Model.ResourceKind.Group, + Definitions.ResourceKind.Import => Model.ResourceKind.Import, + _ => throw new System.IO.InvalidDataException($"Unknown ResourceKind: {this.Kind}") + }; + + /// <summary> + /// Gets or sets the version of the resource. + /// This is a semver version. + /// </summary> + public string? Version { get; set; } + + /// <summary> + /// Gets or sets the capabilities of the resource. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public ResourceCapability[] Capabilities { get; set; } = []; + + /// <summary> + /// Gets or sets the description of the resource. + /// </summary> + public string? Description { get; set; } + + /// <summary> + /// Gets or sets the path to the resource definition file. + /// </summary> + public string? Path { get; set; } + + /// <summary> + /// Gets or sets the path to the directory containing the resource. + /// </summary> + public string? Directory { get; set; } + + /// <summary> + /// Gets or sets a value that indicates implementation details of the resource. + /// </summary> + public JsonObject? ImplementedAs { get; set; } + + /// <summary> + /// Gets or sets the author of the resource. + /// </summary> + public string? Author { get; set; } + + /// <summary> + /// Gets or sets the names of the properties of the resource. + /// </summary> + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public string[] Properties { get; set; } = []; + + /// <summary> + /// Gets or sets the adapter required by the resource. + /// </summary> + public string? RequireAdapter { get; set; } + + /// <summary> + /// Gets or sets the resource definition manifest. + /// </summary> + public JsonObject? Manifest { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetFullItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetFullItem.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------------- +// <copyright file="SetFullItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// The full form of the set output. + /// When the retrieved instance is for group resource, adapter resource, or nested inside a group or adapter resource, DSC returns a full set result, which also includes the resource type and instance name. + /// </summary> + internal class SetFullItem : FullItemBase<SetSimpleItem, SetFullItem>, IResourceSetItem + { + /// <summary> + /// Initializes a new instance of the <see cref="SetFullItem"/> class. + /// </summary> + public SetFullItem() + { + } + + /// <inheritdoc /> + public bool RebootRequired => false; + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetSimpleItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetSimpleItem.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------- +// <copyright file="SetSimpleItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// <summary> + /// The simple form of the set output. + /// DSC returns a simple set response when the instance isn't a group resource, adapter resource, or nested inside a group or adapter resource. + /// </summary> + internal class SetSimpleItem + { + /// <summary> + /// Gets or sets the state of the resource properties before the attempt to set them. + /// </summary> + [JsonRequired] + public JsonObject? BeforeState { get; set; } + + /// <summary> + /// Gets or sets the state of the resource properties after the attempt to set them. + /// </summary> + [JsonRequired] + public JsonObject? AfterState { get; set; } + + /// <summary> + /// Gets or sets the list of properties that changed. + /// </summary> + [JsonRequired] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public string[] ChangedProperties { get; set; } = []; + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestFullItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestFullItem.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestFullItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// The full form of the test output. + /// When the retrieved instance is for group resource, adapter resource, or nested inside a group or adapter resource, DSC returns a full test result, which also includes the resource type and instance name. + /// </summary> + internal class TestFullItem : FullItemBase<TestSimpleItem, TestFullItem>, IResourceTestItem + { + /// <summary> + /// Initializes a new instance of the <see cref="TestFullItem"/> class. + /// </summary> + public TestFullItem() + { + } + + /// <inheritdoc /> + public bool InDesiredState + { + get + { + if (this.SimpleResult != null) + { + return this.SimpleResult.InDesiredState; + } + else if (this.FullResults != null) + { + bool result = true; + + foreach (var item in this.FullResults) + { + result = result && item.InDesiredState; + } + + return result; + } + else + { + throw new System.InvalidOperationException("Test result has not been initialized."); + } + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestSimpleItem.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestSimpleItem.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestSimpleItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// <summary> + /// The simple form of the test output. + /// DSC returns a simple test response when the instance isn't a group resource, adapter resource, or nested inside a group or adapter resource. + /// </summary> + internal class TestSimpleItem + { + /// <summary> + /// Gets or sets the desired state of the resource properties. + /// </summary> + [JsonRequired] + public JsonObject? DesiredState { get; set; } + + /// <summary> + /// Gets or sets the actual state of the resource properties. + /// </summary> + [JsonRequired] + public JsonObject? ActualState { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the resource is in the desired state. + /// </summary> + [JsonRequired] + public bool InDesiredState { get; set; } + + /// <summary> + /// Gets or sets the list of properties that are not in the desired state. + /// </summary> + [JsonRequired] + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1010:Opening square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3687 pending SC 1.2 release")] + public string[] DifferingProperties { get; set; } = []; + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ConfigurationSetProcessor.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Set +{ + using System.Collections.Generic; + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.DSCv3.Unit; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.Set; + + /// <summary> + /// Configuration set processor. + /// </summary> + internal sealed partial class DSCv3ConfigurationSetProcessor : ConfigurationSetProcessorBase, IConfigurationSetProcessor + { + private readonly ProcessorSettings processorSettings; + private Dictionary<string, ResourceDetails> resourceDetailsDictionary = new (); + + /// <summary> + /// Initializes a new instance of the <see cref="DSCv3ConfigurationSetProcessor"/> class. + /// </summary> + /// <param name="processorSettings">The processor settings to use.</param> + /// <param name="configurationSet">Configuration set.</param> + /// <param name="isLimitMode">Whether the set processor should work in limitation mode.</param> + public DSCv3ConfigurationSetProcessor(ProcessorSettings processorSettings, ConfigurationSet? configurationSet, bool isLimitMode = false) + : base(configurationSet, isLimitMode) + { + this.processorSettings = processorSettings; + } + + /// <inheritdoc /> + protected override IConfigurationUnitProcessor CreateUnitProcessorInternal(ConfigurationUnit unit) + { + ConfigurationUnitInternal configurationUnitInternal = new ConfigurationUnitInternal(unit, this.ConfigurationSet?.Path); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.QualifiedName}..."); + + ResourceDetails? resourceDetails = this.GetResourceDetails(configurationUnitInternal, ConfigurationUnitDetailFlags.Local); + if (resourceDetails == null) + { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Resource not found: {configurationUnitInternal.QualifiedName}"); + throw new Exceptions.FindDscResourceNotFoundException(configurationUnitInternal.QualifiedName, null); + } + + return new DSCv3ConfigurationUnitProcessor(this.processorSettings, configurationUnitInternal, this.IsLimitMode); + } + + /// <inheritdoc /> + protected override IConfigurationUnitProcessorDetails? GetUnitProcessorDetailsInternal(ConfigurationUnit unit, ConfigurationUnitDetailFlags detailFlags) + { + ConfigurationUnitInternal configurationUnitInternal = new ConfigurationUnitInternal(unit, this.ConfigurationSet?.Path); + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting resource details [{detailFlags}] for: {configurationUnitInternal.QualifiedName}..."); + + ResourceDetails? resourceDetails = this.GetResourceDetails(configurationUnitInternal, detailFlags); + if (resourceDetails == null) + { + this.OnDiagnostics(DiagnosticLevel.Verbose, $"Resource not found: {configurationUnitInternal.QualifiedName}"); + return null; + } + + return resourceDetails.GetConfigurationUnitProcessorDetails(); + } + + private ResourceDetails? GetResourceDetails(ConfigurationUnitInternal configurationUnitInternal, ConfigurationUnitDetailFlags detailFlags) + { + ResourceDetails? result = null; + bool inDictionary = false; + + lock (this.resourceDetailsDictionary) + { + inDictionary = this.resourceDetailsDictionary.TryGetValue(configurationUnitInternal.QualifiedName, out result); + } + + if (result == null) + { + result = new ResourceDetails(configurationUnitInternal); + } + + result.EnsureDetails(this.processorSettings, detailFlags); + + if (result.Exists) + { + if (!inDictionary) + { + lock (this.resourceDetailsDictionary) + { + this.resourceDetailsDictionary.Add(configurationUnitInternal.QualifiedName, result); + } + } + + return result; + } + else + { + return null; + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ConfigurationUnitProcessor.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Unit +{ + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.Unit; + using Windows.Foundation.Collections; + + /// <summary> + /// Provides access to a specific configuration unit within the runtime. + /// </summary> + internal sealed partial class DSCv3ConfigurationUnitProcessor : ConfigurationUnitProcessorBase, IConfigurationUnitProcessor + { + private readonly ProcessorSettings processorSettings; + + /// <summary> + /// Initializes a new instance of the <see cref="DSCv3ConfigurationUnitProcessor"/> class. + /// </summary> + /// <param name="processorSettings">The processor settings to use.</param> + /// <param name="unitInternal">Internal unit.</param> + /// <param name="isLimitMode">Whether it is under limit mode.</param> + internal DSCv3ConfigurationUnitProcessor(ProcessorSettings processorSettings, ConfigurationUnitInternal unitInternal, bool isLimitMode = false) + : base(unitInternal, isLimitMode) + { + this.processorSettings = processorSettings; + } + + /// <inheritdoc /> + protected override ValueSet GetSettingsInternal() + { + return this.processorSettings.DSCv3.GetResourceSettings(this.UnitInternal).Settings; + } + + /// <inheritdoc /> + protected override bool TestSettingsInternal() + { + return this.processorSettings.DSCv3.TestResource(this.UnitInternal).InDesiredState; + } + + /// <inheritdoc /> + protected override bool ApplySettingsInternal() + { + return this.processorSettings.DSCv3.SetResourceSettings(this.UnitInternal).RebootRequired; + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="InvokeDscResourceException.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -38,7 +38,7 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions /// <param name="method">Method.</param> /// <param name="resourceName">Resource name.</param> /// <param name="module">Optional module.</param> - public InvokeDscResourceException(string method, string resourceName, ModuleSpecification? module) + public InvokeDscResourceException(string method, string resourceName, ModuleSpecification? module = null) : base(CreateMessage(method, resourceName, module, null)) { // No message means that the invoke returned an invalid result. @@ -50,6 +50,21 @@ namespace Microsoft.Management.Configuration.Processor.Exceptions /// <summary> /// Initializes a new instance of the <see cref="InvokeDscResourceException"/> class. + /// Use this constructor when there is a message and the result is not valid. + /// </summary> + /// <param name="method">Method.</param> + /// <param name="resourceName">Resource name.</param> + /// <param name="message">Message.</param> + public InvokeDscResourceException(string method, string resourceName, string message) + : base(CreateMessage(method, resourceName, null, message)) + { + this.HResult = ErrorCodes.WinGetConfigUnitInvokeInvalidResult; + this.Method = method; + this.ResourceName = resourceName; + } + + /// <summary> + /// Initializes a new instance of the <see cref="InvokeDscResourceException"/> class. /// Use this constructor when the invoke fails with an error message. /// </summary> /// <param name="method">Method.</param> diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/JsonObjectExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/JsonObjectExtensions.cs @@ -0,0 +1,69 @@ +// ----------------------------------------------------------------------------- +// <copyright file="JsonObjectExtensions.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Text.Json; + using System.Text.Json.Nodes; + using Windows.Foundation.Collections; + + /// <summary> + /// Extensions for JsonObject. + /// </summary> + internal static class JsonObjectExtensions + { + /// <summary> + /// Converts the JSON object to a ValueSet. + /// </summary> + /// <param name="jsonObject">The object to convert.</param> + /// <returns>The ValueSet.</returns> + public static ValueSet ToValueSet(this JsonObject jsonObject) + { + ValueSet result = new ValueSet(); + + foreach (var item in jsonObject) + { + result.Add(item.Key, ToValue(item.Value)); + } + + return result; + } + + private static object? ToValue(JsonNode? node) => node switch + { + JsonObject obj => obj.ToValueSet(), + JsonArray array => ToValueSet(array), + JsonValue value => ToValue(value), + _ => null, + }; + + private static ValueSet ToValueSet(JsonArray array) + { + ValueSet result = new ValueSet(); + result.Add(ValueSetExtensions.TreatAsArray, true); + + int index = 0; + foreach (var item in array) + { + result.Add(index.ToString(), ToValue(item)); + ++index; + } + + return result; + } + + private static object? ToValue(JsonValue value) => value.GetValueKind() switch + { + JsonValueKind.Null => null, + JsonValueKind.Undefined => null, + JsonValueKind.String => value.GetValue<string>(), + JsonValueKind.Number => value.GetValue<long>(), + JsonValueKind.True => true, + JsonValueKind.False => false, + _ => throw new System.NotImplementedException("Unexpected default case") + }; + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/ValueSetExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/ValueSetExtensions.cs @@ -16,7 +16,10 @@ namespace Microsoft.Management.Configuration.Processor.Extensions /// </summary> internal static class ValueSetExtensions { - private const string TreatAsArray = "treatAsArray"; + /// <summary> + /// The value in a ValueSet that indicates that it is an array of items. + /// </summary> + internal const string TreatAsArray = "treatAsArray"; /// <summary> /// Extension method to transform a ValueSet to a Hashtable. diff --git a/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj b/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj @@ -25,6 +25,10 @@ <WinGetCsWinRTEmbedded Condition="'$(WinGetCsWinRTEmbedded)'==''">true</WinGetCsWinRTEmbedded> </PropertyGroup> + <PropertyGroup Condition="'$(WingetDisableTestHooks)'=='true'"> + <DefineConstants>$(DefineConstants);AICLI_DISABLE_TEST_HOOKS</DefineConstants> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)'=='Release'"> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> @@ -69,17 +73,28 @@ </ProjectReference> </ItemGroup> - <ItemGroup> - <Folder Include="PowerShell\Unit\" /> - <Folder Include="PowerShell\Set\" /> - </ItemGroup> - <PropertyGroup Condition="'$(WinGetCsWinRTEmbedded)'=='true'"> <DefineConstants>$(DefineConstants);WinGetCsWinRTEmbedded</DefineConstants> <CsWinRTComponent>false</CsWinRTComponent> <CsWinRTIncludes> Microsoft.Management.Configuration; + Windows.ApplicationModel.AppDisplayInf; + Windows.ApplicationModel.IAppDisplayInf; + Windows.ApplicationModel.AppExecutionContex; + Windows.ApplicationModel.IAppExecutionContex; + Windows.ApplicationModel.AppInf; + Windows.ApplicationModel.IAppInf; + Windows.ApplicationModel.AppInstallerInf; + Windows.ApplicationModel.IAppInstallerInf; + Windows.ApplicationModel.AppInstallerPolicySourc; + Windows.ApplicationModel.AddResourcePackageOption; + Windows.ApplicationModel.IAddResourcePackageOption; + Windows.ApplicationModel.Packag; + Windows.ApplicationModel.IPackag; + Windows.ApplicationModel.Core.AppListEntr; + Windows.ApplicationModel.Core.IAppListEntr; Windows.Data.Text.TextSegmen; + Windows.Management.Deployment; Windows.Devices.Geolocation; Windows.Foundation; Windows.Globalization.DayOfWee; @@ -91,6 +106,7 @@ Windows.Networking.IHostNam; Windows.Security.Cryptography.Certificates; Windows.Storage; + Windows.System.ProcessorArchitectur; Windows.System.Use; Windows.System.IUse; </CsWinRTIncludes> diff --git a/src/Microsoft.Management.Configuration.Processor/PowerShell/Set/PowerShellConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/PowerShell/Set/PowerShellConfigurationSetProcessor.cs @@ -20,7 +20,7 @@ namespace Microsoft.Management.Configuration.Processor.PowerShell.Set using Windows.Security.Cryptography.Certificates; /// <summary> - /// Configuration set processor. + /// IConfigurationSetProcessor implementation using PowerShell DSC v2. /// </summary> internal sealed partial class PowerShellConfigurationSetProcessor : ConfigurationSetProcessorBase, IConfigurationSetProcessor { diff --git a/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs @@ -0,0 +1,198 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ConfigurationSetProcessorFactory.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.DSCv3.Set; + using Microsoft.Management.Configuration.Processor.Factory; + + /// <summary> + /// IConfigurationSetProcessorFactory implementation using DSC v3. + /// </summary> + internal sealed partial class DSCv3ConfigurationSetProcessorFactory : ConfigurationSetProcessorFactoryBase, IConfigurationSetProcessorFactory, IDictionary<string, string> + { + private const string DscExecutablePathPropertyName = "DscExecutablePath"; + private const string FoundDscExecutablePathPropertyName = "FoundDscExecutablePath"; + + private ProcessorSettings processorSettings = new (); + + /// <summary> + /// Initializes a new instance of the <see cref="DSCv3ConfigurationSetProcessorFactory"/> class. + /// </summary> + public DSCv3ConfigurationSetProcessorFactory() + { + } + + /// <summary> + /// Gets or sets the path to the DSC v3 executable. + /// </summary> + public string? DscExecutablePath + { + get + { + return this.processorSettings.DscExecutablePath; + } + + set + { + if (this.IsLimitMode()) + { + throw new InvalidOperationException("Setting DscExecutablePath in limit mode is invalid."); + } + + this.processorSettings.DscExecutablePath = value; + } + } + +#if !AICLI_DISABLE_TEST_HOOKS + /// <summary> + /// Gets the processor settings; for tests only. + /// </summary> + public ProcessorSettings Settings + { + get + { + return this.processorSettings; + } + } +#endif + + /// <inheritdoc /> + public ICollection<string> Keys => throw new NotImplementedException(); + + /// <inheritdoc /> + public ICollection<string> Values => throw new NotImplementedException(); + + /// <inheritdoc /> + public int Count => throw new NotImplementedException(); + + /// <inheritdoc /> + public bool IsReadOnly => this.IsLimitMode(); + + /// <inheritdoc /> + public string this[string key] { get => this.GetValue(key); set => this.SetValue(key, value); } + + /// <inheritdoc /> + public void Add(string key, string value) + { + this.SetValue(key, value); + } + + /// <inheritdoc /> + public void Add(KeyValuePair<string, string> item) + { + this.SetValue(item.Key, item.Value); + } + + /// <inheritdoc /> + public void Clear() + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public bool Contains(KeyValuePair<string, string> item) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public bool ContainsKey(string key) + { + switch (key) + { + case DscExecutablePathPropertyName: + return this.DscExecutablePath != null; + } + + return false; + } + + /// <inheritdoc /> + public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public bool Remove(string key) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public bool Remove(KeyValuePair<string, string> item) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + value = null; + + switch (key) + { + case DscExecutablePathPropertyName: + value = this.DscExecutablePath!; + return true; + case FoundDscExecutablePathPropertyName: + value = ProcessorSettings.FindDscExecutablePath() !; + return true; + } + + return false; + } + + /// <inheritdoc /> + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// <inheritdoc /> + protected override IConfigurationSetProcessor CreateSetProcessorInternal(ConfigurationSet? set, bool isLimitMode) + { + ProcessorSettings processorSettingsCopy = this.processorSettings.Clone(); + this.OnDiagnostics(DiagnosticLevel.Verbose, "Creating set processor with settings:\n" + processorSettingsCopy.ToString()); + return new DSCv3ConfigurationSetProcessor(processorSettingsCopy, set, isLimitMode); + } + + private string GetValue(string name) + { + if (this.TryGetValue(name, out string? result)) + { + return result; + } + + throw new ArgumentOutOfRangeException($"Invalid property name: {name}"); + } + + private void SetValue(string name, string value) + { + switch (name) + { + case DscExecutablePathPropertyName: + this.DscExecutablePath = value; + break; + default: + throw new ArgumentOutOfRangeException($"Invalid property name: {name}"); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/PowerShellConfigurationSetProcessorFactory.cs @@ -9,7 +9,6 @@ namespace Microsoft.Management.Configuration.Processor using System; using System.Collections.Generic; using System.IO; - using System.Runtime.CompilerServices; using System.Text; using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor.Factory; @@ -19,7 +18,7 @@ namespace Microsoft.Management.Configuration.Processor using static Microsoft.Management.Configuration.Processor.PowerShell.Constants.PowerShellConstants; /// <summary> - /// ConfigurationSetProcessorFactory implementation. + /// IConfigurationSetProcessorFactory implementation using PowerShell DSC v2. /// </summary> #if WinGetCsWinRTEmbedded internal diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs @@ -49,6 +49,11 @@ namespace Microsoft.Management.Configuration.Processor.Unit internal ConfigurationSetProcessorFactoryBase? SetProcessorFactory { get; init; } /// <summary> + /// Gets the internal configuration unit. + /// </summary> + protected ConfigurationUnitInternal UnitInternal => this.unitInternal; + + /// <summary> /// Gets the current system state for the configuration unit. /// Calls Get on the DSC resource. /// </summary> diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs @@ -13,7 +13,7 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <summary> /// Provides information for a specific configuration unit within the runtime. /// </summary> - internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails + internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails, IConfigurationUnitProcessorDetails2 { /// <summary> /// Initializes a new instance of the <see cref="ConfigurationUnitProcessorDetails"/> class. @@ -23,9 +23,9 @@ namespace Microsoft.Management.Configuration.Processor.Unit } /// <summary> - /// Gets the name of the unit of configuration. + /// Gets or sets the name of the unit of configuration. /// </summary> - required public string UnitType { get; init; } + required public string UnitType { get; internal set; } /// <summary> /// Gets or sets the description of the unit of configuration. @@ -111,5 +111,10 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// Gets or sets a value indicating whether the module comes from a public repository. /// </summary> public bool IsPublic { get; internal set; } + + /// <summary> + /// Gets or sets a value indicating whether this resource is a group. + /// </summary> + public bool IsGroup { get; internal set; } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestDSCv3.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestDSCv3.cs @@ -0,0 +1,109 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestDSCv3.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Microsoft.Management.Configuration.Processor.Helpers; + + /// <summary> + /// Implements IDSCv3 for tests. + /// </summary> + internal class TestDSCv3 : IDSCv3 + { + /// <summary> + /// The delegate type for GetResourceByType. + /// </summary> + /// <param name="resourceType">The type name of the resource.</param> + /// <returns>A single resource item.</returns> + internal delegate IResourceListItem? GetResourceByTypeDelegateType(string resourceType); + + /// <summary> + /// The delegate type for GetResourceSettings. + /// </summary> + /// <param name="unitInternal">The unit to get.</param> + /// <returns>A get result.</returns> + internal delegate IResourceGetItem GetResourceSettingsDelegateType(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// The delegate type for SetResourceSettings. + /// </summary> + /// <param name="unitInternal">The unit to set.</param> + /// <returns>A set result.</returns> + internal delegate IResourceSetItem SetResourceSettingsDelegateType(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// The delegate type for TestResource. + /// </summary> + /// <param name="unitInternal">The unit to test.</param> + /// <returns>A test result.</returns> + internal delegate IResourceTestItem TestResourceDelegateType(ConfigurationUnitInternal unitInternal); + + /// <summary> + /// Gets or sets the GetResourceByType result. + /// </summary> + public IResourceListItem? GetResourceByTypeResult { get; set; } + + /// <summary> + /// Gets or sets the GetResourceByType delegate. + /// </summary> + public GetResourceByTypeDelegateType? GetResourceByTypeDelegate { get; set; } + + /// <summary> + /// Gets or sets the GetResourceSettings result. + /// </summary> + public IResourceGetItem? GetResourceSettingsResult { get; set; } + + /// <summary> + /// Gets or sets the GetResourceSettings delegate. + /// </summary> + public GetResourceSettingsDelegateType? GetResourceSettingsDelegate { get; set; } + + /// <summary> + /// Gets or sets the SetResourceSettings result. + /// </summary> + public IResourceSetItem? SetResourceSettingsResult { get; set; } + + /// <summary> + /// Gets or sets the SetResourceSettings delegate. + /// </summary> + public SetResourceSettingsDelegateType? SetResourceSettingsDelegate { get; set; } + + /// <summary> + /// Gets or sets the TestResource result. + /// </summary> + public IResourceTestItem? TestResourceResult { get; set; } + + /// <summary> + /// Gets or sets the TestResource delegate. + /// </summary> + public TestResourceDelegateType? TestResourceDelegate { get; set; } + + /// <inheritdoc/> + public IResourceListItem? GetResourceByType(string resourceType) + { + return this.GetResourceByTypeResult ?? this.GetResourceByTypeDelegate?.Invoke(resourceType); + } + + /// <inheritdoc/> + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal) + { + return this.GetResourceSettingsResult ?? this.GetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); + } + + /// <inheritdoc/> + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal) + { + return this.SetResourceSettingsResult ?? this.SetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); + } + + /// <inheritdoc/> + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal) + { + return this.TestResourceResult ?? this.TestResourceDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); + } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceGetItem.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceGetItem.cs @@ -0,0 +1,22 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestResourceGetItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Windows.Foundation.Collections; + + /// <summary> + /// Implements IResourceGetItem for tests. + /// </summary> + internal class TestResourceGetItem : IResourceGetItem + { + /// <summary> + /// Gets or sets the settings. + /// </summary> + public ValueSet Settings { get; set; } = new ValueSet(); + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceListItem.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceListItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestResourceListItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// Implements IResourceListItem for tests. + /// </summary> + internal class TestResourceListItem : IResourceListItem + { + /// <summary> + /// Gets or sets the type. + /// </summary> + required public string Type { get; set; } + + /// <summary> + /// Gets or sets the kind. + /// </summary> + public ResourceKind Kind { get; set; } + + /// <summary> + /// Gets or sets the version. + /// </summary> + public string? Version { get; set; } + + /// <summary> + /// Gets or sets the description. + /// </summary> + public string? Description { get; set; } + + /// <summary> + /// Gets or sets the directory. + /// </summary> + public string? Directory { get; set; } + + /// <summary> + /// Gets or sets the author. + /// </summary> + public string? Author { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceSetItem.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceSetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestResourceSetItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// Implements IResourceSetItem for tests. + /// </summary> + internal class TestResourceSetItem : IResourceSetItem + { + /// <summary> + /// Gets or sets a value indicating whether a reboot is required. + /// </summary> + public bool RebootRequired { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceTestItem.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceTestItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="TestResourceTestItem.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// <summary> + /// Implements IResourceTestItem for tests. + /// </summary> + internal class TestResourceTestItem : IResourceTestItem + { + /// <summary> + /// Gets or sets a value indicating whether the system is in the desired state. + /// </summary> + public bool InDesiredState { get; set; } + } +} diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ValueSetExtensions.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ValueSetExtensions.cs @@ -48,6 +48,9 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers case int i: sb.Append(i); break; + case long l: + sb.Append(l); + break; case string s: sb.Append(s); break; diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs @@ -10,6 +10,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; + using System.Xml.Linq; using Microsoft.Management.Configuration.Processor.Extensions; using Microsoft.Management.Configuration.UnitTests.Fixtures; using Microsoft.Management.Configuration.UnitTests.Helpers; @@ -399,7 +400,7 @@ properties: Assert.Equal(expectedUnit.Identifier, actualUnit.Identifier); Assert.Equal(expectedUnit.Intent, actualUnit.Intent); Assert.Equal(expectedUnit.Dependencies, actualUnit.Dependencies); - Assert.True(expectedUnit.Metadata.ContentEquals(actualUnit.Metadata)); + Assert.True(expectedUnit.Metadata.ContentEquals(actualUnit.Metadata), $"Metadata not equal: {expectedUnit.Identifier}\n---expected---:\n{expectedUnit.Metadata.ToYaml()}\n---actual---:\n{actualUnit.Metadata.ToYaml()}"); Assert.True(expectedUnit.Settings.ContentEquals(actualUnit.Settings)); Assert.Equal(expectedUnit.IsActive, actualUnit.IsActive); Assert.Equal(expectedUnit.IsGroup, actualUnit.IsGroup); diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetAuthoringTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetAuthoringTests.cs @@ -149,14 +149,14 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Helpers.ConfigurationEnvironmentData[] environments = new Helpers.ConfigurationEnvironmentData[] { - new () { ProcessorIdentifier = "dsc3" }, + new () { ProcessorIdentifier = "dscv3" }, new () { ProcessorIdentifier = "pwsh" }, - new () { ProcessorIdentifier = "dsc3", Context = SecurityContext.Elevated }, + new () { ProcessorIdentifier = "dscv3", Context = SecurityContext.Elevated }, new () { ProcessorIdentifier = "pwsh", Context = SecurityContext.Restricted }, - new () { ProcessorIdentifier = "dsc3", ProcessorProperties = firstProperty }, + new () { ProcessorIdentifier = "dscv3", ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", ProcessorProperties = secondProperty }, - new () { ProcessorIdentifier = "dsc3", Context = SecurityContext.Restricted, ProcessorProperties = firstProperty }, + new () { ProcessorIdentifier = "dscv3", Context = SecurityContext.Restricted, ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", Context = SecurityContext.Elevated, ProcessorProperties = firstProperty }, }; @@ -186,14 +186,14 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests Helpers.ConfigurationEnvironmentData[] environments = new Helpers.ConfigurationEnvironmentData[] { - new () { ProcessorIdentifier = "dsc3" }, + new () { ProcessorIdentifier = "dscv3" }, new () { ProcessorIdentifier = "pwsh" }, - new () { ProcessorIdentifier = "dsc3", Context = SecurityContext.Elevated }, + new () { ProcessorIdentifier = "dscv3", Context = SecurityContext.Elevated }, new () { ProcessorIdentifier = "pwsh", Context = SecurityContext.Restricted }, - new () { ProcessorIdentifier = "dsc3", ProcessorProperties = firstProperty }, + new () { ProcessorIdentifier = "dscv3", ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", ProcessorProperties = secondProperty }, - new () { ProcessorIdentifier = "dsc3", Context = SecurityContext.Restricted, ProcessorProperties = firstProperty }, + new () { ProcessorIdentifier = "dscv3", Context = SecurityContext.Restricted, ProcessorProperties = firstProperty }, new () { ProcessorIdentifier = "pwsh", Context = SecurityContext.Elevated, ProcessorProperties = firstProperty }, new (), // The default environment for the group unit }; diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorTests.cs @@ -0,0 +1,122 @@ +// ----------------------------------------------------------------------------- +// <copyright file="DSCv3ProcessorTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using Microsoft.Management.Configuration.Processor; + using Microsoft.Management.Configuration.Processor.Exceptions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Xunit; + using Xunit.Abstractions; + + /// <summary> + /// Tests for the DSCv3 processor. + /// </summary> + [Collection("UnitTestCollection")] + [InProc] + public class DSCv3ProcessorTests : ConfigurationProcessorTestBase + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// <summary> + /// Initializes a new instance of the <see cref="DSCv3ProcessorTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public DSCv3ProcessorTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + this.fixture = fixture; + this.log = log; + } + + /// <summary> + /// Tests for the unit details caching. + /// </summary> + [Fact] + public void Set_UnitPropertyDetailsCached() + { + var (factory, dsc) = CreateTestFactory(); + var set = this.ConfigurationSet(); + string type1 = "Type1"; + string type2 = "Type2"; + var unit1 = this.ConfigurationUnit().Assign(new { Type = type1 }); + var unit2 = this.ConfigurationUnit().Assign(new { Type = type2 }); + + var setProcessor = factory.CreateSetProcessor(set); + + // Initially, no details + var details = setProcessor.GetUnitProcessorDetails(unit1, ConfigurationUnitDetailFlags.Local); + Assert.Null(details); + + // Null result not cached + dsc.GetResourceByTypeResult = new TestResourceListItem() { Type = type1 }; + details = setProcessor.GetUnitProcessorDetails(unit1, ConfigurationUnitDetailFlags.Local); + Assert.NotNull(details); + Assert.Equal(type1, details.UnitType); + + // Not-null result cached + dsc.GetResourceByTypeResult = null; + dsc.GetResourceByTypeDelegate = s => throw new System.Exception("Shouldn't be called"); + details = setProcessor.GetUnitProcessorDetails(unit1, ConfigurationUnitDetailFlags.Local); + Assert.NotNull(details); + Assert.Equal(type1, details.UnitType); + + // Different type, no details + dsc.GetResourceByTypeDelegate = null; + details = setProcessor.GetUnitProcessorDetails(unit2, ConfigurationUnitDetailFlags.Local); + Assert.Null(details); + + // Null result not cached + dsc.GetResourceByTypeResult = new TestResourceListItem() { Type = type2 }; + details = setProcessor.GetUnitProcessorDetails(unit2, ConfigurationUnitDetailFlags.Local); + Assert.NotNull(details); + Assert.Equal(type2, details.UnitType); + + // First type is still first type + dsc.GetResourceByTypeResult = null; + dsc.GetResourceByTypeDelegate = s => throw new System.Exception("Shouldn't be called"); + details = setProcessor.GetUnitProcessorDetails(unit1, ConfigurationUnitDetailFlags.Local); + Assert.NotNull(details); + Assert.Equal(type1, details.UnitType); + } + + /// <summary> + /// Test for unit processor creation requiring resource to be found. + /// </summary> + [Fact] + public void Set_ResourceNotFoundIsError() + { + var (factory, dsc) = CreateTestFactory(); + var set = this.ConfigurationSet(); + string type1 = "Type1"; + var unit1 = this.ConfigurationUnit().Assign(new { Type = type1 }); + + var setProcessor = factory.CreateSetProcessor(set); + + // Not found is error + Assert.Throws<FindDscResourceNotFoundException>(() => setProcessor.CreateUnitProcessor(unit1)); + + // Found is not error + dsc.GetResourceByTypeResult = new TestResourceListItem() { Type = type1 }; + var unitProcessor = setProcessor.CreateUnitProcessor(unit1); + Assert.NotNull(unitProcessor); + Assert.Equal(type1, unitProcessor.Unit.Type); + } + + private static (DSCv3ConfigurationSetProcessorFactory, TestDSCv3) CreateTestFactory() + { + DSCv3ConfigurationSetProcessorFactory factory = new DSCv3ConfigurationSetProcessorFactory(); + TestDSCv3 dsc = new TestDSCv3(); + factory.Settings.DSCv3 = dsc; + factory.Settings.DscExecutablePath = "Test-Path-Not-Used.txt"; + + return (factory, dsc); + } + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -289,13 +289,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation std::unique_ptr<ConfigurationSetParser> parser = ConfigurationSetParser::Create(inputString); - // Temporary block on parsing 0.3 schema while it is experimental. - if (parser->GetSchemaVersion() == L"0.3" && !m_supportSchema03) - { - result->Initialize(APPINSTALLER_CLI_ERROR_EXPERIMENTAL_FEATURE_DISABLED); - co_return *result; - } - if (FAILED(parser->Result())) { result->Initialize(parser->Result(), parser->Field(), parser->Value(), parser->Line(), parser->Column()); @@ -907,11 +900,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation // While diagnostics can be important, a failure to send them should not cause additional issues. catch (...) {} - void ConfigurationProcessor::SetSupportsSchema03(bool value) - { - m_supportSchema03 = value; - } - void ConfigurationProcessor::SendDiagnosticsImpl(const IDiagnosticInformation& information) { std::lock_guard<std::recursive_mutex> lock{ m_diagnosticsMutex }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -95,9 +95,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Sends diagnostics objects to the event. void SendDiagnostics(const IDiagnosticInformation& information); - // Temporary entry point to enable experimental schema support. - void SetSupportsSchema03(bool value); - // Indicate a configuration change occurred. void ConfigurationChange(const Configuration::ConfigurationSet& set, const Configuration::ConfigurationChangeData& data); @@ -137,8 +134,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation std::recursive_mutex m_diagnosticsMutex; ConfigurationDatabase m_database; bool m_isHandlingDiagnostics = false; - // Temporary value to enable experimental schema support. - bool m_supportSchema03 = true; std::shared_ptr<ConfigurationStatus::ChangeRegistration> m_changeRegistration; #endif }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp @@ -566,9 +566,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation { THROW_HR_IF_NULL(E_POINTER, unit); + ExtractSecurityContext(unit->Metadata(), unit->EnvironmentInternal(), defaultContext); + } + + void ConfigurationSetParser::ExtractSecurityContext(Windows::Foundation::Collections::ValueSet metadata, implementation::ConfigurationEnvironment& environment, SecurityContext defaultContext) + { SecurityContext computedContext = defaultContext; - Windows::Foundation::Collections::ValueSet metadata = unit->Metadata(); auto securityContext = TryLookupProperty(metadata, ConfigurationField::SecurityContextMetadata, Windows::Foundation::PropertyType::String); if (securityContext) { @@ -576,6 +580,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation metadata.Remove(GetConfigurationFieldNameHString(ConfigurationField::SecurityContextMetadata)); } - unit->EnvironmentInternal().Context(computedContext); + environment.Context(computedContext); } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser.h @@ -56,6 +56,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Retrieves the schema version of the parser. virtual hstring GetSchemaVersion() = 0; + // Extracts (and removes) the environment information from the given metadata. + virtual void ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet valueSet, implementation::ConfigurationEnvironment& environment) = 0; + using ConfigurationSetPtr = winrt::com_ptr<implementation::ConfigurationSet>; // Retrieve the configuration set from the parser. @@ -131,6 +134,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Extracts the security context from the metadata in the given unit; if not present use `defaultContext`. void ExtractSecurityContext(implementation::ConfigurationUnit* unit, SecurityContext defaultContext = SecurityContext::Current); + void ExtractSecurityContext(Windows::Foundation::Collections::ValueSet metadata, implementation::ConfigurationEnvironment& environment, SecurityContext defaultContext = SecurityContext::Current); private: // Support older schema parsing. diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h b/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h @@ -23,6 +23,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring GetSchemaVersion() override { return {}; } + void ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet, implementation::ConfigurationEnvironment&) override {} + protected: void SetDocument(AppInstaller::YAML::Node&&) override {} }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp @@ -32,6 +32,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } + void ConfigurationSetParser_0_1::ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet, implementation::ConfigurationEnvironment&) + { + } + void ConfigurationSetParser_0_1::SetDocument(AppInstaller::YAML::Node&& document) { m_document = std::move(document); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h @@ -24,6 +24,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Retrieves the schema version of the parser. hstring GetSchemaVersion() override; + void ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet valueSet, implementation::ConfigurationEnvironment& environment) override; + protected: // Sets (or resets) the document to parse. void SetDocument(AppInstaller::YAML::Node&& document) override; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp @@ -19,6 +19,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } + void ConfigurationSetParser_0_2::ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet valueSet, implementation::ConfigurationEnvironment& environment) + { + ExtractSecurityContext(valueSet, environment); + } + void ConfigurationSetParser_0_2::SetDocument(AppInstaller::YAML::Node&& document) { m_document = std::move(document); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h @@ -22,6 +22,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Retrieves the schema version of the parser. hstring GetSchemaVersion() override; + void ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet valueSet, implementation::ConfigurationEnvironment& environment) override; + protected: // Sets (or resets) the document to parse. void SetDocument(AppInstaller::YAML::Node&& document) override; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp @@ -235,7 +235,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation return false; } - void ConfigurationSetParser_0_3::ExtractEnvironmentFromMetadata(const Collections::ValueSet& metadata, ConfigurationEnvironment& targetEnvironment) + void ConfigurationSetParser_0_3::ExtractEnvironmentFromMetadata(Collections::ValueSet metadata, ConfigurationEnvironment& targetEnvironment) { auto root = TryLookupValueSet(metadata, ConfigurationField::WingetMetadataRoot); if (root) diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h @@ -29,6 +29,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Retrieves the schema version of the parser. hstring GetSchemaVersion() override; + // Extracts the environment configuration from the given metadata. + // This only examines the winget subnode. + void ExtractEnvironmentFromMetadata(Windows::Foundation::Collections::ValueSet valueSet, implementation::ConfigurationEnvironment& environment) override; + protected: // Sets (or resets) the document to parse. void SetDocument(AppInstaller::YAML::Node&& document) override; @@ -58,10 +62,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Determines if the given unit should be converted to a group. bool ShouldConvertToGroup(ConfigurationUnit* unit); - // Extracts the environment configuration from the given metadata. - // This only examines the winget subnode. - void ExtractEnvironmentFromMetadata(const Windows::Foundation::Collections::ValueSet& metadata, ConfigurationEnvironment& targetEnvironment); - // Extracts the environment for a unit. void ExtractEnvironmentForUnit(ConfigurationUnit* unit); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -159,7 +159,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation return emitter.str(); } - void ConfigurationSetSerializer::WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides) + void ConfigurationSetSerializer::WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides) { anon::ValueSetWriter writer{ valueSet, overrides }; @@ -170,13 +170,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } - void ConfigurationSetSerializer::WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides) + void ConfigurationSetSerializer::WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides) { anon::ValueSetWriter writer{ valueSet, overrides }; writer.Write(emitter, WriteYamlValue); } - void ConfigurationSetSerializer::WriteYamlValueSetValues(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides) + void ConfigurationSetSerializer::WriteYamlValueSetValues(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides) { anon::ValueSetWriter writer{ valueSet, overrides }; writer.WriteValues(emitter, WriteYamlValue); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -14,6 +14,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation { struct ConfigurationSetSerializer { + using OverrideMap = std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>; + static std::unique_ptr<ConfigurationSetSerializer> CreateSerializer(hstring version, bool strictVersionMatching = false); virtual ~ConfigurationSetSerializer() noexcept = default; @@ -26,6 +28,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Serializes a configuration set to the original yaml string. virtual hstring Serialize(ConfigurationSet*) = 0; + // Serialize the metadata with the given environment. + virtual std::string SerializeMetadataWithEnvironment(const Windows::Foundation::Collections::ValueSet& metadata, const Configuration::ConfigurationEnvironment& environment) = 0; + // Serializes a value set only. std::string SerializeValueSet(const Windows::Foundation::Collections::ValueSet& valueSet); @@ -35,9 +40,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation protected: ConfigurationSetSerializer() = default; - static void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides = {}); - static void WriteYamlValueSetValues(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides = {}); - static void WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector<std::pair<ConfigurationField, Windows::Foundation::IInspectable>>& overrides = {}); + static void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides = {}); + static void WriteYamlValueSetValues(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides = {}); + static void WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet, const OverrideMap& overrides = {}); static void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); static void WriteYamlStringArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<hstring>& values); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp @@ -59,6 +59,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation return hstring{ std::move(result).str() }; } + std::string ConfigurationSetSerializer_0_2::SerializeMetadataWithEnvironment(const Windows::Foundation::Collections::ValueSet& metadata, const Configuration::ConfigurationEnvironment& environment) + { + Emitter emitter; + WriteYamlValueSet(emitter, metadata, GetMetadataWithEnvironmentOverrides(false, environment.Context())); + return emitter.str(); + } + void ConfigurationSetSerializer_0_2::WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units) { emitter << BeginSeq; @@ -122,10 +129,20 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSetSerializer_0_2::WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit) { - SecurityContext securityContext = unit.Environment().Context(); + WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Directives, unit.Metadata(), GetMetadataWithEnvironmentOverrides(true, unit.Environment().Context())); + } + + ConfigurationSetSerializer::OverrideMap ConfigurationSetSerializer_0_2::GetMetadataWithEnvironmentOverrides(bool includeModuleOverride, SecurityContext securityContext) + { + ConfigurationSetSerializer::OverrideMap result { + { ConfigurationField::SecurityContextMetadata, (securityContext != SecurityContext::Current ? PropertyValue::CreateString(ToWString(securityContext)) : nullptr)} + }; + + if (includeModuleOverride) + { + result.emplace_back(ConfigurationField::ModuleDirective, nullptr); + } - WriteYamlValueSetIfNotEmpty(emitter, ConfigurationField::Directives, unit.Metadata(), - { { ConfigurationField::ModuleDirective, nullptr }, - { ConfigurationField::SecurityContextMetadata, (securityContext != SecurityContext::Current ? PropertyValue::CreateString(ToWString(securityContext)) : nullptr)} }); + return result; } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.h @@ -19,10 +19,13 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring Serialize(ConfigurationSet* configurationSet) override; + std::string SerializeMetadataWithEnvironment(const Windows::Foundation::Collections::ValueSet& metadata, const Configuration::ConfigurationEnvironment& environment) override; + protected: void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); virtual winrt::hstring GetResourceName(const ConfigurationUnit& unit); virtual void WriteResourceDirectives(AppInstaller::YAML::Emitter& emitter, const ConfigurationUnit& unit); + static ConfigurationSetSerializer::OverrideMap GetMetadataWithEnvironmentOverrides(bool includeModuleOverride, SecurityContext securityContext); }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.cpp @@ -148,6 +148,22 @@ namespace winrt::Microsoft::Management::Configuration::implementation return hstring{ std::move(result).str() }; } + std::string ConfigurationSetSerializer_0_3::SerializeMetadataWithEnvironment(const Windows::Foundation::Collections::ValueSet& metadata, const Configuration::ConfigurationEnvironment& environment) + { + Emitter emitter; + + Collections::ValueSet wingetMetadataOverride = nullptr; + AddEnvironmentToMetadata(wingetMetadataOverride, environment); + + WriteYamlValueSet(emitter, metadata, + { + { ConfigurationField::WingetMetadataRoot, wingetMetadataOverride }, + { ConfigurationField::SecurityContextMetadata, nullptr }, + }); + + return emitter.str(); + } + void ConfigurationSetSerializer_0_3::WriteYamlParameters(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationParameter>& values) { if (!values || values.Size() == 0) diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_3.h @@ -20,6 +20,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring Serialize(ConfigurationSet* configurationSet) override; + std::string SerializeMetadataWithEnvironment(const Windows::Foundation::Collections::ValueSet& metadata, const Configuration::ConfigurationEnvironment& environment) override; + protected: void WriteYamlParameters(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<Configuration::ConfigurationParameter>& values); void WriteYamlConfigurationUnits( diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp @@ -39,7 +39,6 @@ namespace winrt::Microsoft::Management::Configuration::implementation { auto result = make_self<wil::details::module_count_wrapper<implementation::ConfigurationProcessor>>(); result->ConfigurationSetProcessorFactory(factory); - result->SetSupportsSchema03(WI_IsFlagSet(m_state, AppInstaller::WinRT::ConfigurationStaticsInternalsStateFlags::Configuration03)); return *result; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.cpp b/src/Microsoft.Management.Configuration/ConfigurationUnitResultInformation.cpp @@ -2,7 +2,8 @@ // Licensed under the MIT License. #include "pch.h" #include "ConfigurationUnitResultInformation.h" -#include "AppInstallerErrors.h" +#include "AppInstallerErrors.h" +#include "AppInstallerStrings.h" namespace winrt::Microsoft::Management::Configuration::implementation { @@ -24,6 +25,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation } return ConfigurationUnitResultSource::Internal; + } + + hstring SanitizeString(std::wstring_view value) + { + using namespace AppInstaller::Utility; + return hstring{ ConvertToUTF16(ConvertControlCodesToPictures(ConvertToUTF8(value))) }; } } @@ -32,8 +39,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (other) { m_resultCode = other.ResultCode(); - m_description = other.Description(); - m_details = other.Details(); + m_description = SanitizeString(other.Description()); + m_details = SanitizeString(other.Details()); m_resultSource = other.ResultSource(); } } @@ -41,14 +48,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationUnitResultInformation::Initialize(hresult resultCode, std::wstring_view description) { m_resultCode = resultCode; - m_description = description; + m_description = SanitizeString(description); m_resultSource = FromHRESULT(resultCode); } void ConfigurationUnitResultInformation::Initialize(hresult resultCode, hstring description) { m_resultCode = resultCode; - m_description = description; + m_description = SanitizeString(description); m_resultSource = FromHRESULT(resultCode); } @@ -61,8 +68,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationUnitResultInformation::Initialize(hresult resultCode, std::wstring_view description, std::wstring_view details, ConfigurationUnitResultSource resultSource) { m_resultCode = resultCode; - m_description = description; - m_details = details; + m_description = SanitizeString(description); + m_details = SanitizeString(details); m_resultSource = resultSource; } diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp @@ -57,6 +57,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: auto parser = ConfigurationSetParser::CreateForSchemaVersion(schemaVersion); configurationSet->Metadata(parser->ParseValueSet(statement.GetColumn<std::string>(6))); + parser->ExtractEnvironmentFromMetadata(configurationSet->Metadata(), configurationSet->EnvironmentInternal()); + THROW_HR_IF(E_NOTIMPL, !statement.GetColumn<std::string>(7).empty()); configurationSet->Variables(parser->ParseValueSet(statement.GetColumn<std::string>(8))); @@ -133,7 +135,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: ConvertToUTF8(configurationSet.Path()), GetCurrentUnixEpoch(), ConvertToUTF8(schemaVersion), - serializer->SerializeValueSet(configurationSet.Metadata()), + serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment()), std::string{}, // Parameters serializer->SerializeValueSet(configurationSet.Variables()) ); @@ -171,7 +173,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: Column(s_SetInfoTable_Column_Origin).Equals(ConvertToUTF8(configurationSet.Origin())). Column(s_SetInfoTable_Column_Path).Equals(ConvertToUTF8(configurationSet.Path())). Column(s_SetInfoTable_Column_SchemaVersion).Equals(ConvertToUTF8(schemaVersion)). - Column(s_SetInfoTable_Column_Metadata).Equals(serializer->SerializeValueSet(configurationSet.Metadata())). + Column(s_SetInfoTable_Column_Metadata).Equals(serializer->SerializeMetadataWithEnvironment(configurationSet.Metadata(), configurationSet.Environment())). Column(s_SetInfoTable_Column_Variables).Equals(serializer->SerializeValueSet(configurationSet.Variables())). Where(RowIDName).Equals(target); diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp @@ -123,7 +123,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: insertStatement.Bind(5, ConvertToUTF8(current.Unit.Identifier())); insertStatement.Bind(6, AppInstaller::ToIntegral(current.Unit.Intent())); insertStatement.Bind(7, serializer->SerializeStringArray(current.Unit.Dependencies())); - insertStatement.Bind(8, serializer->SerializeValueSet(current.Unit.Metadata())); + insertStatement.Bind(8, serializer->SerializeMetadataWithEnvironment(current.Unit.Metadata(), current.Unit.Environment())); insertStatement.Bind(9, serializer->SerializeValueSet(current.Unit.Settings())); insertStatement.Bind(10, current.Unit.IsActive()); insertStatement.Bind(11, isGroup); @@ -210,6 +210,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: unit->IsActive(statement.GetColumn<bool>(9)); unit->IsGroup(statement.GetColumn<bool>(10)); + parser->ExtractEnvironmentFromMetadata(unit->Metadata(), unit->EnvironmentInternal()); + if (statement.GetColumnIsNull(1)) { result.emplace_back(unit); diff --git a/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp @@ -56,13 +56,6 @@ namespace ConfigurationShim if (IsConfigurationAvailable()) { m_statics = winrt::Microsoft::Management::Configuration::ConfigurationStaticFunctions().as<winrt::Microsoft::Management::Configuration::IConfigurationStatics2>(); - - // Forward the current feature state to the internal statics - using namespace AppInstaller; - using Flags = WinRT::ConfigurationStaticsInternalsStateFlags; - - Flags flags = Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Configuration03) ? Flags::Configuration03 : Flags::None; - m_statics.as<AppInstaller::WinRT::IConfigurationStaticsInternals>()->SetExperimentalState(ToIntegral(flags)); } } @@ -108,11 +101,19 @@ namespace ConfigurationShim if (lowerHandler == AppInstaller::Configuration::PowerShellHandlerIdentifier) { - result = AppInstaller::CLI::ConfigurationRemoting::CreateOutOfProcessFactory(); + result = AppInstaller::CLI::ConfigurationRemoting::CreateOutOfProcessFactory(AppInstaller::CLI::ConfigurationRemoting::ProcessorEngine::PowerShell); } else if (lowerHandler == AppInstaller::Configuration::DynamicRuntimeHandlerIdentifier) { - result = AppInstaller::CLI::ConfigurationRemoting::CreateDynamicRuntimeFactory(); + result = AppInstaller::CLI::ConfigurationRemoting::CreateDynamicRuntimeFactory(AppInstaller::CLI::ConfigurationRemoting::ProcessorEngine::PowerShell); + } + else if (lowerHandler == AppInstaller::Configuration::DSCv3HandlerIdentifier) + { + result = AppInstaller::CLI::ConfigurationRemoting::CreateOutOfProcessFactory(AppInstaller::CLI::ConfigurationRemoting::ProcessorEngine::DSCv3); + } + else if (lowerHandler == AppInstaller::Configuration::DSCv3DynamicRuntimeHandlerIdentifier) + { + result = AppInstaller::CLI::ConfigurationRemoting::CreateDynamicRuntimeFactory(AppInstaller::CLI::ConfigurationRemoting::ProcessorEngine::DSCv3); } if (result)