diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 16466a04fb..f4092ee85a 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -74,7 +74,8 @@ CODEOWNERS COINIT COMGLB commandline -compressapi +compressapi +concurrencysal contactsupport contentfiles contoso diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index d19b4af75e..aafca0d9bf 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -355,6 +355,7 @@ + @@ -432,6 +433,7 @@ + @@ -551,4 +553,4 @@ - + \ No newline at end of file diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index a6a7ab1e4b..91c66b0a16 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -254,6 +254,9 @@ Commands + + Commands + @@ -478,6 +481,9 @@ Commands + + Commands + diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index bb05b1c517..7c26b20cb2 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -200,7 +200,7 @@ namespace AppInstaller::CLI // Configuration commands case Execution::Args::Type::ConfigurationFile: - return { type, "file"_liv, 'f' }; + return { type, "file"_liv, 'f', ArgTypeCategory::ConfigurationSetChoice, ArgTypeExclusiveSet::ConfigurationSetChoice }; case Execution::Args::Type::ConfigurationAcceptWarning: return { type, "accept-configuration-agreements"_liv }; case Execution::Args::Type::ConfigurationEnable: @@ -215,6 +215,10 @@ namespace AppInstaller::CLI return { type, "module"_liv }; case Execution::Args::Type::ConfigurationExportResource: return { type, "resource"_liv }; + case Execution::Args::Type::ConfigurationHistoryItem: + return { type, "history"_liv, 'h', ArgTypeCategory::ConfigurationSetChoice, ArgTypeExclusiveSet::ConfigurationSetChoice }; + case Execution::Args::Type::ConfigurationHistoryRemove: + return { type, "remove"_liv }; // Download command case Execution::Args::Type::DownloadDirectory: diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h index c203e4276f..bb70c22ad2 100644 --- a/src/AppInstallerCLICore/Argument.h +++ b/src/AppInstallerCLICore/Argument.h @@ -71,6 +71,8 @@ namespace AppInstaller::CLI // E.g.: --dependency-source // E.g.: --accept-source-agreements ExtendedSource = 0x400, + // Arguments for selecting a configuration set (file or history). + ConfigurationSetChoice = 0x800, }; DEFINE_ENUM_FLAG_OPERATORS(ArgTypeCategory); @@ -87,6 +89,7 @@ namespace AppInstaller::CLI StubType = 0x10, Proxy = 0x20, AllAndTargetVersion = 0x40, + ConfigurationSetChoice = 0x80, // This must always be at the end Max diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp index 93b83cbc8e..6388aefa32 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "ConfigureCommand.h" +#include "ConfigureListCommand.h" #include "ConfigureShowCommand.h" #include "ConfigureTestCommand.h" #include "ConfigureValidateCommand.h" @@ -24,6 +25,7 @@ namespace AppInstaller::CLI { return InitializeFromMoveOnly>>({ std::make_unique(FullName()), + std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), @@ -35,6 +37,7 @@ namespace AppInstaller::CLI return { Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, Argument{ Execution::Args::Type::ConfigurationEnable, Resource::String::ConfigurationEnableMessage, ArgumentType::Flag, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationDisable, Resource::String::ConfigurationDisableMessage, ArgumentType::Flag, Argument::Visibility::Help }, @@ -94,12 +97,15 @@ namespace AppInstaller::CLI } else { - if (!execArgs.Contains(Execution::Args::Type::ConfigurationFile)) - { - throw CommandException(Resource::String::RequiredArgError("file"_liv)); - } + Configuration::ValidateCommonArguments(execArgs, true); + } + } - Configuration::ValidateCommonArguments(execArgs); + void ConfigureCommand::Complete(Execution::Context& context, Execution::Args::Type argType) const + { + if (argType == Execution::Args::Type::ConfigurationHistoryItem) + { + context << CompleteConfigurationHistoryItem; } } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureCommand.h b/src/AppInstallerCLICore/Commands/ConfigureCommand.h index 8586a77018..e30ca3807b 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureCommand.h +++ b/src/AppInstallerCLICore/Commands/ConfigureCommand.h @@ -21,5 +21,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void Complete(Execution::Context& context, Execution::Args::Type argType) const override; }; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp new file mode 100644 index 0000000000..67306af606 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigureListCommand.h" +#include "Workflows/ConfigurationFlow.h" +#include "ConfigurationCommon.h" + +using namespace AppInstaller::CLI::Workflow; + +namespace AppInstaller::CLI +{ + std::vector ConfigureListCommand::GetArguments() const + { + return { + Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard }, + Argument{ Execution::Args::Type::OutputFile, Resource::String::OutputFileArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, + Argument{ Execution::Args::Type::ConfigurationHistoryRemove, Resource::String::ConfigurationHistoryRemoveArgumentDescription, ArgumentType::Flag, Argument::Visibility::Help }, + }; + } + + Resource::LocString ConfigureListCommand::ShortDescription() const + { + return { Resource::String::ConfigureListCommandShortDescription }; + } + + Resource::LocString ConfigureListCommand::LongDescription() const + { + return { Resource::String::ConfigureListCommandLongDescription }; + } + + Utility::LocIndView ConfigureListCommand::HelpLink() const + { + return "https://aka.ms/winget-command-configure#list"_liv; + } + + void ConfigureListCommand::ExecuteInternal(Execution::Context& context) const + { + context << + VerifyIsFullPackage << + CreateConfigurationProcessorWithoutFactory << + GetConfigurationSetHistory; + + if (context.Args.Contains(Execution::Args::Type::ConfigurationHistoryItem)) + { + context << SelectSetFromHistory; + + if (context.Args.Contains(Execution::Args::Type::OutputFile)) + { + context << SerializeConfigurationSetHistory; + } + + if (context.Args.Contains(Execution::Args::Type::ConfigurationHistoryRemove)) + { + context << RemoveConfigurationSetHistory; + } + else + { + context << ShowSingleConfigurationSetHistory; + } + } + else + { + context << ShowConfigurationSetHistory; + } + } + + void ConfigureListCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + if ((execArgs.Contains(Execution::Args::Type::ConfigurationHistoryRemove) || + execArgs.Contains(Execution::Args::Type::OutputFile)) && + !execArgs.Contains(Execution::Args::Type::ConfigurationHistoryItem)) + { + throw CommandException(Resource::String::RequiredArgError(ArgumentCommon::ForType(Execution::Args::Type::ConfigurationHistoryItem).Name)); + } + } + + void ConfigureListCommand::Complete(Execution::Context& context, Execution::Args::Type argType) const + { + if (argType == Execution::Args::Type::ConfigurationHistoryItem) + { + context << CompleteConfigurationHistoryItem; + } + } +} diff --git a/src/AppInstallerCLICore/Commands/ConfigureListCommand.h b/src/AppInstallerCLICore/Commands/ConfigureListCommand.h new file mode 100644 index 0000000000..0c1653f8c4 --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ConfigureListCommand.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct ConfigureListCommand final : public Command + { + ConfigureListCommand(std::string_view parent) : Command("list", { "ls" }, parent) {} + + std::vector GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void Complete(Execution::Context& context, Execution::Args::Type argType) const override; + }; +} diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp index 3c25c0f4da..81a3c3732b 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp @@ -13,8 +13,9 @@ namespace AppInstaller::CLI { return { // Required for now, make exclusive when history implemented - Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, }; } @@ -45,6 +46,14 @@ namespace AppInstaller::CLI void ConfigureShowCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - Configuration::ValidateCommonArguments(execArgs); + Configuration::ValidateCommonArguments(execArgs, true); + } + + void ConfigureShowCommand::Complete(Execution::Context& context, Execution::Args::Type argType) const + { + if (argType == Execution::Args::Type::ConfigurationHistoryItem) + { + context << CompleteConfigurationHistoryItem; + } } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h index bddaecd0e0..b85870be28 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h +++ b/src/AppInstallerCLICore/Commands/ConfigureShowCommand.h @@ -19,5 +19,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void Complete(Execution::Context& context, Execution::Args::Type argType) const override; }; } diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp index 8ced8e83e9..bf0af5acfa 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp +++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp @@ -12,8 +12,9 @@ namespace AppInstaller::CLI std::vector ConfigureTestCommand::GetArguments() const { return { - Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional, true }, + Argument{ Execution::Args::Type::ConfigurationFile, Resource::String::ConfigurationFileArgumentDescription, ArgumentType::Positional }, Argument{ Execution::Args::Type::ConfigurationModulePath, Resource::String::ConfigurationModulePath, ArgumentType::Positional }, + Argument{ Execution::Args::Type::ConfigurationHistoryItem, Resource::String::ConfigurationHistoryItemArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help }, Argument{ Execution::Args::Type::ConfigurationAcceptWarning, Resource::String::ConfigurationAcceptWarningArgumentDescription, ArgumentType::Flag }, }; } @@ -48,6 +49,14 @@ namespace AppInstaller::CLI void ConfigureTestCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - Configuration::ValidateCommonArguments(execArgs); + Configuration::ValidateCommonArguments(execArgs, true); + } + + void ConfigureTestCommand::Complete(Execution::Context& context, Execution::Args::Type argType) const + { + if (argType == Execution::Args::Type::ConfigurationHistoryItem) + { + context << CompleteConfigurationHistoryItem; + } } } diff --git a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h index 2dae184f24..1c866bc7a6 100644 --- a/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h +++ b/src/AppInstallerCLICore/Commands/ConfigureTestCommand.h @@ -19,5 +19,6 @@ namespace AppInstaller::CLI protected: void ExecuteInternal(Execution::Context& context) const override; void ValidateArgumentsInternal(Execution::Args& execArgs) const override; + void Complete(Execution::Context& context, Execution::Args::Type argType) const override; }; } diff --git a/src/AppInstallerCLICore/ConfigurationCommon.cpp b/src/AppInstallerCLICore/ConfigurationCommon.cpp index 99862f5abb..c0ba7ef164 100644 --- a/src/AppInstallerCLICore/ConfigurationCommon.cpp +++ b/src/AppInstallerCLICore/ConfigurationCommon.cpp @@ -54,7 +54,7 @@ namespace AppInstaller::CLI namespace Configuration { - void ValidateCommonArguments(Execution::Args& execArgs) + void ValidateCommonArguments(Execution::Args& execArgs, bool requireConfigurationSetChoice) { auto modulePath = GetModulePathInfo(execArgs); @@ -71,6 +71,12 @@ namespace AppInstaller::CLI throw CommandException(Resource::String::ConfigurationModulePathArgError); } } + + if (requireConfigurationSetChoice && + !WI_IsFlagSet(Argument::GetCategoriesPresent(execArgs), ArgTypeCategory::ConfigurationSetChoice)) + { + throw CommandException(Resource::String::RequiredArgError("file"_liv)); + } } void SetModulePath(Execution::Context& context, IConfigurationSetProcessorFactory const& factory) diff --git a/src/AppInstallerCLICore/ConfigurationCommon.h b/src/AppInstallerCLICore/ConfigurationCommon.h index 279d391609..b63f87b25a 100644 --- a/src/AppInstallerCLICore/ConfigurationCommon.h +++ b/src/AppInstallerCLICore/ConfigurationCommon.h @@ -9,7 +9,7 @@ namespace AppInstaller::CLI namespace Configuration { // Validates common arguments between configuration commands. - void ValidateCommonArguments(Execution::Args& execArgs); + void ValidateCommonArguments(Execution::Args& execArgs, bool requireConfigurationSetChoice = false); // Sets the module path to install modules in the set processor. void SetModulePath(Execution::Context& context, winrt::Microsoft::Management::Configuration::IConfigurationSetProcessorFactory const& factory); diff --git a/src/AppInstallerCLICore/ConfigurationContext.cpp b/src/AppInstallerCLICore/ConfigurationContext.cpp index 69d69afa9a..f16f351ff4 100644 --- a/src/AppInstallerCLICore/ConfigurationContext.cpp +++ b/src/AppInstallerCLICore/ConfigurationContext.cpp @@ -14,6 +14,7 @@ namespace AppInstaller::CLI::Execution { ConfigurationProcessor Processor = nullptr; ConfigurationSet Set = nullptr; + std::vector History; }; } @@ -65,4 +66,21 @@ namespace AppInstaller::CLI::Execution { m_data->Set = std::move(value); } + + std::vector& ConfigurationContext::History() + { + return m_data->History; + } + + const std::vector& ConfigurationContext::History() const + { + return m_data->History; + } + + void ConfigurationContext::History(const winrt::Windows::Foundation::Collections::IVector& value) + { + std::vector history{ value.Size() }; + value.GetMany(0, history); + m_data->History = std::move(history); + } } diff --git a/src/AppInstallerCLICore/ConfigurationContext.h b/src/AppInstallerCLICore/ConfigurationContext.h index d542afca1b..c1cfb83b68 100644 --- a/src/AppInstallerCLICore/ConfigurationContext.h +++ b/src/AppInstallerCLICore/ConfigurationContext.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include +#include namespace winrt::Microsoft::Management::Configuration { @@ -18,6 +19,8 @@ namespace AppInstaller::CLI::Execution struct ConfigurationContext { + using ConfigurationSet = winrt::Microsoft::Management::Configuration::ConfigurationSet; + ConfigurationContext(); ~ConfigurationContext(); @@ -32,10 +35,14 @@ namespace AppInstaller::CLI::Execution void Processor(const winrt::Microsoft::Management::Configuration::ConfigurationProcessor& value); void Processor(winrt::Microsoft::Management::Configuration::ConfigurationProcessor&& value); - winrt::Microsoft::Management::Configuration::ConfigurationSet& Set(); - const winrt::Microsoft::Management::Configuration::ConfigurationSet& Set() const; - void Set(const winrt::Microsoft::Management::Configuration::ConfigurationSet& value); - void Set(winrt::Microsoft::Management::Configuration::ConfigurationSet&& value); + ConfigurationSet& Set(); + const ConfigurationSet& Set() const; + void Set(const ConfigurationSet& value); + void Set(ConfigurationSet&& value); + + std::vector& History(); + const std::vector& History() const; + void History(const winrt::Windows::Foundation::Collections::IVector& value); private: std::unique_ptr m_data; diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 12d57450e6..517ce35a35 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -130,6 +130,8 @@ namespace AppInstaller::CLI::Execution ConfigurationExportPackageId, ConfigurationExportModule, ConfigurationExportResource, + ConfigurationHistoryItem, + ConfigurationHistoryRemove, // Common arguments NoVT, // Disable VirtualTerminal outputs diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index 5d5268ef07..ea46f3c28d 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -83,6 +83,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationFileVersionUnknown); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingDetails); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationGettingResourceSettings); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryEmpty); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryItemArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryItemNotFound); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationHistoryRemoveArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInDesiredState); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInform); WINGET_DEFINE_RESOURCE_STRINGID(ConfigurationInitializing); @@ -144,6 +148,13 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportResource); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureExportUnitInstallDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListFirstApplied); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListIdentifier); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListName); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListOrigin); + WINGET_DEFINE_RESOURCE_STRINGID(ConfigureListPath); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureShowCommandShortDescription); WINGET_DEFINE_RESOURCE_STRINGID(ConfigureTestCommandLongDescription); diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp index 136a9920b9..dc9eaddadd 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp @@ -3,9 +3,11 @@ #include "pch.h" #include "ConfigurationFlow.h" #include "PromptFlow.h" +#include "TableOutput.h" #include "Public/ConfigurationSetProcessorFactoryRemoting.h" #include "ConfigurationCommon.h" #include "ConfigurationWingetDscModuleUnitValidation.h" +#include #include #include #include @@ -114,6 +116,28 @@ namespace AppInstaller::CLI::Workflow return factory; } + void ConfigureProcessorForUse(Execution::Context& context, ConfigurationProcessor&& processor) + { + // Set the processor to the current level of the logging. + processor.MinimumLevel(anon::ConvertLevel(Logging::Log().GetLevel())); + processor.Caller(L"winget"); + // Use same activity as the overall winget command + processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); + // Apply winget telemetry setting to configuration + processor.GenerateTelemetryEvents(!Settings::User().Get()); + + // Route the configuration diagnostics into the context's diagnostics logging + processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const IDiagnosticInformation& diagnostics) + { + context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); + }); + + ConfigurationContext configurationContext; + configurationContext.Processor(std::move(processor)); + + context.Add(std::move(configurationContext)); + } + winrt::hstring GetValueSetString(const ValueSet& valueSet, std::wstring_view value) { if (valueSet.HasKey(value)) @@ -1174,6 +1198,35 @@ namespace AppInstaller::CLI::Workflow return {}; } + + bool HistorySetMatchesInput(const ConfigurationSet& set, const std::string& foldedInput) + { + if (foldedInput.empty()) + { + return false; + } + + if (Utility::FoldCase(Utility::NormalizedString{ set.Name() }) == foldedInput) + { + return true; + } + + std::ostringstream identifierStream; + identifierStream << set.InstanceIdentifier(); + std::string identifier = identifierStream.str(); + THROW_HR_IF(E_UNEXPECTED, identifier.empty()); + + std::size_t startPosition = 0; + if (identifier[0] == '{' && foldedInput[0] != '{') + { + startPosition = 1; + } + + std::string_view identifierView = identifier; + identifierView = identifierView.substr(startPosition); + + return Utility::CaseInsensitiveStartsWith(identifierView, foldedInput); + } } void CreateConfigurationProcessor(Context& context) @@ -1181,32 +1234,29 @@ namespace AppInstaller::CLI::Workflow auto progressScope = context.Reporter.BeginAsyncProgress(true); progressScope->Callback().SetProgressMessage(Resource::String::ConfigurationInitializing()); - ConfigurationProcessor processor{ anon::CreateConfigurationSetProcessorFactory(context)}; - - // Set the processor to the current level of the logging. - processor.MinimumLevel(anon::ConvertLevel(Logging::Log().GetLevel())); - processor.Caller(L"winget"); - // Use same activity as the overall winget command - processor.ActivityIdentifier(*Logging::Telemetry().GetActivityId()); - // Apply winget telemetry setting to configuration - processor.GenerateTelemetryEvents(!Settings::User().Get()); - - // Route the configuration diagnostics into the context's diagnostics logging - processor.Diagnostics([&context](const winrt::Windows::Foundation::IInspectable&, const IDiagnosticInformation& diagnostics) - { - context.GetThreadGlobals().GetDiagnosticLogger().Write(Logging::Channel::Config, anon::ConvertLevel(diagnostics.Level()), Utility::ConvertToUTF8(diagnostics.Message())); - }); - - ConfigurationContext configurationContext; - configurationContext.Processor(std::move(processor)); + anon::ConfigureProcessorForUse(context, ConfigurationProcessor{ anon::CreateConfigurationSetProcessorFactory(context) }); + } - context.Add(std::move(configurationContext)); + void CreateConfigurationProcessorWithoutFactory(Execution::Context& context) + { + anon::ConfigureProcessorForUse(context, ConfigurationProcessor{ IConfigurationSetProcessorFactory{ nullptr } }); } void OpenConfigurationSet(Context& context) { - std::string argPath{ context.Args.GetArg(Args::Type::ConfigurationFile) }; - anon::OpenConfigurationSet(context, argPath, true); + if (context.Args.Contains(Args::Type::ConfigurationFile)) + { + std::string argPath{ context.Args.GetArg(Args::Type::ConfigurationFile) }; + anon::OpenConfigurationSet(context, argPath, true); + } + else + { + THROW_HR_IF(E_UNEXPECTED, !context.Args.Contains(Args::Type::ConfigurationHistoryItem)); + + context << + GetConfigurationSetHistory << + SelectSetFromHistory; + } } void CreateOrOpenConfigurationSet(Context& context) @@ -1754,4 +1804,135 @@ namespace AppInstaller::CLI::Workflow throw; } } + + void GetConfigurationSetHistory(Execution::Context& context) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + + ConfigurationContext& configContext = context.Get(); + configContext.History(configContext.Processor().GetConfigurationHistory()); + } + + void ShowConfigurationSetHistory(Execution::Context& context) + { + const auto& history = context.Get().History(); + + if (history.empty()) + { + context.Reporter.Info() << Resource::String::ConfigurationHistoryEmpty << std::endl; + } + else + { + TableOutput<4> historyTable{ context.Reporter, { Resource::String::ConfigureListIdentifier, Resource::String::ConfigureListName, Resource::String::ConfigureListFirstApplied, Resource::String::ConfigureListOrigin } }; + + for (const auto& set : history) + { + std::ostringstream stream; + Utility::OutputTimePoint(stream, winrt::clock::to_sys(set.FirstApply())); + + winrt::hstring origin = set.Path(); + if (origin.empty()) + { + origin = set.Origin(); + } + + historyTable.OutputLine({ Utility::ConvertGuidToString(set.InstanceIdentifier()), Utility::ConvertToUTF8(set.Name()), std::move(stream).str(), Utility::ConvertToUTF8(origin)}); + } + + historyTable.Complete(); + } + } + + void SelectSetFromHistory(Execution::Context& context) + { + ConfigurationContext& configContext = context.Get(); + ConfigurationSet selectedSet{ nullptr }; + + std::string foldedInput = Utility::FoldCase(context.Args.GetArg(Execution::Args::Type::ConfigurationHistoryItem)); + + for (const ConfigurationSet& historySet : configContext.History()) + { + if (anon::HistorySetMatchesInput(historySet, foldedInput)) + { + if (selectedSet) + { + selectedSet = nullptr; + break; + } + else + { + selectedSet = historySet; + } + } + } + + if (!selectedSet) + { + context.Reporter.Warn() << Resource::String::ConfigurationHistoryItemNotFound << std::endl; + context << ShowConfigurationSetHistory; + AICLI_TERMINATE_CONTEXT(WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND); + } + + configContext.Set(std::move(selectedSet)); + } + + void RemoveConfigurationSetHistory(Execution::Context& context) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + context.Get().Set().Remove(); + } + + void SerializeConfigurationSetHistory(Execution::Context& context) + { + auto progressScope = context.Reporter.BeginAsyncProgress(true); + std::filesystem::path absolutePath = std::filesystem::weakly_canonical(std::filesystem::path{ Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::OutputFile)) }); + auto openAction = Streams::FileRandomAccessStream::OpenAsync(absolutePath.wstring(), FileAccessMode::ReadWrite, StorageOpenOptions::None, Streams::FileOpenDisposition::CreateAlways); + auto cancellationScope = progressScope->Callback().SetCancellationFunction([&]() { openAction.Cancel(); }); + auto outputStream = openAction.get(); + + context.Get().Set().Serialize(outputStream); + } + + void ShowSingleConfigurationSetHistory(Execution::Context& context) + { + const auto& set = context.Get().Set(); + + std::ostringstream stream; + Utility::OutputTimePoint(stream, winrt::clock::to_sys(set.FirstApply())); + + Execution::TableOutput<2> table(context.Reporter, { Resource::String::SourceListField, Resource::String::SourceListValue }); + + table.OutputLine({ Resource::LocString{ Resource::String::ConfigureListIdentifier }, Utility::ConvertGuidToString(set.InstanceIdentifier()) }); + table.OutputLine({ Resource::LocString{ Resource::String::ConfigureListName }, Utility::ConvertToUTF8(set.Name()) }); + table.OutputLine({ Resource::LocString{ Resource::String::ConfigureListFirstApplied }, std::move(stream).str() }); + table.OutputLine({ Resource::LocString{ Resource::String::ConfigureListOrigin }, Utility::ConvertToUTF8(set.Origin()) }); + table.OutputLine({ Resource::LocString{ Resource::String::ConfigureListPath }, Utility::ConvertToUTF8(set.Path()) }); + + table.Complete(); + } + + void CompleteConfigurationHistoryItem(Execution::Context& context) + { + const std::string& word = context.Get().Word(); + auto stream = context.Reporter.Completion(); + + for (const auto& historyItem : ConfigurationProcessor{ IConfigurationSetProcessorFactory{ nullptr } }.GetConfigurationHistory()) + { + std::ostringstream identifierStream; + identifierStream << historyItem.InstanceIdentifier(); + std::string identifier = identifierStream.str(); + + if (word.empty() || Utility::CaseInsensitiveContainsSubstring(identifier, word)) + { + stream << '"' << identifier << '"' << std::endl; + } + + std::string name = Utility::ConvertToUTF8(historyItem.Name()); + + if (word.empty() || Utility::CaseInsensitiveStartsWith(name, word)) + { + stream << '"' << name << '"' << std::endl; + } + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h index 274cb3fa31..1064276c10 100644 --- a/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h +++ b/src/AppInstallerCLICore/Workflows/ConfigurationFlow.h @@ -5,12 +5,18 @@ namespace AppInstaller::CLI::Workflow { - // Composite flow that chooses what to do based on the installer type. + // Creates a configuration processor with a processor factory for full functionality. // Required Args: None // Inputs: None // Outputs: ConfigurationProcessor void CreateConfigurationProcessor(Execution::Context& context); + // Creates a configuration processor without a processor factory for reduced functionality. + // Required Args: None + // Inputs: None + // Outputs: ConfigurationProcessor + void CreateConfigurationProcessorWithoutFactory(Execution::Context& context); + // Opens the configuration set. // Required Args: ConfigurationFile // Inputs: ConfigurationProcessor @@ -102,4 +108,46 @@ namespace AppInstaller::CLI::Workflow // Inputs: ConfigurationProcessor, ConfigurationSet // Outputs: None void WriteConfigFile(Execution::Context& context); + + // Gets the configuration set history. + // Required Args: None + // Inputs: ConfigurationProcessor + // Outputs: ConfigurationSetHistory + void GetConfigurationSetHistory(Execution::Context& context); + + // Outputs the configuration set history. + // Required Args: None + // Inputs: ConfigurationSetHistory + // Outputs: None + void ShowConfigurationSetHistory(Execution::Context& context); + + // Selects a specific configuration set history item. + // Required Args: ConfigurationHistoryItem + // Inputs: ConfigurationSetHistory + // Outputs: ConfigurationSet + void SelectSetFromHistory(Execution::Context& context); + + // Removes the configuration set from history. + // Required Args: None + // Inputs: ConfigurationSet + // Outputs: None + void RemoveConfigurationSetHistory(Execution::Context& context); + + // Write the configuration set history item to a file. + // Required Args: OutputFile + // Inputs: ConfigurationSet + // Outputs: None + void SerializeConfigurationSetHistory(Execution::Context& context); + + // Outputs a single configuration set (from history). + // Required Args: None + // Inputs: ConfigurationSet + // Outputs: None + void ShowSingleConfigurationSetHistory(Execution::Context& context); + + // Completes the configuration history item. + // Required Args: None + // Inputs: None + // Outputs: None + void CompleteConfigurationHistoryItem(Execution::Context& context); } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp index b946af471d..752862de52 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -1172,6 +1172,12 @@ namespace AppInstaller::CLI::Workflow void VerifyFileOrUri::operator()(Execution::Context& context) const { + // Argument requirement is handled elsewhere. + if (!context.Args.Contains(m_arg)) + { + return; + } + auto path = context.Args.GetArg(m_arg); // try uri first diff --git a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs index 8e1b7fdb0d..b530739fc1 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -63,7 +63,7 @@ public void ConfigureFromTestRepo() // The configuration creates a file next to itself with the given contents string targetFilePath = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.txt"); FileAssert.Exists(targetFilePath); - Assert.AreEqual("Contents!", System.IO.File.ReadAllText(targetFilePath)); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); Assert.True(Directory.Exists( Path.Combine( @@ -122,7 +122,7 @@ public void IndependentResourceWithSingleFailure() // The configuration creates a file next to itself with the given contents string targetFilePath = TestCommon.GetTestDataFile("Configuration\\IndependentResources_OneFailure.txt"); FileAssert.Exists(targetFilePath); - Assert.AreEqual("Contents!", System.IO.File.ReadAllText(targetFilePath)); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); } /// @@ -167,7 +167,7 @@ public void ResourceCaseInsensitive() // The configuration creates a file next to itself with the given contents string targetFilePath = TestCommon.GetTestDataFile("Configuration\\ResourceCaseInsensitive.txt"); FileAssert.Exists(targetFilePath); - Assert.AreEqual("Contents!", System.IO.File.ReadAllText(targetFilePath)); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); } /// @@ -182,6 +182,30 @@ public void ConfigureFromHttpsConfigurationFile() Assert.AreEqual(0, result.ExitCode); } + /// + /// Runs a configuration, then changes the state and runs it again from history. + /// + [Test] + public void ConfigureFromHistory() + { + var result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + // The configuration creates a file next to itself with the given contents + string targetFilePath = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.txt"); + FileAssert.Exists(targetFilePath); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); + + File.WriteAllText(targetFilePath, "Changed contents!"); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor("Configure_TestRepo.yml"); + result = TestCommon.RunAICLICommand(CommandAndAgreementsAndVerbose, $"-h {guid}"); + Assert.AreEqual(0, result.ExitCode); + + FileAssert.Exists(targetFilePath); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); + } + private void DeleteTxtFiles() { // Delete all .txt files in the test directory; they are placed there by the tests diff --git a/src/AppInstallerCLIE2ETests/ConfigureListCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureListCommand.cs new file mode 100644 index 0000000000..8effa9ac84 --- /dev/null +++ b/src/AppInstallerCLIE2ETests/ConfigureListCommand.cs @@ -0,0 +1,105 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// + /// `Configure list` command tests. + /// + public class ConfigureListCommand + { + private const string ConfigureWithAgreementsAndVerbose = "configure --accept-configuration-agreements --verbose"; + private const string ConfigureTestRepoFile = "Configure_TestRepo.yml"; + + /// + /// Teardown done once after all the tests here. + /// + [OneTimeTearDown] + public void OneTimeTeardown() + { + this.DeleteTxtFiles(); + } + + /// + /// Applies a configuration, then verifies that it is in the overall list. + /// + [Test] + public void ListAllConfigurations() + { + var result = TestCommon.RunAICLICommand(ConfigureWithAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + result = TestCommon.RunAICLICommand("configure list", "--verbose"); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains(ConfigureTestRepoFile)); + } + + /// + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the details about the first configuration. + /// + [Test] + public void ListSpecificConfiguration() + { + var result = TestCommon.RunAICLICommand(ConfigureWithAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor(ConfigureTestRepoFile); + result = TestCommon.RunAICLICommand("configure list", $"-h {guid}"); + Assert.AreEqual(0, result.ExitCode); + Assert.True(result.StdOut.Contains(guid)); + Assert.True(result.StdOut.Contains(ConfigureTestRepoFile)); + } + + /// + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the removes the first configuration. + /// + [Test] + public void RemoveConfiguration() + { + var result = TestCommon.RunAICLICommand(ConfigureWithAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor(ConfigureTestRepoFile); + result = TestCommon.RunAICLICommand("configure list", $"-h {guid} --remove"); + Assert.AreEqual(0, result.ExitCode); + + result = TestCommon.RunAICLICommand("configure list", "--verbose"); + Assert.AreEqual(0, result.ExitCode); + Assert.False(result.StdOut.Contains(guid)); + } + + /// + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the outputs the first configuration. + /// + [Test] + public void OutputConfiguration() + { + var result = TestCommon.RunAICLICommand(ConfigureWithAgreementsAndVerbose, TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor(ConfigureTestRepoFile); + string tempFile = TestCommon.GetRandomTestFile(".yml"); + result = TestCommon.RunAICLICommand("configure list", $"-h {guid} --output {tempFile}"); + Assert.AreEqual(0, result.ExitCode); + + result = TestCommon.RunAICLICommand("configure validate", $"--verbose {tempFile}"); + Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); + } + + private void DeleteTxtFiles() + { + // Delete all .txt files in the test directory; they are placed there by the tests + foreach (string file in Directory.GetFiles(TestCommon.GetTestDataFile("Configuration"), "*.txt")) + { + File.Delete(file); + } + } + } +} diff --git a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs index 66fdc60571..bbe4b33242 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureShowCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,8 +6,8 @@ namespace AppInstallerCLIE2ETests { + using System.IO; using AppInstallerCLIE2ETests.Helpers; - using Microsoft.VisualBasic; using NUnit.Framework; /// @@ -22,6 +22,7 @@ public class ConfigureShowCommand public void OneTimeTearDown() { WinGetSettingsHelper.ConfigureFeature("configuration03", false); + this.DeleteTxtFiles(); } /// @@ -133,5 +134,28 @@ public void ShowTruncatedDetailsAndFileContent() Assert.True(result.StdOut.Contains("Some of the data present in the configuration file was truncated for this output; inspect the file contents for the complete content.")); Assert.False(result.StdOut.Contains("Line5")); } + + /// + /// Runs a configuration, then shows it from history. + /// + [Test] + public void ShowFromHistory() + { + var result = TestCommon.RunAICLICommand("configure --accept-configuration-agreements --verbose", TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor("Configure_TestRepo.yml"); + result = TestCommon.RunAICLICommand("configure show", $"-h {guid}"); + Assert.AreEqual(0, result.ExitCode); + } + + private void DeleteTxtFiles() + { + // Delete all .txt files in the test directory; they are placed there by the tests + foreach (string file in Directory.GetFiles(TestCommon.GetTestDataFile("Configuration"), "*.txt")) + { + File.Delete(file); + } + } } } diff --git a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs index e186c71635..b5e39404c4 100644 --- a/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs +++ b/src/AppInstallerCLIE2ETests/ConfigureTestCommand.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -90,6 +90,30 @@ public void ConfigureTest_HttpsConfigurationFile() Assert.True(result.StdOut.Contains("System is in the described configuration state.")); } + /// + /// Runs a configuration, then tests it from history. + /// + [Test] + public void TestFromHistory() + { + var result = TestCommon.RunAICLICommand("configure --accept-configuration-agreements --verbose", TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.yml")); + Assert.AreEqual(0, result.ExitCode); + + // The configuration creates a file next to itself with the given contents + string targetFilePath = TestCommon.GetTestDataFile("Configuration\\Configure_TestRepo.txt"); + FileAssert.Exists(targetFilePath); + Assert.AreEqual("Contents!", File.ReadAllText(targetFilePath)); + + string guid = TestCommon.GetConfigurationInstanceIdentifierFor("Configure_TestRepo.yml"); + result = TestCommon.RunAICLICommand(CommandAndAgreements, $"-h {guid}"); + Assert.AreEqual(0, result.ExitCode); + + File.WriteAllText(targetFilePath, "Changed contents!"); + + result = TestCommon.RunAICLICommand(CommandAndAgreements, $"-h {guid}"); + Assert.AreEqual(Constants.ErrorCode.S_FALSE, result.ExitCode); + } + private void DeleteTxtFiles() { // Delete all .txt files in the test directory; they are placed there by the tests diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 9d60109edc..567e2f6953 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -978,6 +978,36 @@ public static string GetExpectedModulePath(TestModuleLocation location) } } + /// + /// Gets the instance identifier of the first configuration history item with name in its output line. + /// + /// The string to search for. + /// The instance identifier of a configuration that matched the search, or any empty string if none did. + public static string GetConfigurationInstanceIdentifierFor(string name) + { + var result = TestCommon.RunAICLICommand("configure list", string.Empty); + Assert.AreEqual(0, result.ExitCode); + + string[] lines = result.StdOut.Split('\n', StringSplitOptions.RemoveEmptyEntries); + + foreach (string line in lines) + { + if (line.Contains(name)) + { + // Find the first GUID in the output + int left = line.IndexOf('{'); + int right = line.IndexOfAny(new char[] { '}', '…' }); + Assert.AreNotEqual(-1, left); + Assert.AreNotEqual(-1, right); + Assert.LessOrEqual(right - left, 38); + + return line.Substring(left, right - left); + } + } + + return string.Empty; + } + /// /// Copy the installer file to the ARP InstallSource directory. /// @@ -1049,6 +1079,7 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command, p.StartInfo = new ProcessStartInfo(TestSetup.Parameters.AICLIPath, command + ' ' + parameters); p.StartInfo.UseShellExecute = false; + p.StartInfo.StandardOutputEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardOutput = true; StringBuilder outputData = new (); p.OutputDataReceived += (sender, args) => @@ -1059,6 +1090,7 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command, } }; + p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardError = true; StringBuilder errorData = new (); p.ErrorDataReceived += (sender, args) => @@ -1102,7 +1134,7 @@ private static RunCommandResult RunAICLICommandViaDirectProcess(string command, if (TestSetup.Parameters.VerboseLogging && !string.IsNullOrEmpty(result.StdOut)) { - TestContext.Out.WriteLine("Command run output. Output: " + result.StdOut); + TestContext.Out.WriteLine("Command run output. Output:\n" + result.StdOut); } } else if (throwOnTimeout) diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index adf6e9f359..335ecbbfa5 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -41,9 +41,12 @@ public static void InitializeWingetSettings() Hashtable experimentalFeatures = new Hashtable(); var forcedExperimentalFeatures = ForcedExperimentalFeatures; - foreach (var feature in forcedExperimentalFeatures) + if (forcedExperimentalFeatures != null) { - experimentalFeatures[feature] = true; + foreach (var feature in forcedExperimentalFeatures) + { + experimentalFeatures[feature] = true; + } } var settingsJson = new Hashtable() diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index bddbc7e078..84ed70abe1 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3007,4 +3007,42 @@ Please specify one of them using the --source option to proceed. <this value has been truncated; inspect the file contents for the complete text> Keep some form of separator like the "<>" around the text so that it stands out from the preceding text. - + + Shows the high level details for configurations that have been applied to the system. This data can then be used with `configure` commands to get more details. + {Locked="`configure`"} + + + Shows configuration history + + + There are no configurations in the history. + + + Select items from history + + + No single configuration could be found that matched the provided data. Provide either the full name or part of the identifier that unambiguously matches the desired configuration. + + + Remove the item from history + + + First Applied + Column header for date values indicating when a configuration was first applied to the system. + + + Identifier + + + Name + + + Origin + + + Path + + + The specified configuration could not be found. + + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index ae9972d71e..84a159454e 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -298,6 +298,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index f1eefe76ed..e450823d45 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -356,6 +356,9 @@ Source Files\Common + + Source Files\Repository + diff --git a/src/AppInstallerCLITests/Runtime.cpp b/src/AppInstallerCLITests/Runtime.cpp index d52addcded..e1003a22d1 100644 --- a/src/AppInstallerCLITests/Runtime.cpp +++ b/src/AppInstallerCLITests/Runtime.cpp @@ -134,9 +134,9 @@ TEST_CASE("EnsureUserProfileNotPresentInDisplayPaths", "[runtime]") std::filesystem::path userProfilePath = Filesystem::GetKnownFolderPath(FOLDERID_Profile); std::string userProfileString = userProfilePath.u8string(); - for (auto i = ToIntegral(ToEnum(0)); i < ToIntegral(PathName::Max); ++i) + for (auto i = ToIntegral(ToEnum(0)); i < ToIntegral(Runtime::PathName::Max); ++i) { - std::filesystem::path displayPath = GetPathTo(ToEnum(i), true); + std::filesystem::path displayPath = GetPathTo(ToEnum(i), true); std::string displayPathString = displayPath.u8string(); INFO(i << " = " << displayPathString); REQUIRE(displayPathString.find(userProfileString) == std::string::npos); diff --git a/src/AppInstallerCLITests/SQLiteDynamicStorage.cpp b/src/AppInstallerCLITests/SQLiteDynamicStorage.cpp new file mode 100644 index 0000000000..9f27008a7b --- /dev/null +++ b/src/AppInstallerCLITests/SQLiteDynamicStorage.cpp @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include +#include +#include +#include + +using namespace AppInstaller::SQLite; +using namespace std::string_literals; + +TEST_CASE("SQLiteDynamicStorage_UpgradeDetection", "[sqlite_dynamic]") +{ + TestCommon::TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + // Create a database with version 1.0 + SQLiteDynamicStorage storage{ tempFile.GetPath(), Version{ 1, 0 } }; + + { + auto transactionLock = storage.TryBeginTransaction("test"); + REQUIRE(transactionLock); + } + + // Update the database to version 2.0 + { + Connection connection = Connection::Create(tempFile, Connection::OpenDisposition::Create); + Version version{ 2, 0 }; + version.SetSchemaVersion(connection); + } + + REQUIRE(storage.GetVersion() == Version{ 1, 0 }); + + auto transactionLock = storage.TryBeginTransaction("test"); + REQUIRE(!transactionLock); + + REQUIRE(storage.GetVersion() == Version{ 2, 0 }); + + transactionLock = storage.TryBeginTransaction("test"); + REQUIRE(transactionLock); +} diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp index 570992ee42..0474f7eb23 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -22,8 +22,6 @@ namespace AppInstaller::Runtime { using namespace std::string_view_literals; constexpr std::string_view s_DefaultTempDirectory = "WinGet"sv; - constexpr std::string_view s_AppDataDir_Settings = "Settings"sv; - constexpr std::string_view s_AppDataDir_State = "State"sv; constexpr std::string_view s_SecureSettings_Base = "Microsoft\\WinGet"sv; constexpr std::string_view s_SecureSettings_UserRelative = "settings"sv; constexpr std::string_view s_SecureSettings_Relative_Unpackaged = "win"sv; @@ -131,31 +129,6 @@ namespace AppInstaller::Runtime } } - // Gets the path to the appdata root. - // *Only used by non packaged version!* - std::filesystem::path GetPathToAppDataRoot(bool forDisplay) - { - THROW_HR_IF(E_NOT_VALID_STATE, IsRunningInPackagedContext()); - - std::filesystem::path result = (forDisplay && Settings::User().Get()) ? s_LocalAppDataEnvironmentVariable : GetKnownFolderPath(FOLDERID_LocalAppData); - result /= "Microsoft/WinGet"; - - return result; - } - - // Gets the path to the app data relative directory. - std::filesystem::path GetPathToAppDataDir(const std::filesystem::path& relative, bool forDisplay) - { - THROW_HR_IF(E_INVALIDARG, !relative.has_relative_path()); - THROW_HR_IF(E_INVALIDARG, relative.has_root_path()); - THROW_HR_IF(E_INVALIDARG, !relative.has_filename()); - - std::filesystem::path result = GetPathToAppDataRoot(forDisplay); - result /= relative; - - return result; - } - // Gets the current user's SID for use in paths. std::filesystem::path GetUserSID() { @@ -375,6 +348,7 @@ namespace AppInstaller::Runtime PathDetails result; // We should not create directories by default when they are retrieved for display purposes. result.Create = !forDisplay; + bool anonymize = forDisplay && Settings::User().Get(); switch (path) { @@ -393,19 +367,15 @@ namespace AppInstaller::Runtime } break; case PathName::LocalState: - result.Path = GetPathToAppDataDir(s_AppDataDir_State, forDisplay); + result = Filesystem::GetPathDetailsFor(Filesystem::PathName::UnpackagedLocalStateRoot, anonymize); + result.Create = !forDisplay; result.Path /= GetRuntimePathStateName(); - result.SetOwner(ACEPrincipal::CurrentUser); - result.ACL[ACEPrincipal::System] = ACEPermissions::All; - result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; break; case PathName::StandardSettings: case PathName::UserFileSettings: - result.Path = GetPathToAppDataDir(s_AppDataDir_Settings, forDisplay); + result = Filesystem::GetPathDetailsFor(Filesystem::PathName::UnpackagedSettingsRoot, anonymize); + result.Create = !forDisplay; result.Path /= GetRuntimePathStateName(); - result.SetOwner(ACEPrincipal::CurrentUser); - result.ACL[ACEPrincipal::System] = ACEPermissions::All; - result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; break; case PathName::SecureSettingsForRead: case PathName::SecureSettingsForWrite: diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj index 25c174ef83..8cd19fa849 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj @@ -419,11 +419,13 @@ + + @@ -461,6 +463,7 @@ + diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters index 88a41d4769..8baff14af5 100644 --- a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters +++ b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -134,6 +134,12 @@ Public\winget + + Public\winget + + + Public\winget + @@ -220,6 +226,9 @@ Source Files + + SQLite + diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index 6b207e5629..0503aacade 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -268,6 +268,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_FAILED, "Some of the configuration units failed while testing their state."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_TEST_NOT_RUN, "Configuration state was not tested."), WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_GET_FAILED, "The configuration unit failed getting its properties."), + WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND, "The specified configuration could not be found."), // Configuration Processor Errors WINGET_HRESULT_INFO(WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED, "The configuration unit was not installed."), diff --git a/src/AppInstallerSharedLib/Filesystem.cpp b/src/AppInstallerSharedLib/Filesystem.cpp index d3dfbb8c57..f135c0ab60 100644 --- a/src/AppInstallerSharedLib/Filesystem.cpp +++ b/src/AppInstallerSharedLib/Filesystem.cpp @@ -14,6 +14,11 @@ namespace AppInstaller::Filesystem { namespace anon { + constexpr std::string_view s_AppDataDir_Settings = "Settings"sv; + constexpr std::string_view s_AppDataDir_State = "State"sv; + + constexpr std::string_view s_LocalAppDataEnvironmentVariable = "%LOCALAPPDATA%"; + // Contains the information about an ACE entry for a given principal. struct ACEDetails { @@ -50,6 +55,29 @@ namespace AppInstaller::Filesystem return result; } + + // Gets the path to the appdata root. + // *Only used by non packaged version!* + std::filesystem::path GetPathToAppDataRoot(bool anonymize) + { + std::filesystem::path result = anonymize ? s_LocalAppDataEnvironmentVariable : GetKnownFolderPath(FOLDERID_LocalAppData); + result /= "Microsoft/WinGet"; + + return result; + } + + // Gets the path to the app data relative directory. + std::filesystem::path GetPathToAppDataDir(const std::filesystem::path& relative, bool anonymize) + { + THROW_HR_IF(E_INVALIDARG, !relative.has_relative_path()); + THROW_HR_IF(E_INVALIDARG, relative.has_root_path()); + THROW_HR_IF(E_INVALIDARG, !relative.has_filename()); + + std::filesystem::path result = GetPathToAppDataRoot(anonymize); + result /= relative; + + return result; + } } DWORD GetVolumeInformationFlagsByHandle(HANDLE anyFileHandle) @@ -421,4 +449,31 @@ namespace AppInstaller::Filesystem return std::move(details.Path); } + + PathDetails GetPathDetailsFor(PathName path, bool forDisplay) + { + PathDetails result; + // We should not create directories by default when they are retrieved for display purposes. + result.Create = !forDisplay; + + switch (path) + { + case PathName::UnpackagedLocalStateRoot: + result.Path = anon::GetPathToAppDataDir(anon::s_AppDataDir_State, forDisplay); + result.SetOwner(ACEPrincipal::CurrentUser); + result.ACL[ACEPrincipal::System] = ACEPermissions::All; + result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; + break; + case PathName::UnpackagedSettingsRoot: + result.Path = anon::GetPathToAppDataDir(anon::s_AppDataDir_Settings, forDisplay); + result.SetOwner(ACEPrincipal::CurrentUser); + result.ACL[ACEPrincipal::System] = ACEPermissions::All; + result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; + break; + default: + THROW_HR(E_UNEXPECTED); + } + + return result; + } } diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index 47b1d59d39..33c03906cf 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -203,6 +203,7 @@ #define WINGET_CONFIG_ERROR_TEST_FAILED ((HRESULT)0x8A15C00F) #define WINGET_CONFIG_ERROR_TEST_NOT_RUN ((HRESULT)0x8A15C010) #define WINGET_CONFIG_ERROR_GET_FAILED ((HRESULT)0x8A15C011) +#define WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND ((HRESULT)0x8A15C012) // Configuration Processor Errors #define WINGET_CONFIG_ERROR_UNIT_NOT_INSTALLED ((HRESULT)0x8A15C101) diff --git a/src/AppInstallerSharedLib/Public/winget/Filesystem.h b/src/AppInstallerSharedLib/Public/winget/Filesystem.h index 6871713723..336769b154 100644 --- a/src/AppInstallerSharedLib/Public/winget/Filesystem.h +++ b/src/AppInstallerSharedLib/Public/winget/Filesystem.h @@ -103,4 +103,17 @@ namespace AppInstaller::Filesystem { return InitializeAndGetPathTo(GetPathDetailsFor(path, forDisplay)); } + + // A shared path. + enum class PathName + { + // Local state root that is specifically unpackaged (even if used from a packaged process). + UnpackagedLocalStateRoot, + // Local settings root that is specifically unpackaged (even if used from a packaged process). + UnpackagedSettingsRoot, + }; + + // Gets the PathDetails used for the given path. + // This is exposed primarily to allow for testing, GetPathTo should be preferred. + PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); } diff --git a/src/AppInstallerSharedLib/Public/winget/ModuleCountBase.h b/src/AppInstallerSharedLib/Public/winget/ModuleCountBase.h new file mode 100644 index 0000000000..f4aa1eda68 --- /dev/null +++ b/src/AppInstallerSharedLib/Public/winget/ModuleCountBase.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +namespace AppInstaller::WinRT +{ + // Implements module count interactions. + struct ModuleCountBase + { + ModuleCountBase() + { + if (auto modulePtr = ::Microsoft::WRL::GetModuleBase()) + { + modulePtr->IncrementObjectCount(); + } + } + + ~ModuleCountBase() + { + if (auto modulePtr = ::Microsoft::WRL::GetModuleBase()) + { + modulePtr->DecrementObjectCount(); + } + } + }; +} diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteDynamicStorage.h b/src/AppInstallerSharedLib/Public/winget/SQLiteDynamicStorage.h new file mode 100644 index 0000000000..2e5ac7a021 --- /dev/null +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteDynamicStorage.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include +#include +#include +#include + +namespace AppInstaller::SQLite +{ + // Type the allows for the schema version of the underlying storage to be changed dynamically. + struct SQLiteDynamicStorage : public SQLiteStorageBase + { + // Creates a new database with the given schema version. + SQLiteDynamicStorage(const std::string& target, const Version& version); + SQLiteDynamicStorage(const std::filesystem::path& target, const Version& version); + + // Opens an existing database with the given disposition. + SQLiteDynamicStorage(const std::string& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& file = {}); + SQLiteDynamicStorage(const std::filesystem::path& filePath, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& file = {}); + + // Implicit conversion to a connection object for convenience. + operator Connection& (); + operator const Connection& () const; + Connection& GetConnection(); + const Connection& GetConnection() const; + + using SQLiteStorageBase::SetLastWriteTime; + + // Must be kept alive to ensure consistent schema view and exclusive use of the owned connection. + struct TransactionLock + { + _Acquires_lock_(mutex) + TransactionLock(std::mutex& mutex); + + _Acquires_lock_(mutex) + TransactionLock(std::mutex& mutex, Connection& connection, std::string_view name); + + // Abandons the transaction and any changes; releases the connection lock. + void Rollback(bool throwOnError = true); + + // Commits the transaction and releases the connection lock. + void Commit(); + + private: + std::lock_guard m_lock; + Savepoint m_transaction; + }; + + // Acquires the connection lock and begins a transaction on the database. + // If the returned result is empty, the schema version has changed and the caller must handle this. + std::unique_ptr TryBeginTransaction(std::string_view name); + + // Locks the connection for use during the schema upgrade. + std::unique_ptr LockConnection(); + }; +} diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h index 50e2eb1e19..56b18daa0a 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -112,6 +112,7 @@ namespace AppInstaller::SQLite::Builder Text, Blob, Integer, // Type for specifying a primary key column as a row id alias. + None, // Does not declare a type }; template diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h index 1950e4cee4..93d805d671 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h @@ -9,6 +9,7 @@ namespace AppInstaller::SQLite { + // Type that wraps the basic SQLite storage functionality; the connection and metadata like schema version. struct SQLiteStorageBase { // The disposition for opening the database. diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteVersion.h b/src/AppInstallerSharedLib/Public/winget/SQLiteVersion.h index 6d66dee5ec..eef4256833 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteVersion.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteVersion.h @@ -56,7 +56,7 @@ namespace AppInstaller::SQLite static Version GetSchemaVersion(Connection& connection); // Writes the current version to the given database. - void SetSchemaVersion(Connection& connection); + void SetSchemaVersion(Connection& connection) const; }; // Output the version diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h index 1f1ee57fd6..6eb67d3df1 100644 --- a/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h +++ b/src/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h @@ -109,6 +109,14 @@ namespace AppInstaller::SQLite static blob_t GetColumn(sqlite3_stmt* stmt, int column); }; + template <> + struct ParameterSpecificsImpl + { + static std::string ToLog(const GUID& v); + static void Bind(sqlite3_stmt* stmt, int index, const GUID& v); + static GUID GetColumn(sqlite3_stmt* stmt, int column); + }; + template struct ParameterSpecificsImpl>> { @@ -251,6 +259,11 @@ namespace AppInstaller::SQLite // Sets the busy timeout for the connection. void SetBusyTimeout(std::chrono::milliseconds timeout); + // Sets the journal mode. + // Returns true if successful, false if not. + // Must be performed outside of a transaction. + bool SetJournalMode(std::string_view mode); + operator sqlite3* () const { return m_dbconn->Get(); } protected: @@ -370,6 +383,8 @@ namespace AppInstaller::SQLite // Creates a savepoint, beginning it. static Savepoint Create(Connection& connection, std::string name); + Savepoint(); + Savepoint(const Savepoint&) = delete; Savepoint& operator=(const Savepoint&) = delete; diff --git a/src/AppInstallerSharedLib/Public/winget/Yaml.h b/src/AppInstallerSharedLib/Public/winget/Yaml.h index 3774941df5..dcf6bf006e 100644 --- a/src/AppInstallerSharedLib/Public/winget/Yaml.h +++ b/src/AppInstallerSharedLib/Public/winget/Yaml.h @@ -226,6 +226,17 @@ namespace AppInstaller::YAML Value, }; + // Sets the scalar style to use for the next scalar output. + enum class ScalarStyle + { + Any, + Plain, + SingleQuoted, + DoubleQuoted, + Literal, + Folded, + }; + // Forward declaration to allow pImpl in this Emitter. namespace Wrapper { @@ -252,6 +263,8 @@ namespace AppInstaller::YAML Emitter& operator<<(int value); Emitter& operator<<(bool value); + Emitter& operator<<(ScalarStyle style); + // Gets the result of the emitter; can only be retrieved once. std::string str(); @@ -293,7 +306,10 @@ namespace AppInstaller::YAML }; // If set, defines the type of the next scalar (Key or Value). - std::optional m_scalarInfo; + std::optional m_scalarType; + + // If set, defines the style of the next scalar. + std::optional m_scalarStyle; // Converts the input type to a bitmask value. size_t GetInputBitmask(InputType type); diff --git a/src/AppInstallerSharedLib/SQLiteDynamicStorage.cpp b/src/AppInstallerSharedLib/SQLiteDynamicStorage.cpp new file mode 100644 index 0000000000..0925aae37e --- /dev/null +++ b/src/AppInstallerSharedLib/SQLiteDynamicStorage.cpp @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Public/winget/SQLiteDynamicStorage.h" + +namespace AppInstaller::SQLite +{ + SQLiteDynamicStorage::SQLiteDynamicStorage(const std::string& target, const Version& version) : SQLiteStorageBase(target, version) + { + version.SetSchemaVersion(m_dbconn); + } + + SQLiteDynamicStorage::SQLiteDynamicStorage(const std::filesystem::path& target, const Version& version) : SQLiteDynamicStorage(target.u8string(), version) + {} + + SQLiteDynamicStorage::SQLiteDynamicStorage( + const std::string& filePath, + SQLiteStorageBase::OpenDisposition disposition, + Utility::ManagedFile&& file) + : SQLiteStorageBase(filePath, disposition, std::move(file)) + {} + + SQLiteDynamicStorage::SQLiteDynamicStorage( + const std::filesystem::path& filePath, + SQLiteStorageBase::OpenDisposition disposition, + Utility::ManagedFile&& file) + : SQLiteDynamicStorage(filePath.u8string(), disposition, std::move(file)) + {} + + SQLiteDynamicStorage::operator Connection& () + { + return m_dbconn; + } + + SQLiteDynamicStorage::operator const Connection& () const + { + return m_dbconn; + } + + Connection& SQLiteDynamicStorage::GetConnection() + { + return m_dbconn; + } + + const Connection& SQLiteDynamicStorage::GetConnection() const + { + return m_dbconn; + } + + _Acquires_lock_(mutex) + SQLiteDynamicStorage::TransactionLock::TransactionLock(std::mutex& mutex) : + m_lock(mutex) + { + } + + _Acquires_lock_(mutex) + SQLiteDynamicStorage::TransactionLock::TransactionLock(std::mutex& mutex, Connection& connection, std::string_view name) : + m_lock(mutex) + { + m_transaction = Savepoint::Create(connection, std::string{ name }); + } + + void SQLiteDynamicStorage::TransactionLock::Rollback(bool throwOnError) + { + m_transaction.Rollback(throwOnError); + } + + void SQLiteDynamicStorage::TransactionLock::Commit() + { + m_transaction.Commit(); + } + + std::unique_ptr SQLiteDynamicStorage::TryBeginTransaction(std::string_view name) + { + auto result = std::make_unique(*m_interfaceLock, m_dbconn, name); + + Version currentVersion = Version::GetSchemaVersion(m_dbconn); + if (currentVersion != m_version) + { + m_version = currentVersion; + result.reset(); + } + + return result; + } + + std::unique_ptr SQLiteDynamicStorage::LockConnection() + { + return std::make_unique(*m_interfaceLock); + } +} diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp index bdf3e7649d..57b01b7222 100644 --- a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -145,6 +145,8 @@ namespace AppInstaller::SQLite::Builder case Type::Integer: out << "INTEGER"; break; + case Type::None: + break; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerSharedLib/SQLiteVersion.cpp b/src/AppInstallerSharedLib/SQLiteVersion.cpp index 43c94481e3..367ecc3dae 100644 --- a/src/AppInstallerSharedLib/SQLiteVersion.cpp +++ b/src/AppInstallerSharedLib/SQLiteVersion.cpp @@ -16,7 +16,7 @@ namespace AppInstaller::SQLite return { static_cast(major), static_cast(minor) }; } - void Version::SetSchemaVersion(Connection& connection) + void Version::SetSchemaVersion(Connection& connection) const { Savepoint savepoint = Savepoint::Create(connection, "version_setschemaversion"); diff --git a/src/AppInstallerSharedLib/SQLiteWrapper.cpp b/src/AppInstallerSharedLib/SQLiteWrapper.cpp index 8af16ca09b..925aabdd59 100644 --- a/src/AppInstallerSharedLib/SQLiteWrapper.cpp +++ b/src/AppInstallerSharedLib/SQLiteWrapper.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "Public/winget/SQLiteWrapper.h" #include "Public/AppInstallerErrors.h" +#include "Public/AppInstallerStrings.h" #include "ICU/SQLiteICU.h" #include @@ -149,6 +150,32 @@ namespace AppInstaller::SQLite } } + std::string ParameterSpecificsImpl::ToLog(const GUID& v) + { + std::ostringstream strstr; + strstr << v; + return strstr.str(); + } + + void ParameterSpecificsImpl::Bind(sqlite3_stmt* stmt, int index, const GUID& v) + { + static_assert(sizeof(v) == 16); + THROW_IF_SQLITE_FAILED(sqlite3_bind_blob64(stmt, index, &v, sizeof(v), SQLITE_TRANSIENT), sqlite3_db_handle(stmt)); + } + + GUID ParameterSpecificsImpl::GetColumn(sqlite3_stmt* stmt, int column) + { + GUID result{}; + + const void* blobPtr = sqlite3_column_blob(stmt, column); + if (blobPtr) + { + result = *reinterpret_cast(blobPtr); + } + + return result; + } + void SharedConnection::Disable() { m_active = false; @@ -212,6 +239,18 @@ namespace AppInstaller::SQLite THROW_IF_SQLITE_FAILED(sqlite3_busy_timeout(m_dbconn->Get(), static_cast(timeout.count())), m_dbconn->Get()); } + bool Connection::SetJournalMode(std::string_view mode) + { + using namespace AppInstaller::Utility; + + std::ostringstream stream; + stream << "PRAGMA journal_mode=" << mode; + + Statement setJournalMode = Statement::Create(*this, stream.str()); + THROW_HR_IF(E_UNEXPECTED, !setJournalMode.Step()); + return ToLower(setJournalMode.GetColumn(0)) == ToLower(mode); + } + std::shared_ptr Connection::GetSharedConnection() const { return m_dbconn; @@ -335,6 +374,9 @@ namespace AppInstaller::SQLite m_state = State::Prepared; } + Savepoint::Savepoint() : m_inProgress(false) + {} + Savepoint::Savepoint(Connection& connection, std::string&& name) : m_name(std::move(name)) { diff --git a/src/AppInstallerSharedLib/Yaml.cpp b/src/AppInstallerSharedLib/Yaml.cpp index 4125c31059..a263cf7253 100644 --- a/src/AppInstallerSharedLib/Yaml.cpp +++ b/src/AppInstallerSharedLib/Yaml.cpp @@ -646,12 +646,12 @@ namespace AppInstaller::YAML break; case AppInstaller::YAML::Key: CheckInput(InputType::Key); - m_scalarInfo = InputType::Key; + m_scalarType = InputType::Key; SetAllowedInputs(); break; case AppInstaller::YAML::Value: CheckInput(InputType::Value); - m_scalarInfo = InputType::Value; + m_scalarType = InputType::Value; SetAllowedInputs(); break; default: @@ -665,25 +665,26 @@ namespace AppInstaller::YAML { CheckInput(InputType::Scalar); - int id = m_document->AddScalar(value); + int id = m_document->AddScalar(value, m_scalarStyle.value_or(ScalarStyle::Any)); + m_scalarStyle = std::nullopt; - if (!m_scalarInfo) + if (!m_scalarType) { // Part of a sequence AppendNode(id); // No change to allowed inputs } - else if (m_scalarInfo.value() == InputType::Key) + else if (m_scalarType.value() == InputType::Key) { m_keyId = id; - m_scalarInfo = std::nullopt; + m_scalarType = std::nullopt; SetAllowedInputs(); } - else if (m_scalarInfo.value() == InputType::Value) + else if (m_scalarType.value() == InputType::Value) { // Mapping pair complete AppendNode(id); - m_scalarInfo = std::nullopt; + m_scalarType = std::nullopt; SetAllowedInputsForContainer(); } else @@ -713,6 +714,14 @@ namespace AppInstaller::YAML return operator<<(value ? "true"sv : "false"sv); } + Emitter& Emitter::operator<<(ScalarStyle style) + { + m_scalarStyle = style; + // Because without this you get a C26815... + (void)0; + return *this; + } + std::string Emitter::str() { std::ostringstream stream; diff --git a/src/AppInstallerSharedLib/YamlWrapper.cpp b/src/AppInstallerSharedLib/YamlWrapper.cpp index 812082e3f0..a9664b0e0b 100644 --- a/src/AppInstallerSharedLib/YamlWrapper.cpp +++ b/src/AppInstallerSharedLib/YamlWrapper.cpp @@ -84,6 +84,20 @@ namespace AppInstaller::YAML::Wrapper { return ConvertYamlString(node->data.scalar.value, mark, node->data.scalar.length); } + + yaml_scalar_style_t ConvertStyle(ScalarStyle style) + { + switch (style) + { + case ScalarStyle::Any: return yaml_scalar_style_t::YAML_ANY_SCALAR_STYLE; + case ScalarStyle::Plain: return yaml_scalar_style_t::YAML_PLAIN_SCALAR_STYLE; + case ScalarStyle::SingleQuoted: return yaml_scalar_style_t::YAML_SINGLE_QUOTED_SCALAR_STYLE; + case ScalarStyle::DoubleQuoted: return yaml_scalar_style_t::YAML_DOUBLE_QUOTED_SCALAR_STYLE; + case ScalarStyle::Literal: return yaml_scalar_style_t::YAML_LITERAL_SCALAR_STYLE; + case ScalarStyle::Folded: return yaml_scalar_style_t::YAML_FOLDED_SCALAR_STYLE; + default: THROW_HR(E_UNEXPECTED); + } + } } Document::Document(bool init) : @@ -207,9 +221,9 @@ namespace AppInstaller::YAML::Wrapper return result; } - int Document::AddScalar(std::string_view value) + int Document::AddScalar(std::string_view value, ScalarStyle style) { - int result = yaml_document_add_scalar(&m_document, NULL, reinterpret_cast(value.data()), static_cast(value.size()), YAML_ANY_SCALAR_STYLE); + int result = yaml_document_add_scalar(&m_document, NULL, reinterpret_cast(value.data()), static_cast(value.size()), ConvertStyle(style)); THROW_HR_IF(APPINSTALLER_CLI_ERROR_YAML_DOC_BUILD_FAILED, result == 0); return result; } diff --git a/src/AppInstallerSharedLib/YamlWrapper.h b/src/AppInstallerSharedLib/YamlWrapper.h index 4f7fa074f8..d87141f56e 100644 --- a/src/AppInstallerSharedLib/YamlWrapper.h +++ b/src/AppInstallerSharedLib/YamlWrapper.h @@ -41,7 +41,7 @@ namespace AppInstaller::YAML::Wrapper Node GetRoot(); // Adds a scalar node to the document. - int AddScalar(std::string_view value); + int AddScalar(std::string_view value, ScalarStyle style = ScalarStyle::Any); // Adds a sequence node to the document. int AddSequence(); diff --git a/src/AppInstallerSharedLib/pch.h b/src/AppInstallerSharedLib/pch.h index 9304b0556f..d9bf1dd1ec 100644 --- a/src/AppInstallerSharedLib/pch.h +++ b/src/AppInstallerSharedLib/pch.h @@ -10,6 +10,7 @@ #include #include #include +#include #define YAML_DECLARE_STATIC #include diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs new file mode 100644 index 0000000000..db71cd428e --- /dev/null +++ b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs @@ -0,0 +1,391 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.Management.Configuration.UnitTests.Tests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using Microsoft.Management.Configuration.Processor.Extensions; + using Microsoft.Management.Configuration.UnitTests.Fixtures; + using Microsoft.Management.Configuration.UnitTests.Helpers; + using Microsoft.VisualBasic; + using Xunit; + using Xunit.Abstractions; + + /// + /// Unit tests for configuration history. + /// + [Collection("UnitTestCollection")] + [OutOfProc] + public class ConfigurationHistoryTests : ConfigurationProcessorTestBase + { + /// + /// Initializes a new instance of the class. + /// + /// Unit test fixture. + /// Log helper. + public ConfigurationHistoryTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + } + + /// + /// Checks that the history matches the applied set. + /// + [Fact] + public void ApplySet_HistoryMatches_0_1() + { + this.RunApplyHistoryMatchTest( + @" +properties: + configurationVersion: 0.1 + assertions: + - resource: Assert + id: AssertIdentifier1 + directives: + module: Module + settings: + Setting1: '1' + Setting2: 2 + - resource: Assert + id: AssertIdentifier2 + dependsOn: + - AssertIdentifier1 + directives: + module: Module + settings: + Setting1: + Setting2: 2 + parameters: + - resource: Inform + id: InformIdentifier1 + directives: + module: Module2 + settings: + Setting1: + Setting2: + Setting3: 3 + resources: + - resource: Apply +", new string[] { "AssertIdentifier2" }); + } + + /// + /// Checks that the history matches the applied set. + /// + [Fact] + public void ApplySet_HistoryMatches_0_2() + { + this.RunApplyHistoryMatchTest( + @" +properties: + configurationVersion: 0.2 + assertions: + - resource: Module/Assert + id: AssertIdentifier1 + settings: + Setting1: '1' + Setting2: 2 + - resource: Module/Assert + id: AssertIdentifier2 + dependsOn: + - AssertIdentifier1 + directives: + description: Describe! + settings: + Setting1: + Setting2: 2 + parameters: + - resource: Module2/Inform + id: InformIdentifier1 + settings: + Setting1: + Setting2: + Setting3: 3 + resources: + - resource: Apply +", new string[] { "AssertIdentifier2" }); + } + + /// + /// Checks that the history matches the applied set. + /// + [Fact] + public void ApplySet_HistoryMatches_0_3() + { + this.RunApplyHistoryMatchTest( + @" +$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json +metadata: + a: 1 + b: '2' +variables: + v1: var1 + v2: 42 +resources: + - name: Name + type: Module/Resource + metadata: + e: '5' + f: 6 + properties: + c: 3 + d: '4' + - name: Name2 + type: Module/Resource2 + dependsOn: + - Name + properties: + l: '10' + metadata: + i: '7' + j: 8 + q: 42 + - name: Group + type: Module2/Resource + metadata: + isGroup: true + properties: + resources: + - name: Child1 + type: Module3/Resource + metadata: + e: '5' + f: 6 + properties: + c: 3 + d: '4' + - name: Child2 + type: Module4/Resource2 + properties: + l: '10' + metadata: + i: '7' + j: 8 + q: 42 +", new string[] { "AssertIdentifier2" }); + } + + /// + /// Applies a set, reads the history, changes the read set and reapplies it. + /// + [Fact] + public void ApplySet_ChangeHistory() + { + string disabledIdentifier = "AssertIdentifier2"; + + ConfigurationSet returnedSet = this.RunApplyHistoryMatchTest( + @" +properties: + configurationVersion: 0.2 + assertions: + - resource: Module/Assert + id: AssertIdentifier1 + settings: + Setting1: '1' + Setting2: 2 + - resource: Module/Assert + id: AssertIdentifier2 + dependsOn: + - AssertIdentifier1 + directives: + description: Describe! + settings: + Setting1: + Setting2: 2 + parameters: + - resource: Module2/Inform + id: InformIdentifier1 + settings: + Setting1: + Setting2: + Setting3: 3 + resources: + - resource: Apply +", new string[] { disabledIdentifier }); + + foreach (ConfigurationUnit unit in returnedSet.Units) + { + if (unit.Identifier == disabledIdentifier) + { + unit.IsActive = true; + } + } + + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + ApplyConfigurationSetResult result = processor.ApplySet(returnedSet, ApplyConfigurationSetFlags.None); + Assert.NotNull(result); + Assert.Null(result.ResultCode); + + ConfigurationSet? historySet = null; + + foreach (ConfigurationSet set in processor.GetConfigurationHistory()) + { + if (set.InstanceIdentifier == returnedSet.InstanceIdentifier) + { + historySet = set; + } + } + + this.AssertSetsEqual(returnedSet, historySet); + } + + /// + /// Applies a set, reads the history and removes it. + /// + [Fact] + public void ApplySet_RemoveHistory() + { + ConfigurationSet returnedSet = this.RunApplyHistoryMatchTest( + @" +properties: + configurationVersion: 0.2 + assertions: + - resource: Module/Assert + id: AssertIdentifier1 + settings: + Setting1: '1' + Setting2: 2 + - resource: Module/Assert + id: AssertIdentifier2 + dependsOn: + - AssertIdentifier1 + directives: + description: Describe! + settings: + Setting1: + Setting2: 2 + parameters: + - resource: Module2/Inform + id: InformIdentifier1 + settings: + Setting1: + Setting2: + Setting3: 3 + resources: + - resource: Apply +"); + + returnedSet.Remove(); + + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + ConfigurationSet? historySet = null; + + foreach (ConfigurationSet set in processor.GetConfigurationHistory()) + { + if (set.InstanceIdentifier == returnedSet.InstanceIdentifier) + { + historySet = set; + } + } + + Assert.Null(historySet); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1011:Closing square brackets should be spaced correctly", Justification = "https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/2927")] + private ConfigurationSet RunApplyHistoryMatchTest(string contents, string[]? inactiveIdentifiers = null) + { + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + OpenConfigurationSetResult configurationSetResult = processor.OpenConfigurationSet(this.CreateStream(contents)); + ConfigurationSet configurationSet = configurationSetResult.Set; + Assert.NotNull(configurationSet); + + configurationSet.Name = "Test Name"; + configurationSet.Origin = "Test Origin"; + configurationSet.Path = "Test Path"; + + if (inactiveIdentifiers != null) + { + foreach (string identifier in inactiveIdentifiers) + { + foreach (ConfigurationUnit unit in configurationSet.Units) + { + if (unit.Identifier == identifier) + { + unit.IsActive = false; + } + } + } + } + + ApplyConfigurationSetResult result = processor.ApplySet(configurationSet, ApplyConfigurationSetFlags.None); + Assert.NotNull(result); + Assert.Null(result.ResultCode); + + ConfigurationSet? historySet = null; + + foreach (ConfigurationSet set in processor.GetConfigurationHistory()) + { + if (set.InstanceIdentifier == configurationSet.InstanceIdentifier) + { + historySet = set; + } + } + + this.AssertSetsEqual(configurationSet, historySet); + return historySet; + } + + private void AssertSetsEqual(ConfigurationSet expectedSet, [NotNull] ConfigurationSet? actualSet) + { + Assert.NotNull(actualSet); + Assert.Equal(expectedSet.Name, actualSet.Name); + Assert.Equal(expectedSet.Origin, actualSet.Origin); + Assert.Equal(expectedSet.Path, actualSet.Path); + Assert.NotEqual(DateTimeOffset.UnixEpoch, actualSet.FirstApply); + Assert.Equal(expectedSet.SchemaVersion, actualSet.SchemaVersion); + Assert.Equal(expectedSet.SchemaUri, actualSet.SchemaUri); + Assert.True(expectedSet.Metadata.ContentEquals(actualSet.Metadata)); + + this.AssertUnitsListEqual(expectedSet.Units, actualSet.Units); + } + + private void AssertUnitsListEqual(IList expectedUnits, IList actualUnits) + { + Assert.Equal(expectedUnits.Count, actualUnits.Count); + + foreach (ConfigurationUnit expectedUnit in expectedUnits) + { + ConfigurationUnit? actualUnit = null; + foreach (ConfigurationUnit historyUnit in actualUnits) + { + if (historyUnit.InstanceIdentifier == expectedUnit.InstanceIdentifier) + { + actualUnit = historyUnit; + } + } + + this.AssertUnitsEqual(expectedUnit, actualUnit); + } + } + + private void AssertUnitsEqual(ConfigurationUnit expectedUnit, ConfigurationUnit? actualUnit) + { + Assert.NotNull(actualUnit); + Assert.Equal(expectedUnit.Type, actualUnit.Type); + Assert.Equal(expectedUnit.Identifier, actualUnit.Identifier); + Assert.Equal(expectedUnit.Intent, actualUnit.Intent); + Assert.Equal(expectedUnit.Dependencies, actualUnit.Dependencies); + Assert.True(expectedUnit.Metadata.ContentEquals(actualUnit.Metadata)); + Assert.True(expectedUnit.Settings.ContentEquals(actualUnit.Settings)); + Assert.Equal(expectedUnit.IsActive, actualUnit.IsActive); + Assert.Equal(expectedUnit.IsGroup, actualUnit.IsGroup); + + if (expectedUnit.IsGroup) + { + this.AssertUnitsListEqual(expectedUnit.Units, actualUnit.Units); + } + } + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp index c8ccdefde2..9db5014cf9 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -195,12 +195,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector ConfigurationProcessor::GetConfigurationHistory() { - THROW_HR(E_NOTIMPL); + return GetConfigurationHistoryImpl(); } Windows::Foundation::IAsyncOperation> ConfigurationProcessor::GetConfigurationHistoryAsync() { - co_return GetConfigurationHistory(); + auto strong_this{ get_strong() }; + co_await winrt::resume_background(); + co_return GetConfigurationHistoryImpl({ co_await winrt::get_cancellation_token() }); } Configuration::OpenConfigurationSetResult ConfigurationProcessor::OpenConfigurationSet(const Windows::Storage::Streams::IInputStream& stream) @@ -341,6 +343,23 @@ namespace winrt::Microsoft::Management::Configuration::implementation co_return GetSetDetailsImpl(localSet, detailFlags, { co_await winrt::get_progress_token(), co_await winrt::get_cancellation_token()}); } + Windows::Foundation::Collections::IVector ConfigurationProcessor::GetConfigurationHistoryImpl(AppInstaller::WinRT::AsyncCancellation cancellation) + { + auto threadGlobals = m_threadGlobals.SetForCurrentThread(); + + m_database.EnsureOpened(false); + cancellation.ThrowIfCancelled(); + + std::vector result; + for (const auto& set : m_database.GetSetHistory()) + { + PropagateLifetimeWatcher(*set); + result.emplace_back(*set); + } + + return multi_threaded_vector(std::move(result)); + } + Configuration::GetConfigurationSetDetailsResult ConfigurationProcessor::GetSetDetailsImpl( const ConfigurationSet& configurationSet, ConfigurationUnitDetailFlags detailFlags, @@ -460,6 +479,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation else { groupProcessor = GetSetGroupProcessor(configurationSet); + + // Write this set to the database history + // This is a somewhat arbitrary time to write it, but it should not be done if PerformConsistencyCheckOnly is passed, so this is convenient. + m_database.EnsureOpened(); + progress.ThrowIfCancelled(); + m_database.WriteSetHistory(configurationSet, WI_IsFlagSet(flags, ApplyConfigurationSetFlags::DoNotOverwriteMatchingOriginSet)); } auto result = make_self>(); @@ -838,6 +863,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation m_supportSchema03 = value; } + void ConfigurationProcessor::RemoveHistory(const ConfigurationSet& configurationSet) + { + m_database.EnsureOpened(false); + m_database.RemoveSetHistory(configurationSet); + } + void ConfigurationProcessor::SendDiagnosticsImpl(const IDiagnosticInformation& information) { std::lock_guard lock{ m_diagnosticsMutex }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h index c3dac9445c..5d0119c9dd 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h +++ b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -6,6 +6,7 @@ #include #include #include "ConfigThreadGlobals.h" +#include "Database/ConfigurationDatabase.h" #include #include @@ -97,7 +98,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Temporary entry point to enable experimental schema support. void SetSupportsSchema03(bool value); + // Removes the history for the given set. + void RemoveHistory(const ConfigurationSet& configurationSet); + private: + Windows::Foundation::Collections::IVector GetConfigurationHistoryImpl(AppInstaller::WinRT::AsyncCancellation cancellation = {}); + GetConfigurationSetDetailsResult GetSetDetailsImpl( const ConfigurationSet& configurationSet, ConfigurationUnitDetailFlags detailFlags, @@ -129,6 +135,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation IConfigurationSetProcessorFactory::Diagnostics_revoker m_factoryDiagnosticsEventRevoker; DiagnosticLevel m_minimumLevel = DiagnosticLevel::Informational; std::recursive_mutex m_diagnosticsMutex; + ConfigurationDatabase m_database; bool m_isHandlingDiagnostics = false; // Temporary value to enable experimental schema support. bool m_supportSchema03 = true; diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.cpp b/src/Microsoft.Management.Configuration/ConfigurationSet.cpp index 24d6bdd8d3..fbf33cebae 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSet.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSet.cpp @@ -5,6 +5,7 @@ #include "ConfigurationSet.g.cpp" #include "ConfigurationSetParser.h" #include "ConfigurationSetSerializer.h" +#include "Database/ConfigurationDatabase.h" namespace winrt::Microsoft::Management::Configuration::implementation { @@ -13,11 +14,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation GUID instanceIdentifier; THROW_IF_FAILED(CoCreateGuid(&instanceIdentifier)); m_instanceIdentifier = instanceIdentifier; - m_schemaVersion = ConfigurationSetParser::LatestVersion(); + std::tie(m_schemaVersion, m_schemaUri) = ConfigurationSetParser::LatestVersion(); } ConfigurationSet::ConfigurationSet(const guid& instanceIdentifier) : - m_instanceIdentifier(instanceIdentifier) + m_instanceIdentifier(instanceIdentifier), m_fromHistory(true) { } @@ -33,7 +34,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation bool ConfigurationSet::IsFromHistory() const { - return false; + return m_fromHistory; } hstring ConfigurationSet::Name() @@ -81,6 +82,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation return m_firstApply; } + void ConfigurationSet::FirstApply(clock::time_point value) + { + m_firstApply = value; + } + clock::time_point ConfigurationSet::ApplyBegun() { return clock::time_point{}; @@ -139,7 +145,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSet::Remove() { - THROW_HR(E_NOTIMPL); + ConfigurationDatabase database; + database.EnsureOpened(false); + database.RemoveSetHistory(*get_strong()); } Windows::Foundation::Collections::ValueSet ConfigurationSet::Metadata() diff --git a/src/Microsoft.Management.Configuration/ConfigurationSet.h b/src/Microsoft.Management.Configuration/ConfigurationSet.h index 0c4900d6d0..fd16cc1f58 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSet.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSet.h @@ -3,13 +3,14 @@ #pragma once #include "ConfigurationSet.g.h" #include +#include #include #include #include namespace winrt::Microsoft::Management::Configuration::implementation { - struct ConfigurationSet : ConfigurationSetT>, AppInstaller::WinRT::LifetimeWatcherBase + struct ConfigurationSet : ConfigurationSetT>, AppInstaller::WinRT::LifetimeWatcherBase, AppInstaller::WinRT::ModuleCountBase { using WinRT_Self = ::winrt::Microsoft::Management::Configuration::ConfigurationSet; using ConfigurationUnit = ::winrt::Microsoft::Management::Configuration::ConfigurationUnit; @@ -19,6 +20,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation #if !defined(INCLUDE_ONLY_INTERFACE_METHODS) ConfigurationSet(const guid& instanceIdentifier); + void FirstApply(clock::time_point value); void Units(std::vector&& units); void Parameters(std::vector&& value); @@ -85,6 +87,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::ValueSet m_variables; Windows::Foundation::Uri m_schemaUri = nullptr; std::string m_inputHash; + bool m_fromHistory = false; #endif }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp index 2397ff515d..318265b9ee 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser.cpp @@ -211,20 +211,27 @@ namespace winrt::Microsoft::Management::Configuration::implementation } // Create the parser based on the version selected - SemanticVersion schemaVersion(std::move(schemaVersionString)); + auto result = CreateForSchemaVersion(std::move(schemaVersionString)); + result->SetDocument(std::move(document)); + return result; + } + + std::unique_ptr ConfigurationSetParser::CreateForSchemaVersion(std::string input) + { + SemanticVersion schemaVersion(std::move(input)); // TODO: Consider having the version/uri/type information all together in the future if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 1) { - return std::make_unique(std::move(document)); + return std::make_unique(); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 2) { - return std::make_unique(std::move(document)); + return std::make_unique(); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) { - return std::make_unique(std::move(document)); + return std::make_unique(); } AICLI_LOG(Config, Error, << "Unknown configuration version: " << schemaVersion.ToString()); @@ -306,9 +313,27 @@ namespace winrt::Microsoft::Management::Configuration::implementation return {}; } - hstring ConfigurationSetParser::LatestVersion() + std::pair ConfigurationSetParser::LatestVersion() + { + auto latest = std::rbegin(SchemaVersionAndUriMap); + return { hstring{ latest->VersionWide }, Windows::Foundation::Uri{ latest->UriWide } }; + } + + Windows::Foundation::Collections::ValueSet ConfigurationSetParser::ParseValueSet(std::string_view input) + { + Windows::Foundation::Collections::ValueSet result; + FillValueSetFromMap(Load(input), result); + return result; + } + + std::vector ConfigurationSetParser::ParseStringArray(std::string_view input) { - return hstring{ std::rbegin(SchemaVersionAndUriMap)->VersionWide }; + std::vector result; + ParseSequence(Load(input), "string_array", Node::Type::Scalar, [&](const AppInstaller::YAML::Node& item) + { + result.emplace_back(item.as()); + }); + return result; } void ConfigurationSetParser::SetError(hresult result, std::string_view field, std::string_view value, uint32_t line, uint32_t column) @@ -405,11 +430,16 @@ namespace winrt::Microsoft::Management::Configuration::implementation return; } + ParseSequence(sequenceNode, GetConfigurationFieldName(field), elementType, operation); + } + + void ConfigurationSetParser::ParseSequence(const AppInstaller::YAML::Node& node, std::string_view nameForErrors, std::optional elementType, std::function operation) + { std::ostringstream strstr; - strstr << GetConfigurationFieldName(field); + strstr << nameForErrors; size_t index = 0; - for (const Node& item : sequenceNode.Sequence()) + for (const Node& item : node.Sequence()) { if (elementType && item.GetType() != elementType.value()) { diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser.h index c0fa73cee8..626811e4b2 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser.h @@ -6,8 +6,10 @@ #include #include #include +#include #include #include +#include #include namespace winrt::Microsoft::Management::Configuration::implementation @@ -18,6 +20,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Create a parser from the given bytes (the encoding is detected). static std::unique_ptr Create(std::string_view input); + // Create a parser for the given schema version. + static std::unique_ptr CreateForSchemaVersion(std::string schemaVersion); + // Determines if the given value is a recognized schema version. // This will only return true for a version that we fully recognize. static bool IsRecognizedSchemaVersion(hstring value); @@ -36,7 +41,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation static std::string GetSchemaVersionForUri(std::string_view value); // Gets the latest schema version. - static hstring LatestVersion(); + static std::pair LatestVersion(); virtual ~ConfigurationSetParser() noexcept = default; @@ -51,7 +56,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Retrieves the schema version of the parser. virtual hstring GetSchemaVersion() = 0; - using ConfigurationSetPtr = decltype(make_self>()); + using ConfigurationSetPtr = winrt::com_ptr; // Retrieve the configuration set from the parser. ConfigurationSetPtr GetConfigurationSet() const { return m_configurationSet; } @@ -71,9 +76,18 @@ namespace winrt::Microsoft::Management::Configuration::implementation // The column related to the result code. uint32_t Column() const { return m_column; } + // Parse a ValueSet from the given input. + Windows::Foundation::Collections::ValueSet ParseValueSet(std::string_view input); + + // Parse a string array from the given input. + std::vector ParseStringArray(std::string_view input); + protected: ConfigurationSetParser() = default; + // Sets (or resets) the document to parse. + virtual void SetDocument(AppInstaller::YAML::Node&& document) = 0; + // Set the error state void SetError(hresult result, std::string_view field = {}, std::string_view value = {}, uint32_t line = 0, uint32_t column = 0); void SetError(hresult result, std::string_view field, const AppInstaller::YAML::Mark& mark, std::string_view value = {}); @@ -100,6 +114,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Parse the sequence named `field` from the given `node`. void ParseSequence(const AppInstaller::YAML::Node& node, ConfigurationField field, bool required, std::optional elementType, std::function operation); + // Parse the sequence from the given `node`. + void ParseSequence(const AppInstaller::YAML::Node& node, std::string_view nameForErrors, std::optional elementType, std::function operation); + // Gets the string value in `field` from the given `node`, setting this value on `unit` using the `propertyFunction`. void GetStringValueForUnit(const AppInstaller::YAML::Node& node, ConfigurationField field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h b/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h index 4a2ea61d18..4d316bca55 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParserError.h @@ -22,5 +22,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation void Parse() override {} hstring GetSchemaVersion() override { return {}; } + + protected: + void SetDocument(AppInstaller::YAML::Node&&) override {} }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp index e679677f9e..7757660b38 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp @@ -21,7 +21,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation ParseConfigurationUnitsFromField(properties, ConfigurationField::Parameters, ConfigurationUnitIntent::Inform, units); ParseConfigurationUnitsFromField(properties, ConfigurationField::Resources, ConfigurationUnitIntent::Apply, units); - m_configurationSet = make_self>(); + m_configurationSet = make_self(); m_configurationSet->Units(std::move(units)); m_configurationSet->SchemaVersion(GetSchemaVersion()); } @@ -32,11 +32,16 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } + void ConfigurationSetParser_0_1::SetDocument(AppInstaller::YAML::Node&& document) + { + m_document = std::move(document); + } + void ConfigurationSetParser_0_1::ParseConfigurationUnitsFromField(const Node& document, ConfigurationField field, ConfigurationUnitIntent intent, std::vector& result) { ParseSequence(document, field, false, Node::Type::Mapping, [&](const Node& item) { - auto configurationUnit = make_self>(); + auto configurationUnit = make_self(); ParseConfigurationUnit(configurationUnit.get(), item, intent); result.emplace_back(*configurationUnit); }); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h index 0762fef505..22b5a0b7a7 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h @@ -10,7 +10,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Parser for schema version 0.1 struct ConfigurationSetParser_0_1 : public ConfigurationSetParser { - ConfigurationSetParser_0_1(AppInstaller::YAML::Node&& document) : m_document(std::move(document)) {} + ConfigurationSetParser_0_1() = default; virtual ~ConfigurationSetParser_0_1() noexcept = default; @@ -25,6 +25,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring GetSchemaVersion() override; protected: + // Sets (or resets) the document to parse. + void SetDocument(AppInstaller::YAML::Node&& document) override; + void ParseConfigurationUnitsFromField(const AppInstaller::YAML::Node& document, ConfigurationField field, ConfigurationUnitIntent intent, std::vector& result); virtual void ParseConfigurationUnit(ConfigurationUnit* unit, const AppInstaller::YAML::Node& unitNode, ConfigurationUnitIntent intent); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp index 8b2234fae7..7140197e06 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp @@ -19,6 +19,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } + void ConfigurationSetParser_0_2::SetDocument(AppInstaller::YAML::Node&& document) + { + m_document = std::move(document); + } + void ConfigurationSetParser_0_2::ParseConfigurationUnit(ConfigurationUnit* unit, const Node& unitNode, ConfigurationUnitIntent intent) { CHECK_ERROR(ConfigurationSetParser_0_1::ParseConfigurationUnit(unit, unitNode, intent)); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h index b07bb84560..04aad207d2 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h @@ -10,7 +10,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Parser for schema version 0.2 struct ConfigurationSetParser_0_2 : public ConfigurationSetParser_0_1 { - ConfigurationSetParser_0_2(AppInstaller::YAML::Node&& document) : ConfigurationSetParser_0_1(std::move(document)) {} + ConfigurationSetParser_0_2() = default; virtual ~ConfigurationSetParser_0_2() noexcept = default; @@ -23,6 +23,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring GetSchemaVersion() override; protected: + // Sets (or resets) the document to parse. + void SetDocument(AppInstaller::YAML::Node&& document) override; + void ParseConfigurationUnit(ConfigurationUnit* unit, const AppInstaller::YAML::Node& unitNode, ConfigurationUnitIntent intent) override; }; } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp index 7ef8162286..39c8d2c9b9 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp @@ -16,7 +16,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSetParser_0_3::Parse() { - auto result = make_self>(); + auto result = make_self(); CHECK_ERROR(ParseValueSet(m_document, ConfigurationField::Metadata, false, result->Metadata())); CHECK_ERROR(ParseParameters(result)); @@ -36,6 +36,11 @@ namespace winrt::Microsoft::Management::Configuration::implementation return s_schemaVersion; } + void ConfigurationSetParser_0_3::SetDocument(AppInstaller::YAML::Node&& document) + { + m_document = std::move(document); + } + void ConfigurationSetParser_0_3::ParseParameters(ConfigurationSetParser::ConfigurationSetPtr& set) { std::vector parameters; @@ -195,7 +200,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { ParseSequence(document, field, false, Node::Type::Mapping, [&](const Node& item) { - auto configurationUnit = make_self>(); + auto configurationUnit = make_self(); ParseConfigurationUnit(configurationUnit.get(), item); result.emplace_back(*configurationUnit); }); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h index 60b8430b05..de2c85207d 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h @@ -11,7 +11,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Parser for schema version 0.3 struct ConfigurationSetParser_0_3 : public ConfigurationSetParser { - ConfigurationSetParser_0_3(AppInstaller::YAML::Node&& document) : m_document(std::move(document)) {} + ConfigurationSetParser_0_3() = default; virtual ~ConfigurationSetParser_0_3() noexcept = default; @@ -27,6 +27,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation hstring GetSchemaVersion() override; protected: + // Sets (or resets) the document to parse. + void SetDocument(AppInstaller::YAML::Node&& document) override; + void ParseParameters(ConfigurationSetParser::ConfigurationSetPtr& set); void ParseParameter(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node); void ParseParameterType(ConfigurationParameter* parameter, const AppInstaller::YAML::Node& node); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp index a73bd5496a..8aacd93f8c 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp @@ -20,7 +20,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation static constexpr std::string_view s_nullValue = "null"; } - std::unique_ptr ConfigurationSetSerializer::CreateSerializer(hstring version) + // The `forHistory` parameter is temporary until the other serializers are implemented. + // It is only applicable as long as the serializers that are not implemented do not have differences in the value set or string array serialization. + std::unique_ptr ConfigurationSetSerializer::CreateSerializer(hstring version, bool forHistory) { // Create the parser based on the version selected AppInstaller::Utility::SemanticVersion schemaVersion(std::move(winrt::to_string(version))); @@ -28,6 +30,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation // TODO: Consider having the version/uri/type information all together in the future if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 1) { + // Remove this one the 0.1 serializer is implemented. + if (forHistory) + { + return std::make_unique(); + } + THROW_HR(E_NOTIMPL); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 2) @@ -36,6 +44,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) { + // Remove this one the 0.3 serializer is implemented. + if (forHistory) + { + return std::make_unique(); + } + THROW_HR(E_NOTIMPL); } else @@ -45,6 +59,20 @@ namespace winrt::Microsoft::Management::Configuration::implementation } } + std::string ConfigurationSetSerializer::SerializeValueSet(const Windows::Foundation::Collections::ValueSet& valueSet) + { + Emitter emitter; + WriteYamlValueSet(emitter, valueSet); + return emitter.str(); + } + + std::string ConfigurationSetSerializer::SerializeStringArray(const Windows::Foundation::Collections::IVector& stringArray) + { + Emitter emitter; + WriteYamlStringArray(emitter, stringArray); + return emitter.str(); + } + void ConfigurationSetSerializer::WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, std::initializer_list exclusions) { // Create a sorted list of the field names to exclude @@ -71,6 +99,17 @@ namespace winrt::Microsoft::Management::Configuration::implementation emitter << EndMap; } + void ConfigurationSetSerializer::WriteYamlStringArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector& values) + { + emitter << BeginSeq; + + for (const auto& value : values) + { + emitter << AppInstaller::Utility::ConvertToUTF8(value); + } + + emitter << EndSeq; + } void ConfigurationSetSerializer::WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value) { @@ -103,7 +142,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } else if (type == PropertyType::String) { - emitter << AppInstaller::Utility::ConvertToUTF8(property.GetString()); + emitter << ScalarStyle::DoubleQuoted << AppInstaller::Utility::ConvertToUTF8(property.GetString()); } else if (type == PropertyType::Int64) { @@ -111,7 +150,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation } else { - THROW_HR(E_NOTIMPL);; + THROW_HR(E_NOTIMPL); } } } diff --git a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h index 5dea31e523..210d5a90ba 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h +++ b/src/Microsoft.Management.Configuration/ConfigurationSetSerializer.h @@ -12,7 +12,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { struct ConfigurationSetSerializer { - static std::unique_ptr CreateSerializer(hstring version); + static std::unique_ptr CreateSerializer(hstring version, bool forHistory = false); virtual ~ConfigurationSetSerializer() noexcept = default; @@ -24,11 +24,18 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Serializes a configuration set to the original yaml string. virtual hstring Serialize(ConfigurationSet*) = 0; + // Serializes a value set only. + std::string SerializeValueSet(const Windows::Foundation::Collections::ValueSet& valueSet); + + // Serializes a value set only. + std::string SerializeStringArray(const Windows::Foundation::Collections::IVector& stringArray); + protected: ConfigurationSetSerializer() = default; void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, std::initializer_list exclusions = {}); + void WriteYamlStringArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector& values); void WriteYamlValue(AppInstaller::YAML::Emitter& emitter, const winrt::Windows::Foundation::IInspectable& value); void WriteYamlValueSetAsArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSetArray); winrt::hstring GetSchemaVersionComment(winrt::hstring version); diff --git a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp index 8ceebb8ac1..78065ebb16 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp @@ -14,12 +14,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation { Configuration::ConfigurationUnit ConfigurationStaticFunctions::CreateConfigurationUnit() { - return *make_self>(); + return *make_self(); } Configuration::ConfigurationSet ConfigurationStaticFunctions::CreateConfigurationSet() { - return *make_self>(); + return *make_self(); } Windows::Foundation::IAsyncOperation ConfigurationStaticFunctions::CreateConfigurationSetProcessorFactoryAsync(hstring const& handler) diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp b/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp index decb149c83..25043da53e 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp +++ b/src/Microsoft.Management.Configuration/ConfigurationUnit.cpp @@ -157,7 +157,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation Configuration::ConfigurationUnit ConfigurationUnit::Copy() { - auto result = make_self>(); + auto result = make_self(); result->m_type = m_type; result->m_intent = m_intent; diff --git a/src/Microsoft.Management.Configuration/ConfigurationUnit.h b/src/Microsoft.Management.Configuration/ConfigurationUnit.h index 4050c3e40f..25b494f35b 100644 --- a/src/Microsoft.Management.Configuration/ConfigurationUnit.h +++ b/src/Microsoft.Management.Configuration/ConfigurationUnit.h @@ -3,12 +3,13 @@ #pragma once #include "ConfigurationUnit.g.h" #include +#include #include #include namespace winrt::Microsoft::Management::Configuration::implementation { - struct ConfigurationUnit : ConfigurationUnitT>, AppInstaller::WinRT::LifetimeWatcherBase + struct ConfigurationUnit : ConfigurationUnitT>, AppInstaller::WinRT::LifetimeWatcherBase, AppInstaller::WinRT::ModuleCountBase { ConfigurationUnit(); @@ -83,4 +84,4 @@ namespace winrt::Microsoft::Management::Configuration::factory_implementation { }; } -#endif \ No newline at end of file +#endif diff --git a/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp new file mode 100644 index 0000000000..0b866501f6 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Database/ConfigurationDatabase.h" +#include "Database/Schema/IConfigurationDatabase.h" +#include +#include "Filesystem.h" + +using namespace AppInstaller::SQLite; + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + namespace + { + // Use an alternate location for the dev build history. +#ifdef AICLI_DISABLE_TEST_HOOKS + constexpr std::string_view s_Database_DirectoryName = "History"sv; +#else + constexpr std::string_view s_Database_DirectoryName = "DevHistory"sv; +#endif + + constexpr std::string_view s_Database_FileName = "config.db"sv; + + #define s_Database_MutexName L"WindowsPackageManager_Configuration_DatabaseMutex" + } + + ConfigurationDatabase::ConfigurationDatabase() = default; + + ConfigurationDatabase::ConfigurationDatabase(ConfigurationDatabase&&) = default; + ConfigurationDatabase& ConfigurationDatabase::operator=(ConfigurationDatabase&&) = default; + + ConfigurationDatabase::~ConfigurationDatabase() = default; + + void ConfigurationDatabase::EnsureOpened(bool createIfNeeded) + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + if (!m_database) + { + std::filesystem::path databaseDirectory = AppInstaller::Filesystem::GetPathTo(PathName::LocalState) / s_Database_DirectoryName; + std::filesystem::path databaseFile = databaseDirectory / s_Database_FileName; + + { + wil::unique_mutex databaseMutex; + databaseMutex.create(s_Database_MutexName); + auto databaseLock = databaseMutex.acquire(); + + if (!std::filesystem::is_regular_file(databaseFile) && createIfNeeded) + { + if (std::filesystem::exists(databaseFile)) + { + std::filesystem::remove_all(databaseDirectory); + } + + std::filesystem::create_directories(databaseDirectory); + + m_connection = std::make_shared(databaseFile, IConfigurationDatabase::GetLatestVersion()); + m_database = IConfigurationDatabase::CreateFor(m_connection); + m_database->InitializeDatabase(); + } + } + + if (!m_database && std::filesystem::is_regular_file(databaseFile)) + { + m_connection = std::make_shared(databaseFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + m_database = IConfigurationDatabase::CreateFor(m_connection); + } + } +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + std::vector ConfigurationDatabase::GetSetHistory() const + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + if (!m_database) + { + return {}; + } + + auto transaction = BeginTransaction("GetSetHistory"); + return m_database->GetSets(); +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); + + return {}; +#endif + } + + void ConfigurationDatabase::WriteSetHistory(const Configuration::ConfigurationSet& configurationSet, bool preferNewHistory) + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + THROW_HR_IF_NULL(E_POINTER, configurationSet); + THROW_HR_IF_NULL(E_NOT_VALID_STATE, m_database); + + auto transaction = BeginTransaction("WriteSetHistory"); + + std::optional setRowId = m_database->GetSetRowId(configurationSet.InstanceIdentifier()); + + if (!setRowId && !preferNewHistory) + { + // TODO: Use conflict detection code to check for a matching set + } + + if (setRowId) + { + m_database->UpdateSet(setRowId.value(), configurationSet); + } + else + { + m_database->AddSet(configurationSet); + } + + m_connection->SetLastWriteTime(); + + transaction->Commit(); +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + void ConfigurationDatabase::RemoveSetHistory(const Configuration::ConfigurationSet& configurationSet) + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + THROW_HR_IF_NULL(E_POINTER, configurationSet); + + if (!m_database) + { + return; + } + + auto transaction = BeginTransaction("RemoveSetHistory"); + + std::optional setRowId = m_database->GetSetRowId(configurationSet.InstanceIdentifier()); + + if (!setRowId) + { + // TODO: Use conflict detection code to check for a matching set + } + + if (setRowId) + { + m_database->RemoveSet(setRowId.value()); + m_connection->SetLastWriteTime(); + } + + transaction->Commit(); +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + ConfigurationDatabase::TransactionLock ConfigurationDatabase::BeginTransaction(std::string_view name) const + { + THROW_HR_IF_NULL(E_NOT_VALID_STATE, m_connection); + + TransactionLock result = m_connection->TryBeginTransaction(name); + + while (!result) + { + { + auto connectionLock = m_connection->LockConnection(); + m_database = IConfigurationDatabase::CreateFor(m_connection); + } + + result = m_connection->TryBeginTransaction(name); + } + + return result; + } +} diff --git a/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h new file mode 100644 index 0000000000..798adefa90 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ConfigurationSet.h" +#include +#include +#include +#include + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Forward declaration of internal interface. + struct IConfigurationDatabase; + + // Allows access to the configuration database. + struct ConfigurationDatabase + { + using ConfigurationSetPtr = winrt::com_ptr; + + ConfigurationDatabase(); + + ConfigurationDatabase(const ConfigurationDatabase&) = delete; + ConfigurationDatabase& operator=(const ConfigurationDatabase&) = delete; + + ConfigurationDatabase(ConfigurationDatabase&&); + ConfigurationDatabase& operator=(ConfigurationDatabase&&); + + ~ConfigurationDatabase(); + + // Ensures that the database connection is established and the schema interface is created appropriately. + // If `createIfNeeded` is false, this function will not create the database if it does not exist. + // If not connected, any read methods will return empty results and any write methods will throw. + void EnsureOpened(bool createIfNeeded = true); + + // Gets all of the configuration sets from the database. + std::vector GetSetHistory() const; + + // Writes the given set to the database history, attempting to merge with a matching set if one exists unless preferNewHistory is true. + void WriteSetHistory(const Configuration::ConfigurationSet& configurationSet, bool preferNewHistory); + + // Removes the given set from the database history if it is present. + void RemoveSetHistory(const Configuration::ConfigurationSet& configurationSet); + + private: + std::shared_ptr m_connection; + mutable std::unique_ptr m_database; + + using TransactionLock = decltype(m_connection->TryBeginTransaction({})); + + // Begins a transaction, which may require upgrading to a newer schema version. + TransactionLock BeginTransaction(std::string_view name) const; + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h new file mode 100644 index 0000000000..e39077ecff --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Database/Schema/IConfigurationDatabase.h" + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + struct Interface : public IConfigurationDatabase + { + Interface(std::shared_ptr storage); + + // Version 0.1 + void InitializeDatabase() override; + void AddSet(const Configuration::ConfigurationSet& configurationSet) override; + void UpdateSet(AppInstaller::SQLite::rowid_t target, const Configuration::ConfigurationSet& configurationSet) override; + void RemoveSet(AppInstaller::SQLite::rowid_t target) override; + std::vector GetSets() override; + std::optional GetSetRowId(const GUID& instanceIdentifier) override; + + private: + std::shared_ptr m_storage; + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp new file mode 100644 index 0000000000..f1a27006a5 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Interface.h" +#include "SetInfoTable.h" +#include "UnitInfoTable.h" + +using namespace AppInstaller::SQLite; +using namespace AppInstaller::Utility; + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + Interface::Interface(std::shared_ptr storage) : + m_storage(std::move(storage)) + {} + + void Interface::InitializeDatabase() + { + // Must enable WAL mode outside of a transaction + THROW_HR_IF(E_UNEXPECTED, !m_storage->GetConnection().SetJournalMode("WAL")); + + Savepoint savepoint = Savepoint::Create(*m_storage, "InitializeDatabase_0_1"); + + SetInfoTable setInfoTable(*m_storage); + setInfoTable.Create(); + + UnitInfoTable unitInfoTable(*m_storage); + unitInfoTable.Create(); + + savepoint.Commit(); + } + + void Interface::AddSet(const Configuration::ConfigurationSet& configurationSet) + { + Savepoint savepoint = Savepoint::Create(*m_storage, "AddSet_0_1"); + + SetInfoTable setInfoTable(*m_storage); + setInfoTable.Add(configurationSet); + + savepoint.Commit(); + } + + void Interface::UpdateSet(rowid_t target, const Configuration::ConfigurationSet& configurationSet) + { + Savepoint savepoint = Savepoint::Create(*m_storage, "UpdateSet_0_1"); + + SetInfoTable setInfoTable(*m_storage); + setInfoTable.Update(target, configurationSet); + + savepoint.Commit(); + } + + void Interface::RemoveSet(rowid_t target) + { + Savepoint savepoint = Savepoint::Create(*m_storage, "RemoveSet_0_1"); + + SetInfoTable setInfoTable(*m_storage); + setInfoTable.Remove(target); + + savepoint.Commit(); + } + + std::vector Interface::GetSets() + { + SetInfoTable setInfoTable(*m_storage); + return setInfoTable.GetAllSets(); + } + + std::optional Interface::GetSetRowId(const GUID& instanceIdentifier) + { + SetInfoTable setInfoTable(*m_storage); + return setInfoTable.GetSetRowId(instanceIdentifier); + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp new file mode 100644 index 0000000000..a959ef17f3 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "SetInfoTable.h" +#include "UnitInfoTable.h" +#include "ConfigurationSetSerializer.h" +#include "ConfigurationSetParser.h" +#include +#include +#include + +using namespace AppInstaller::SQLite; +using namespace AppInstaller::SQLite::Builder; +using namespace AppInstaller::Utility; + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + namespace + { + constexpr std::string_view s_SetInfoTable_Table = "set_info"sv; + + constexpr std::string_view s_SetInfoTable_Column_InstanceIdentifier = "instance_identifier"sv; + constexpr std::string_view s_SetInfoTable_Column_Name = "name"sv; + constexpr std::string_view s_SetInfoTable_Column_Origin = "origin"sv; + constexpr std::string_view s_SetInfoTable_Column_Path = "path"sv; + constexpr std::string_view s_SetInfoTable_Column_FirstApply = "first_apply"sv; + constexpr std::string_view s_SetInfoTable_Column_SchemaVersion = "schema_version"sv; + constexpr std::string_view s_SetInfoTable_Column_Metadata = "metadata"sv; + constexpr std::string_view s_SetInfoTable_Column_Parameters = "parameters"sv; + constexpr std::string_view s_SetInfoTable_Column_Variables = "variables"sv; + } + + SetInfoTable::SetInfoTable(Connection& connection) : m_connection(connection) {} + + void SetInfoTable::Create() + { + Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Create_0_1"); + + StatementBuilder tableBuilder; + tableBuilder.CreateTable(s_SetInfoTable_Table).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(s_SetInfoTable_Column_InstanceIdentifier, Type::Blob).Unique().NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Name, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Origin, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Path, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_FirstApply, Type::Int64).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_SchemaVersion, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Metadata, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Parameters, Type::Text).NotNull(), + ColumnBuilder(s_SetInfoTable_Column_Variables, Type::Text).NotNull(), + }); + + tableBuilder.Execute(m_connection); + + savepoint.Commit(); + } + + rowid_t SetInfoTable::Add(const Configuration::ConfigurationSet& configurationSet) + { + THROW_HR_IF(E_NOTIMPL, configurationSet.Parameters().Size() > 0); + + Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Add_0_1"); + + hstring schemaVersion = configurationSet.SchemaVersion(); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + + StatementBuilder builder; + builder.InsertInto(s_SetInfoTable_Table).Columns({ + s_SetInfoTable_Column_InstanceIdentifier, + s_SetInfoTable_Column_Name, + s_SetInfoTable_Column_Origin, + s_SetInfoTable_Column_Path, + s_SetInfoTable_Column_FirstApply, + s_SetInfoTable_Column_SchemaVersion, + s_SetInfoTable_Column_Metadata, + s_SetInfoTable_Column_Parameters, + s_SetInfoTable_Column_Variables, + }).Values( + static_cast(configurationSet.InstanceIdentifier()), + ConvertToUTF8(configurationSet.Name()), + ConvertToUTF8(configurationSet.Origin()), + ConvertToUTF8(configurationSet.Path()), + GetCurrentUnixEpoch(), + ConvertToUTF8(schemaVersion), + serializer->SerializeValueSet(configurationSet.Metadata()), + std::string{}, // Parameters + serializer->SerializeValueSet(configurationSet.Variables()) + ); + + builder.Execute(m_connection); + rowid_t result = m_connection.GetLastInsertRowID(); + + UnitInfoTable unitInfoTable(m_connection); + + auto winrtUnits = configurationSet.Units(); + std::vector units{ winrtUnits.Size() }; + winrtUnits.GetMany(0, units); + + for (const auto& unit : units) + { + unitInfoTable.Add(unit, result, schemaVersion); + } + + savepoint.Commit(); + return result; + } + + void SetInfoTable::Update(rowid_t target, const Configuration::ConfigurationSet& configurationSet) + { + THROW_HR_IF(E_NOTIMPL, configurationSet.Parameters().Size() > 0); + + Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Update_0_1"); + + hstring schemaVersion = configurationSet.SchemaVersion(); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + + StatementBuilder builder; + builder.Update(s_SetInfoTable_Table).Set(). + Column(s_SetInfoTable_Column_Name).Equals(ConvertToUTF8(configurationSet.Name())). + Column(s_SetInfoTable_Column_Origin).Equals(ConvertToUTF8(configurationSet.Origin())). + Column(s_SetInfoTable_Column_Path).Equals(ConvertToUTF8(configurationSet.Path())). + Column(s_SetInfoTable_Column_SchemaVersion).Equals(ConvertToUTF8(schemaVersion)). + Column(s_SetInfoTable_Column_Metadata).Equals(serializer->SerializeValueSet(configurationSet.Metadata())). + Column(s_SetInfoTable_Column_Variables).Equals(serializer->SerializeValueSet(configurationSet.Variables())). + Where(RowIDName).Equals(target); + + builder.Execute(m_connection); + + UnitInfoTable unitInfoTable(m_connection); + unitInfoTable.UpdateForSet(target, configurationSet.Units(), schemaVersion); + + savepoint.Commit(); + } + + void SetInfoTable::Remove(rowid_t target) + { + Savepoint savepoint = Savepoint::Create(m_connection, "SetInfoTable_Remove_0_1"); + + StatementBuilder builder; + builder.DeleteFrom(s_SetInfoTable_Table).Where(RowIDName).Equals(target); + builder.Execute(m_connection); + + UnitInfoTable unitInfoTable(m_connection); + unitInfoTable.RemoveForSet(target); + + savepoint.Commit(); + } + + std::vector SetInfoTable::GetAllSets() + { + std::vector result; + + StatementBuilder builder; + builder.Select({ + RowIDName, // 0 + s_SetInfoTable_Column_InstanceIdentifier, // 1 + s_SetInfoTable_Column_Name, // 2 + s_SetInfoTable_Column_Origin, // 3 + s_SetInfoTable_Column_Path, // 4 + s_SetInfoTable_Column_FirstApply, // 5 + s_SetInfoTable_Column_SchemaVersion, // 6 + s_SetInfoTable_Column_Metadata, // 7 + s_SetInfoTable_Column_Parameters, // 8 + s_SetInfoTable_Column_Variables, // 9 + }).From(s_SetInfoTable_Table); + + Statement getAllSets = builder.Prepare(m_connection); + + UnitInfoTable unitInfoTable(m_connection); + + while (getAllSets.Step()) + { + auto configurationSet = make_self(getAllSets.GetColumn(1)); + + configurationSet->Name(hstring{ ConvertToUTF16(getAllSets.GetColumn(2)) }); + configurationSet->Origin(hstring{ ConvertToUTF16(getAllSets.GetColumn(3)) }); + configurationSet->Path(hstring{ ConvertToUTF16(getAllSets.GetColumn(4)) }); + configurationSet->FirstApply(clock::from_sys(ConvertUnixEpochToSystemClock(getAllSets.GetColumn(5)))); + + std::string schemaVersion = getAllSets.GetColumn(6); + configurationSet->SchemaVersion(hstring{ ConvertToUTF16(schemaVersion) }); + + auto parser = ConfigurationSetParser::CreateForSchemaVersion(schemaVersion); + configurationSet->Metadata(parser->ParseValueSet(getAllSets.GetColumn(7))); + THROW_HR_IF(E_NOTIMPL, !getAllSets.GetColumn(8).empty()); + configurationSet->Variables(parser->ParseValueSet(getAllSets.GetColumn(9))); + + std::vector winrtUnits; + for (const auto& unit : unitInfoTable.GetAllUnitsForSet(getAllSets.GetColumn(0), schemaVersion)) + { + winrtUnits.emplace_back(*unit); + } + configurationSet->Units(std::move(winrtUnits)); + + result.emplace_back(std::move(configurationSet)); + } + + return result; + } + + std::optional SetInfoTable::GetSetRowId(const GUID& instanceIdentifier) + { + StatementBuilder builder; + builder.Select(RowIDName).From(s_SetInfoTable_Table).Where(s_SetInfoTable_Column_InstanceIdentifier).Equals(instanceIdentifier); + + Statement select = builder.Prepare(m_connection); + + if (select.Step()) + { + return select.GetColumn(0); + } + + return std::nullopt; + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.h b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.h new file mode 100644 index 0000000000..648582d618 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.h @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winrt/Microsoft.Management.Configuration.h" +#include "Database/Schema/IConfigurationDatabase.h" +#include +#include +#include + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + struct SetInfoTable + { + SetInfoTable(AppInstaller::SQLite::Connection& connection); + + // Creates the set info table. + void Create(); + + // Adds the given configuration set to the table. + // Returns the row id of the added set. + AppInstaller::SQLite::rowid_t Add(const Configuration::ConfigurationSet& configurationSet); + + // Updates the set with the target row id using the given set. + void Update(AppInstaller::SQLite::rowid_t target, const Configuration::ConfigurationSet& configurationSet); + + // Removes the set with the target row id. + void Remove(AppInstaller::SQLite::rowid_t target); + + // Gets all of the sets from the table. + std::vector GetAllSets(); + + // Gets the row id of the set with the given instance identifier. + std::optional GetSetRowId(const GUID& instanceIdentifier); + + private: + AppInstaller::SQLite::Connection& m_connection; + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp new file mode 100644 index 0000000000..5cc1e00c56 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "UnitInfoTable.h" +#include "ConfigurationUnit.h" +#include "ConfigurationSetParser.h" +#include "ConfigurationSetSerializer.h" +#include +#include +#include + +using namespace AppInstaller::SQLite; +using namespace AppInstaller::SQLite::Builder; +using namespace AppInstaller::Utility; + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + namespace + { + constexpr std::string_view s_UnitInfoTable_Table = "unit_info"sv; + constexpr std::string_view s_UnitInfoTable_SetRowIdIndex = "unit_info_set_idx"sv; + + constexpr std::string_view s_UnitInfoTable_Column_SetRowId = "set_rowid"sv; + constexpr std::string_view s_UnitInfoTable_Column_ParentRowId = "parent_rowid"sv; + constexpr std::string_view s_UnitInfoTable_Column_InstanceIdentifier = "instance_identifier"sv; + constexpr std::string_view s_UnitInfoTable_Column_Type = "type"sv; + constexpr std::string_view s_UnitInfoTable_Column_Identifier = "identifier"sv; + constexpr std::string_view s_UnitInfoTable_Column_Intent = "intent"sv; + constexpr std::string_view s_UnitInfoTable_Column_Dependencies = "dependencies"sv; + constexpr std::string_view s_UnitInfoTable_Column_Metadata = "metadata"sv; + constexpr std::string_view s_UnitInfoTable_Column_Settings = "settings"sv; + constexpr std::string_view s_UnitInfoTable_Column_IsActive = "is_active"sv; + constexpr std::string_view s_UnitInfoTable_Column_IsGroup = "is_group"sv; + } + + UnitInfoTable::UnitInfoTable(Connection& connection) : m_connection(connection) {} + + void UnitInfoTable::Create() + { + Savepoint savepoint = Savepoint::Create(m_connection, "UnitInfoTable_Create_0_1"); + + StatementBuilder tableBuilder; + tableBuilder.CreateTable(s_UnitInfoTable_Table).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(s_UnitInfoTable_Column_SetRowId, Type::RowId).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_ParentRowId, Type::RowId), + ColumnBuilder(s_UnitInfoTable_Column_InstanceIdentifier, Type::Blob).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Type, Type::Text).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Identifier, Type::Text).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Intent, Type::Int).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Dependencies, Type::Text).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Metadata, Type::Text).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_Settings, Type::Text).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_IsActive, Type::Bool).NotNull(), + ColumnBuilder(s_UnitInfoTable_Column_IsGroup, Type::Bool).NotNull(), + }); + + tableBuilder.Execute(m_connection); + + StatementBuilder indexBuilder; + indexBuilder.CreateIndex(s_UnitInfoTable_SetRowIdIndex).On(s_UnitInfoTable_Table).Columns(s_UnitInfoTable_Column_SetRowId); + + indexBuilder.Execute(m_connection); + + savepoint.Commit(); + } + + void UnitInfoTable::Add(const Configuration::ConfigurationUnit& configurationUnit, AppInstaller::SQLite::rowid_t setRowId, hstring schemaVersion) + { + Savepoint savepoint = Savepoint::Create(m_connection, "UnitInfoTable_Add_0_1"); + + StatementBuilder builder; + builder.InsertInto(s_UnitInfoTable_Table).Columns({ + s_UnitInfoTable_Column_SetRowId, + s_UnitInfoTable_Column_ParentRowId, + s_UnitInfoTable_Column_InstanceIdentifier, + s_UnitInfoTable_Column_Type, + s_UnitInfoTable_Column_Identifier, + s_UnitInfoTable_Column_Intent, + s_UnitInfoTable_Column_Dependencies, + s_UnitInfoTable_Column_Metadata, + s_UnitInfoTable_Column_Settings, + s_UnitInfoTable_Column_IsActive, + s_UnitInfoTable_Column_IsGroup, + }).Values( + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound, + Unbound + ); + + Statement insertStatement = builder.Prepare(m_connection); + + struct UnitsToInsert + { + std::optional Parent; + Configuration::ConfigurationUnit Unit; + }; + + std::queue unitsToInsert; + unitsToInsert.emplace(UnitsToInsert{ std::nullopt, configurationUnit }); + auto serializer = ConfigurationSetSerializer::CreateSerializer(schemaVersion, true); + + while (!unitsToInsert.empty()) + { + const auto& current = unitsToInsert.front(); + + insertStatement.Reset(); + + bool isGroup = current.Unit.IsGroup(); + + insertStatement.Bind(1, setRowId); + insertStatement.Bind(2, current.Parent); + insertStatement.Bind(3, static_cast(current.Unit.InstanceIdentifier())); + insertStatement.Bind(4, ConvertToUTF8(current.Unit.Type())); + insertStatement.Bind(5, ConvertToUTF8(current.Unit.Identifier())); + insertStatement.Bind(6, AppInstaller::ToIntegral(current.Unit.Intent())); + insertStatement.Bind(7, serializer->SerializeStringArray(current.Unit.Dependencies())); + insertStatement.Bind(8, serializer->SerializeValueSet(current.Unit.Metadata())); + insertStatement.Bind(9, serializer->SerializeValueSet(current.Unit.Settings())); + insertStatement.Bind(10, current.Unit.IsActive()); + insertStatement.Bind(11, isGroup); + + insertStatement.Execute(); + + if (isGroup) + { + rowid_t currentRowId = m_connection.GetLastInsertRowID(); + + auto winrtUnits = current.Unit.Units(); + std::vector units{ winrtUnits.Size() }; + winrtUnits.GetMany(0, units); + + for (const auto& unit : units) + { + unitsToInsert.emplace(UnitsToInsert{ currentRowId, unit }); + } + } + + unitsToInsert.pop(); + } + + savepoint.Commit(); + } + + void UnitInfoTable::UpdateForSet(AppInstaller::SQLite::rowid_t target, const Windows::Foundation::Collections::IVector& winrtUnits, hstring schemaVersion) + { + Savepoint savepoint = Savepoint::Create(m_connection, "UnitInfoTable_UpdateForSet_0_1"); + + RemoveForSet(target); + + std::vector units{ winrtUnits.Size() }; + winrtUnits.GetMany(0, units); + + for (const auto& unit : units) + { + Add(unit, target, schemaVersion); + } + + savepoint.Commit(); + } + + void UnitInfoTable::RemoveForSet(AppInstaller::SQLite::rowid_t target) + { + StatementBuilder builder; + builder.DeleteFrom(s_UnitInfoTable_Table).Where(s_UnitInfoTable_Column_SetRowId).Equals(target); + builder.Execute(m_connection); + } + + std::vector UnitInfoTable::GetAllUnitsForSet(AppInstaller::SQLite::rowid_t setRowId, std::string_view schemaVersion) + { + StatementBuilder builder; + builder.Select({ + RowIDName, // 0 + s_UnitInfoTable_Column_ParentRowId, // 1 + s_UnitInfoTable_Column_InstanceIdentifier, // 2 + s_UnitInfoTable_Column_Type, // 3 + s_UnitInfoTable_Column_Identifier, // 4 + s_UnitInfoTable_Column_Intent, // 5 + s_UnitInfoTable_Column_Dependencies, // 6 + s_UnitInfoTable_Column_Metadata, // 7 + s_UnitInfoTable_Column_Settings, // 8 + s_UnitInfoTable_Column_IsActive, // 9 + s_UnitInfoTable_Column_IsGroup, // 10 + }).From(s_UnitInfoTable_Table).Where(s_UnitInfoTable_Column_SetRowId).Equals(setRowId); + + Statement statement = builder.Prepare(m_connection); + + std::vector result; + std::map rowToUnitMap; + auto parser = ConfigurationSetParser::CreateForSchemaVersion(std::string{ schemaVersion }); + + while (statement.Step()) + { + auto unit = make_self(statement.GetColumn(2)); + + unit->Type(hstring{ ConvertToUTF16(statement.GetColumn(3)) }); + unit->Identifier(hstring{ ConvertToUTF16(statement.GetColumn(4)) }); + unit->Intent(statement.GetColumn(5)); + unit->Dependencies(parser->ParseStringArray(statement.GetColumn(6))); + unit->Metadata(parser->ParseValueSet(statement.GetColumn(7))); + unit->Settings(parser->ParseValueSet(statement.GetColumn(8))); + unit->IsActive(statement.GetColumn(9)); + unit->IsGroup(statement.GetColumn(10)); + + if (statement.GetColumnIsNull(1)) + { + result.emplace_back(unit); + } + else + { + rowToUnitMap.at(statement.GetColumn(1))->Units().Append(*unit); + } + + rowToUnitMap.emplace(statement.GetColumn(0), unit); + } + + return result; + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.h b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.h new file mode 100644 index 0000000000..1bce7700fe --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winrt/Microsoft.Management.Configuration.h" +#include "Database/Schema/IConfigurationDatabase.h" +#include + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 +{ + struct UnitInfoTable + { + UnitInfoTable(AppInstaller::SQLite::Connection& connection); + + // Creates the unit info table. + void Create(); + + // Adds the given configuration unit to the table. + void Add(const Configuration::ConfigurationUnit& configurationUnit, AppInstaller::SQLite::rowid_t setRowId, hstring schemaVersion); + + // Updates the units for the target set. + void UpdateForSet(AppInstaller::SQLite::rowid_t target, const Windows::Foundation::Collections::IVector& units, hstring schemaVersion); + + // Removes the units from the target set. + void RemoveForSet(AppInstaller::SQLite::rowid_t target); + + // Gets all of the units for the given set. + std::vector GetAllUnitsForSet(AppInstaller::SQLite::rowid_t setRowId, std::string_view schemaVersion); + + private: + AppInstaller::SQLite::Connection& m_connection; + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp b/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp new file mode 100644 index 0000000000..6162bf7c11 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Database/Schema/IConfigurationDatabase.h" + +#include "Database/Schema/0_1/Interface.h" + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + AppInstaller::SQLite::Version IConfigurationDatabase::GetLatestVersion() + { + return { 0, 1 }; + } + + std::unique_ptr IConfigurationDatabase::CreateFor(std::shared_ptr storage) + { + using StorageT = std::shared_ptr; + const AppInstaller::SQLite::Version& version = storage->GetVersion(); + + if (version.MajorVersion == 0) + { + constexpr std::array(*)(StorageT&& s), 1> versionCreatorMap = + { + [](StorageT&& s) { return std::unique_ptr(std::make_unique(std::move(s))); }, + }; + + size_t minorVersion = static_cast(version.MinorVersion); + if (minorVersion >= 1 && minorVersion <= versionCreatorMap.size()) + { + return versionCreatorMap[minorVersion - 1](std::move(storage)); + } + } + + // We do not have the capacity to operate on this schema version + THROW_WIN32(ERROR_NOT_SUPPORTED); + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h b/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h new file mode 100644 index 0000000000..b78c1f18f8 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "winrt/Microsoft.Management.Configuration.h" +#include "ConfigurationSet.h" +#include "ConfigurationUnit.h" +#include +#include +#include +#include + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Interface for interacting with the configuration database. + struct IConfigurationDatabase + { + using ConfigurationSetPtr = winrt::com_ptr; + using ConfigurationUnitPtr = winrt::com_ptr; + + virtual ~IConfigurationDatabase() = default; + + // Gets the latest schema version for the configuration database. + static AppInstaller::SQLite::Version GetLatestVersion(); + + // Creates the version appropriate database object for the given storage. + static std::unique_ptr CreateFor(std::shared_ptr storage); + + // Version 0.1 + + // Acts on a database that has been created but contains no tables beyond metadata. + virtual void InitializeDatabase() = 0; + + // Adds the given set to the database. + virtual void AddSet(const Configuration::ConfigurationSet& configurationSet) = 0; + + // Updates the set with the given row id using the given set. + virtual void UpdateSet(AppInstaller::SQLite::rowid_t target, const Configuration::ConfigurationSet& configurationSet) = 0; + + // Removes the set with the given row id from the database. + virtual void RemoveSet(AppInstaller::SQLite::rowid_t target) = 0; + + // Gets all of the sets in the database. + virtual std::vector GetSets() = 0; + + // Gets the row id of the set with the given instance identifier, if present. + virtual std::optional GetSetRowId(const GUID& instanceIdentifier) = 0; + }; +} diff --git a/src/Microsoft.Management.Configuration/Filesystem.cpp b/src/Microsoft.Management.Configuration/Filesystem.cpp new file mode 100644 index 0000000000..bf22896c5d --- /dev/null +++ b/src/Microsoft.Management.Configuration/Filesystem.cpp @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Filesystem.h" + +using namespace std::string_view_literals; + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + namespace anon + { + constexpr std::string_view s_Configuration_LocalState = "Configuration"sv; + } + + AppInstaller::Filesystem::PathDetails GetPathDetailsFor(PathName path, bool forDisplay) + { + AppInstaller::Filesystem::PathDetails result; + // We should not create directories by default when they are retrieved for display purposes. + result.Create = !forDisplay; + + switch (path) + { + case PathName::LocalState: + result = GetPathDetailsFor(AppInstaller::Filesystem::PathName::UnpackagedLocalStateRoot, forDisplay); + result.Path /= anon::s_Configuration_LocalState; + break; + default: + THROW_HR(E_UNEXPECTED); + } + + return result; + } +} diff --git a/src/Microsoft.Management.Configuration/Filesystem.h b/src/Microsoft.Management.Configuration/Filesystem.h new file mode 100644 index 0000000000..934066af69 --- /dev/null +++ b/src/Microsoft.Management.Configuration/Filesystem.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Paths used by configuration. + enum class PathName + { + // Local state root for configuration. + LocalState, + }; + + // Gets the PathDetails used for the given path. + // This is exposed primarily to allow for testing, GetPathTo should be preferred. + AppInstaller::Filesystem::PathDetails GetPathDetailsFor(PathName path, bool forDisplay = false); +} diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj index f8f6102d10..6b284b9a77 100644 --- a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj +++ b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -131,7 +131,7 @@ false Microsoft_Management_Configuration.def $(OutDir)$(ProjectName).winmd - Advapi32.lib;onecoreuap.lib;%(AdditionalDependencies) + Advapi32.lib;onecoreuap.lib;winsqlite3.lib;%(AdditionalDependencies) @@ -221,9 +221,15 @@ + + + + + + @@ -262,8 +268,14 @@ + + + + + + @@ -308,4 +320,4 @@ - + \ No newline at end of file diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters index 5c2caffcba..78f8641c15 100644 --- a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters +++ b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -111,6 +111,24 @@ Parser + + Database + + + Database\Schema + + + Database\Schema\0_1 + + + Internals + + + Database\Schema\0_1 + + + Database\Schema\0_1 + @@ -231,6 +249,24 @@ Parser + + Database + + + Database\Schema + + + Database\Schema\0_1 + + + Internals + + + Database\Schema\0_1 + + + Database\Schema\0_1 + @@ -256,6 +292,15 @@ {5a02f1a5-14f3-4a28-8bed-212f3e6b1a00} + + {c82c1df2-4ef3-4d54-9c18-a13ade2ab16a} + + + {6f544d8a-2c3f-4d26-9b53-84dbd2144d43} + + + {efb71f71-31e4-42db-9105-f10c2e89e1d5} + diff --git a/src/Microsoft.Management.Configuration/pch.h b/src/Microsoft.Management.Configuration/pch.h index 830dec73f5..0eadcb302a 100644 --- a/src/Microsoft.Management.Configuration/pch.h +++ b/src/Microsoft.Management.Configuration/pch.h @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once @@ -18,6 +18,7 @@ #pragma warning( pop ) #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include #include +#include #include #include #include diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs index ce79ea7c72..244a5162c0 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -22,16 +22,33 @@ public abstract class OpenConfiguration : PSCmdlet Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, - ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + ParameterSetName = Constants.ParameterSet.OpenConfigurationSetFromFile)] public string File { get; set; } + /// + /// Gets or sets the configuration history item identifier. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSet.OpenConfigurationSetFromHistory)] + public string InstanceIdentifier { get; set; } + + /// + /// Gets or sets a value indicating whether all configuration history items should be returned. + /// + [Parameter( + Mandatory = true, + ParameterSetName = Constants.ParameterSet.OpenAllConfigurationSetsFromHistory)] + public SwitchParameter All { get; set; } + /// /// Gets or sets custom location to install modules. /// [Parameter( Position = 1, - ValueFromPipelineByPropertyName = true, - ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + ValueFromPipelineByPropertyName = true)] public string ModulePath { get; set; } /// diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs new file mode 100644 index 0000000000..fbe0d65804 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// + /// ConvertTo-WinGetConfigurationYaml + /// Serializes a PSConfigurationSet to a YAML string. + /// + [Cmdlet(VerbsData.ConvertTo, "WinGetConfigurationYaml")] + public sealed class ConvertToWinGetConfigurationYamlCmdlet : PSCmdlet + { + /// + /// Gets or sets the configuration set. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// + /// Converts the given set to a string. + /// + protected override void ProcessRecord() + { + var configCommand = new ConfigurationCommand(this); + configCommand.Serialize(this.Set); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs index f77b3f80ab..03cf4162a9 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -14,7 +14,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Get-WinGetConfiguration. /// Opens a configuration set. /// - [Cmdlet(VerbsCommon.Get, "WinGetConfiguration")] + [Cmdlet(VerbsCommon.Get, "WinGetConfiguration", DefaultParameterSetName = Helpers.Constants.ParameterSet.OpenConfigurationSetFromFile)] public sealed class GetWinGetConfigurationCmdlet : OpenConfiguration { /// @@ -23,11 +23,30 @@ public sealed class GetWinGetConfigurationCmdlet : OpenConfiguration protected override void ProcessRecord() { var configCommand = new ConfigurationCommand(this); - configCommand.Get( - this.File, - this.ModulePath, - this.ExecutionPolicy, - this.CanUseTelemetry); + + if (this.ParameterSetName == Helpers.Constants.ParameterSet.OpenConfigurationSetFromFile) + { + configCommand.Get( + this.File, + this.ModulePath, + this.ExecutionPolicy, + this.CanUseTelemetry); + } + else if (this.ParameterSetName == Helpers.Constants.ParameterSet.OpenConfigurationSetFromHistory) + { + configCommand.GetFromHistory( + this.InstanceIdentifier, + this.ModulePath, + this.ExecutionPolicy, + this.CanUseTelemetry); + } + else if (this.ParameterSetName == Helpers.Constants.ParameterSet.OpenAllConfigurationSetsFromHistory) + { + configCommand.GetAllFromHistory( + this.ModulePath, + this.ExecutionPolicy, + this.CanUseTelemetry); + } } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/RemoveWinGetConfigurationHistoryCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/RemoveWinGetConfigurationHistoryCmdlet.cs new file mode 100644 index 0000000000..5d12307af0 --- /dev/null +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/RemoveWinGetConfigurationHistoryCmdlet.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// + /// Remove-WinGetConfigurationHistory. + /// Removes the given configuration set from history. + /// + [Cmdlet(VerbsCommon.Remove, "WinGetConfigurationHistory")] + public sealed class RemoveWinGetConfigurationHistoryCmdlet : PSCmdlet + { + /// + /// Gets or sets the configuration set. + /// + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// + /// Removes the given set from history. + /// + protected override void ProcessRecord() + { + var configCommand = new ConfigurationCommand(this); + configCommand.Remove(this.Set); + } + } +} diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs index ee4ca5bfe9..4ad1e0b463 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -14,7 +14,10 @@ internal static class Constants #pragma warning disable SA1600 // ElementsMustBeDocumented internal static class ParameterSet { - internal const string OpenConfigurationSet = "OpenConfigurationSet"; + internal const string OpenConfigurationSetFromFile = "OpenConfigurationSetFromFile"; + internal const string OpenConfigurationSetFromString = "OpenConfigurationSetFromString"; + internal const string OpenConfigurationSetFromHistory = "OpenConfigurationSetFromHistory"; + internal const string OpenAllConfigurationSetsFromHistory = "OpenAllConfigurationSetsFromHistory"; } #pragma warning restore SA1600 // ElementsMustBeDocumented } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs index 46e117886e..3733932363 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs @@ -11,6 +11,7 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands using System.IO; using System.Linq; using System.Management.Automation; + using System.Text; using System.Threading.Tasks; using Microsoft.Management.Configuration; using Microsoft.Management.Configuration.Processor; @@ -94,11 +95,66 @@ public void Get( // Start task. var runningTask = this.RunOnMTA( + async () => + { + return (await this.OpenConfigurationSetAsync(openParams)) !; + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// + /// Open a configuration set from history. + /// + /// Instance identifier. + /// The module path to use. + /// Execution policy. + /// If telemetry can be used. + public void GetFromHistory( + string instanceIdentifier, + string modulePath, + ExecutionPolicy executionPolicy, + bool canUseTelemetry) + { + var openParams = new OpenConfigurationParameters( + this, instanceIdentifier, modulePath, executionPolicy, canUseTelemetry, fromHistory: true); + + // Start task. + var runningTask = this.RunOnMTA( async () => { return await this.OpenConfigurationSetAsync(openParams); }); + this.Wait(runningTask); + if (runningTask.Result != null) + { + this.Write(StreamType.Object, runningTask.Result); + } + } + + /// + /// Opens all configuration sets from history. + /// + /// The module path to use. + /// Execution policy. + /// If telemetry can be used. + public void GetAllFromHistory( + string modulePath, + ExecutionPolicy executionPolicy, + bool canUseTelemetry) + { + var openParams = new OpenConfigurationParameters( + this, modulePath, executionPolicy, canUseTelemetry); + + // Start task. + var runningTask = this.RunOnMTA( + async () => + { + return await this.GetConfigurationSetHistoryAsync(openParams); + }); + this.Wait(runningTask); this.Write(StreamType.Object, runningTask.Result); } @@ -272,6 +328,31 @@ public void Cancel(PSConfigurationJob psConfigurationJob) psConfigurationJob.StartCommand.Cancel(); } + /// + /// Removes a configuration set from history. + /// + /// PSConfiguration set. + public void Remove(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.Set.Remove(); + } + + /// + /// Serializes a configuration set and outputs the string. + /// + /// PSConfiguration set. + public void Serialize(PSConfigurationSet psConfigurationSet) + { + // Start task. + var result = this.RunOnMTA( + () => + { + return this.SerializeMTA(psConfigurationSet); + }); + + this.Write(StreamType.Object, result); + } + private void ContinueHelper(PSConfigurationJob psConfigurationJob) { // Signal the command that it can write to streams and wait for task. @@ -296,30 +377,72 @@ private IConfigurationSetProcessorFactory CreateFactory(OpenConfigurationParamet return factory; } - private async Task OpenConfigurationSetAsync(OpenConfigurationParameters openParams) + private async Task OpenConfigurationSetAsync(OpenConfigurationParameters openParams) { this.Write(StreamType.Verbose, Resources.ConfigurationInitializing); var psProcessor = new PSConfigurationProcessor(this.CreateFactory(openParams), this, openParams.CanUseTelemetry); - this.Write(StreamType.Verbose, Resources.ConfigurationReadingConfigFile); - var stream = await FileRandomAccessStream.OpenAsync(openParams.ConfigFile, FileAccessMode.Read); + if (!openParams.FromHistory) + { + this.Write(StreamType.Verbose, Resources.ConfigurationReadingConfigFile); + var stream = await FileRandomAccessStream.OpenAsync(openParams.ConfigFile, FileAccessMode.Read); + + OpenConfigurationSetResult openResult = await psProcessor.Processor.OpenConfigurationSetAsync(stream); + if (openResult.ResultCode != null) + { + throw new OpenConfigurationSetException(openResult, openParams.ConfigFile); + } + + var set = openResult.Set; - OpenConfigurationSetResult openResult = await psProcessor.Processor.OpenConfigurationSetAsync(stream); - if (openResult.ResultCode != null) + // This should match winget's OpenConfigurationSet or OpenConfigurationSetAsync + // should be modify to take the full path and handle it. + set.Name = Path.GetFileName(openParams.ConfigFile); + set.Origin = Path.GetDirectoryName(openParams.ConfigFile); + set.Path = openParams.ConfigFile; + + return new PSConfigurationSet(psProcessor, set); + } + else { - throw new OpenConfigurationSetException(openResult, openParams.ConfigFile); + Guid instanceIdentifier = Guid.Parse(openParams.ConfigFile); + + this.Write(StreamType.Verbose, Resources.ConfigurationReadingConfigHistory); + + var historySets = await psProcessor.Processor.GetConfigurationHistoryAsync(); + + ConfigurationSet? result = null; + foreach (var historySet in historySets) + { + if (historySet.InstanceIdentifier == instanceIdentifier) + { + result = historySet; + break; + } + } + + return result != null ? new PSConfigurationSet(psProcessor, result) : null; } + } - var set = openResult.Set; + private async Task GetConfigurationSetHistoryAsync(OpenConfigurationParameters openParams) + { + this.Write(StreamType.Verbose, Resources.ConfigurationInitializing); + + var psProcessor = new PSConfigurationProcessor(this.CreateFactory(openParams), this, openParams.CanUseTelemetry); - // This should match winget's OpenConfigurationSet or OpenConfigurationSetAsync - // should be modify to take the full path and handle it. - set.Name = Path.GetFileName(openParams.ConfigFile); - set.Origin = Path.GetDirectoryName(openParams.ConfigFile); - set.Path = openParams.ConfigFile; + this.Write(StreamType.Verbose, Resources.ConfigurationReadingConfigHistory); - return new PSConfigurationSet(psProcessor, set); + var historySets = await psProcessor.Processor.GetConfigurationHistoryAsync(); + + PSConfigurationSet[] result = new PSConfigurationSet[historySets.Count]; + for (int i = 0; i < historySets.Count; ++i) + { + result[i] = new PSConfigurationSet(psProcessor, historySets[i]); + } + + return result; } private PSConfigurationJob StartApplyInternal(PSConfigurationSet psConfigurationSet) @@ -474,5 +597,17 @@ private async Task GetSetDetailsAsync(PSConfigurationSet psC return psConfigurationSet; } + + /// + /// Serializes a configuration set and outputs the string. + /// + /// PSConfiguration set. + /// The string version of the set. + private string SerializeMTA(PSConfigurationSet psConfigurationSet) + { + MemoryStream stream = new MemoryStream(); + psConfigurationSet.Set.Serialize(stream.AsOutputStream()); + return Encoding.UTF8.GetString(stream.ToArray()); + } } } diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs index cc0107c4c3..c6fb49233c 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -31,14 +31,44 @@ internal class OpenConfigurationParameters /// The module path to use. /// Execution policy. /// If telemetry can be used. + /// If the configuration is from history; changes the meaning of `ConfigFile` to the instance identifier. public OpenConfigurationParameters( PowerShellCmdlet pwshCmdlet, string file, string modulePath, ExecutionPolicy executionPolicy, + bool canUseTelemetry, + bool fromHistory = false) + { + if (!fromHistory) + { + this.ConfigFile = this.VerifyFile(file, pwshCmdlet); + } + else + { + this.ConfigFile = file; + } + + this.InitializeModulePath(modulePath); + this.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); + this.CanUseTelemetry = canUseTelemetry; + this.FromHistory = fromHistory; + } + + /// + /// Initializes a new instance of the class. + /// + /// PowerShellCmdlet. + /// The module path to use. + /// Execution policy. + /// If telemetry can be used. + public OpenConfigurationParameters( + PowerShellCmdlet pwshCmdlet, + string modulePath, + ExecutionPolicy executionPolicy, bool canUseTelemetry) { - this.ConfigFile = this.VerifyFile(file, pwshCmdlet); + this.ConfigFile = string.Empty; this.InitializeModulePath(modulePath); this.Policy = this.GetConfigurationProcessorPolicy(executionPolicy); this.CanUseTelemetry = canUseTelemetry; @@ -69,6 +99,11 @@ public OpenConfigurationParameters( /// public bool CanUseTelemetry { get; } + /// + /// Gets a value indicating whether the configuration is from history. + /// + public bool FromHistory { get; } + private string VerifyFile(string filePath, PowerShellCmdlet pwshCmdlet) { if (!Path.IsPathRooted(filePath)) diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs index 4cb2ebc884..ae8725ab00 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { + using System; using Microsoft.Management.Configuration; /// @@ -39,6 +40,17 @@ public string Name } } + /// + /// Gets the instance identifier. + /// + public Guid InstanceIdentifier + { + get + { + return this.Set.InstanceIdentifier; + } + } + /// /// Gets the origin. /// diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs index 4e45b5d170..4e7f0c5510 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs @@ -267,6 +267,15 @@ internal static string ConfigurationReadingConfigFile { } } + /// + /// Looks up a localized string similar to Reading configuration history. + /// + internal static string ConfigurationReadingConfigHistory { + get { + return ResourceManager.GetString("ConfigurationReadingConfigHistory", resourceCulture); + } + } + /// /// Looks up a localized string similar to Settings:. /// diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx index 56a39d0835..78a7575a70 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx +++ b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -315,4 +315,7 @@ A unit contains a setting that requires the config root. + + Reading configuration history + \ No newline at end of file diff --git a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 index f819fc7d70..b05169f759 100644 --- a/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 +++ b/src/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 @@ -23,6 +23,8 @@ CmdletsToExport = @( "Test-WinGetConfiguration" "Confirm-WinGetConfiguration" "Stop-WinGetConfiguration" + "Remove-WinGetConfigurationHistory" + "ConvertTo-WinGetConfigurationYaml" ) PrivateData = @{ diff --git a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 index 2e5f6a8eb6..da7c89b1d6 100644 --- a/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 +++ b/src/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 @@ -59,6 +59,7 @@ class WinGetModule [void]PrepareScriptFiles() { + Write-Verbose "Copying files: $($this.ModuleRoot) -> $($this.Output)" xcopy $this.ModuleRoot $this.Output /d /s /f /y } @@ -191,8 +192,8 @@ if ($moduleToConfigure.HasFlag([ModuleType]::Client)) { Write-Host "Setting up Microsoft.WinGet.Client" $module = [WinGetModule]::new("Microsoft.WinGet.Client", "$PSScriptRoot\..\Microsoft.WinGet.Client\ModuleFiles\", $moduleRootOutput) - $module.PrepareScriptFiles() $module.PrepareBinaryFiles($BuildRoot, $Configuration) + $module.PrepareScriptFiles() $additionalFiles = @( "Microsoft.Management.Deployment.InProc\Microsoft.Management.Deployment.dll" "Microsoft.Management.Deployment\Microsoft.Management.Deployment.winmd" @@ -216,8 +217,8 @@ if ($moduleToConfigure.HasFlag([ModuleType]::Configuration)) { Write-Host "Setting up Microsoft.WinGet.Configuration" $module = [WinGetModule]::new("Microsoft.WinGet.Configuration", "$PSScriptRoot\..\Microsoft.WinGet.Configuration\ModuleFiles\", $moduleRootOutput) - $module.PrepareScriptFiles() $module.PrepareBinaryFiles($BuildRoot, $Configuration) + $module.PrepareScriptFiles() $additionalFiles = @( "Microsoft.Management.Configuration\Microsoft.Management.Configuration.dll" ) diff --git a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 index def58b37f8..49a25e0a9f 100644 --- a/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 +++ b/src/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 @@ -878,9 +878,59 @@ Describe 'Confirm-WinGetConfiguration' { } } +Describe 'Configuration History' { + + BeforeEach { + DeleteConfigTxtFiles + } + + It 'History Lifecycle' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = Invoke-WinGetConfiguration -AcceptConfigurationAgreements -Set $set + $result | Should -Not -BeNullOrEmpty + $result.ResultCode | Should -Be 0 + $result.UnitResults.Count | Should -Be 1 + $result.UnitResults[0].State | Should -Be "Completed" + $result.UnitResults[0].ResultCode | Should -Be 0 + + $historySet = Get-WinGetConfiguration -InstanceIdentifier $set.InstanceIdentifier + $historySet | Should -Not -BeNullOrEmpty + $historySet.InstanceIdentifier | Should -Be $set.InstanceIdentifier + + $allHistory = Get-WinGetConfiguration -All + $allHistory | Should -Not -BeNullOrEmpty + + $historySet | Remove-WinGetConfigurationHistory + + $historySetAfterRemove = Get-WinGetConfiguration -InstanceIdentifier $set.InstanceIdentifier + $historySetAfterRemove | Should -BeNullOrEmpty + } +} + +Describe 'Configuration Serialization' { + + It 'Basic Serialization' { + $testFile = GetConfigTestDataFile "Configure_TestRepo.yml" + $set = Get-WinGetConfiguration -File $testFile + $set | Should -Not -BeNullOrEmpty + + $result = ConvertTo-WinGetConfigurationYaml -Set $set + $result | Should -Not -BeNullOrEmpty + + $tempFile = New-TemporaryFile + Set-Content -Path $tempFile -Value $result + + $roundTripSet = Get-WinGetConfiguration -File $tempFile.VersionInfo.FileName + $roundTripSet | Should -Not -BeNullOrEmpty + } +} + AfterAll { CleanupGroupPolicies CleanupGroupPolicyKeyIfExists CleanupPsModulePath DeleteConfigTxtFiles -} \ No newline at end of file +}