commit 2f344f090bce1dd633d2a655f61d5b5c16a702f0 parent a3cea271bb84cf2a3358d97242effc348cce18a5 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Fri, 14 Mar 2025 13:42:43 -0700 Dscv3 command base (#5280) ## Change Adds infrastructure for writing DSC v3 resource commands. Adds a new hidden command `dscv3` to put the resources under. Adds a file content resource (`DscTestFileResource`) that is only available when tests hooks are enabled. The resource infrastructure generates the resource manifest based on the defined functions and modifiers given to the base via `winget dscv3 <resource> --manifest`. It directs the `DscFunctions` it is given to the appropriate virtual method (`--get` maps to `ResourceFunctionGet`). Resources can define properties using the helper macro `WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY` and create an object to represent their properties with `DscComposableObject`. This makes extracting and writing properties to JSON easy, as well as generating a schema. Diffstat:
40 files changed, 1326 insertions(+), 129 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -302,6 +302,10 @@ <ClInclude Include="Commands\ConfigureValidateCommand.h" /> <ClInclude Include="Commands\DebugCommand.h" /> <ClInclude Include="Commands\DownloadCommand.h" /> + <ClInclude Include="Commands\DscCommand.h" /> + <ClInclude Include="Commands\DscCommandBase.h" /> + <ClInclude Include="Commands\DscComposableObject.h" /> + <ClInclude Include="Commands\DscTestFileResource.h" /> <ClInclude Include="Commands\ErrorCommand.h" /> <ClInclude Include="Commands\ExperimentalCommand.h" /> <ClInclude Include="Commands\ExportCommand.h" /> @@ -383,6 +387,10 @@ <ClCompile Include="Commands\ConfigureValidateCommand.cpp" /> <ClCompile Include="Commands\DebugCommand.cpp" /> <ClCompile Include="Commands\DownloadCommand.cpp" /> + <ClCompile Include="Commands\DscCommand.cpp" /> + <ClCompile Include="Commands\DscCommandBase.cpp" /> + <ClCompile Include="Commands\DscComposableObject.cpp" /> + <ClCompile Include="Commands\DscTestFileResource.cpp" /> <ClCompile Include="Commands\ErrorCommand.cpp" /> <ClCompile Include="Commands\FontCommand.cpp" /> <ClCompile Include="Commands\ImportCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -18,6 +18,9 @@ <Filter Include="Workflows"> <UniqueIdentifier>{fdeb940e-93e2-4bb0-a59c-1e2d1c0588d1}</UniqueIdentifier> </Filter> + <Filter Include="Commands\Configuration"> + <UniqueIdentifier>{bd9d8dfe-7a37-4a53-b4e7-ddbd9694c0ab}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -197,18 +200,6 @@ <ClInclude Include="Workflows\MultiQueryFlow.h"> <Filter>Workflows</Filter> </ClInclude> - <ClInclude Include="Commands\ConfigureCommand.h"> - <Filter>Commands</Filter> - </ClInclude> - <ClInclude Include="Commands\ConfigureShowCommand.h"> - <Filter>Commands</Filter> - </ClInclude> - <ClInclude Include="Commands\ConfigureTestCommand.h"> - <Filter>Commands</Filter> - </ClInclude> - <ClInclude Include="Commands\ConfigureValidateCommand.h"> - <Filter>Commands</Filter> - </ClInclude> <ClInclude Include="ConfigurationContext.h"> <Filter>Header Files</Filter> </ClInclude> @@ -251,12 +242,6 @@ <ClInclude Include="ConfigurationWingetDscModuleUnitValidation.h"> <Filter>Header Files</Filter> </ClInclude> - <ClInclude Include="ConfigureExportCommand.h"> - <Filter>Commands</Filter> - </ClInclude> - <ClInclude Include="Commands\ConfigureListCommand.h"> - <Filter>Commands</Filter> - </ClInclude> <ClInclude Include="Sixel.h"> <Filter>Header Files</Filter> </ClInclude> @@ -266,6 +251,36 @@ <ClInclude Include="Workflows\FontFlow.h"> <Filter>Workflows</Filter> </ClInclude> + <ClInclude Include="Commands\ConfigureCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="ConfigureExportCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\ConfigureListCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\ConfigureShowCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\ConfigureTestCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\ConfigureValidateCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\DscCommand.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\DscCommandBase.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\DscTestFileResource.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> + <ClInclude Include="Commands\DscComposableObject.h"> + <Filter>Commands\Configuration</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -430,18 +445,6 @@ <ClCompile Include="Workflows\MultiQueryFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> - <ClCompile Include="Commands\ConfigureCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> - <ClCompile Include="Commands\ConfigureShowCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> - <ClCompile Include="Commands\ConfigureTestCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> - <ClCompile Include="Commands\ConfigureValidateCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> <ClCompile Include="ConfigurationContext.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -487,12 +490,6 @@ <ClCompile Include="ConfigurationDynamicRuntimeFactory.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="ConfigureExportCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> - <ClCompile Include="Commands\ConfigureListCommand.cpp"> - <Filter>Commands</Filter> - </ClCompile> <ClCompile Include="Sixel.cpp"> <Filter>Source Files</Filter> </ClCompile> @@ -502,6 +499,36 @@ <ClCompile Include="Workflows\FontFlow.cpp"> <Filter>Workflows</Filter> </ClCompile> + <ClCompile Include="Commands\ConfigureCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="ConfigureExportCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\ConfigureListCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\ConfigureShowCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\ConfigureTestCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\ConfigureValidateCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\DscCommand.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\DscCommandBase.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\DscTestFileResource.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> + <ClCompile Include="Commands\DscComposableObject.cpp"> + <Filter>Commands\Configuration</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -234,6 +234,30 @@ namespace AppInstaller::CLI case Execution::Args::Type::ConfigurationStatusWatch: return { type, "live"_liv }; + // DSCv3 resources + case Execution::Args::Type::DscResourceFunctionGet: + return { type, "get"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionSet: + return { type, "set"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionWhatIf: + return { type, "whatIf"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionTest: + return { type, "test"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionDelete: + return { type, "delete"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionExport: + return { type, "export"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionValidate: + return { type, "validate"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionResolve: + return { type, "resolve"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionAdapter: + return { type, "adapter"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionSchema: + return { type, "schema"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + case Execution::Args::Type::DscResourceFunctionManifest: + return { type, "manifest"_liv, ArgTypeCategory::None, ArgTypeExclusiveSet::DscResourceFunction }; + // Download command case Execution::Args::Type::DownloadDirectory: return { type, "download-directory"_liv, 'd', ArgTypeCategory::None }; diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h @@ -90,6 +90,7 @@ namespace AppInstaller::CLI Proxy = 0x20, AllAndTargetVersion = 0x40, ConfigurationSetChoice = 0x80, + DscResourceFunction = 0x100, // This must always be at the end Max diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -65,6 +65,8 @@ namespace AppInstaller::CLI Command(name, aliases, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, groupPolicy, CommandOutputFlags::None) {} Command(std::string_view name, std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : Command(name, aliases, parent, visibility, feature, Settings::TogglePolicy::Policy::None, CommandOutputFlags::None) {} + Command(std::string_view name, std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility, Settings::TogglePolicy::Policy groupPolicy) : + Command(name, aliases, parent, visibility, Settings::ExperimentalFeature::Feature::None, groupPolicy, CommandOutputFlags::None) {} Command(std::string_view name, std::vector<std::string_view> aliases, @@ -98,8 +100,8 @@ namespace AppInstaller::CLI std::vector<std::unique_ptr<Command>> GetVisibleCommands() const; std::vector<Argument> GetVisibleArguments() const; - virtual Resource::LocString ShortDescription() const { return {}; } - virtual Resource::LocString LongDescription() const { return {}; } + virtual Resource::LocString ShortDescription() const = 0; + virtual Resource::LocString LongDescription() const = 0; virtual void OutputIntroHeader(Execution::Reporter& reporter) const; virtual void OutputHelp(Execution::Reporter& reporter, const CommandException* exception = nullptr) const; diff --git a/src/AppInstallerCLICore/Commands/COMCommand.h b/src/AppInstallerCLICore/Commands/COMCommand.h @@ -9,7 +9,10 @@ namespace AppInstaller::CLI struct COMDownloadCommand final : public Command { constexpr static std::string_view CommandName = "download"sv; - COMDownloadCommand(std::string_view parent) : Command(CommandName, parent) {} + COMDownloadCommand(std::string_view parent) : Command(CommandName, parent) {} + + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } protected: void ExecuteInternal(Execution::Context& context) const override; @@ -19,7 +22,10 @@ namespace AppInstaller::CLI struct COMInstallCommand final : public Command { constexpr static std::string_view CommandName = "install"sv; - COMInstallCommand(std::string_view parent) : Command(CommandName, parent) {} + COMInstallCommand(std::string_view parent) : Command(CommandName, parent) {} + + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } protected: void ExecuteInternal(Execution::Context& context) const override; @@ -29,7 +35,10 @@ namespace AppInstaller::CLI struct COMUninstallCommand final : public Command { constexpr static std::string_view CommandName = "uninstall"sv; - COMUninstallCommand(std::string_view parent) : Command(CommandName, parent) {} + COMUninstallCommand(std::string_view parent) : Command(CommandName, parent) {} + + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } protected: void ExecuteInternal(Execution::Context& context) const override; @@ -41,6 +50,9 @@ namespace AppInstaller::CLI constexpr static std::string_view CommandName = "repair"sv; COMRepairCommand(std::string_view parent) : Command(CommandName, parent) {} + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } + protected: void ExecuteInternal(Execution::Context& context) const override; }; diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.h b/src/AppInstallerCLICore/Commands/ConfigureCommand.h @@ -2,7 +2,6 @@ // Licensed under the MIT License. #pragma once #include "Command.h" -#include <winget/ExperimentalFeature.h> namespace AppInstaller::CLI { diff --git a/src/AppInstallerCLICore/Commands/DscCommand.cpp b/src/AppInstallerCLICore/Commands/DscCommand.cpp @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscCommand.h" + +#ifndef AICLI_DISABLE_TEST_HOOKS +#include "DscTestFileResource.h" +#endif + +namespace AppInstaller::CLI +{ + std::vector<std::unique_ptr<Command>> DscCommand::GetCommands() const + { + return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ +#ifndef AICLI_DISABLE_TEST_HOOKS + std::make_unique<DscTestFileResource>(FullName()), +#endif + }); + } + + Resource::LocString DscCommand::ShortDescription() const + { + return { Resource::String::DscCommandShortDescription }; + } + + Resource::LocString DscCommand::LongDescription() const + { + return { Resource::String::DscCommandLongDescription }; + } + + Utility::LocIndView DscCommand::HelpLink() const + { + return "https://aka.ms/winget-dsc-resources"_liv; + } + + void DscCommand::ExecuteInternal(Execution::Context& context) const + { + OutputHelp(context.Reporter); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscCommand.h b/src/AppInstallerCLICore/Commands/DscCommand.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" +#include <winget/ExperimentalFeature.h> + +namespace AppInstaller::CLI +{ + struct DscCommand final : public Command + { + DscCommand(std::string_view parent) : Command(StaticName(), parent, Settings::ExperimentalFeature::Feature::ConfigurationDSCv3) {} + + static constexpr std::string_view StaticName() { return "dscv3"sv; }; + + std::vector<std::unique_ptr<Command>> GetCommands() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/DscCommandBase.cpp b/src/AppInstallerCLICore/Commands/DscCommandBase.cpp @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscCommandBase.h" +#include "DscCommand.h" +#include <winget/Runtime.h> + +#define WINGET_DSC_FUNCTION_FOREACH(_macro_) \ + _macro_(Get); \ + _macro_(Set); \ + _macro_(WhatIf); \ + _macro_(Test); \ + _macro_(Delete); \ + _macro_(Export); \ + _macro_(Validate); \ + _macro_(Resolve); \ + _macro_(Adapter); \ + _macro_(Schema); \ + +namespace AppInstaller::CLI +{ + namespace + { + constexpr std::string_view s_WingetModuleName = "Microsoft.WinGet"sv; + + std::string GetFunctionManifestString(DscFunctions function) + { + THROW_HR_IF(E_INVALIDARG, !WI_IsSingleFlagSet(function)); + + switch (function) + { + case DscFunctions::Get: return "get"; + case DscFunctions::Set: return "set"; + case DscFunctions::WhatIf: return "whatIf"; + case DscFunctions::Test: return "test"; + case DscFunctions::Delete: return "delete"; + case DscFunctions::Export: return "export"; + case DscFunctions::Validate: return "validate"; + case DscFunctions::Resolve: return "resolve"; + case DscFunctions::Adapter: return "adapter"; + case DscFunctions::Schema: return "schema"; + } + + THROW_HR(E_NOTIMPL); + } + + std::string GetFunctionArgumentString(DscFunctions function) + { + return std::string{ "--" } + GetFunctionManifestString(function); + } + + bool FunctionSpecifiesInput(DscFunctions function) + { + switch (function) + { + case DscFunctions::Get: + case DscFunctions::Set: + case DscFunctions::WhatIf: + case DscFunctions::Test: + case DscFunctions::Delete: + case DscFunctions::Export: + case DscFunctions::Validate: + case DscFunctions::Resolve: + return true; + } + + return false; + } + + bool FunctionIsSetLike(DscFunctions function) + { + switch (function) + { + case DscFunctions::Set: + case DscFunctions::WhatIf: + return true; + } + + return false; + } + + bool FunctionSpecifiesReturn(DscFunctions function) + { + switch (function) + { + case DscFunctions::Set: + case DscFunctions::WhatIf: + case DscFunctions::Test: + return true; + } + + return false; + } + + Json::Value CreateJsonDefinitionFor(std::string_view name, DscFunctions function, DscFunctionModifiers modifiers) + { + THROW_HR_IF(E_INVALIDARG, !WI_IsSingleFlagSet(function)); + THROW_HR_IF(E_NOTIMPL, function == DscFunctions::Adapter); + + Json::Value result{ Json::ValueType::objectValue }; + +#ifndef AICLI_DISABLE_TEST_HOOKS + result["executable"] = "wingetdev"; +#else + result["executable"] = "winget"; +#endif + + Json::Value args{ Json::ValueType::arrayValue }; + args.append(std::string{ DscCommand::StaticName() }); + args.append(std::string{ name }); + args.append(GetFunctionArgumentString(function)); + result["args"] = std::move(args); + + if (FunctionSpecifiesInput(function)) + { + result["input"] = "stdin"; + } + + if (FunctionIsSetLike(function)) + { + if (WI_IsFlagSet(modifiers, DscFunctionModifiers::ImplementsPretest)) + { + result["implementsPretest"] = true; + } + + if (WI_IsFlagSet(modifiers, DscFunctionModifiers::HandlesExist)) + { + result["handlesExist"] = true; + } + } + + if (FunctionSpecifiesReturn(function)) + { + result["return"] = "stateAndDiff"; + } + + if (function == DscFunctions::Schema) + { + Json::Value newResult{ Json::ValueType::objectValue }; + newResult["command"] = std::move(result); + result = std::move(newResult); + } + + return result; + } + } + + DscCommandBase::DscCommandBase(std::string_view parent, std::string_view resourceName, DscResourceKind kind, DscFunctions functions, DscFunctionModifiers modifiers) : + Command(resourceName, parent, CommandOutputFlags::IgnoreSettingsWarnings), m_kind(kind), m_functions(functions), m_modifiers(modifiers) + { + // Limits on current implementation + THROW_HR_IF(E_NOTIMPL, kind != DscResourceKind::Resource); + THROW_HR_IF(E_NOTIMPL, WI_IsFlagSet(functions, DscFunctions::Adapter)); + } + + std::vector<Argument> DscCommandBase::GetArguments() const + { + std::vector<Argument> result; + +#define WINGET_DSC_FUNCTION_ARGUMENT(_function_) \ + if (WI_IsFlagSet(m_functions, DscFunctions::_function_)) \ + { \ + result.emplace_back(Execution::Args::Type::DscResourceFunction ## _function_, Resource::String::DscResourceFunctionDescription ## _function_, ArgumentType::Flag); \ + } + + WINGET_DSC_FUNCTION_FOREACH(WINGET_DSC_FUNCTION_ARGUMENT); + +#undef WINGET_DSC_FUNCTION_ARGUMENT + + result.emplace_back(Execution::Args::Type::DscResourceFunctionManifest, Resource::String::DscResourceFunctionDescriptionManifest, ArgumentType::Flag); + result.emplace_back(Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Standard); + + return result; + } + + Utility::LocIndView DscCommandBase::HelpLink() const + { + return "https://aka.ms/winget-dsc-resources"_liv; + } + + void DscCommandBase::ExecuteInternal(Execution::Context& context) const + { + context.Reporter.SetChannel(Execution::Reporter::Channel::Json); + + // TODO: Consider adding a stderr logger + +#define WINGET_DSC_FUNCTION_ARGUMENT(_function_) \ + if (context.Args.Contains(Execution::Args::Type::DscResourceFunction ## _function_)) \ + { \ + return ResourceFunction ## _function_(context); \ + } + + WINGET_DSC_FUNCTION_FOREACH(WINGET_DSC_FUNCTION_ARGUMENT); + WINGET_DSC_FUNCTION_ARGUMENT(Manifest); + +#undef WINGET_DSC_FUNCTION_ARGUMENT + } + +#define WINGET_DSC_FUNCTION_METHOD(_function_) \ + void DscCommandBase::ResourceFunction ## _function_(Execution::Context&) const \ + { \ + THROW_HR(E_NOTIMPL); \ + } \ + + WINGET_DSC_FUNCTION_FOREACH(WINGET_DSC_FUNCTION_METHOD); + + void DscCommandBase::ResourceFunctionManifest(Execution::Context& context) const + { + Json::Value json{ Json::ValueType::objectValue }; + + // TODO: Move to release schema when released (there should be an aka.ms link as well, but it wasn't active yet) + //json["$schema"] = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/bundled/resource/manifest.json"; + json["$schema"] = "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json"; + json["type"] = std::string{ s_WingetModuleName } + '/' + ResourceType(); + json["description"] = LongDescription().get(); + json["version"] = Runtime::GetClientVersion().get(); + + Json::Value tags{ Json::ValueType::arrayValue }; + tags.append("WinGet"); + json["tags"] = std::move(tags); + +#define WINGET_DSC_FUNCTION_MANIFEST(_function_) \ + if (WI_IsFlagSet(m_functions, DscFunctions::_function_)) \ + { \ + json[GetFunctionManifestString(DscFunctions::_function_)] = CreateJsonDefinitionFor(Name(), DscFunctions::_function_, m_modifiers); \ + } + + WINGET_DSC_FUNCTION_FOREACH(WINGET_DSC_FUNCTION_MANIFEST); + +#undef WINGET_DSC_FUNCTION_MANIFEST + + Json::StreamWriterBuilder writerBuilder; + writerBuilder.settings_["indentation"] = " "; + std::string jsonString = Json::writeString(writerBuilder, json); + + if (context.Args.Contains(Execution::Args::Type::OutputFile)) + { + std::ofstream stream{ Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::OutputFile)), std::ios::binary }; + stream.write(jsonString.c_str(), jsonString.length()); + } + else + { + context.Reporter.Json() << jsonString; + } + } + +#undef WINGET_DSC_FUNCTION_METHOD + + std::optional<Json::Value> DscCommandBase::GetJsonFromInput(Execution::Context& context) const + { + Json::Value result; + Json::CharReaderBuilder builder; + Json::String errors; + if (!Json::parseFromStream(builder, context.Reporter.RawInputStream(), &result, &errors)) + { + AICLI_LOG(CLI, Error, << "Failed to read input JSON: " << errors); + AICLI_TERMINATE_CONTEXT_RETURN(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, std::nullopt); + } + + return result; + } + + void DscCommandBase::WriteJsonOutputLine(Execution::Context& context, const Json::Value& value) const + { + Json::StreamWriterBuilder writerBuilder; + writerBuilder.settings_["indentation"] = ""; + context.Reporter.Json() << Json::writeString(writerBuilder, value) << std::endl; + } +} diff --git a/src/AppInstallerCLICore/Commands/DscCommandBase.h b/src/AppInstallerCLICore/Commands/DscCommandBase.h @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" +#include <json/json.h> +#include <optional> + +namespace AppInstaller::CLI +{ + // The kind of resource that this command is implementing. + enum class DscResourceKind + { + // Standard resource + Resource, + // Group resource + Group, + // Adapter resource + Adapter, + // Import resource + Import, + }; + + // The functions that a DSC resource can provide. + enum class DscFunctions + { + None = 0x000, + // Gets the current state; should always be implemented. + Get = 0x001, + // Sets the state; should always be implemented. + Set = 0x002, + // Produces the output of Set without modifying state. + WhatIf = 0x004, + // Determines if the current state matches the given state. + Test = 0x008, + // Deletes the given state. + Delete = 0x010, + // Gets all instances of the resource. + Export = 0x020, + // Required for a Group resource, ignored for all others. + Validate = 0x040, + // Required for an Import resource. + Resolve = 0x080, + // Required for an Adapter resource. + Adapter = 0x100, + // Gets the schema for the resource's properties. + Schema = 0x200, + }; + + DEFINE_ENUM_FLAG_OPERATORS(DscFunctions); + + // Behavior changes for DSC functions. + enum class DscFunctionModifiers + { + None = 0x00, + // The resource implements a check during Set (and WhatIf) to determine if already in the correct state. + // If not provided, DSC will ensure that the state is tested beforehand. + ImplementsPretest = 0x01, + // The resource will act on the `_exist` property during Set (and WhatIf). + // If not provided, the resource should implement Delete. + HandlesExist = 0x02, + }; + + DEFINE_ENUM_FLAG_OPERATORS(DscFunctionModifiers); + + // Provides infrastructure for DSC commands to be implemented. + struct DscCommandBase : public Command + { + DscCommandBase(std::string_view parent, std::string_view resourceName, DscResourceKind kind, DscFunctions functions, DscFunctionModifiers modifiers); + + std::vector<Argument> GetArguments() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + + // Gets the resource specific type name. + virtual std::string ResourceType() const = 0; + + virtual void ResourceFunctionGet(Execution::Context& context) const; + virtual void ResourceFunctionSet(Execution::Context& context) const; + virtual void ResourceFunctionWhatIf(Execution::Context& context) const; + virtual void ResourceFunctionTest(Execution::Context& context) const; + virtual void ResourceFunctionDelete(Execution::Context& context) const; + virtual void ResourceFunctionExport(Execution::Context& context) const; + virtual void ResourceFunctionValidate(Execution::Context& context) const; + virtual void ResourceFunctionResolve(Execution::Context& context) const; + virtual void ResourceFunctionAdapter(Execution::Context& context) const; + virtual void ResourceFunctionSchema(Execution::Context& context) const; + virtual void ResourceFunctionManifest(Execution::Context& context) const; + + // Parses a JSON object from stdin. + std::optional<Json::Value> GetJsonFromInput(Execution::Context& context) const; + + // Writes the value to the context output. + void WriteJsonOutputLine(Execution::Context& context, const Json::Value& value) const; + + private: + DscResourceKind m_kind = DscResourceKind::Resource; + DscFunctions m_functions = DscFunctions::None; + DscFunctionModifiers m_modifiers = DscFunctionModifiers::None; + }; +} diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.cpp b/src/AppInstallerCLICore/Commands/DscComposableObject.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscComposableObject.h" + +namespace AppInstaller::CLI +{ + namespace details + { + const Json::Value* GetProperty(const Json::Value& object, std::string_view name) + { + return object.find(name.data(), name.data() + name.length()); + } + + void AddProperty(Json::Value& object, std::string_view name, std::optional<Json::Value>&& value) + { + if (value) + { + object[std::string{ name }] = std::move(value).value(); + } + } + + Json::Value GetBaseSchema(const std::string& title) + { + Json::Value result{ Json::ValueType::objectValue }; + + result["$schema"] = "http://json-schema.org/draft-07/schema#"; + result["title"] = title; + result["type"] = "object"; + result["additionalProperties"] = false; + + return result; + } + + void AddPropertySchema(Json::Value& object, std::string_view name, DscComposablePropertyFlag flags, std::string_view type, std::string_view description) + { + Json::Value& propertiesObject = object["properties"]; + + if (propertiesObject.isNull()) + { + propertiesObject = Json::Value{ Json::ValueType::objectValue }; + } + + std::string nameString{ name }; + + Json::Value property{ Json::ValueType::objectValue }; + + property["type"] = std::string{ type }; + property["description"] = std::string{ description }; + + propertiesObject[nameString] = std::move(property); + + if (WI_IsFlagSet(flags, DscComposablePropertyFlag::Required)) + { + Json::Value& requiredArray = object["required"]; + + if (requiredArray.isNull()) + { + requiredArray = Json::Value{ Json::ValueType::arrayValue }; + } + + requiredArray.append(nameString); + } + } + } +} diff --git a/src/AppInstallerCLICore/Commands/DscComposableObject.h b/src/AppInstallerCLICore/Commands/DscComposableObject.h @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <AppInstallerErrors.h> +#include <AppInstallerLanguageUtilities.h> +#include <json/json.h> +#include <optional> + +using namespace std::string_view_literals; + +namespace AppInstaller::CLI +{ + // Flags that define how to treat properties. + enum DscComposablePropertyFlag + { + None = 0x0, + Required = 0x1, + CopyToOutput = 0x2, + }; + + DEFINE_ENUM_FLAG_OPERATORS(DscComposablePropertyFlag); + + namespace details + { + // Gets a property or null if not present. + const Json::Value* GetProperty(const Json::Value& object, std::string_view name); + + // Adds the given property and value to the object, if provided. + void AddProperty(Json::Value& object, std::string_view name, std::optional<Json::Value>&& value); + + // Gets the default schema object. + Json::Value GetBaseSchema(const std::string& title); + + // Adds a property to the schema object. + void AddPropertySchema(Json::Value& object, std::string_view name, DscComposablePropertyFlag flags, std::string_view type, std::string_view description); + } + + template <typename PropertyType> + struct GetJsonTypeValue + { + static_assert(false, "Implement for this type."); + }; + + template <> + struct GetJsonTypeValue<bool> + { + static bool Get(const Json::Value& value) + { + return value.asBool(); + } + + static std::string_view SchemaTypeName() + { + return "boolean"sv; + } + }; + + template <> + struct GetJsonTypeValue<std::string> + { + static std::string Get(const Json::Value& value) + { + return value.asString(); + } + + static std::string_view SchemaTypeName() + { + return "string"sv; + } + }; + + // Template useful for composing objects for DSC resources. + // Properties should be of the shape: + // + // struct Property + // { + // using Type = { bool, std::string }; + // static std::string_view Name(); + // static void FromJson(Property*, const Json::Value*); + // static std::optional<Json::Value> ToJson(const Property*); + // + // const Type& PROPERTY_NAME() const; + // void PROPERTY_NAME(const Type&); + // } + template <typename... Property> + struct DscComposableObject : public Property... + { + DscComposableObject() = default; + + DscComposableObject(const std::optional<Json::Value>& input) + { + THROW_HR_IF(E_POINTER, !input); + FromJson(input.value()); + } + + // Read values for each property + void FromJson(const Json::Value& input) + { + (FoldHelper{}, ..., Property::FromJson(this, details::GetProperty(input, Property::Name()))); + } + + // Populate JSON object with properties. + Json::Value ToJson() + { + Json::Value result{ Json::ValueType::objectValue }; + (FoldHelper{}, ..., details::AddProperty(result, Property::Name(), Property::ToJson(this))); + return result; + } + + // Copies the appropriate values to a new object for output. + DscComposableObject CopyForOutput() + { + DscComposableObject result; + (FoldHelper{}, ..., Property::CopyForOutput(this, &result)); + return result; + } + + // Get the JSON Schema for this object + static Json::Value Schema(const std::string& title) + { + Json::Value result = details::GetBaseSchema(title); + (FoldHelper{}, ..., details::AddPropertySchema(result, Property::Name(), Property::Flags, GetJsonTypeValue<typename Property::Type>::SchemaTypeName(), Property::Description())); + return result; + } + }; + + template <typename Derived, typename PropertyType, DscComposablePropertyFlag PropertyFlags> + struct DscComposableProperty + { + using Type = PropertyType; + static constexpr DscComposablePropertyFlag Flags = PropertyFlags; + + static void FromJson(Derived* self, const Json::Value* value) + { + if (value) + { + self->m_value = GetJsonTypeValue<PropertyType>::Get(*value); + } + else + { + if constexpr (WI_IsFlagSet(PropertyFlags, DscComposablePropertyFlag::Required)) + { + THROW_HR(WINGET_CONFIG_ERROR_MISSING_FIELD); + } + else + { + self->m_value = std::nullopt; + } + } + } + + static std::optional<Json::Value> ToJson(const Derived* self) + { + if constexpr (WI_IsFlagSet(PropertyFlags, DscComposablePropertyFlag::Required)) + { + THROW_HR_IF(WINGET_CONFIG_ERROR_MISSING_FIELD, !self->m_value); + return self->m_value.value(); + } + else + { + return self->m_value ? std::optional<Json::Value>{ self->m_value.value() } : std::nullopt; + } + } + + static void CopyForOutput(const Derived* self, Derived* other) + { + if constexpr (WI_IsFlagSet(PropertyFlags, DscComposablePropertyFlag::CopyToOutput)) + { + other->m_value = self->m_value; + } + } + + protected: + std::optional<Type> m_value; + }; + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ + struct _property_type_ : public DscComposableProperty<_property_type_, _value_type_, _flags_> \ + { \ + static std::string_view Name() { return _json_name_; } \ + static std::string_view Description() { return _description_; } \ + std::optional<Type>& _property_name_() { return m_value; } \ + const std::optional<Type>& _property_name_() const { return m_value; } \ + void _property_name_(std::optional<Type> value) { m_value = std::move(value); } \ + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) \ + }; + +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(_property_type_, _value_type_, _property_name_, _json_name_, _description_) WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, DscComposablePropertyFlag::None, _description_) +#define WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL(_property_type_, _value_type_, _property_name_, _json_name_, _flags_, _description_) + + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_IMPL_START(StandardExistProperty, bool, Exist, "_exist", DscComposablePropertyFlag::None, "Indicates whether an instance should/does exist.") + bool ShouldExist() { return m_value.value_or(true); } + }; + + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(StandardInDesiredStateProperty, bool, InDesiredState, "_inDesiredState", "Indicates whether an instance is in the desired state."); +} diff --git a/src/AppInstallerCLICore/Commands/DscTestFileResource.cpp b/src/AppInstallerCLICore/Commands/DscTestFileResource.cpp @@ -0,0 +1,236 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "DscTestFileResource.h" +#include "DscComposableObject.h" + +using namespace AppInstaller::Utility::literals; + +namespace AppInstaller::CLI +{ + namespace anon + { + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY_FLAGS(PathProperty, std::string, Path, "path", DscComposablePropertyFlag::Required | DscComposablePropertyFlag::CopyToOutput, "The absolute path to a file."); + WINGET_DSC_DEFINE_COMPOSABLE_PROPERTY(ContentProperty, std::string, Content, "content", "The content of the file."); + + using TestFileObject = DscComposableObject<StandardExistProperty, StandardInDesiredStateProperty, PathProperty, ContentProperty>; + + struct FunctionData + { + FunctionData(const std::optional<Json::Value>& json) : Input(json), Output(Input.CopyForOutput()) + { + Path = Utility::ConvertToUTF16(Input.Path().value()); + THROW_HR_IF(E_INVALIDARG, !Path.is_absolute()); + } + + TestFileObject Input; + TestFileObject Output; + std::filesystem::path Path; + + // Fills the Output object with the current state + void Get() + { + if (std::filesystem::exists(Path) && std::filesystem::is_regular_file(Path)) + { + Output.Exist(true); + + std::ifstream stream{ Path, std::ios::binary }; + Output.Content(Utility::ReadEntireStream(stream)); + } + else + { + Output.Exist(false); + } + } + + // Determines if the current Output values match the Input values state. + bool Test() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + if (Input.ShouldExist()) + { + if (Output.Exist().value()) + { + return ContentMatches(); + } + else + { + return false; + } + } + else + { + return !Output.Exist().value(); + } + } + + Json::Value DiffJson() + { + // Need to populate Output before calling + THROW_HR_IF(E_UNEXPECTED, !Output.Exist().has_value()); + + Json::Value result{ Json::ValueType::arrayValue }; + + if (Input.ShouldExist() != Output.Exist().value()) + { + result.append(std::string{ StandardExistProperty::Name() }); + } + else + { + if (!ContentMatches()) + { + result.append(std::string{ ContentProperty::Name() }); + } + } + + return result; + } + + private: + bool ContentMatches() + { + bool hasInput = Input.Content().has_value() && !Input.Content().value().empty(); + bool hasOutput = Output.Content().has_value() && !Output.Content().value().empty(); + + return + (hasInput && hasOutput && Input.Content().value() == Output.Content().value()) || + (!hasInput && !hasOutput); + } + }; + } + + DscTestFileResource::DscTestFileResource(std::string_view parent) : + DscCommandBase(parent, "test-file", DscResourceKind::Resource, + DscFunctions::Get | DscFunctions::Set | DscFunctions::Test | DscFunctions::Export | DscFunctions::Schema, + DscFunctionModifiers::ImplementsPretest | DscFunctionModifiers::HandlesExist) + { + } + + Resource::LocString DscTestFileResource::ShortDescription() const + { + return "[TEST] File content resource"_lis; + } + + Resource::LocString DscTestFileResource::LongDescription() const + { + return "[TEST] This resource is only available for tests. It provides file content configuration."_lis; + } + + std::string DscTestFileResource::ResourceType() const + { + return "TestFile"; + } + + void DscTestFileResource::ResourceFunctionGet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + anon::FunctionData data{ json }; + + data.Get(); + + WriteJsonOutputLine(context, data.Output.ToJson()); + } + } + + void DscTestFileResource::ResourceFunctionSet(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + anon::FunctionData data{ json }; + + data.Get(); + + if (!data.Test()) + { + bool exists = std::filesystem::exists(data.Path); + if (exists) + { + // Don't delete a directory or other special files in this test resource + THROW_WIN32_IF(ERROR_DIRECTORY_NOT_SUPPORTED, !std::filesystem::is_regular_file(data.Path)); + } + + if (data.Input.ShouldExist()) + { + std::filesystem::create_directories(data.Path.parent_path()); + + std::ofstream stream{ data.Path, std::ios::binary | std::ios::trunc }; + if (data.Input.Content()) + { + stream.write(data.Input.Content().value().c_str(), data.Input.Content().value().length()); + } + } + else if (exists) + { + std::filesystem::remove(data.Path); + } + } + + // Capture the diff before updating the output + auto diff = data.DiffJson(); + + data.Output.Exist(data.Input.ShouldExist()); + if (data.Output.Exist().value()) + { + data.Output.Content(data.Input.Content().value_or("")); + } + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, diff); + } + } + + void DscTestFileResource::ResourceFunctionTest(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + anon::FunctionData data{ json }; + + data.Get(); + data.Output.InDesiredState(data.Test()); + + WriteJsonOutputLine(context, data.Output.ToJson()); + WriteJsonOutputLine(context, data.DiffJson()); + } + } + + void DscTestFileResource::ResourceFunctionExport(Execution::Context& context) const + { + if (auto json = GetJsonFromInput(context)) + { + anon::FunctionData data{ json }; + + if (std::filesystem::exists(data.Path)) + { + if (std::filesystem::is_regular_file(data.Path)) + { + data.Get(); + WriteJsonOutputLine(context, data.Output.ToJson()); + } + else if (std::filesystem::is_directory(data.Path)) + { + for (const auto& file : std::filesystem::directory_iterator{ data.Path }) + { + if (std::filesystem::is_regular_file(file)) + { + anon::TestFileObject output; + output.Path(file.path().u8string()); + + std::ifstream stream{ file.path(), std::ios::binary}; + output.Content(Utility::ReadEntireStream(stream)); + + WriteJsonOutputLine(context, output.ToJson()); + } + } + } + } + } + } + + void DscTestFileResource::ResourceFunctionSchema(Execution::Context& context) const + { + WriteJsonOutputLine(context, anon::TestFileObject::Schema(ResourceType())); + } +} diff --git a/src/AppInstallerCLICore/Commands/DscTestFileResource.h b/src/AppInstallerCLICore/Commands/DscTestFileResource.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "DscCommandBase.h" + +namespace AppInstaller::CLI +{ + // A test resource implementing file content configuration. + struct DscTestFileResource : public DscCommandBase + { + DscTestFileResource(std::string_view parent); + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + protected: + std::string ResourceType() const override; + + void ResourceFunctionGet(Execution::Context& context) const override; + void ResourceFunctionSet(Execution::Context& context) const override; + void ResourceFunctionTest(Execution::Context& context) const override; + void ResourceFunctionExport(Execution::Context& context) const override; + void ResourceFunctionSchema(Execution::Context& context) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -28,6 +28,7 @@ #include "ErrorCommand.h" #include "ResumeCommand.h" #include "RepairCommand.h" +#include "DscCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -197,6 +198,7 @@ namespace AppInstaller::CLI std::make_unique<ResumeCommand>(FullName()), std::make_unique<RepairCommand>(FullName()), std::make_unique<FontCommand>(FullName()), + std::make_unique<DscCommand>(FullName()), #if _DEBUG std::make_unique<DebugCommand>(FullName()), #endif @@ -215,6 +217,11 @@ namespace AppInstaller::CLI }; } + Resource::LocString RootCommand::ShortDescription() const + { + return {}; + } + Resource::LocString RootCommand::LongDescription() const { return { Resource::String::ToolDescription }; diff --git a/src/AppInstallerCLICore/Commands/RootCommand.h b/src/AppInstallerCLICore/Commands/RootCommand.h @@ -14,6 +14,7 @@ namespace AppInstaller::CLI std::vector<std::unique_ptr<Command>> GetCommands() const override; std::vector<Argument> GetArguments() const override; + Resource::LocString ShortDescription() const override; Resource::LocString LongDescription() const override; Utility::LocIndView HelpLink() const override; diff --git a/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp b/src/AppInstallerCLICore/ConfigurationSetProcessorFactoryRemoting.cpp @@ -379,6 +379,7 @@ namespace AppInstaller::CLI::ConfigurationRemoting { case PropertyName::DscExecutablePath: return L"DscExecutablePath"; case PropertyName::FoundDscExecutablePath: return L"FoundDscExecutablePath"; + case PropertyName::DiagnosticTraceLevel: return L"DiagnosticTraceLevel"; } THROW_HR(E_UNEXPECTED); diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -141,6 +141,19 @@ namespace AppInstaller::CLI::Execution ConfigurationHistoryRemove, ConfigurationStatusWatch, + // DSCv3 resources + DscResourceFunctionGet, + DscResourceFunctionSet, + DscResourceFunctionWhatIf, + DscResourceFunctionTest, + DscResourceFunctionDelete, + DscResourceFunctionExport, + DscResourceFunctionValidate, + DscResourceFunctionResolve, + DscResourceFunctionAdapter, + DscResourceFunctionSchema, + DscResourceFunctionManifest, + // Common arguments NoVT, // Disable VirtualTerminal outputs RetroStyle, // Makes progress display as retro diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -131,6 +131,11 @@ namespace AppInstaller::CLI::Execution } } + std::istream& Reporter::RawInputStream() + { + return m_in; + } + bool Reporter::PromptForBoolResponse(Resource::LocString message, Level level, bool resultIfDisabled) { auto out = GetOutputStream(level); diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -45,6 +45,7 @@ namespace AppInstaller::CLI::Execution { Output, Completion, + Json, Disabled, }; @@ -90,6 +91,9 @@ namespace AppInstaller::CLI::Execution // Get a stream for outputting completion words. OutputStream Completion() { return OutputStream(*m_out, m_channel == Channel::Completion, false); } + // Get a stream for outputting completion words. + OutputStream Json() { return OutputStream(*m_out, m_channel == Channel::Json, false); } + // Gets a stream for output of the given level. OutputStream GetOutputStream(Level level); @@ -102,6 +106,9 @@ namespace AppInstaller::CLI::Execution // Sets the visual style (mostly for progress currently) void SetStyle(AppInstaller::Settings::VisualStyle style); + // Get the raw input stream. + std::istream& RawInputStream(); + // Prompts the user, return true if they consented. bool PromptForBoolResponse(Resource::LocString message, Level level = Level::Info, bool resultIfDisabled = false); diff --git a/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h b/src/AppInstallerCLICore/Public/ConfigurationSetProcessorFactoryRemoting.h @@ -35,6 +35,9 @@ namespace AppInstaller::CLI::ConfigurationRemoting // The path to the dsc.exe executable, as discovered. // Read only. FoundDscExecutablePath, + // Whether to request detailed traces from the processor. + // Read / Write + DiagnosticTraceLevel, }; // Gets the string for a property name. diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -210,6 +210,19 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(DownloadCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(DownloadDirectoryArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(Downloading); + WINGET_DEFINE_RESOURCE_STRINGID(DscCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionGet); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionSet); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionWhatIf); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionTest); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionDelete); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionExport); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionValidate); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionResolve); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionAdapter); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionSchema); + WINGET_DEFINE_RESOURCE_STRINGID(DscResourceFunctionDescriptionManifest); WINGET_DEFINE_RESOURCE_STRINGID(EnableAdminSettingFailed); WINGET_DEFINE_RESOURCE_STRINGID(EnableWindowsFeaturesSuccess); WINGET_DEFINE_RESOURCE_STRINGID(EnablingWindowsFeature); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -139,11 +139,17 @@ namespace AppInstaller::CLI::Workflow } else if (processorEngine == ConfigurationRemoting::ProcessorEngine::DSCv3) { + auto factoryMap = factory.as<IMap<winrt::hstring, winrt::hstring>>(); + if (context.Args.Contains(Args::Type::ConfigurationProcessorPath)) { - auto factoryMap = factory.as<IMap<winrt::hstring, winrt::hstring>>(); factoryMap.Insert(ConfigurationRemoting::ToHString(ConfigurationRemoting::PropertyName::DscExecutablePath), Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::ConfigurationProcessorPath))); } + + if (Logging::Log().IsEnabled(Logging::Channel::Config, Logging::Level::Verbose)) + { + factoryMap.Insert(ConfigurationRemoting::ToHString(ConfigurationRemoting::PropertyName::DiagnosticTraceLevel), L"True"); + } } return factory; diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -6,7 +6,9 @@ namespace AppInstallerCLIE2ETests { + using System; using System.IO; + using System.Linq; using AppInstallerCLIE2ETests.Helpers; using Microsoft.Win32; using NUnit.Framework; @@ -19,14 +21,26 @@ namespace AppInstallerCLIE2ETests private const string CommandAndAgreementsAndVerbose = "configure --accept-configuration-agreements --verbose"; /// <summary> + /// Ensures that the test resources manifests are present. + /// </summary> + public static void EnsureTestResourcePresence() + { + string outputDirectory = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\WindowsApps"); + Assert.IsNotEmpty(outputDirectory); + + var result = TestCommon.RunAICLICommand("dscv3 test-file", $"--manifest -o {outputDirectory}\\test-file.dsc.resource.json"); + Assert.AreEqual(0, result.ExitCode); + } + + /// <summary> /// Setup done once before all the tests here. /// </summary> [OneTimeSetUp] public void OneTimeSetup() { WinGetSettingsHelper.ConfigureFeature("dsc3", true); - WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", true); this.DeleteResourceArtifacts(); + EnsureTestResourcePresence(); } /// <summary> @@ -36,7 +50,6 @@ namespace AppInstallerCLIE2ETests public void OneTimeTeardown() { WinGetSettingsHelper.ConfigureFeature("dsc3", false); - WinGetSettingsHelper.ConfigureFeature("configureSelfElevate", false); this.DeleteResourceArtifacts(); } @@ -259,29 +272,37 @@ namespace AppInstallerCLIE2ETests /// Runs a DSCv3 configuration, then changes the state and runs it again from history. /// </summary> [Test] - [Ignore("The registry resource is failing for unknown and undiagnosable reasons in the ADO pipeline. Replace these with test resources when we implement them next.")] public void ConfigureThroughHistory_DSCv3() { var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")); Assert.AreEqual(0, result.ExitCode); // The configuration creates a file next to itself with the given contents - string valueName = "TestVal"; - var registryKey = Registry.CurrentUser.OpenSubKey(Constants.TestRegistryPath, true); - Assert.NotNull(registryKey); - var registryValue = (string)registryKey.GetValue(valueName); - Assert.NotNull(registryValue); - Assert.AreEqual("Value!", registryValue); + string targetFilePath = TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.txt"); + FileAssert.Exists(targetFilePath); + Assert.AreEqual("DSCv3 Contents!", File.ReadAllText(targetFilePath)); - registryKey.SetValue(valueName, "New Value!", RegistryValueKind.String); + File.WriteAllText(targetFilePath, "Changed contents!"); 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); + FileAssert.Exists(targetFilePath); + Assert.AreEqual("DSCv3 Contents!", File.ReadAllText(targetFilePath)); + } + + /// <summary> + /// Ensures that the test file resource schema function works. + /// </summary> + [Test] + public void TestFileResourceSchema() + { + var result = TestCommon.RunAICLICommand("dscv3 test-file", "--schema"); + Assert.AreEqual(0, result.ExitCode); + + var lines = result.StdOut.Split("\r\n", StringSplitOptions.RemoveEmptyEntries); + Assert.AreEqual(1, lines.Length); } private void DeleteResourceArtifacts() @@ -291,15 +312,6 @@ namespace AppInstallerCLIE2ETests { 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 @@ -24,6 +24,7 @@ namespace AppInstallerCLIE2ETests { WinGetSettingsHelper.ConfigureFeature("dsc3", true); this.DeleteResourceArtifacts(); + ConfigureCommand.EnsureTestResourcePresence(); } /// <summary> @@ -171,7 +172,7 @@ namespace AppInstallerCLIE2ETests int startLine = -1; for (int i = 0; i < outputLines.Length; ++i) { - if (outputLines[i].Trim() == "Microsoft.Windows/Registry [RegVal]") + if (outputLines[i].Trim() == "Microsoft.WinGet/TestFile [Test File]") { startLine = i; } @@ -188,7 +189,6 @@ namespace AppInstallerCLIE2ETests /// Runs a DSCv3 configuration, then shows it from history. /// </summary> [Test] - [Ignore("The registry resource is failing for unknown and undiagnosable reasons in the ADO pipeline. Replace these with test resources when we implement them next.")] public void ShowFromHistory_DSCv3() { var result = TestCommon.RunAICLICommand("configure --accept-configuration-agreements --verbose", TestCommon.GetTestDataFile("Configuration\\ShowDetails_DSCv3.yml")); @@ -202,7 +202,7 @@ namespace AppInstallerCLIE2ETests int startLine = -1; for (int i = 0; i < outputLines.Length; ++i) { - if (outputLines[i].Trim() == "Microsoft.Windows/Registry [RegVal]") + if (outputLines[i].Trim() == "Microsoft.WinGet/TestFile [Test File]") { startLine = i; } @@ -222,15 +222,6 @@ namespace AppInstallerCLIE2ETests { 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 @@ -26,6 +26,7 @@ namespace AppInstallerCLIE2ETests { WinGetSettingsHelper.ConfigureFeature("dsc3", true); this.DeleteResourceArtifacts(); + ConfigureCommand.EnsureTestResourcePresence(); } /// <summary> @@ -34,7 +35,7 @@ namespace AppInstallerCLIE2ETests [OneTimeTearDown] public void OneTimeTeardown() { - WinGetSettingsHelper.ConfigureFeature("dsc3", true); + WinGetSettingsHelper.ConfigureFeature("dsc3", false); this.DeleteResourceArtifacts(); } @@ -134,15 +135,6 @@ namespace AppInstallerCLIE2ETests { 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/TestData/Configuration/ShowDetails_DSCv3.yml b/src/AppInstallerCLIE2ETests/TestData/Configuration/ShowDetails_DSCv3.yml @@ -3,12 +3,10 @@ metadata: winget: processor: dscv3 resources: - - name: RegVal - type: Microsoft.Windows/Registry + - name: Test File + type: Microsoft.WinGet/TestFile metadata: description: Description 1. properties: - keyPath: HKEY_CURRENT_USER\Software\Microsoft\WinGet\Tests - valueName: TestVal - valueData: - String: Value! + path: ${WinGetConfigRoot}\ShowDetails_DSCv3.txt + content: DSCv3 Contents! diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3207,4 +3207,44 @@ Please specify one of them using the --source option to proceed.</value> <data name="ConfigurationProcessorPath" xml:space="preserve"> <value>Specify the path to the configuration processor</value> </data> -</root> + <data name="DscCommandShortDescription" xml:space="preserve"> + <value>DSC v3 resource commands</value> + <comment>DSC stands for "Desired State Configuration". It should already have a locked translation.</comment> + </data> + <data name="DscCommandLongDescription" xml:space="preserve"> + <value>The sub-commands here implement Desired State Configuration (DSC) v3 resources for configuring winget and packages.</value> + </data> + <data name="DscResourceFunctionDescriptionGet" xml:space="preserve"> + <value>Get the resource state</value> + </data> + <data name="DscResourceFunctionDescriptionSet" xml:space="preserve"> + <value>Set the resource state</value> + </data> + <data name="DscResourceFunctionDescriptionWhatIf" xml:space="preserve"> + <value>Describe required state changes</value> + </data> + <data name="DscResourceFunctionDescriptionTest" xml:space="preserve"> + <value>Test the resource state</value> + </data> + <data name="DscResourceFunctionDescriptionDelete" xml:space="preserve"> + <value>Delete the resource state</value> + </data> + <data name="DscResourceFunctionDescriptionExport" xml:space="preserve"> + <value>Get all state instances</value> + </data> + <data name="DscResourceFunctionDescriptionValidate" xml:space="preserve"> + <value>Validate group contents</value> + </data> + <data name="DscResourceFunctionDescriptionResolve" xml:space="preserve"> + <value>Resolve external state</value> + </data> + <data name="DscResourceFunctionDescriptionAdapter" xml:space="preserve"> + <value>Run the adapter</value> + </data> + <data name="DscResourceFunctionDescriptionSchema" xml:space="preserve"> + <value>Get the resource schema</value> + </data> + <data name="DscResourceFunctionDescriptionManifest" xml:space="preserve"> + <value>Get the resource manifest</value> + </data> +</root>+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -211,6 +211,9 @@ struct TestCommand : public Command return m_args; } + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } + std::vector<Argument> m_args; }; @@ -661,4 +664,4 @@ TEST_CASE("ParseArguments_PositionalWithTooManyValues", "[command]") Invocation inv{ std::vector<std::string>(values) }; REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), CLI::Resource::String::ExtraPositionalError(Utility::LocIndView{ values.back() })); -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLITests/Completion.cpp b/src/AppInstallerCLITests/Completion.cpp @@ -231,6 +231,9 @@ struct CompletionTestCommand : public Command return Arguments; } + CLI::Resource::LocString ShortDescription() const override { return {}; } + CLI::Resource::LocString LongDescription() const override { return {}; } + using Command::Complete; void Complete(Execution::Context& context, Execution::Args::Type valueType) const override diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/IDiagnosticsSink.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/IDiagnosticsSink.cs @@ -0,0 +1,21 @@ +// ----------------------------------------------------------------------------- +// <copyright file="IDiagnosticsSink.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers +{ + /// <summary> + /// Defines the interface for a diagnostics sink. + /// </summary> + internal interface IDiagnosticsSink + { + /// <summary> + /// Sends a diagnostic message. + /// </summary> + /// <param name="level">The level of the message.</param> + /// <param name="message">The message.</param> + public void OnDiagnostics(DiagnosticLevel level, string message); + } +} diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Helpers/ProcessorSettings.cs @@ -99,6 +99,11 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers } /// <summary> + /// Gets or sets a value indicating whether the processor should produce more verbose output. + /// </summary> + public bool DiagnosticTraceLevel { get; set; } = false; + + /// <summary> /// Find the DSC v3 executable. /// </summary> /// <returns>The full path to the dsc.exe executable, or null if not found.</returns> @@ -137,6 +142,7 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers ProcessorSettings result = new ProcessorSettings(); result.DscExecutablePath = this.DscExecutablePath; + result.DiagnosticTraceLevel = this.DiagnosticTraceLevel; #if !AICLI_DISABLE_TEST_HOOKS result.dscV3 = this.DSCv3; #endif @@ -153,7 +159,10 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Helpers StringBuilder sb = new StringBuilder(); sb.Append("EffectiveDscExecutablePath: "); - sb.Append(this.EffectiveDscExecutablePath); + sb.AppendLine(this.EffectiveDscExecutablePath); + + sb.Append("DiagnosticTraceLevel: "); + sb.Append(this.DiagnosticTraceLevel); return sb.ToString(); } diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IDSCv3.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Model/IDSCv3.cs @@ -29,28 +29,32 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Model /// Gets a single resource by its type name. /// </summary> /// <param name="resourceType">The type name of the resource.</param> + /// <param name="diagnosticsSink">The diagnostics sink if provided.</param> /// <returns>A single resource item.</returns> - public IResourceListItem? GetResourceByType(string resourceType); + public IResourceListItem? GetResourceByType(string resourceType, IDiagnosticsSink? diagnosticsSink = null); /// <summary> /// Tests a configuration unit. /// </summary> /// <param name="unitInternal">The unit to test.</param> + /// <param name="diagnosticsSink">The diagnostics sink if provided.</param> /// <returns>A test result.</returns> - public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal); + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null); /// <summary> /// Gets a configuration unit settings. /// </summary> /// <param name="unitInternal">The unit to get.</param> + /// <param name="diagnosticsSink">The diagnostics sink if provided.</param> /// <returns>A get result.</returns> - public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal); + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null); /// <summary> /// Sets a configuration unit settings. /// </summary> /// <param name="unitInternal">The unit to set.</param> + /// <param name="diagnosticsSink">The diagnostics sink if provided.</param> /// <returns>A set result.</returns> - public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal); + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null); } } 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 @@ -23,6 +23,7 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 internal class DSCv3 : IDSCv3 { private const string PlainTextTraces = "-t plaintext"; + private const string DiagnosticTraceLevelArguments = "-l trace"; private const string ResourceCommand = "resource"; private const string ListCommand = "list"; private const string TestCommand = "test"; @@ -43,16 +44,24 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 this.processorSettings = processorSettings; } + private string DiagnosticTraceLevel + { + get + { + return this.processorSettings.DiagnosticTraceLevel ? DiagnosticTraceLevelArguments : string.Empty; + } + } + /// <inheritdoc /> - public IResourceListItem? GetResourceByType(string resourceType) + public IResourceListItem? GetResourceByType(string resourceType, IDiagnosticsSink? diagnosticsSink = null) { ProcessExecution processExecution = new ProcessExecution() { ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, - Arguments = new[] { PlainTextTraces, ResourceCommand, ListCommand, resourceType }, + Arguments = new[] { PlainTextTraces, this.DiagnosticTraceLevel, ResourceCommand, ListCommand, resourceType }, }; - RunSynchronously(processExecution); + RunSynchronously(processExecution, diagnosticsSink); if (processExecution.Output.Count > 1) { @@ -63,16 +72,16 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 } /// <inheritdoc /> - public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal) + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { ProcessExecution processExecution = new ProcessExecution() { ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, - Arguments = new[] { PlainTextTraces, ResourceCommand, TestCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Arguments = new[] { PlainTextTraces, this.DiagnosticTraceLevel, ResourceCommand, TestCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), }; - if (RunSynchronously(processExecution)) + if (RunSynchronously(processExecution, diagnosticsSink)) { throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Test, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); } @@ -81,16 +90,16 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 } /// <inheritdoc /> - public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal) + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { ProcessExecution processExecution = new ProcessExecution() { ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, - Arguments = new[] { PlainTextTraces, ResourceCommand, GetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Arguments = new[] { PlainTextTraces, this.DiagnosticTraceLevel, ResourceCommand, GetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), }; - if (RunSynchronously(processExecution)) + if (RunSynchronously(processExecution, diagnosticsSink)) { throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Get, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); } @@ -99,16 +108,16 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 } /// <inheritdoc /> - public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal) + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { ProcessExecution processExecution = new ProcessExecution() { ExecutablePath = this.processorSettings.EffectiveDscExecutablePath, - Arguments = new[] { PlainTextTraces, ResourceCommand, SetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, + Arguments = new[] { PlainTextTraces, this.DiagnosticTraceLevel, ResourceCommand, SetCommand, ResourceParameter, unitInternal.QualifiedName, FileParameter, StdInputIdentifier }, Input = ConvertValueSetToJSON(unitInternal.GetExpandedSettings()), }; - if (RunSynchronously(processExecution)) + if (RunSynchronously(processExecution, diagnosticsSink)) { throw new Exceptions.InvokeDscResourceException(Exceptions.InvokeDscResourceException.Set, unitInternal.QualifiedName, null, processExecution.GetAllErrorLines()); } @@ -120,11 +129,16 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Schema_2024_04 /// Runs the process, waiting until it completes. /// </summary> /// <param name="processExecution">The process to run.</param> + /// <param name="diagnosticsSink">The diagnostics sink.</param> /// <returns>True if the exit code was not 0.</returns> - private static bool RunSynchronously(ProcessExecution processExecution) + private static bool RunSynchronously(ProcessExecution processExecution, IDiagnosticsSink? diagnosticsSink) { + diagnosticsSink?.OnDiagnostics(DiagnosticLevel.Verbose, $"Starting process: {processExecution.CommandLine}"); + processExecution.Start().WaitForExit(); + diagnosticsSink?.OnDiagnostics(DiagnosticLevel.Verbose, $"Process exited with code: {processExecution.ExitCode}\n--- Output Stream ---\n{processExecution.GetAllOutputLines()}\n--- Error Stream ---\n{processExecution.GetAllErrorLines()}"); + return processExecution.ExitCode != 0; } diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Set/DSCv3ConfigurationSetProcessor.cs @@ -45,7 +45,7 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Set throw new Exceptions.FindDscResourceNotFoundException(configurationUnitInternal.QualifiedName, null); } - return new DSCv3ConfigurationUnitProcessor(this.processorSettings, configurationUnitInternal, this.IsLimitMode); + return new DSCv3ConfigurationUnitProcessor(this.processorSettings, configurationUnitInternal, this.IsLimitMode) { SetProcessorFactory = this.SetProcessorFactory }; } /// <inheritdoc /> diff --git a/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs b/src/Microsoft.Management.Configuration.Processor/DSCv3/Unit/DSCv3ConfigurationUnitProcessor.cs @@ -15,7 +15,7 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Unit /// <summary> /// Provides access to a specific configuration unit within the runtime. /// </summary> - internal sealed partial class DSCv3ConfigurationUnitProcessor : ConfigurationUnitProcessorBase, IConfigurationUnitProcessor + internal sealed partial class DSCv3ConfigurationUnitProcessor : ConfigurationUnitProcessorBase, IConfigurationUnitProcessor, IDiagnosticsSink { private readonly ProcessorSettings processorSettings; @@ -32,21 +32,27 @@ namespace Microsoft.Management.Configuration.Processor.DSCv3.Unit } /// <inheritdoc /> + void IDiagnosticsSink.OnDiagnostics(DiagnosticLevel level, string message) + { + this.OnDiagnostics(level, message); + } + + /// <inheritdoc /> protected override ValueSet GetSettingsInternal() { - return this.processorSettings.DSCv3.GetResourceSettings(this.UnitInternal).Settings; + return this.processorSettings.DSCv3.GetResourceSettings(this.UnitInternal, this).Settings; } /// <inheritdoc /> protected override bool TestSettingsInternal() { - return this.processorSettings.DSCv3.TestResource(this.UnitInternal).InDesiredState; + return this.processorSettings.DSCv3.TestResource(this.UnitInternal, this).InDesiredState; } /// <inheritdoc /> protected override bool ApplySettingsInternal() { - return this.processorSettings.DSCv3.SetResourceSettings(this.UnitInternal).RebootRequired; + return this.processorSettings.DSCv3.SetResourceSettings(this.UnitInternal, this).RebootRequired; } } } diff --git a/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs b/src/Microsoft.Management.Configuration.Processor/Public/DSCv3ConfigurationSetProcessorFactory.cs @@ -22,6 +22,7 @@ namespace Microsoft.Management.Configuration.Processor { private const string DscExecutablePathPropertyName = "DscExecutablePath"; private const string FoundDscExecutablePathPropertyName = "FoundDscExecutablePath"; + private const string DiagnosticTraceLevelPropertyName = "DiagnosticTraceLevel"; private ProcessorSettings processorSettings = new (); @@ -154,6 +155,9 @@ namespace Microsoft.Management.Configuration.Processor case FoundDscExecutablePathPropertyName: value = ProcessorSettings.FindDscExecutablePath() !; return true; + case DiagnosticTraceLevelPropertyName: + value = this.processorSettings.DiagnosticTraceLevel.ToString(); + return true; } return false; @@ -170,7 +174,7 @@ namespace Microsoft.Management.Configuration.Processor { ProcessorSettings processorSettingsCopy = this.processorSettings.Clone(); this.OnDiagnostics(DiagnosticLevel.Verbose, "Creating set processor with settings:\n" + processorSettingsCopy.ToString()); - return new DSCv3ConfigurationSetProcessor(processorSettingsCopy, set, isLimitMode); + return new DSCv3ConfigurationSetProcessor(processorSettingsCopy, set, isLimitMode) { SetProcessorFactory = this }; } private string GetValue(string name) @@ -190,6 +194,9 @@ namespace Microsoft.Management.Configuration.Processor case DscExecutablePathPropertyName: this.DscExecutablePath = value; break; + case DiagnosticTraceLevelPropertyName: + this.processorSettings.DiagnosticTraceLevel = bool.Parse(value); + break; default: throw new ArgumentOutOfRangeException($"Invalid property name: {name}"); } diff --git a/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs b/src/Microsoft.Management.Configuration.Processor/Unit/ConfigurationUnitProcessorBase.cs @@ -160,6 +160,16 @@ namespace Microsoft.Management.Configuration.Processor.Unit /// <returns>A boolean indicating whether a reboot is required.</returns> protected abstract bool ApplySettingsInternal(); + /// <summary> + /// Sends diagnostics if appropriate. + /// </summary> + /// <param name="level">The level of this diagnostic message.</param> + /// <param name="message">The diagnostic message.</param> + protected void OnDiagnostics(DiagnosticLevel level, string message) + { + this.SetProcessorFactory?.OnDiagnostics(level, message); + } + private void ExtractExceptionInformation(Exception e, ConfigurationUnitResultInformation resultInformation) { this.OnDiagnostics(DiagnosticLevel.Verbose, e.ToString()); @@ -223,10 +233,5 @@ namespace Microsoft.Management.Configuration.Processor.Unit // Get is always allowed now. } - - private void OnDiagnostics(DiagnosticLevel level, string message) - { - this.SetProcessorFactory?.OnDiagnostics(level, message); - } } } diff --git a/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestDSCv3.cs b/src/Microsoft.Management.Configuration.UnitTests/Helpers/TestDSCv3.cs @@ -6,6 +6,7 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers { + using Microsoft.Management.Configuration.Processor.DSCv3.Helpers; using Microsoft.Management.Configuration.Processor.DSCv3.Model; using Microsoft.Management.Configuration.Processor.Helpers; @@ -83,25 +84,25 @@ namespace Microsoft.Management.Configuration.UnitTests.Helpers public TestResourceDelegateType? TestResourceDelegate { get; set; } /// <inheritdoc/> - public IResourceListItem? GetResourceByType(string resourceType) + public IResourceListItem? GetResourceByType(string resourceType, IDiagnosticsSink? diagnosticsSink = null) { return this.GetResourceByTypeResult ?? this.GetResourceByTypeDelegate?.Invoke(resourceType); } /// <inheritdoc/> - public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal) + public IResourceGetItem GetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { return this.GetResourceSettingsResult ?? this.GetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); } /// <inheritdoc/> - public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal) + public IResourceSetItem SetResourceSettings(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { return this.SetResourceSettingsResult ?? this.SetResourceSettingsDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); } /// <inheritdoc/> - public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal) + public IResourceTestItem TestResource(ConfigurationUnitInternal unitInternal, IDiagnosticsSink? diagnosticsSink = null) { return this.TestResourceResult ?? this.TestResourceDelegate?.Invoke(unitInternal) ?? throw new System.NotImplementedException(); }