diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 4c8d5d12b6..6528b6230a 100644 --- 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 index 3d5467d8b7..bf1f17412a 100644 --- 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 index 7940112a71..1b6a6ef33d 100644 --- 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 index f60eb24663..3be17b7fc6 100644 --- 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 index 62455f0981..5cfab39c34 100644 --- 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 index 8738a382eb..5c72287958 100644 --- 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 index 81a3c3732b..90594aa20b 100644 --- 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 index bf0af5acfa..c383ba3d9e 100644 --- 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 index 9adb7dd82a..a767b09b20 100644 --- 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 index 272bd72a53..0636f41760 100644 --- a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp +++ b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -4,6 +4,7 @@ #include "Public/ConfigurationSetProcessorFactoryRemoting.h" #include #include +#include #include #include #include @@ -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>, WinRT::LifetimeWatcherBase + struct DynamicFactory : winrt::implements, winrt::cloaked>, 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 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>().Insert(key, value); + m_factoryMapValues[key] = value; + return result; + } + + winrt::hstring Lookup(winrt::hstring key) + { + return m_defaultRemoteFactory.as>().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> 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 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(); + return winrt::make(processorEngine); } } diff --git a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp index 8e7d241f77..c14d5c4561 100644 --- 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 #include #include #include #include +#include #include #include 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 @@ -119,9 +138,9 @@ namespace AppInstaller::CLI::ConfigurationRemoting }; // Represents a remote factory object that was created from a specific process. - struct RemoteFactory : winrt::implements>, WinRT::LifetimeWatcherBase + struct RemoteFactory : winrt::implements, winrt::cloaked>, 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().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 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>().Insert(key, value); + } + + winrt::hstring Lookup(winrt::hstring key) + { + return m_remoteFactory.as>().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(processorEngine, useRunAs, properties, restrictions); + } + + ProcessorEngine DetermineProcessorEngine(ConfigurationSet set) { - return winrt::make(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 index 2f6bc74dea..0ea29f3244 100644 --- 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 index 9c0329aa21..5cb0689608 100644 --- 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 index 9c44521f13..256560bc32 100644 --- 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 index 2b72479a41..b94541cc1a 100644 --- 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 index 5d8d641a98..59fc58ff1a 100644 --- 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(); + 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>(); + 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().Processor(std::move(processor)); + } + else + { + ConfigurationContext configurationContext; + configurationContext.Processor(std::move(processor)); - context.Add(std::move(configurationContext)); + context.Add(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 [Identifier] + // UnitType [Identifier] // UnitDocumentationUri // Description // "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 [identifier] + // Type [identifier] // Description (from directives) // "Module": module 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 index ae8a2b0a90..79f3ca65e6 100644 --- 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; /// @@ -23,9 +24,9 @@ public class ConfigureCommand [OneTimeSetUp] public void OneTimeSetup() { - WinGetSettingsHelper.ConfigureFeature("configuration03", true); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", true); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); } /// @@ -34,9 +35,9 @@ public void OneTimeSetup() [OneTimeTearDown] public void OneTimeTeardown() { - WinGetSettingsHelper.ConfigureFeature("configuration03", false); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", false); - this.DeleteTxtFiles(); + this.DeleteResourceArtifacts(); } /// @@ -254,13 +255,51 @@ public void SpecifyModulePathToHighIntegrityServer() Assert.True(testFileContents.StartsWith(testDirectory)); } - private void DeleteTxtFiles() + /// + /// Runs a DSCv3 configuration, then changes the state and runs it again from history. + /// + [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 index 8eeec9c049..8eaaaa43bd 100644 --- 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; /// @@ -15,14 +16,24 @@ namespace AppInstallerCLIE2ETests /// public class ConfigureShowCommand { + /// + /// Setup done once before all the tests here. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); + } + /// /// One time teardown. /// [OneTimeTearDown] public void OneTimeTearDown() { - WinGetSettingsHelper.ConfigureFeature("configuration03", false); - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); + this.DeleteResourceArtifacts(); } /// @@ -75,18 +86,6 @@ public void ShowDetailsFromLocal(TestCommon.TestModuleLocation location) Assert.True(result.StdOut.Contains(Constants.LocalModuleDescriptor)); } - /// - /// A schema 0.3 config file is not allowed without the experimental feature. - /// - [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); - } - /// /// A schema 0.3 config file is allowed with the experimental feature. /// @@ -94,7 +93,6 @@ public void ShowDetails_Schema0_3_Fails() 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 @@ public void ShowDetails_Schema0_3_Succeeds() [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 @@ public void ShowFromHistory() Assert.AreEqual(0, result.ExitCode); } - private void DeleteTxtFiles() + /// + /// Runs a configuration, then shows it from history. + /// + [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); + } + + /// + /// Simple test to confirm that a resource is discoverable with DSC v3. + /// + [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()); + } + + /// + /// Runs a DSCv3 configuration, then shows it from history. + /// + [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 index 2e9f2c2c48..ccebaf8744 100644 --- 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; /// @@ -23,7 +24,8 @@ public class ConfigureTestCommand [OneTimeSetUp] public void OneTimeSetup() { - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); } /// @@ -32,7 +34,8 @@ public void OneTimeSetup() [OneTimeTearDown] public void OneTimeTeardown() { - this.DeleteTxtFiles(); + WinGetSettingsHelper.ConfigureFeature("dsc3", true); + this.DeleteResourceArtifacts(); } /// @@ -42,7 +45,7 @@ public void OneTimeTeardown() 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 @@ public void ConfigureTest_NotInDesiredState() 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 @@ public void TestFromHistory() Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); } - private void DeleteTxtFiles() + /// + /// Simple test to confirm that a resource is testable with DSC v3. + /// + [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 index 70d3023a18..20b105501b 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -133,6 +133,7 @@ public class Constants 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 index 88fb61d5c0..909754027f 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -975,7 +975,7 @@ public static string GetExpectedModulePath(TestModuleLocation location) /// Gets the instance identifier of the first configuration history item with name in its output line. /// /// The string to search for. - /// The instance identifier of a configuration that matched the search, or any empty string if none did. + /// The instance identifier of a configuration that matched the search, or an empty string if none did. 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 new file mode 100644 index 0000000000..ced3515df8 --- /dev/null +++ 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 new file mode 100644 index 0000000000..6f22adfd11 --- /dev/null +++ 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 index 2f0c9a0051..2154708bfe 100644 --- 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. Failed to download installer. Authentication failed. - + + Specify the path to the configuration processor + + \ No newline at end of file diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 8cc77d6ee7..0b5bb0ca5a 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -42,8 +42,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::Resume: return userSettings.Get(); - case ExperimentalFeature::Feature::Configuration03: - return userSettings.Get(); + case ExperimentalFeature::Feature::ConfigurationDSCv3: + return userSettings.Get(); case ExperimentalFeature::Feature::ConfigureExport: return userSettings.Get(); 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 index 257ffd2152..8a8b49d7c5 100644 --- 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 index f791c5f3d7..79db912643 100644 --- 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 index abf4b6fcff..e19e70560a 100644 --- 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 index 3d675211e8..31e97b58b1 100644 --- 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 index 8a4620786c..1c71a4b39c 100644 --- 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 index b38176373f..50e437e7c9 100644 --- a/src/ConfigurationRemotingServer/Program.cs +++ b/src/ConfigurationRemotingServer/Program.cs @@ -9,6 +9,7 @@ using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; using WinRT; +using IConfigurationSetProcessorFactory = global::Microsoft.Management.Configuration.IConfigurationSetProcessorFactory; namespace ConfigurationRemotingServer { @@ -82,19 +83,10 @@ static int Main(string[] args) { 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() { externalModulesPath }; - factory.ProcessorType = PowerShellConfigurationProcessorType.Hosted; + ConfigurationSet? limitationSet = null; + LimitationSetMetadata? limitationSetMetadata = null; // Parse limitation set if applicable. // The format will be: @@ -124,7 +116,7 @@ static int Main(string[] args) 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 @@ static int Main(string[] args) 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(commandStr.Substring( + limitationSetMetadata = JsonSerializer.Deserialize(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(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.CreateMarshaler(factory); + IConfigurationSetProcessorFactory factory = CreateFactory(processorEngine, limitationSet, limitationSetMetadata); + IObjectReference factoryInterface = MarshalInterface.CreateMarshaler(factory); return WindowsPackageManagerConfigurationCompleteOutOfProcessFactoryInitialization(0, factoryInterface.ThisPtr, staticsCallback, completionEventName, parentProcessId); } @@ -185,6 +161,90 @@ private class LimitationSetMetadata [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() { 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 new file mode 100644 index 0000000000..0d4fb4e4ae --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessExecution.cs @@ -0,0 +1,242 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + using System.Threading; + + /// + /// Wrapper for a single process execution and its output. + /// + internal class ProcessExecution + { + private List outputLines = new List(); + private List errorLines = new List(); + + /// + /// Initializes a new instance of the class. + /// + public ProcessExecution() + { + } + + /// + /// An event that receives the output lines as they are delivered. + /// + public event EventHandler? OutputLineReceived; + + /// + /// An event that receives the error lines as they are delivered. + /// + public event EventHandler? ErrorLineReceived; + + /// + /// Gets the executable path. + /// + required public string ExecutablePath { get; init; } + + /// + /// Gets the arguments to use for the process. + /// + [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 Arguments { get; init; } = []; + + /// + /// Gets the data to write to standard input of the process. + /// + public string? Input { get; init; } = null; + + /// + /// Gets the argument string passed to the process. + /// + 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(); + } + } + + /// + /// Gets the full command line that the process should see. + /// + public string CommandLine + { + get + { + return $"{this.ExecutablePath} {this.SerializedArguments}"; + } + } + + /// + /// Gets the current set of output lines. + /// Not thread safe, use OutputLineReceived for async flows. + /// + public IReadOnlyCollection Output + { + get { return this.outputLines; } + } + + /// + /// Gets the current set of error lines. + /// Not thread safe, use ErrorLineReceived for async flows. + /// + public IReadOnlyCollection Error + { + get { return this.errorLines; } + } + + /// + /// Gets the exit code of the process. + /// Will be null until the process exits. + /// + public int? ExitCode { get; private set; } = null; + + /// + /// Gets or sets the process object; null until Start called. + /// + private Process? Process { get; set; } + + /// + /// Starts the process. + /// + /// This object. + /// Thrown if Start has already been called. + 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; + } + + /// + /// Waits for the process to exit. + /// + /// The minimum amount of time to wait for the process to exit, in milliseconds. + /// True if the process exited; false if not. + /// Thrown if Start has not been called. + 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; + } + } + + /// + /// Gets all of the output lines as a single string. + /// + /// The output lines as a string. + public string GetAllOutputLines() + { + return GetAllLines(this.outputLines); + } + + /// + /// Gets all of the error lines as a single string. + /// + /// The error lines as a string. + public string GetAllErrorLines() + { + return GetAllLines(this.errorLines); + } + + private static string GetAllLines(List 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 new file mode 100644 index 0000000000..0ca6ab2008 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs @@ -0,0 +1,161 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// Contains settings for the DSC v3 processor components to share. + /// + 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; + + /// + /// Gets or sets the path to the DSC v3 executable. + /// + public string? DscExecutablePath { get; set; } + + /// + /// Gets the path to the DSC v3 executable. + /// + 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; + } + } + } + + /// + /// Gets an object for interacting with the DSC executable at EffectiveDscExecutablePath. + /// + [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 + } + + /// + /// Find the DSC v3 executable. + /// + /// The full path to the dsc.exe executable, or null if not found. + 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; + } + + /// + /// Create a deep copy of this settings object. + /// + /// A deep copy of this object. + public ProcessorSettings Clone() + { + ProcessorSettings result = new ProcessorSettings(); + + result.DscExecutablePath = this.DscExecutablePath; +#if !AICLI_DISABLE_TEST_HOOKS + result.dscV3 = this.DSCv3; +#endif + + return result; + } + + /// + /// Gets a string representation of this object. + /// + /// A string representation of this object. + 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 new file mode 100644 index 0000000000..d4853c9130 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ResourceDetails.cs @@ -0,0 +1,164 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Cached data about a resource. + /// + internal class ResourceDetails + { + private readonly ConfigurationUnitInternal configurationUnitInternal; + + private object detailsUpdateLock = new object(); + private IResourceListItem? resourceListItem = null; + + /// + /// 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. + /// + private ConfigurationUnitDetailFlags currentDetailLevel = ConfigurationUnitDetailFlags.None; + + /// + /// Initializes a new instance of the class. + /// + /// The internal configuration unit data. + public ResourceDetails(ConfigurationUnitInternal configurationUnitInternal) + { + this.configurationUnitInternal = configurationUnitInternal; + } + + /// + /// Gets a value indicating whether this resource exists. + /// Will be false until EnsureDetails is called and the resource is found. + /// + public bool Exists + { + get + { + lock (this.detailsUpdateLock) + { + return this.currentDetailLevel != ConfigurationUnitDetailFlags.None; + } + } + } + + /// + /// Ensures that the given detail level is present. + /// + /// The processor settings to use when getting details. + /// The detail level flags. + 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); + } + } + + /// + /// Gets a ConfigurationUnitProcessorDetails populated with all available data. + /// + /// A ConfigurationUnitProcessorDetails populated with all available data. + 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 new file mode 100644 index 0000000000..0fbd6f9579 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IDSCv3.cs @@ -0,0 +1,56 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; + using Microsoft.Management.Configuration.Processor.Helpers; + + /// + /// Interface for interacting with DSC v3. + /// + internal interface IDSCv3 + { + /// + /// Creates the appropriate instance of the DSCv3 interface for the given executable. + /// + /// The processor settings. + /// An object that properly interacts with the specific version of DSC v3. + 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); + } + + /// + /// Gets a single resource by its type name. + /// + /// The type name of the resource. + /// A single resource item. + public IResourceListItem? GetResourceByType(string resourceType); + + /// + /// Tests a configuration unit. + /// + /// The unit to test. + /// A test result. + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal); + + /// + /// Gets a configuration unit settings. + /// + /// The unit to get. + /// A get result. + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal); + + /// + /// Sets a configuration unit settings. + /// + /// The unit to set. + /// A set result. + 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 new file mode 100644 index 0000000000..d05dcb35dd --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceGetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using Windows.Foundation.Collections; + + /// + /// The interface to a `resource get` command result. + /// + internal interface IResourceGetItem + { + /// + /// Gets the settings for this item. + /// + 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 new file mode 100644 index 0000000000..b8bec9cd49 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceListItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// + /// The interface to a single JSON line output by the `resource list` command. + /// + internal interface IResourceListItem + { + /// + /// Gets the type of the resource. + /// Should match the regex "^\\w+(\\.\\w+){0,2}\\/\\w+$". + /// + public string Type { get; } + + /// + /// Gets the kind of the resource. + /// + public ResourceKind Kind { get; } + + /// + /// Gets the version of the resource. + /// This is a semver version. + /// + public string? Version { get; } + + /// + /// Gets the description of the resource. + /// + public string? Description { get; } + + /// + /// Gets the path to the directory containing the resource. + /// + public string? Directory { get; } + + /// + /// Gets the author of the resource. + /// + 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 new file mode 100644 index 0000000000..f8cc39ec4e --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceSetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + using System.Collections.Generic; + + /// + /// The interface to a `resource set` command result. + /// + internal interface IResourceSetItem + { + /// + /// Gets a value indicating whether a reboot is required. + /// + 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 new file mode 100644 index 0000000000..5aa9ace755 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IResourceTestItem.cs @@ -0,0 +1,19 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// + /// The interface to a `resource test` command result. + /// + internal interface IResourceTestItem + { + /// + /// Gets a value indicating whether the resource is in the desired state. + /// + 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 new file mode 100644 index 0000000000..310a2c384d --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/ResourceKind.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Model +{ + /// + /// https://learn.microsoft.com/en-us/powershell/dsc/reference/schemas/definitions/resourcekind?view=dsc-3.0 + /// The kind of resource. + /// + internal enum ResourceKind + { + /// + /// The kind is unknown. + /// + Unknown, + + /// + /// A standard resource. + /// + Resource, + + /// + /// An adapter resource. + /// + Adapter, + + /// + /// A group resource. + /// + Group, + + /// + /// An import resource. + /// + 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 new file mode 100644 index 0000000000..cd26569dfb --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/DSCv3.cs @@ -0,0 +1,182 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// An instance of IDSCv3 for interacting with 1.0. + /// + 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; + + /// + /// Initializes a new instance of the class. + /// + /// The processor settings. + public DSCv3(ProcessorSettings processorSettings) + { + this.processorSettings = processorSettings; + } + + /// + 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(processExecution); + } + + /// + 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()); + } + + /// + 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()); + } + + /// + 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()); + } + + /// + /// Runs the process, waiting until it completes. + /// + /// The process to run. + /// True if the exit code was not 0. + 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(ProcessExecution processExecution) + { + if (processExecution.Output.Count == 0) + { + return default; + } + + return JsonSerializer.Deserialize(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 new file mode 100644 index 0000000000..7c2d3ae856 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Definitions/ResourceKind.cs @@ -0,0 +1,40 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Definitions +{ + /// + /// https://learn.microsoft.com/en-us/powershell/dsc/reference/schemas/definitions/resourcekind?view=dsc-3.0 + /// The kind of resource. + /// + internal enum ResourceKind + { + /// + /// The kind is unknown. + /// + Unknown, + + /// + /// A standard resource. + /// + Resource, + + /// + /// An adapter resource. + /// + Adapter, + + /// + /// A group resource. + /// + Group, + + /// + /// An import resource. + /// + 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 new file mode 100644 index 0000000000..0fedf0f14e --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Metadata/ResourceInstanceResult.cs @@ -0,0 +1,36 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Metadata +{ + using System; + using System.Text.Json.Serialization; + + /// + /// Defines metadata DSC returns for a DSC configuration operation against a resource instance. + /// + internal class ResourceInstanceResult + { + /// + /// Gets or sets the context metadata for this instance result. + /// + [JsonRequired] + [JsonPropertyName("Microsoft.DSC")] + public ContextMetadata? MicrosoftDSC { get; set; } + + /// + /// Contains properties generated by the DSC v3 platform. + /// + public class ContextMetadata + { + /// + /// Gets or sets the duration of a resource instance execution. + /// + [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 new file mode 100644 index 0000000000..bf7cabdea9 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/FullItemBase.cs @@ -0,0 +1,127 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// 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. + /// + /// The simple item type. + /// The full item type. + internal class FullItemBase + where TFull : FullItemBase, new() + { + private const string NameProperty = "name"; + + /// + /// Initializes a new instance of the class. + /// + public FullItemBase() + { + } + + /// + /// Gets or sets the metadata for this test result. + /// + [JsonRequired] + public ResourceInstanceResult? Metadata { get; set; } + + /// + /// Gets or sets the name for this test result. + /// + [JsonRequired] + public string? Name { get; set; } + + /// + /// Gets or sets the type for the resource. + /// + [JsonRequired] + public string? Type { get; set; } + + /// + /// Gets or sets the result of the test. + /// + [JsonRequired] + public JsonNode? Result { get; set; } + + /// + /// Gets or sets the simple result. + /// + [JsonIgnore] + public TSimple? SimpleResult { get; set; } + + /// + /// Gets or sets the full results. + /// + [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; } + + /// + /// Initializes a new instance of the TFull class. + /// + /// The document to construct from. + /// The options to use. + /// The item created. + public static TFull CreateFrom(JsonDocument document, JsonSerializerOptions options) + { + if (!document.RootElement.TryGetProperty(NameProperty, out JsonElement jsonElement)) + { + return new () { SimpleResult = JsonSerializer.Deserialize(document, options) }; + } + else + { + TFull? result = JsonSerializer.Deserialize(document, options); + + if (result == null) + { + throw new InvalidDataException("Unable to deserialize full result."); + } + + result.ProcessResult(options); + return result; + } + } + + /// + /// Converts the Result property into the appropriate simple or full results. + /// + /// The options to use. + 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(this.Result, options); + } + else + { + this.FullResults = JsonSerializer.Deserialize(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 new file mode 100644 index 0000000000..804476e3e1 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetFullItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// 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. + /// + internal class GetFullItem : FullItemBase, IResourceGetItem + { + /// + /// Initializes a new instance of the class. + /// + public GetFullItem() + { + } + + /// + 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 new file mode 100644 index 0000000000..bc45975a2c --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/GetSimpleItem.cs @@ -0,0 +1,24 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// + /// 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. + /// + internal class GetSimpleItem + { + /// + /// Gets or sets the state of the resource properties. + /// + [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 new file mode 100644 index 0000000000..37a652fa52 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceCapability.cs @@ -0,0 +1,60 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + /// + /// 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. + /// + internal enum ResourceCapability + { + /// + /// Can call get on the resource. + /// Required. + /// + Get, + + /// + /// Can call set on the resource. + /// + Set, + + /// + /// The resource operates properly in the presence of the `_exist` property. + /// If not present, DSC will use `delete` when `_exist == false`. + /// + SetHandlesExist, + + /// + /// The resource can handle a "what if" query directly. + /// Otherwise, DSC will handle it synthetically. + /// + WhatIf, + + /// + /// The resource can handle a "test" query directly. + /// Otherwise, DSC will handle it synthetically. + /// + Test, + + /// + /// Can call delete on the resource. + /// + Delete, + + /// + /// Can call export on the resource. + /// + Export, + + /// + /// Can call resolve on the resource. + /// This can produce new resources, such as importing another configuration document. + /// + 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 new file mode 100644 index 0000000000..078a6ab7f3 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceListItem.cs @@ -0,0 +1,95 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// The object type from a single JSON line output by the `resource list` command. + /// + internal class ResourceListItem : IResourceListItem + { + /// + /// Gets or sets the type of the resource. + /// Should match the regex "^\\w+(\\.\\w+){0,2}\\/\\w+$". + /// + [JsonRequired] + required public string Type { get; set; } + + /// + /// Gets or sets the kind of the resource. + /// + public Definitions.ResourceKind Kind { get; set; } = Definitions.ResourceKind.Unknown; + + /// + [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}") + }; + + /// + /// Gets or sets the version of the resource. + /// This is a semver version. + /// + public string? Version { get; set; } + + /// + /// Gets or sets the capabilities of the resource. + /// + [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; } = []; + + /// + /// Gets or sets the description of the resource. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the path to the resource definition file. + /// + public string? Path { get; set; } + + /// + /// Gets or sets the path to the directory containing the resource. + /// + public string? Directory { get; set; } + + /// + /// Gets or sets a value that indicates implementation details of the resource. + /// + public JsonObject? ImplementedAs { get; set; } + + /// + /// Gets or sets the author of the resource. + /// + public string? Author { get; set; } + + /// + /// Gets or sets the names of the properties of the resource. + /// + [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; } = []; + + /// + /// Gets or sets the adapter required by the resource. + /// + public string? RequireAdapter { get; set; } + + /// + /// Gets or sets the resource definition manifest. + /// + 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 new file mode 100644 index 0000000000..7ceaf96b07 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetFullItem.cs @@ -0,0 +1,27 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// 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. + /// + internal class SetFullItem : FullItemBase, IResourceSetItem + { + /// + /// Initializes a new instance of the class. + /// + public SetFullItem() + { + } + + /// + 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 new file mode 100644 index 0000000000..626a5753e1 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/SetSimpleItem.cs @@ -0,0 +1,37 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// + /// 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. + /// + internal class SetSimpleItem + { + /// + /// Gets or sets the state of the resource properties before the attempt to set them. + /// + [JsonRequired] + public JsonObject? BeforeState { get; set; } + + /// + /// Gets or sets the state of the resource properties after the attempt to set them. + /// + [JsonRequired] + public JsonObject? AfterState { get; set; } + + /// + /// Gets or sets the list of properties that changed. + /// + [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 new file mode 100644 index 0000000000..c976ef1350 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestFullItem.cs @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// 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. + /// + internal class TestFullItem : FullItemBase, IResourceTestItem + { + /// + /// Initializes a new instance of the class. + /// + public TestFullItem() + { + } + + /// + 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 new file mode 100644 index 0000000000..aea72383b7 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/TestSimpleItem.cs @@ -0,0 +1,43 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + + /// + /// 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. + /// + internal class TestSimpleItem + { + /// + /// Gets or sets the desired state of the resource properties. + /// + [JsonRequired] + public JsonObject? DesiredState { get; set; } + + /// + /// Gets or sets the actual state of the resource properties. + /// + [JsonRequired] + public JsonObject? ActualState { get; set; } + + /// + /// Gets or sets a value indicating whether the resource is in the desired state. + /// + [JsonRequired] + public bool InDesiredState { get; set; } + + /// + /// Gets or sets the list of properties that are not in the desired state. + /// + [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 new file mode 100644 index 0000000000..32cb6a2635 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Configuration set processor. + /// + internal sealed partial class DSCv3ConfigurationSetProcessor : ConfigurationSetProcessorBase, IConfigurationSetProcessor + { + private readonly ProcessorSettings processorSettings; + private Dictionary resourceDetailsDictionary = new (); + + /// + /// Initializes a new instance of the class. + /// + /// The processor settings to use. + /// Configuration set. + /// Whether the set processor should work in limitation mode. + public DSCv3ConfigurationSetProcessor(ProcessorSettings processorSettings, ConfigurationSet? configurationSet, bool isLimitMode = false) + : base(configurationSet, isLimitMode) + { + this.processorSettings = processorSettings; + } + + /// + 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); + } + + /// + 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 new file mode 100644 index 0000000000..d4977a1cb3 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs @@ -0,0 +1,52 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Provides access to a specific configuration unit within the runtime. + /// + internal sealed partial class DSCv3ConfigurationUnitProcessor : ConfigurationUnitProcessorBase, IConfigurationUnitProcessor + { + private readonly ProcessorSettings processorSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The processor settings to use. + /// Internal unit. + /// Whether it is under limit mode. + internal DSCv3ConfigurationUnitProcessor(ProcessorSettings processorSettings, ConfigurationUnitInternal unitInternal, bool isLimitMode = false) + : base(unitInternal, isLimitMode) + { + this.processorSettings = processorSettings; + } + + /// + protected override ValueSet GetSettingsInternal() + { + return this.processorSettings.DSCv3.GetResourceSettings(this.UnitInternal).Settings; + } + + /// + protected override bool TestSettingsInternal() + { + return this.processorSettings.DSCv3.TestResource(this.UnitInternal).InDesiredState; + } + + /// + 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 index d5d5bbf74e..118a1c5bd0 100644 --- a/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs +++ b/src/Microsoft.Management.Configuration.Processor/Exceptions/InvokeDscResourceException.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -38,7 +38,7 @@ internal class InvokeDscResourceException : Exception, IConfigurationUnitResultE /// Method. /// Resource name. /// Optional module. - 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. @@ -48,6 +48,21 @@ public InvokeDscResourceException(string method, string resourceName, ModuleSpec this.Module = module; } + /// + /// Initializes a new instance of the class. + /// Use this constructor when there is a message and the result is not valid. + /// + /// Method. + /// Resource name. + /// Message. + public InvokeDscResourceException(string method, string resourceName, string message) + : base(CreateMessage(method, resourceName, null, message)) + { + this.HResult = ErrorCodes.WinGetConfigUnitInvokeInvalidResult; + this.Method = method; + this.ResourceName = resourceName; + } + /// /// Initializes a new instance of the class. /// Use this constructor when the invoke fails with an error message. diff --git a/src/Microsoft.Management.Configuration.Processor/Extensions/JsonObjectExtensions.cs b/src/Microsoft.Management.Configuration.Processor/Extensions/JsonObjectExtensions.cs new file mode 100644 index 0000000000..8669fa8d27 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Extensions/JsonObjectExtensions.cs @@ -0,0 +1,69 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.Extensions +{ + using System.Text.Json; + using System.Text.Json.Nodes; + using Windows.Foundation.Collections; + + /// + /// Extensions for JsonObject. + /// + internal static class JsonObjectExtensions + { + /// + /// Converts the JSON object to a ValueSet. + /// + /// The object to convert. + /// The ValueSet. + 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(), + JsonValueKind.Number => value.GetValue(), + 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 index baea81dc19..57795c8b3c 100644 --- 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 /// internal static class ValueSetExtensions { - private const string TreatAsArray = "treatAsArray"; + /// + /// The value in a ValueSet that indicates that it is an array of items. + /// + internal const string TreatAsArray = "treatAsArray"; /// /// 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 index dd0cb85764..26d3ceda3e 100644 --- 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 @@ true + + $(DefineConstants);AICLI_DISABLE_TEST_HOOKS + + true @@ -69,17 +73,28 @@ - - - - - $(DefineConstants);WinGetCsWinRTEmbedded false 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; diff --git a/src/Microsoft.Management.Configuration.Processor/PowerShell/Set/PowerShellConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/PowerShell/Set/PowerShellConfigurationSetProcessor.cs index dae182dfe0..1fa26e436c 100644 --- 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; /// - /// Configuration set processor. + /// IConfigurationSetProcessor implementation using PowerShell DSC v2. /// 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 new file mode 100644 index 0000000000..a15d1cc43d --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs @@ -0,0 +1,198 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// IConfigurationSetProcessorFactory implementation using DSC v3. + /// + internal sealed partial class DSCv3ConfigurationSetProcessorFactory : ConfigurationSetProcessorFactoryBase, IConfigurationSetProcessorFactory, IDictionary + { + private const string DscExecutablePathPropertyName = "DscExecutablePath"; + private const string FoundDscExecutablePathPropertyName = "FoundDscExecutablePath"; + + private ProcessorSettings processorSettings = new (); + + /// + /// Initializes a new instance of the class. + /// + public DSCv3ConfigurationSetProcessorFactory() + { + } + + /// + /// Gets or sets the path to the DSC v3 executable. + /// + 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 + /// + /// Gets the processor settings; for tests only. + /// + public ProcessorSettings Settings + { + get + { + return this.processorSettings; + } + } +#endif + + /// + public ICollection Keys => throw new NotImplementedException(); + + /// + public ICollection Values => throw new NotImplementedException(); + + /// + public int Count => throw new NotImplementedException(); + + /// + public bool IsReadOnly => this.IsLimitMode(); + + /// + public string this[string key] { get => this.GetValue(key); set => this.SetValue(key, value); } + + /// + public void Add(string key, string value) + { + this.SetValue(key, value); + } + + /// + public void Add(KeyValuePair item) + { + this.SetValue(item.Key, item.Value); + } + + /// + public void Clear() + { + throw new NotImplementedException(); + } + + /// + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + public bool ContainsKey(string key) + { + switch (key) + { + case DscExecutablePathPropertyName: + return this.DscExecutablePath != null; + } + + return false; + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + /// + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + /// + public bool Remove(string key) + { + throw new NotImplementedException(); + } + + /// + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + 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; + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + 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 index dba9f14c1b..257052a6a6 100644 --- 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; /// - /// ConfigurationSetProcessorFactory implementation. + /// IConfigurationSetProcessorFactory implementation using PowerShell DSC v2. /// #if WinGetCsWinRTEmbedded internal diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs index 30ef6765b9..1ba69a7dea 100644 --- a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs +++ b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs @@ -48,6 +48,11 @@ internal ConfigurationUnitProcessorBase(ConfigurationUnitInternal unitInternal, /// internal ConfigurationSetProcessorFactoryBase? SetProcessorFactory { get; init; } + /// + /// Gets the internal configuration unit. + /// + protected ConfigurationUnitInternal UnitInternal => this.unitInternal; + /// /// Gets the current system state for the configuration unit. /// Calls Get on the DSC resource. diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorDetails.cs index 0f45db70bb..0411f81fbd 100644 --- 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 /// /// Provides information for a specific configuration unit within the runtime. /// - internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails + internal sealed partial class ConfigurationUnitProcessorDetails : IConfigurationUnitProcessorDetails, IConfigurationUnitProcessorDetails2 { /// /// Initializes a new instance of the class. @@ -23,9 +23,9 @@ public ConfigurationUnitProcessorDetails() } /// - /// Gets the name of the unit of configuration. + /// Gets or sets the name of the unit of configuration. /// - required public string UnitType { get; init; } + required public string UnitType { get; internal set; } /// /// Gets or sets the description of the unit of configuration. @@ -111,5 +111,10 @@ public ConfigurationUnitProcessorDetails() /// Gets or sets a value indicating whether the module comes from a public repository. /// public bool IsPublic { get; internal set; } + + /// + /// Gets or sets a value indicating whether this resource is a group. + /// + 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 new file mode 100644 index 0000000000..a10cebefb9 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestDSCv3.cs @@ -0,0 +1,109 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Microsoft.Management.Configuration.Processor.Helpers; + + /// + /// Implements IDSCv3 for tests. + /// + internal class TestDSCv3 : IDSCv3 + { + /// + /// The delegate type for GetResourceByType. + /// + /// The type name of the resource. + /// A single resource item. + internal delegate IResourceListItem? GetResourceByTypeDelegateType(string resourceType); + + /// + /// The delegate type for GetResourceSettings. + /// + /// The unit to get. + /// A get result. + internal delegate IResourceGetItem GetResourceSettingsDelegateType(ConfigurationUnitInternal unitInternal); + + /// + /// The delegate type for SetResourceSettings. + /// + /// The unit to set. + /// A set result. + internal delegate IResourceSetItem SetResourceSettingsDelegateType(ConfigurationUnitInternal unitInternal); + + /// + /// The delegate type for TestResource. + /// + /// The unit to test. + /// A test result. + internal delegate IResourceTestItem TestResourceDelegateType(ConfigurationUnitInternal unitInternal); + + /// + /// Gets or sets the GetResourceByType result. + /// + public IResourceListItem? GetResourceByTypeResult { get; set; } + + /// + /// Gets or sets the GetResourceByType delegate. + /// + public GetResourceByTypeDelegateType? GetResourceByTypeDelegate { get; set; } + + /// + /// Gets or sets the GetResourceSettings result. + /// + public IResourceGetItem? GetResourceSettingsResult { get; set; } + + /// + /// Gets or sets the GetResourceSettings delegate. + /// + public GetResourceSettingsDelegateType? GetResourceSettingsDelegate { get; set; } + + /// + /// Gets or sets the SetResourceSettings result. + /// + public IResourceSetItem? SetResourceSettingsResult { get; set; } + + /// + /// Gets or sets the SetResourceSettings delegate. + /// + public SetResourceSettingsDelegateType? SetResourceSettingsDelegate { get; set; } + + /// + /// Gets or sets the TestResource result. + /// + public IResourceTestItem? TestResourceResult { get; set; } + + /// + /// Gets or sets the TestResource delegate. + /// + public TestResourceDelegateType? TestResourceDelegate { get; set; } + + /// + public IResourceListItem? GetResourceByType(string resourceType) + { + return this.GetResourceByTypeResult ?? this.GetResourceByTypeDelegate?.Invoke(resourceType); + } + + /// + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal) + { + return this.GetResourceSettingsResult ?? this.GetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); + } + + /// + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal) + { + return this.SetResourceSettingsResult ?? this.SetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); + } + + /// + 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 new file mode 100644 index 0000000000..9d376f5841 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceGetItem.cs @@ -0,0 +1,22 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + using Windows.Foundation.Collections; + + /// + /// Implements IResourceGetItem for tests. + /// + internal class TestResourceGetItem : IResourceGetItem + { + /// + /// Gets or sets the settings. + /// + 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 new file mode 100644 index 0000000000..f4d0cd2085 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceListItem.cs @@ -0,0 +1,46 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// Implements IResourceListItem for tests. + /// + internal class TestResourceListItem : IResourceListItem + { + /// + /// Gets or sets the type. + /// + required public string Type { get; set; } + + /// + /// Gets or sets the kind. + /// + public ResourceKind Kind { get; set; } + + /// + /// Gets or sets the version. + /// + public string? Version { get; set; } + + /// + /// Gets or sets the description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the directory. + /// + public string? Directory { get; set; } + + /// + /// Gets or sets the author. + /// + 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 new file mode 100644 index 0000000000..fb35872846 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceSetItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// Implements IResourceSetItem for tests. + /// + internal class TestResourceSetItem : IResourceSetItem + { + /// + /// Gets or sets a value indicating whether a reboot is required. + /// + 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 new file mode 100644 index 0000000000..c015743665 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestResourceTestItem.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Helpers +{ + using Microsoft.Management.Configuration.Processor.DSCv3.Model; + + /// + /// Implements IResourceTestItem for tests. + /// + internal class TestResourceTestItem : IResourceTestItem + { + /// + /// Gets or sets a value indicating whether the system is in the desired state. + /// + 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 index f87f093157..9354cd0238 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Helpers/ValueSetExtensions.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Helpers/ValueSetExtensions.cs @@ -48,6 +48,9 @@ private static void ToYaml(ValueSet set, StringBuilder sb, int indentation = 0) 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 index 210a5d4d93..5d1b706d52 100644 --- 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 @@ private void AssertUnitsEqual(ConfigurationUnit expectedUnit, ConfigurationUnit? 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 index e27d9eaa75..f37a738c1c 100644 --- a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetAuthoringTests.cs +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationSetAuthoringTests.cs @@ -149,14 +149,14 @@ public void ConfigurationSet_UnitEnvironments() 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 @@ public void ConfigurationSet_GroupUnitEnvironments() 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 new file mode 100644 index 0000000000..19d9547369 --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/DSCv3ProcessorTests.cs @@ -0,0 +1,122 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +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; + + /// + /// Tests for the DSCv3 processor. + /// + [Collection("UnitTestCollection")] + [InProc] + public class DSCv3ProcessorTests : ConfigurationProcessorTestBase + { + private readonly UnitTestFixture fixture; + private readonly ITestOutputHelper log; + + /// + /// Initializes a new instance of the class. + /// + /// Unit test fixture. + /// Log helper. + public DSCv3ProcessorTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + this.fixture = fixture; + this.log = log; + } + + /// + /// Tests for the unit details caching. + /// + [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); + } + + /// + /// Test for unit processor creation requiring resource to be found. + /// + [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(() => 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 index f10b51e4f4..d5b819b6d0 100644 --- 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 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 lock{ m_diagnosticsMutex }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h index b4aff84c95..338ae46606 100644 --- 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 m_changeRegistration; #endif }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp index a549af797a..0b5721c6ab 100644 --- 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 index 362fd2320c..df7f9ca0bc 100644 --- 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; // 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 index 4d316bca55..d5dfee3c25 100644 --- 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 index 7757660b38..17b109fe4d 100644 --- 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 index 22b5a0b7a7..57cf2333e1 100644 --- 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 index 86541dcaf7..6e24aace31 100644 --- 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 index 04aad207d2..04d75dfe9d 100644 --- 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 index 8cdcd9c7e8..7c67af20b4 100644 --- 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 index faa1d6a62d..2db3ab18bc 100644 --- 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 index 86211d3529..2d66c9540f 100644 --- 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>& 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>& 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>& 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 index 26ef2492b0..dacb2c12b9 100644 --- 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>; + static std::unique_ptr 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>& overrides = {}); - static void WriteYamlValueSetValues(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector>& overrides = {}); - static void WriteYamlValueSetIfNotEmpty(AppInstaller::YAML::Emitter& emitter, ConfigurationField key, const Windows::Foundation::Collections::ValueSet& valueSet, const std::vector>& 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& values); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer_0_2.cpp index 36e85a8ecf..754c28055f 100644 --- 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& 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 index d766729d07..0f0346c970 100644 --- 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& 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 index a621f4bfba..18ce84f131 100644 --- 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& 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 index 1cd421dcda..2f407ffc97 100644 --- 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& values); void WriteYamlConfigurationUnits( diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp index 78065ebb16..ba3e19beb8 100644 --- 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>(); 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 index bc66181c3f..bea6eb0517 100644 --- 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 index b9778d4bed..e2003aa68f 100644 --- 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(6))); + parser->ExtractEnvironmentFromMetadata(configurationSet->Metadata(), configurationSet->EnvironmentInternal()); + THROW_HR_IF(E_NOTIMPL, !statement.GetColumn(7).empty()); configurationSet->Variables(parser->ParseValueSet(statement.GetColumn(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 index 672f6cca49..708accd1b1 100644 --- 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(9)); unit->IsGroup(statement.GetColumn(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 index 0e2a9a2151..9fd9ff2916 100644 --- 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(); - - // 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()->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)