From c074cbe995d1b4b7ed3cd571f388b7ebcf44d071 Mon Sep 17 00:00:00 2001 From: JohnMcPMS Date: Fri, 21 Jun 2024 10:17:57 -0700 Subject: [PATCH] Configuration history support (#4552) ## Change This change adds basic configuration history support. This is stored in an SQLite database that is shared by all of the configuration code (so both `winget.exe` and PowerShell modules will use the same database). The database currently holds a representation of every configuration set that has been applied (or at least attempted to be applied). Basic fields are stored directly, while more complex data is stored by serializing to YAML. While the database currently has only basic data, it will eventually contain status information for configuration units and other information used during synchronization of multiple configuration users. >[!Note] >The dev build (based on `AICLI_DISABLE_TEST_HOOKS`) uses a different location for history to prevent local tests runs from adversely affecting the configuration usage experience for us. ### winget.exe interface changes A new command is added under the `configure` top level command, `list`. This shows details about items in the history. ```PowerShell > wingetdev configure list Identifier Name First Applied Origin ---------------------------------------------------------------------------------- {9C8386B3-6C06-46D8-A0B1-83F3C73D86CE} Test Name 2024-06-13 11:43:20.000 Test Path {F9DB9D25-92F3-4FBC-AD34-BEEEE53F08A0} Test Name 2024-06-13 11:43:21.000 Test Path {49B3FDDA-9ABA-475C-A9FE-296CE3D7ED48} Test Name 2024-06-13 11:43:21.000 Test Path {436B929A-E717-4F3B-B16E-BD268D5916D6} Test Name 2024-06-13 11:43:21.000 Test Path > wingetdev configure list -h "{9C8386B3-6C06-46D8-A0B1-83F3C73D86CE}" Field Value ---------------------------------------------------- Identifier {9C8386B3-6C06-46D8-A0B1-83F3C73D86CE} Name Test Name First Applied 2024-06-13 11:43:20.000 Origin Test Origin Path Test Path ``` In addition to listing everything, one can provide the listed name of the configuration or any unique starting sequence of the identifier to select a single item to view. The selection options apply to all other commands that take in the history item parameter, and completion has been added for the parameter so that substrings of either identifier or name can be expanded. The `configure`, `configure show`, and `configure test` commands have all had the history parameter added so that one can operate directly against a historical set. In addition to displaying information about history, the `configure list` command also allows for removing items from history with `--remove` and creating a YAML file for the set with `--output`. ### PowerShell interface changes The existing cmdlet `Get-WinGetConfiguration` was updated to include parameter set options `-All` to get all set from history and `-InstanceIdentifier` to get a single one (these are exclusive with `-File` and each other). `Remove-WinGetConfigurationHistory` was added to allow removing sets from history, and `ConvertTo-WinGetConfigurationYaml` was added to enable serializing a set to a string. --- .github/actions/spelling/expect.txt | 3 +- .../AppInstallerCLICore.vcxproj | 4 +- .../AppInstallerCLICore.vcxproj.filters | 6 + src/AppInstallerCLICore/Argument.cpp | 6 +- src/AppInstallerCLICore/Argument.h | 3 + .../Commands/ConfigureCommand.cpp | 16 +- .../Commands/ConfigureCommand.h | 1 + .../Commands/ConfigureListCommand.cpp | 84 ++++ .../Commands/ConfigureListCommand.h | 24 ++ .../Commands/ConfigureShowCommand.cpp | 13 +- .../Commands/ConfigureShowCommand.h | 1 + .../Commands/ConfigureTestCommand.cpp | 13 +- .../Commands/ConfigureTestCommand.h | 1 + .../ConfigurationCommon.cpp | 8 +- src/AppInstallerCLICore/ConfigurationCommon.h | 2 +- .../ConfigurationContext.cpp | 18 + .../ConfigurationContext.h | 15 +- src/AppInstallerCLICore/ExecutionArgs.h | 2 + src/AppInstallerCLICore/Resources.h | 11 + .../Workflows/ConfigurationFlow.cpp | 223 +++++++++- .../Workflows/ConfigurationFlow.h | 50 ++- .../Workflows/WorkflowBase.cpp | 6 + .../ConfigureCommand.cs | 32 +- .../ConfigureListCommand.cs | 105 +++++ .../ConfigureShowCommand.cs | 28 +- .../ConfigureTestCommand.cs | 26 +- .../Helpers/TestCommon.cs | 34 +- .../Helpers/WinGetSettingsHelper.cs | 7 +- .../Shared/Strings/en-us/winget.resw | 40 +- .../AppInstallerCLITests.vcxproj | 1 + .../AppInstallerCLITests.vcxproj.filters | 3 + src/AppInstallerCLITests/Runtime.cpp | 4 +- .../SQLiteDynamicStorage.cpp | 43 ++ src/AppInstallerCommonCore/Runtime.cpp | 40 +- .../AppInstallerSharedLib.vcxproj | 3 + .../AppInstallerSharedLib.vcxproj.filters | 9 + src/AppInstallerSharedLib/Errors.cpp | 1 + src/AppInstallerSharedLib/Filesystem.cpp | 55 +++ .../Public/AppInstallerErrors.h | 1 + .../Public/winget/Filesystem.h | 13 + .../Public/winget/ModuleCountBase.h | 27 ++ .../Public/winget/SQLiteDynamicStorage.h | 57 +++ .../Public/winget/SQLiteStatementBuilder.h | 1 + .../Public/winget/SQLiteStorageBase.h | 1 + .../Public/winget/SQLiteVersion.h | 2 +- .../Public/winget/SQLiteWrapper.h | 15 + .../Public/winget/Yaml.h | 18 +- .../SQLiteDynamicStorage.cpp | 91 ++++ .../SQLiteStatementBuilder.cpp | 2 + src/AppInstallerSharedLib/SQLiteVersion.cpp | 2 +- src/AppInstallerSharedLib/SQLiteWrapper.cpp | 42 ++ src/AppInstallerSharedLib/Yaml.cpp | 25 +- src/AppInstallerSharedLib/YamlWrapper.cpp | 18 +- src/AppInstallerSharedLib/YamlWrapper.h | 2 +- src/AppInstallerSharedLib/pch.h | 1 + .../Tests/ConfigurationHistoryTests.cs | 391 ++++++++++++++++++ .../ConfigurationProcessor.cpp | 35 +- .../ConfigurationProcessor.h | 7 + .../ConfigurationSet.cpp | 16 +- .../ConfigurationSet.h | 5 +- .../ConfigurationSetParser.cpp | 46 ++- .../ConfigurationSetParser.h | 21 +- .../ConfigurationSetParserError.h | 3 + .../ConfigurationSetParser_0_1.cpp | 9 +- .../ConfigurationSetParser_0_1.h | 5 +- .../ConfigurationSetParser_0_2.cpp | 5 + .../ConfigurationSetParser_0_2.h | 5 +- .../ConfigurationSetParser_0_3.cpp | 9 +- .../ConfigurationSetParser_0_3.h | 5 +- .../ConfigurationSetSerializer.cpp | 45 +- .../ConfigurationSetSerializer.h | 9 +- .../ConfigurationStaticFunctions.cpp | 4 +- .../ConfigurationUnit.cpp | 2 +- .../ConfigurationUnit.h | 5 +- .../Database/ConfigurationDatabase.cpp | 191 +++++++++ .../Database/ConfigurationDatabase.h | 53 +++ .../Database/Schema/0_1/Interface.h | 23 ++ .../Database/Schema/0_1/Interface_0_1.cpp | 74 ++++ .../Database/Schema/0_1/SetInfoTable.cpp | 215 ++++++++++ .../Database/Schema/0_1/SetInfoTable.h | 38 ++ .../Database/Schema/0_1/UnitInfoTable.cpp | 227 ++++++++++ .../Database/Schema/0_1/UnitInfoTable.h | 32 ++ .../Schema/IConfigurationDatabase.cpp | 37 ++ .../Database/Schema/IConfigurationDatabase.h | 48 +++ .../Filesystem.cpp | 33 ++ .../Filesystem.h | 18 + ...Microsoft.Management.Configuration.vcxproj | 16 +- ...t.Management.Configuration.vcxproj.filters | 45 ++ src/Microsoft.Management.Configuration/pch.h | 4 +- .../Cmdlets/Common/OpenConfiguration.cs | 25 +- .../ConvertToWinGetConfigurationYamlCmdlet.cs | 39 ++ .../Cmdlets/GetWinGetConfigurationCmdlet.cs | 33 +- .../RemoveWinGetConfigurationHistoryCmdlet.cs | 39 ++ .../Helpers/Constants.cs | 7 +- .../Commands/ConfigurationCommand.cs | 161 +++++++- .../Helpers/OpenConfigurationParameters.cs | 39 +- .../PSObjects/PSConfigurationSet.cs | 14 +- .../Resources/Resources.Designer.cs | 9 + .../Resources/Resources.resx | 3 + .../Microsoft.WinGet.Configuration.psd1 | 2 + .../scripts/Initialize-LocalWinGetModules.ps1 | 5 +- .../Microsoft.WinGet.Configuration.Tests.ps1 | 52 ++- 102 files changed, 3198 insertions(+), 170 deletions(-) create mode 100644 src/AppInstallerCLICore/Commands/ConfigureListCommand.cpp create mode 100644 src/AppInstallerCLICore/Commands/ConfigureListCommand.h create mode 100644 src/AppInstallerCLIE2ETests/ConfigureListCommand.cs create mode 100644 src/AppInstallerCLITests/SQLiteDynamicStorage.cpp create mode 100644 src/AppInstallerSharedLib/Public/winget/ModuleCountBase.h create mode 100644 src/AppInstallerSharedLib/Public/winget/SQLiteDynamicStorage.h create mode 100644 src/AppInstallerSharedLib/SQLiteDynamicStorage.cpp create mode 100644 src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs create mode 100644 src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp create mode 100644 src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.h create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.h create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp create mode 100644 src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h create mode 100644 src/Microsoft.Management.Configuration/Filesystem.cpp create mode 100644 src/Microsoft.Management.Configuration/Filesystem.h create mode 100644 src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs create mode 100644 src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/RemoveWinGetConfigurationHistoryCmdlet.cs 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 +}