diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp index 8738a382eb..a32f196f0b 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -77,8 +77,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..95a2d9a155 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -39,8 +39,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..d34d4a0cec 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -39,8 +39,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..a10799272d 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureValidateCommand.cpp @@ -37,8 +37,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..aea0dc56af 100644 --- a/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp +++ b/src/AppInstallerCLICore/ConfigurationDynamicRuntimeFactory.cpp @@ -45,7 +45,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // 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 { - DynamicFactory(); + DynamicFactory(ProcessorEngine processorEngine); IConfigurationSetProcessor CreateSetProcessor(const ConfigurationSet& configurationSet); @@ -105,6 +105,11 @@ namespace AppInstaller::CLI::ConfigurationRemoting m_customLocation = value; } + ProcessorEngine Engine() const + { + return m_processorEngine; + } + private: IConfigurationSetProcessorFactory m_defaultRemoteFactory; winrt::event> m_diagnostics; @@ -113,6 +118,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational; SetProcessorFactory::PwshConfigurationProcessorLocation m_location = SetProcessorFactory::PwshConfigurationProcessorLocation::Default; winrt::hstring m_customLocation; + ProcessorEngine m_processorEngine; }; struct DynamicProcessorInfo @@ -337,7 +343,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting useRunAs = !m_enableTestMode; #endif - factory = CreateOutOfProcessFactory(useRunAs, SerializeSetProperties(), SerializeHighIntegrityLevelSet()); + factory = CreateOutOfProcessFactory(m_dynamicFactory->Engine(), useRunAs, SerializeSetProperties(), SerializeHighIntegrityLevelSet()); } else { @@ -373,9 +379,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) { @@ -437,8 +444,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..5ae298e113 100644 --- a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp +++ b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp @@ -11,6 +11,7 @@ using namespace winrt::Windows::Foundation; using namespace winrt::Microsoft::Management::Configuration; +using namespace std::string_view_literals; namespace AppInstaller::CLI::ConfigurationRemoting { @@ -22,6 +23,19 @@ namespace AppInstaller::CLI::ConfigurationRemoting // The string used to divide the arguments sent to the remote server constexpr std::wstring_view s_ArgumentsDivider = L"\n~~~~~~\n"; + std::wstring_view ToString(ProcessorEngine value) + { + switch (value) + { + case ProcessorEngine::PowerShell: + return L"pwsh"sv; + case ProcessorEngine::DSCv3: + return L"dscv3"sv; + default: + THROW_HR(E_UNEXPECTED); + } + } + // A helper with a convenient function that we use to receive the remote factory object. struct RemoteFactoryCallback : winrt::implements { @@ -121,7 +135,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting // Represents a remote factory object that was created from a specific process. struct RemoteFactory : winrt::implements>, 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 +176,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()) { @@ -298,9 +312,33 @@ 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) + { + 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" }) + { + // Default to PowerShell + ProcessorEngine result = ProcessorEngine::PowerShell; + + winrt::hstring processorIdentifier = set.Environment().ProcessorIdentifier(); + if (processorIdentifier == L"dscv3") + { + result = ProcessorEngine::DSCv3; + } + + return result; + } + else + { + // Intentionally fail out here until a decision is made. + THROW_HR(E_NOTIMPL); + } } } diff --git a/src/AppInstallerCLICore/ConfigureExportCommand.cpp b/src/AppInstallerCLICore/ConfigureExportCommand.cpp index 2f6bc74dea..f68374b614 100644 --- a/src/AppInstallerCLICore/ConfigureExportCommand.cpp +++ b/src/AppInstallerCLICore/ConfigureExportCommand.cpp @@ -44,8 +44,9 @@ namespace AppInstaller::CLI context << VerifyIsFullPackage << SearchSourceForPackageExport << - CreateConfigurationProcessor << + CreateConfigurationProcessorWithoutFactory << CreateOrOpenConfigurationSet << + CreateConfigurationProcessor << PopulateConfigurationSetForExport << WriteConfigFile; } diff --git a/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h b/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h index 9c44521f13..8f608b80e1 100644 --- a/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h +++ b/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h @@ -6,11 +6,23 @@ namespace AppInstaller::CLI::ConfigurationRemoting { + // The processor engine being used by the factory. + enum class ProcessorEngine + { + // 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); } // Export for use by the out of process factory server to report its initialization. diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 5d8d641a98..c07425754b 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -104,19 +104,29 @@ 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()); // 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); } - Configuration::SetModulePath(context, factory); return factory; } @@ -136,10 +146,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) 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/ConfigurationRemotingServer/Program.cs b/src/ConfigurationRemotingServer/Program.cs index b38176373f..66ca60ea3d 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); } @@ -187,6 +163,87 @@ private class LimitationSetMetadata public string? ModulePath { 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.ModulePath != null) + { + factory.DscExecutablePath = limitationSetMetadata.ModulePath; + } + 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() { var currentAssemblyDirectoryPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 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..c33e6ccf68 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessExecution.cs @@ -0,0 +1,210 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.PowerShell.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 command to execute. + /// This is ultimately just a required argument. + /// + required public string Command { 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(); + processArguments.Append(this.Command); + + foreach (string arg in this.Arguments) + { + 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 IEnumerable Output + { + get { return this.outputLines; } + } + + /// + /// Gets the current set of error lines. + /// Not thread safe, use ErrorLineReceived for async flows. + /// + public IEnumerable 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. + /// + /// Thrown if Start has already been called. + public void 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.StandardInputEncoding = Encoding.UTF8; + startInfo.StandardOutputEncoding = Encoding.UTF8; + startInfo.StandardErrorEncoding = 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.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.RedirectStandardInput = true; + } + + this.Process.Start(); + this.Process.BeginOutputReadLine(); + this.Process.BeginErrorReadLine(); + + if (this.Input != null) + { + this.Process.StandardInput.Write(this.Input); + } + } + + /// + /// 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; + } + } + } +} 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..6657509d24 --- /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.PowerShell.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/Outputs/ResourceCapability.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceCapability.cs new file mode 100644 index 0000000000..6d83d02144 --- /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.PowerShell.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/ResourceList.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceList.cs new file mode 100644 index 0000000000..b40901c598 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Schema_2024_04/Outputs/ResourceList.cs @@ -0,0 +1,83 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.PowerShell.Schema_2024_04.Outputs +{ + using System.Text.Json.Nodes; + using System.Text.Json.Serialization; + using Microsoft.Management.Configuration.Processor.PowerShell.Schema_2024_04.Definitions; + + /// + /// The object type from a single JSON line output by the `resource list` command. + /// + internal class ResourceList + { + /// + /// 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 ResourceKind Kind { get; set; } = ResourceKind.Unknown; + + /// + /// 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/Set/DSCv3ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs new file mode 100644 index 0000000000..70f0c0b486 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs @@ -0,0 +1,44 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Set +{ + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.PowerShell.Unit; + using Microsoft.Management.Configuration.Processor.Set; + using Microsoft.Management.Configuration.Processor.Unit; + + /// + /// Configuration set processor. + /// + internal sealed partial class DSCv3ConfigurationSetProcessor : ConfigurationSetProcessorBase, IConfigurationSetProcessor + { + /// + /// Initializes a new instance of the class. + /// + /// Configuration set. + /// Whether the set processor should work in limitation mode. + public DSCv3ConfigurationSetProcessor(ConfigurationSet? configurationSet, bool isLimitMode = false) + : base(configurationSet, isLimitMode) + { + } + + /// + protected override IConfigurationUnitProcessor CreateUnitProcessorInternal(ConfigurationUnit unit) + { + return new DSCv3ConfigurationUnitProcessor(new ConfigurationUnitInternal(unit, this.ConfigurationSet?.Path), this.IsLimitMode); + } + + /// + protected override IConfigurationUnitProcessorDetails? GetUnitProcessorDetailsInternal(ConfigurationUnit unit, ConfigurationUnitDetailFlags detailFlags) + { + // TODO: Actual implementation + ConfigurationUnitProcessorDetails result = new ConfigurationUnitProcessorDetails() { UnitType = unit.Type }; + result.UnitDescription = "This is a test!"; + return result; + } + } +} 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..b18bea3749 --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs @@ -0,0 +1,54 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.PowerShell.Unit +{ + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.Helpers; + using Microsoft.Management.Configuration.Processor.PowerShell.Helpers; + using Microsoft.Management.Configuration.Processor.PowerShell.ProcessorEnvironments; + 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 + { + /// + /// Initializes a new instance of the class. + /// + /// Internal unit. + /// Whether it is under limit mode. + internal DSCv3ConfigurationUnitProcessor(ConfigurationUnitInternal unitInternal, bool isLimitMode = false) + : base(unitInternal, isLimitMode) + { + } + + /// + protected override ValueSet GetSettingsInternal() + { + // TODO: Actual implementation + var result = new ValueSet(); + result.Add("Test", "Value"); + return result; + } + + /// + protected override bool TestSettingsInternal() + { + // TODO: Actual implementation + return false; + } + + /// + protected override bool ApplySettingsInternal() + { + // TODO: Actual implementation + return false; + } + } +} 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..0e0e3fb869 100644 --- a/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj +++ b/src/Microsoft.Management.Configuration.Processor/Microsoft.Management.Configuration.Processor.csproj @@ -69,11 +69,6 @@ - - - - - $(DefineConstants);WinGetCsWinRTEmbedded false 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..29aadb621c --- /dev/null +++ b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs @@ -0,0 +1,55 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor +{ + using Microsoft.Management.Configuration; + using Microsoft.Management.Configuration.Processor.DSCv3.Set; + using Microsoft.Management.Configuration.Processor.Factory; + using System; + + /// + /// IConfigurationSetProcessorFactory implementation using DSC v3. + /// + internal sealed partial class DSCv3ConfigurationSetProcessorFactory : ConfigurationSetProcessorFactoryBase, IConfigurationSetProcessorFactory + { + private string? dscExecutablePath = null; + + /// + /// 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.dscExecutablePath; + } + + set + { + if (this.IsLimitMode()) + { + throw new InvalidOperationException("Setting DscExecutablePath in limit mode is invalid."); + } + + this.dscExecutablePath = value; + } + } + + /// + protected override IConfigurationSetProcessor CreateSetProcessorInternal(ConfigurationSet? set, bool isLimitMode) + { + return new DSCv3ConfigurationSetProcessor(set, isLimitMode); + } + } +} 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/WindowsPackageManager/ConfigurationStaticFunctions.cpp b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp index 0e2a9a2151..795277e99a 100644 --- a/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp +++ b/src/WindowsPackageManager/ConfigurationStaticFunctions.cpp @@ -108,11 +108,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)