winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit c074cbe995d1b4b7ed3cd571f388b7ebcf44d071
parent b433fa4309fbbee02d4a0acf1b22e57dd891d2b6
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Fri, 21 Jun 2024 10:17:57 -0700

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.
Diffstat:
M.github/actions/spelling/expect.txt | 3++-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 5++++-
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 6++++++
Msrc/AppInstallerCLICore/Argument.cpp | 6+++++-
Msrc/AppInstallerCLICore/Argument.h | 3+++
Msrc/AppInstallerCLICore/Commands/ConfigureCommand.cpp | 16+++++++++++-----
Msrc/AppInstallerCLICore/Commands/ConfigureCommand.h | 1+
Asrc/AppInstallerCLICore/Commands/ConfigureListCommand.cpp | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/ConfigureListCommand.h | 24++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/ConfigureShowCommand.cpp | 13+++++++++++--
Msrc/AppInstallerCLICore/Commands/ConfigureShowCommand.h | 1+
Msrc/AppInstallerCLICore/Commands/ConfigureTestCommand.cpp | 13+++++++++++--
Msrc/AppInstallerCLICore/Commands/ConfigureTestCommand.h | 1+
Msrc/AppInstallerCLICore/ConfigurationCommon.cpp | 8+++++++-
Msrc/AppInstallerCLICore/ConfigurationCommon.h | 2+-
Msrc/AppInstallerCLICore/ConfigurationContext.cpp | 18++++++++++++++++++
Msrc/AppInstallerCLICore/ConfigurationContext.h | 15+++++++++++----
Msrc/AppInstallerCLICore/ExecutionArgs.h | 2++
Msrc/AppInstallerCLICore/Resources.h | 11+++++++++++
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.cpp | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msrc/AppInstallerCLICore/Workflows/ConfigurationFlow.h | 50+++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 6++++++
Msrc/AppInstallerCLIE2ETests/ConfigureCommand.cs | 32++++++++++++++++++++++++++++----
Asrc/AppInstallerCLIE2ETests/ConfigureListCommand.cs | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIE2ETests/ConfigureShowCommand.cs | 28++++++++++++++++++++++++++--
Msrc/AppInstallerCLIE2ETests/ConfigureTestCommand.cs | 26+++++++++++++++++++++++++-
Msrc/AppInstallerCLIE2ETests/Helpers/TestCommon.cs | 34+++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs | 7+++++--
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 41++++++++++++++++++++++++++++++++++++++++-
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 1+
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Msrc/AppInstallerCLITests/Runtime.cpp | 4++--
Asrc/AppInstallerCLITests/SQLiteDynamicStorage.cpp | 43+++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/Runtime.cpp | 40+++++-----------------------------------
Msrc/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj | 3+++
Msrc/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters | 9+++++++++
Msrc/AppInstallerSharedLib/Errors.cpp | 1+
Msrc/AppInstallerSharedLib/Filesystem.cpp | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 1+
Msrc/AppInstallerSharedLib/Public/winget/Filesystem.h | 13+++++++++++++
Asrc/AppInstallerSharedLib/Public/winget/ModuleCountBase.h | 27+++++++++++++++++++++++++++
Asrc/AppInstallerSharedLib/Public/winget/SQLiteDynamicStorage.h | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h | 1+
Msrc/AppInstallerSharedLib/Public/winget/SQLiteStorageBase.h | 1+
Msrc/AppInstallerSharedLib/Public/winget/SQLiteVersion.h | 2+-
Msrc/AppInstallerSharedLib/Public/winget/SQLiteWrapper.h | 15+++++++++++++++
Msrc/AppInstallerSharedLib/Public/winget/Yaml.h | 18+++++++++++++++++-
Asrc/AppInstallerSharedLib/SQLiteDynamicStorage.cpp | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/SQLiteStatementBuilder.cpp | 2++
Msrc/AppInstallerSharedLib/SQLiteVersion.cpp | 2+-
Msrc/AppInstallerSharedLib/SQLiteWrapper.cpp | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerSharedLib/Yaml.cpp | 25+++++++++++++++++--------
Msrc/AppInstallerSharedLib/YamlWrapper.cpp | 18++++++++++++++++--
Msrc/AppInstallerSharedLib/YamlWrapper.h | 2+-
Msrc/AppInstallerSharedLib/pch.h | 1+
Asrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs | 391+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.cpp | 35+++++++++++++++++++++++++++++++++--
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.h | 7+++++++
Msrc/Microsoft.Management.Configuration/ConfigurationSet.cpp | 16++++++++++++----
Msrc/Microsoft.Management.Configuration/ConfigurationSet.h | 5++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser.cpp | 46++++++++++++++++++++++++++++++++++++++--------
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser.h | 21+++++++++++++++++++--
Msrc/Microsoft.Management.Configuration/ConfigurationSetParserError.h | 3+++
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.cpp | 9+++++++--
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_1.h | 5++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.cpp | 5+++++
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_2.h | 5++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.cpp | 9+++++++--
Msrc/Microsoft.Management.Configuration/ConfigurationSetParser_0_3.h | 5++++-
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer.cpp | 45++++++++++++++++++++++++++++++++++++++++++---
Msrc/Microsoft.Management.Configuration/ConfigurationSetSerializer.h | 9++++++++-
Msrc/Microsoft.Management.Configuration/ConfigurationStaticFunctions.cpp | 4++--
Msrc/Microsoft.Management.Configuration/ConfigurationUnit.cpp | 2+-
Msrc/Microsoft.Management.Configuration/ConfigurationUnit.h | 6+++---
Asrc/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h | 23+++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.cpp | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/SetInfoTable.h | 38++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.cpp | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_1/UnitInfoTable.h | 32++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp | 37+++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Filesystem.cpp | 33+++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Filesystem.h | 18++++++++++++++++++
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj | 17+++++++++++++++--
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters | 45+++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/pch.h | 4+++-
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/Common/OpenConfiguration.cs | 25+++++++++++++++++++++----
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs | 39+++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/GetWinGetConfigurationCmdlet.cs | 33++++++++++++++++++++++++++-------
Asrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/RemoveWinGetConfigurationHistoryCmdlet.cs | 39+++++++++++++++++++++++++++++++++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Helpers/Constants.cs | 7+++++--
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Commands/ConfigurationCommand.cs | 161++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Helpers/OpenConfigurationParameters.cs | 39+++++++++++++++++++++++++++++++++++++--
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/PSObjects/PSConfigurationSet.cs | 14+++++++++++++-
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.Designer.cs | 9+++++++++
Msrc/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx | 3+++
Msrc/PowerShell/Microsoft.WinGet.Configuration/ModuleFiles/Microsoft.WinGet.Configuration.psd1 | 2++
Msrc/PowerShell/scripts/Initialize-LocalWinGetModules.ps1 | 5+++--
Msrc/PowerShell/tests/Microsoft.WinGet.Configuration.Tests.ps1 | 53+++++++++++++++++++++++++++++++++++++++++++++++++++--
102 files changed, 3201 insertions(+), 172 deletions(-)

diff --git 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 @@ -355,6 +355,7 @@ <ClInclude Include="Commands\COMCommand.h" /> <ClInclude Include="Commands\CompleteCommand.h" /> <ClInclude Include="Commands\ConfigureCommand.h" /> + <ClInclude Include="Commands\ConfigureListCommand.h" /> <ClInclude Include="Commands\ConfigureShowCommand.h" /> <ClInclude Include="Commands\ConfigureTestCommand.h" /> <ClInclude Include="Commands\ConfigureValidateCommand.h" /> @@ -432,6 +433,7 @@ <ClCompile Include="CheckpointManager.cpp" /> <ClCompile Include="Commands\COMCommand.cpp" /> <ClCompile Include="Commands\ConfigureCommand.cpp" /> + <ClCompile Include="Commands\ConfigureListCommand.cpp" /> <ClCompile Include="Commands\ConfigureShowCommand.cpp" /> <ClCompile Include="Commands\ConfigureTestCommand.cpp" /> <ClCompile Include="Commands\ConfigureValidateCommand.cpp" /> @@ -551,4 +553,4 @@ </Reference> </ItemGroup> </Target> -</Project> +</Project>+ \ No newline at end of file diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -254,6 +254,9 @@ <ClInclude Include="ConfigureExportCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Commands\ConfigureListCommand.h"> + <Filter>Commands</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -478,6 +481,9 @@ <ClCompile Include="ConfigureExportCommand.cpp"> <Filter>Commands</Filter> </ClCompile> + <ClCompile Include="Commands\ConfigureListCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git 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 @@ -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 @@ -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::vector<std::unique_ptr<Command>>>({ std::make_unique<ConfigureShowCommand>(FullName()), + std::make_unique<ConfigureListCommand>(FullName()), std::make_unique<ConfigureTestCommand>(FullName()), std::make_unique<ConfigureValidateCommand>(FullName()), std::make_unique<ConfigureExportCommand>(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 @@ -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 @@ -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<Argument> 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 @@ -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<Argument> 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 @@ -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 @@ -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 @@ -12,8 +12,9 @@ namespace AppInstaller::CLI std::vector<Argument> 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 @@ -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 @@ -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 @@ -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 @@ -14,6 +14,7 @@ namespace AppInstaller::CLI::Execution { ConfigurationProcessor Processor = nullptr; ConfigurationSet Set = nullptr; + std::vector<ConfigurationSet> History; }; } @@ -65,4 +66,21 @@ namespace AppInstaller::CLI::Execution { m_data->Set = std::move(value); } + + std::vector<ConfigurationSet>& ConfigurationContext::History() + { + return m_data->History; + } + + const std::vector<ConfigurationSet>& ConfigurationContext::History() const + { + return m_data->History; + } + + void ConfigurationContext::History(const winrt::Windows::Foundation::Collections::IVector<ConfigurationSet>& value) + { + std::vector<ConfigurationSet> 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 @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once #include <memory> +#include <vector> 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<ConfigurationSet>& History(); + const std::vector<ConfigurationSet>& History() const; + void History(const winrt::Windows::Foundation::Collections::IVector<ConfigurationSet>& value); private: std::unique_ptr<details::ConfigurationContextData> m_data; diff --git 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 @@ -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 @@ -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 <AppInstallerDateTime.h> #include <AppInstallerDownloader.h> #include <AppInstallerErrors.h> #include <AppInstallerRuntime.h> @@ -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<Settings::Setting::TelemetryDisable>()); + + // 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<Data::ConfigurationContext>(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<Settings::Setting::TelemetryDisable>()); - - // 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<Data::ConfigurationContext>(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<Data::ConfigurationContext>(); + configContext.History(configContext.Processor().GetConfigurationHistory()); + } + + void ShowConfigurationSetHistory(Execution::Context& context) + { + const auto& history = context.Get<Data::ConfigurationContext>().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<Data::ConfigurationContext>(); + 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<Data::ConfigurationContext>().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<Data::ConfigurationContext>().Set().Serialize(outputStream); + } + + void ShowSingleConfigurationSetHistory(Execution::Context& context) + { + const auto& set = context.Get<Data::ConfigurationContext>().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<Data::CompletionData>().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 @@ -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 @@ -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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ConfigureCommand.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -63,7 +63,7 @@ namespace AppInstallerCLIE2ETests // 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 @@ namespace AppInstallerCLIE2ETests // 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)); } /// <summary> @@ -167,7 +167,7 @@ namespace AppInstallerCLIE2ETests // 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)); } /// <summary> @@ -182,6 +182,30 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(0, result.ExitCode); } + /// <summary> + /// Runs a configuration, then changes the state and runs it again from history. + /// </summary> + [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 @@ -0,0 +1,105 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigureListCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// `Configure list` command tests. + /// </summary> + public class ConfigureListCommand + { + private const string ConfigureWithAgreementsAndVerbose = "configure --accept-configuration-agreements --verbose"; + private const string ConfigureTestRepoFile = "Configure_TestRepo.yml"; + + /// <summary> + /// Teardown done once after all the tests here. + /// </summary> + [OneTimeTearDown] + public void OneTimeTeardown() + { + this.DeleteTxtFiles(); + } + + /// <summary> + /// Applies a configuration, then verifies that it is in the overall list. + /// </summary> + [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)); + } + + /// <summary> + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the details about the first configuration. + /// </summary> + [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)); + } + + /// <summary> + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the removes the first configuration. + /// </summary> + [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)); + } + + /// <summary> + /// Applies a configuration (to ensure at least one exists), gets the overall list, then the outputs the first configuration. + /// </summary> + [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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ConfigureShowCommand.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -6,8 +6,8 @@ namespace AppInstallerCLIE2ETests { + using System.IO; using AppInstallerCLIE2ETests.Helpers; - using Microsoft.VisualBasic; using NUnit.Framework; /// <summary> @@ -22,6 +22,7 @@ namespace AppInstallerCLIE2ETests public void OneTimeTearDown() { WinGetSettingsHelper.ConfigureFeature("configuration03", false); + this.DeleteTxtFiles(); } /// <summary> @@ -133,5 +134,28 @@ namespace AppInstallerCLIE2ETests 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")); } + + /// <summary> + /// Runs a configuration, then shows it from history. + /// </summary> + [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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="ConfigureTestCommand.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -90,6 +90,30 @@ namespace AppInstallerCLIE2ETests Assert.True(result.StdOut.Contains("System is in the described configuration state.")); } + /// <summary> + /// Runs a configuration, then tests it from history. + /// </summary> + [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 @@ -979,6 +979,36 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> + /// Gets the instance identifier of the first configuration history item with name in its output line. + /// </summary> + /// <param name="name">The string to search for.</param> + /// <returns>The instance identifier of a configuration that matched the search, or any empty string if none did.</returns> + 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; + } + + /// <summary> /// Copy the installer file to the ARP InstallSource directory. /// </summary> /// <param name="installerFilePath">Test installer to be copied.</param> @@ -1049,6 +1079,7 @@ namespace AppInstallerCLIE2ETests.Helpers 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 @@ namespace AppInstallerCLIE2ETests.Helpers } }; + p.StartInfo.StandardErrorEncoding = Encoding.UTF8; p.StartInfo.RedirectStandardError = true; StringBuilder errorData = new (); p.ErrorDataReceived += (sender, args) => @@ -1102,7 +1134,7 @@ namespace AppInstallerCLIE2ETests.Helpers 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 @@ -41,9 +41,12 @@ namespace AppInstallerCLIE2ETests.Helpers 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 @@ -3007,4 +3007,42 @@ Please specify one of them using the --source option to proceed.</value> <value>&lt;this value has been truncated; inspect the file contents for the complete text&gt;</value> <comment>Keep some form of separator like the "&lt;&gt;" around the text so that it stands out from the preceding text.</comment> </data> -</root> + <data name="ConfigureListCommandLongDescription" xml:space="preserve"> + <value>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.</value> + <comment>{Locked="`configure`"}</comment> + </data> + <data name="ConfigureListCommandShortDescription" xml:space="preserve"> + <value>Shows configuration history</value> + </data> + <data name="ConfigurationHistoryEmpty" xml:space="preserve"> + <value>There are no configurations in the history.</value> + </data> + <data name="ConfigurationHistoryItemArgumentDescription" xml:space="preserve"> + <value>Select items from history</value> + </data> + <data name="ConfigurationHistoryItemNotFound" xml:space="preserve"> + <value>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.</value> + </data> + <data name="ConfigurationHistoryRemoveArgumentDescription" xml:space="preserve"> + <value>Remove the item from history</value> + </data> + <data name="ConfigureListFirstApplied" xml:space="preserve"> + <value>First Applied</value> + <comment>Column header for date values indicating when a configuration was first applied to the system.</comment> + </data> + <data name="ConfigureListIdentifier" xml:space="preserve"> + <value>Identifier</value> + </data> + <data name="ConfigureListName" xml:space="preserve"> + <value>Name</value> + </data> + <data name="ConfigureListOrigin" xml:space="preserve"> + <value>Origin</value> + </data> + <data name="ConfigureListPath" xml:space="preserve"> + <value>Path</value> + </data> + <data name="WINGET_CONFIG_ERROR_HISTORY_ITEM_NOT_FOUND" xml:space="preserve"> + <value>The specified configuration could not be found.</value> + </data> +</root>+ \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -298,6 +298,7 @@ <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="ShowFlow.cpp" /> <ClCompile Include="SourceFlow.cpp" /> + <ClCompile Include="SQLiteDynamicStorage.cpp" /> <ClCompile Include="SQLiteIndexSource.cpp" /> <ClCompile Include="Strings.cpp" /> <ClCompile Include="TestConfiguration.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -356,6 +356,9 @@ <ClCompile Include="FileCache.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="SQLiteDynamicStorage.cpp"> + <Filter>Source Files\Repository</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git 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<PathName>(0)); i < ToIntegral(PathName::Max); ++i) + for (auto i = ToIntegral(ToEnum<Runtime::PathName>(0)); i < ToIntegral(Runtime::PathName::Max); ++i) { - std::filesystem::path displayPath = GetPathTo(ToEnum<PathName>(i), true); + std::filesystem::path displayPath = GetPathTo(ToEnum<Runtime::PathName>(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 @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <AppInstallerErrors.h> +#include <winget/SQLiteDynamicStorage.h> +#include <winget/SQLiteWrapper.h> +#include <winget/SQLiteStatementBuilder.h> +#include <winget/SQLiteVersion.h> + +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 @@ -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<Setting::AnonymizePathForDisplay>()) ? 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<Setting::AnonymizePathForDisplay>(); 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 @@ -419,11 +419,13 @@ <ClInclude Include="Public\winget\JsonUtil.h" /> <ClInclude Include="Public\winget\LocIndependent.h" /> <ClInclude Include="Public\winget\ManagedFile.h" /> + <ClInclude Include="Public\winget\ModuleCountBase.h" /> <ClInclude Include="Public\winget\Registry.h" /> <ClInclude Include="Public\winget\Resources.h" /> <ClInclude Include="Public\winget\Runtime.h" /> <ClInclude Include="Public\winget\Security.h" /> <ClInclude Include="Public\winget\SharedThreadGlobals.h" /> + <ClInclude Include="Public\winget\SQLiteDynamicStorage.h" /> <ClInclude Include="Public\winget\SQLiteMetadataTable.h" /> <ClInclude Include="Public\winget\SQLiteStatementBuilder.h" /> <ClInclude Include="Public\winget\SQLiteStorageBase.h" /> @@ -461,6 +463,7 @@ <ClCompile Include="JsonSchemaValidation.cpp" /> <ClCompile Include="JsonUtil.cpp" /> <ClCompile Include="ManagedFile.cpp" /> + <ClCompile Include="SQLiteDynamicStorage.cpp" /> <ClCompile Include="SQLiteMetadataTable.cpp" /> <ClCompile Include="Registry.cpp" /> <ClCompile Include="Resources.cpp" /> diff --git a/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters b/src/AppInstallerSharedLib/AppInstallerSharedLib.vcxproj.filters @@ -134,6 +134,12 @@ <ClInclude Include="Public\winget\Filesystem.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\SQLiteDynamicStorage.h"> + <Filter>Public\winget</Filter> + </ClInclude> + <ClInclude Include="Public\winget\ModuleCountBase.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -220,6 +226,9 @@ <ClCompile Include="Filesystem.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="SQLiteDynamicStorage.cpp"> + <Filter>SQLite</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git 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 @@ -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 @@ -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 @@ -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 @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <wrl\module.h> + +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 @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/SQLiteStorageBase.h> +#include <concurrencysal.h> +#include <filesystem> +#include <memory> + +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<std::mutex> 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<TransactionLock> TryBeginTransaction(std::string_view name); + + // Locks the connection for use during the schema upgrade. + std::unique_ptr<TransactionLock> LockConnection(); + }; +} diff --git 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 <Type type, bool allowNull> diff --git 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 @@ -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 @@ -109,6 +109,14 @@ namespace AppInstaller::SQLite static blob_t GetColumn(sqlite3_stmt* stmt, int column); }; + template <> + struct ParameterSpecificsImpl<GUID> + { + 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 <typename E> struct ParameterSpecificsImpl<E, typename std::enable_if_t<std::is_enum_v<E>>> { @@ -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 @@ -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<InputType> m_scalarInfo; + std::optional<InputType> m_scalarType; + + // If set, defines the style of the next scalar. + std::optional<ScalarStyle> 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 @@ -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::TransactionLock> SQLiteDynamicStorage::TryBeginTransaction(std::string_view name) + { + auto result = std::make_unique<TransactionLock>(*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::TransactionLock> SQLiteDynamicStorage::LockConnection() + { + return std::make_unique<TransactionLock>(*m_interfaceLock); + } +} diff --git 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 @@ -16,7 +16,7 @@ namespace AppInstaller::SQLite return { static_cast<uint32_t>(major), static_cast<uint32_t>(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 @@ -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 <wil/result_macros.h> @@ -149,6 +150,32 @@ namespace AppInstaller::SQLite } } + std::string ParameterSpecificsImpl<GUID>::ToLog(const GUID& v) + { + std::ostringstream strstr; + strstr << v; + return strstr.str(); + } + + void ParameterSpecificsImpl<GUID>::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<GUID>::GetColumn(sqlite3_stmt* stmt, int column) + { + GUID result{}; + + const void* blobPtr = sqlite3_column_blob(stmt, column); + if (blobPtr) + { + result = *reinterpret_cast<const GUID*>(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<int>(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<std::string>(0)) == ToLower(mode); + } + std::shared_ptr<details::SharedConnection> 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 @@ -646,12 +646,12 @@ namespace AppInstaller::YAML break; case AppInstaller::YAML::Key: CheckInput(InputType::Key); - m_scalarInfo = InputType::Key; + m_scalarType = InputType::Key; SetAllowedInputs<InputType::Scalar>(); break; case AppInstaller::YAML::Value: CheckInput(InputType::Value); - m_scalarInfo = InputType::Value; + m_scalarType = InputType::Value; SetAllowedInputs<InputType::Scalar, InputType::BeginMap, InputType::BeginSeq>(); 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<InputType::Value, InputType::BeginMap, InputType::BeginSeq>(); } - 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 @@ -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<const yaml_char_t*>(value.data()), static_cast<int>(value.size()), YAML_ANY_SCALAR_STYLE); + int result = yaml_document_add_scalar(&m_document, NULL, reinterpret_cast<const yaml_char_t*>(value.data()), static_cast<int>(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 @@ -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 @@ -10,6 +10,7 @@ #include <sddl.h> #include <Shlobj.h> #include <compressapi.h> +#include <concurrencysal.h> #define YAML_DECLARE_STATIC #include <yaml.h> diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationHistoryTests.cs @@ -0,0 +1,391 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConfigurationHistoryTests.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +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; + + /// <summary> + /// Unit tests for configuration history. + /// </summary> + [Collection("UnitTestCollection")] + [OutOfProc] + public class ConfigurationHistoryTests : ConfigurationProcessorTestBase + { + /// <summary> + /// Initializes a new instance of the <see cref="ConfigurationHistoryTests"/> class. + /// </summary> + /// <param name="fixture">Unit test fixture.</param> + /// <param name="log">Log helper.</param> + public ConfigurationHistoryTests(UnitTestFixture fixture, ITestOutputHelper log) + : base(fixture, log) + { + } + + /// <summary> + /// Checks that the history matches the applied set. + /// </summary> + [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" }); + } + + /// <summary> + /// Checks that the history matches the applied set. + /// </summary> + [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" }); + } + + /// <summary> + /// Checks that the history matches the applied set. + /// </summary> + [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" }); + } + + /// <summary> + /// Applies a set, reads the history, changes the read set and reapplies it. + /// </summary> + [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); + } + + /// <summary> + /// Applies a set, reads the history and removes it. + /// </summary> + [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<ConfigurationUnit> expectedUnits, IList<ConfigurationUnit> 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 @@ -195,12 +195,14 @@ namespace winrt::Microsoft::Management::Configuration::implementation Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet> ConfigurationProcessor::GetConfigurationHistory() { - THROW_HR(E_NOTIMPL); + return GetConfigurationHistoryImpl(); } Windows::Foundation::IAsyncOperation<Windows::Foundation::Collections::IVector<Configuration::ConfigurationSet>> 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<Configuration::ConfigurationSet> ConfigurationProcessor::GetConfigurationHistoryImpl(AppInstaller::WinRT::AsyncCancellation cancellation) + { + auto threadGlobals = m_threadGlobals.SetForCurrentThread(); + + m_database.EnsureOpened(false); + cancellation.ThrowIfCancelled(); + + std::vector<ConfigurationSet> 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<wil::details::module_count_wrapper<implementation::ApplyConfigurationSetResult>>(); @@ -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<std::recursive_mutex> lock{ m_diagnosticsMutex }; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.h b/src/Microsoft.Management.Configuration/ConfigurationProcessor.h @@ -6,6 +6,7 @@ #include <winrt/Windows.Foundation.Collections.h> #include <winrt/Windows.Storage.Streams.h> #include "ConfigThreadGlobals.h" +#include "Database/ConfigurationDatabase.h" #include <winget/AsyncTokens.h> #include <winget/ILifetimeWatcher.h> @@ -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<ConfigurationSet> 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 @@ -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 @@ -3,13 +3,14 @@ #pragma once #include "ConfigurationSet.g.h" #include <winget/ILifetimeWatcher.h> +#include <winget/ModuleCountBase.h> #include <winrt/Windows.Foundation.h> #include <winrt/Windows.Foundation.Collections.h> #include <vector> namespace winrt::Microsoft::Management::Configuration::implementation { - struct ConfigurationSet : ConfigurationSetT<ConfigurationSet, winrt::cloaked<AppInstaller::WinRT::ILifetimeWatcher>>, AppInstaller::WinRT::LifetimeWatcherBase + struct ConfigurationSet : ConfigurationSetT<ConfigurationSet, winrt::cloaked<AppInstaller::WinRT::ILifetimeWatcher>>, 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<Configuration::ConfigurationUnit>&& units); void Parameters(std::vector<Configuration::ConfigurationParameter>&& 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 @@ -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> 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<ConfigurationSetParser_0_1>(std::move(document)); + return std::make_unique<ConfigurationSetParser_0_1>(); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 2) { - return std::make_unique<ConfigurationSetParser_0_2>(std::move(document)); + return std::make_unique<ConfigurationSetParser_0_2>(); } else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) { - return std::make_unique<ConfigurationSetParser_0_3>(std::move(document)); + return std::make_unique<ConfigurationSetParser_0_3>(); } 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<hstring, Windows::Foundation::Uri> 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<hstring> ConfigurationSetParser::ParseStringArray(std::string_view input) { - return hstring{ std::rbegin(SchemaVersionAndUriMap)->VersionWide }; + std::vector<hstring> result; + ParseSequence(Load(input), "string_array", Node::Type::Scalar, [&](const AppInstaller::YAML::Node& item) + { + result.emplace_back(item.as<std::wstring>()); + }); + 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<Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> 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 @@ -6,8 +6,10 @@ #include <ConfigurationSetUtilities.h> #include <winget/Yaml.h> #include <winrt/Windows.Storage.Streams.h> +#include <functional> #include <memory> #include <string_view> +#include <utility> #include <vector> 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<ConfigurationSetParser> Create(std::string_view input); + // Create a parser for the given schema version. + static std::unique_ptr<ConfigurationSetParser> 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<hstring, Windows::Foundation::Uri> 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<wil::details::module_count_wrapper<implementation::ConfigurationSet>>()); + using ConfigurationSetPtr = winrt::com_ptr<implementation::ConfigurationSet>; // 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<hstring> 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<AppInstaller::YAML::Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> operation); + // Parse the sequence from the given `node`. + void ParseSequence(const AppInstaller::YAML::Node& node, std::string_view nameForErrors, std::optional<AppInstaller::YAML::Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> 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 @@ -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 @@ -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<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); + m_configurationSet = make_self<implementation::ConfigurationSet>(); 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<Configuration::ConfigurationUnit>& result) { ParseSequence(document, field, false, Node::Type::Mapping, [&](const Node& item) { - auto configurationUnit = make_self<wil::details::module_count_wrapper<ConfigurationUnit>>(); + auto configurationUnit = make_self<ConfigurationUnit>(); 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 @@ -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<Configuration::ConfigurationUnit>& 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 @@ -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 @@ -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 @@ -16,7 +16,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation void ConfigurationSetParser_0_3::Parse() { - auto result = make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); + auto result = make_self<implementation::ConfigurationSet>(); 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<Configuration::ConfigurationParameter> 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<wil::details::module_count_wrapper<ConfigurationUnit>>(); + auto configurationUnit = make_self<ConfigurationUnit>(); 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 @@ -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 @@ -20,7 +20,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation static constexpr std::string_view s_nullValue = "null"; } - std::unique_ptr<ConfigurationSetSerializer> 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> 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<ConfigurationSetSerializer_0_2>(); + } + 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<ConfigurationSetSerializer_0_2>(); + } + 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<hstring>& 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<ConfigurationField> 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<hstring>& 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 @@ -12,7 +12,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation { struct ConfigurationSetSerializer { - static std::unique_ptr<ConfigurationSetSerializer> CreateSerializer(hstring version); + static std::unique_ptr<ConfigurationSetSerializer> 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<hstring>& stringArray); + protected: ConfigurationSetSerializer() = default; void WriteYamlConfigurationUnits(AppInstaller::YAML::Emitter& emitter, const std::vector<ConfigurationUnit>& units); void WriteYamlValueSet(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::ValueSet& valueSet, std::initializer_list<ConfigurationField> exclusions = {}); + void WriteYamlStringArray(AppInstaller::YAML::Emitter& emitter, const Windows::Foundation::Collections::IVector<hstring>& 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 @@ -14,12 +14,12 @@ namespace winrt::Microsoft::Management::Configuration::implementation { Configuration::ConfigurationUnit ConfigurationStaticFunctions::CreateConfigurationUnit() { - return *make_self<wil::details::module_count_wrapper<implementation::ConfigurationUnit>>(); + return *make_self<implementation::ConfigurationUnit>(); } Configuration::ConfigurationSet ConfigurationStaticFunctions::CreateConfigurationSet() { - return *make_self<wil::details::module_count_wrapper<implementation::ConfigurationSet>>(); + return *make_self<implementation::ConfigurationSet>(); } Windows::Foundation::IAsyncOperation<IConfigurationSetProcessorFactory> ConfigurationStaticFunctions::CreateConfigurationSetProcessorFactoryAsync(hstring const& handler) diff --git 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<wil::details::module_count_wrapper<ConfigurationUnit>>(); + auto result = make_self<ConfigurationUnit>(); 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 @@ -3,12 +3,13 @@ #pragma once #include "ConfigurationUnit.g.h" #include <winget/ILifetimeWatcher.h> +#include <winget/ModuleCountBase.h> #include <winrt/Windows.Foundation.Collections.h> #include <vector> namespace winrt::Microsoft::Management::Configuration::implementation { - struct ConfigurationUnit : ConfigurationUnitT<ConfigurationUnit, winrt::cloaked<AppInstaller::WinRT::ILifetimeWatcher>>, AppInstaller::WinRT::LifetimeWatcherBase + struct ConfigurationUnit : ConfigurationUnitT<ConfigurationUnit, winrt::cloaked<AppInstaller::WinRT::ILifetimeWatcher>>, 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 @@ -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 <winget/Filesystem.h> +#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<SQLiteDynamicStorage>(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<SQLiteDynamicStorage>(databaseFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + m_database = IConfigurationDatabase::CreateFor(m_connection); + } + } +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + std::vector<ConfigurationDatabase::ConfigurationSetPtr> 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<rowid_t> 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<rowid_t> 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 @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ConfigurationSet.h" +#include <winget/SQLiteWrapper.h> +#include <winget/SQLiteDynamicStorage.h> +#include <memory> +#include <vector> + +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<implementation::ConfigurationSet>; + + 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<ConfigurationSetPtr> 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<AppInstaller::SQLite::SQLiteDynamicStorage> m_connection; + mutable std::unique_ptr<IConfigurationDatabase> 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 @@ -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<AppInstaller::SQLite::SQLiteDynamicStorage> 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<ConfigurationSetPtr> GetSets() override; + std::optional<AppInstaller::SQLite::rowid_t> GetSetRowId(const GUID& instanceIdentifier) override; + + private: + std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> 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 @@ -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<AppInstaller::SQLite::SQLiteDynamicStorage> 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<IConfigurationDatabase::ConfigurationSetPtr> Interface::GetSets() + { + SetInfoTable setInfoTable(*m_storage); + return setInfoTable.GetAllSets(); + } + + std::optional<rowid_t> 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 @@ -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 <AppInstallerDateTime.h> +#include <AppInstallerStrings.h> +#include <winget/SQLiteStatementBuilder.h> + +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<GUID>(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<Configuration::ConfigurationUnit> 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<IConfigurationDatabase::ConfigurationSetPtr> SetInfoTable::GetAllSets() + { + std::vector<IConfigurationDatabase::ConfigurationSetPtr> 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<implementation::ConfigurationSet>(getAllSets.GetColumn<GUID>(1)); + + configurationSet->Name(hstring{ ConvertToUTF16(getAllSets.GetColumn<std::string>(2)) }); + configurationSet->Origin(hstring{ ConvertToUTF16(getAllSets.GetColumn<std::string>(3)) }); + configurationSet->Path(hstring{ ConvertToUTF16(getAllSets.GetColumn<std::string>(4)) }); + configurationSet->FirstApply(clock::from_sys(ConvertUnixEpochToSystemClock(getAllSets.GetColumn<int64_t>(5)))); + + std::string schemaVersion = getAllSets.GetColumn<std::string>(6); + configurationSet->SchemaVersion(hstring{ ConvertToUTF16(schemaVersion) }); + + auto parser = ConfigurationSetParser::CreateForSchemaVersion(schemaVersion); + configurationSet->Metadata(parser->ParseValueSet(getAllSets.GetColumn<std::string>(7))); + THROW_HR_IF(E_NOTIMPL, !getAllSets.GetColumn<std::string>(8).empty()); + configurationSet->Variables(parser->ParseValueSet(getAllSets.GetColumn<std::string>(9))); + + std::vector<Configuration::ConfigurationUnit> winrtUnits; + for (const auto& unit : unitInfoTable.GetAllUnitsForSet(getAllSets.GetColumn<rowid_t>(0), schemaVersion)) + { + winrtUnits.emplace_back(*unit); + } + configurationSet->Units(std::move(winrtUnits)); + + result.emplace_back(std::move(configurationSet)); + } + + return result; + } + + std::optional<rowid_t> 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<rowid_t>(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 @@ -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 <winget/SQLiteWrapper.h> +#include <optional> +#include <vector> + +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<IConfigurationDatabase::ConfigurationSetPtr> GetAllSets(); + + // Gets the row id of the set with the given instance identifier. + std::optional<AppInstaller::SQLite::rowid_t> 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 @@ -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 <AppInstallerLanguageUtilities.h> +#include <AppInstallerStrings.h> +#include <winget/SQLiteStatementBuilder.h> + +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<rowid_t> Parent; + Configuration::ConfigurationUnit Unit; + }; + + std::queue<UnitsToInsert> 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<GUID>(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<Configuration::ConfigurationUnit> 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<Configuration::ConfigurationUnit>& winrtUnits, hstring schemaVersion) + { + Savepoint savepoint = Savepoint::Create(m_connection, "UnitInfoTable_UpdateForSet_0_1"); + + RemoveForSet(target); + + std::vector<Configuration::ConfigurationUnit> 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<IConfigurationDatabase::ConfigurationUnitPtr> 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<IConfigurationDatabase::ConfigurationUnitPtr> result; + std::map<rowid_t, IConfigurationDatabase::ConfigurationUnitPtr> rowToUnitMap; + auto parser = ConfigurationSetParser::CreateForSchemaVersion(std::string{ schemaVersion }); + + while (statement.Step()) + { + auto unit = make_self<implementation::ConfigurationUnit>(statement.GetColumn<GUID>(2)); + + unit->Type(hstring{ ConvertToUTF16(statement.GetColumn<std::string>(3)) }); + unit->Identifier(hstring{ ConvertToUTF16(statement.GetColumn<std::string>(4)) }); + unit->Intent(statement.GetColumn<ConfigurationUnitIntent>(5)); + unit->Dependencies(parser->ParseStringArray(statement.GetColumn<std::string>(6))); + unit->Metadata(parser->ParseValueSet(statement.GetColumn<std::string>(7))); + unit->Settings(parser->ParseValueSet(statement.GetColumn<std::string>(8))); + unit->IsActive(statement.GetColumn<bool>(9)); + unit->IsGroup(statement.GetColumn<bool>(10)); + + if (statement.GetColumnIsNull(1)) + { + result.emplace_back(unit); + } + else + { + rowToUnitMap.at(statement.GetColumn<rowid_t>(1))->Units().Append(*unit); + } + + rowToUnitMap.emplace(statement.GetColumn<rowid_t>(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 @@ -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 <winget/SQLiteWrapper.h> + +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<Configuration::ConfigurationUnit>& 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<IConfigurationDatabase::ConfigurationUnitPtr> 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 @@ -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> IConfigurationDatabase::CreateFor(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> storage) + { + using StorageT = std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>; + const AppInstaller::SQLite::Version& version = storage->GetVersion(); + + if (version.MajorVersion == 0) + { + constexpr std::array<std::unique_ptr<IConfigurationDatabase>(*)(StorageT&& s), 1> versionCreatorMap = + { + [](StorageT&& s) { return std::unique_ptr<IConfigurationDatabase>(std::make_unique<Database::Schema::V0_1::Interface>(std::move(s))); }, + }; + + size_t minorVersion = static_cast<size_t>(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 @@ -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 <winget/SQLiteVersion.h> +#include <winget/SQLiteDynamicStorage.h> +#include <wil/cppwinrt_wrl.h> +#include <memory> + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Interface for interacting with the configuration database. + struct IConfigurationDatabase + { + using ConfigurationSetPtr = winrt::com_ptr<implementation::ConfigurationSet>; + using ConfigurationUnitPtr = winrt::com_ptr<implementation::ConfigurationUnit>; + + 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<IConfigurationDatabase> CreateFor(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> 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<ConfigurationSetPtr> GetSets() = 0; + + // Gets the row id of the set with the given instance identifier, if present. + virtual std::optional<AppInstaller::SQLite::rowid_t> GetSetRowId(const GUID& instanceIdentifier) = 0; + }; +} diff --git a/src/Microsoft.Management.Configuration/Filesystem.cpp 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 @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/Filesystem.h> + +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 @@ -131,7 +131,7 @@ <GenerateWindowsMetadata>false</GenerateWindowsMetadata> <ModuleDefinitionFile>Microsoft_Management_Configuration.def</ModuleDefinitionFile> <WindowsMetadataFile>$(OutDir)$(ProjectName).winmd</WindowsMetadataFile> - <AdditionalDependencies>Advapi32.lib;onecoreuap.lib;%(AdditionalDependencies)</AdditionalDependencies> + <AdditionalDependencies>Advapi32.lib;onecoreuap.lib;winsqlite3.lib;%(AdditionalDependencies)</AdditionalDependencies> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'"> @@ -221,9 +221,15 @@ <ClInclude Include="ConfigurationStaticFunctions.h" /> <ClInclude Include="ConfigurationUnit.h" /> <ClInclude Include="ConfigurationUnitResultInformation.h" /> + <ClInclude Include="Database\ConfigurationDatabase.h" /> + <ClInclude Include="Database\Schema\0_1\Interface.h" /> + <ClInclude Include="Database\Schema\0_1\SetInfoTable.h" /> + <ClInclude Include="Database\Schema\0_1\UnitInfoTable.h" /> + <ClInclude Include="Database\Schema\IConfigurationDatabase.h" /> <ClInclude Include="DefaultSetGroupProcessor.h" /> <ClInclude Include="DiagnosticInformationInstance.h" /> <ClInclude Include="ExceptionResultHelpers.h" /> + <ClInclude Include="Filesystem.h" /> <ClInclude Include="GetAllConfigurationUnitSettingsResult.h" /> <ClInclude Include="GetConfigurationSetDetailsResult.h" /> <ClInclude Include="GetConfigurationUnitDetailsResult.h" /> @@ -262,8 +268,14 @@ <ClCompile Include="ConfigurationStaticFunctions.cpp" /> <ClCompile Include="ConfigurationUnit.cpp" /> <ClCompile Include="ConfigurationUnitResultInformation.cpp" /> + <ClCompile Include="Database\ConfigurationDatabase.cpp" /> + <ClCompile Include="Database\Schema\0_1\Interface_0_1.cpp" /> + <ClCompile Include="Database\Schema\0_1\SetInfoTable.cpp" /> + <ClCompile Include="Database\Schema\0_1\UnitInfoTable.cpp" /> + <ClCompile Include="Database\Schema\IConfigurationDatabase.cpp" /> <ClCompile Include="DefaultSetGroupProcessor.cpp" /> <ClCompile Include="DiagnosticInformationInstance.cpp" /> + <ClCompile Include="Filesystem.cpp" /> <ClCompile Include="GetAllConfigurationUnitSettingsResult.cpp" /> <ClCompile Include="GetConfigurationSetDetailsResult.cpp" /> <ClCompile Include="GetConfigurationUnitDetailsResult.cpp" /> @@ -308,4 +320,4 @@ <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.230706.1\build\native\Microsoft.Windows.CppWinRT.targets'))" /> <Error Condition="!Exists('$(SolutionDir)\packages\Microsoft.Windows.ImplementationLibrary.1.0.231028.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '$(SolutionDir)\packages\Microsoft.Windows.ImplementationLibrary.1.0.231028.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" /> </Target> -</Project> +</Project>+ \ 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 @@ -111,6 +111,24 @@ <ClCompile Include="ConfigurationSetUtilities.cpp"> <Filter>Parser</Filter> </ClCompile> + <ClCompile Include="Database\ConfigurationDatabase.cpp"> + <Filter>Database</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\IConfigurationDatabase.cpp"> + <Filter>Database\Schema</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\0_1\Interface_0_1.cpp"> + <Filter>Database\Schema\0_1</Filter> + </ClCompile> + <ClCompile Include="Filesystem.cpp"> + <Filter>Internals</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\0_1\SetInfoTable.cpp"> + <Filter>Database\Schema\0_1</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\0_1\UnitInfoTable.cpp"> + <Filter>Database\Schema\0_1</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -231,6 +249,24 @@ <ClInclude Include="ConfigurationSetSerializer_0_2.h"> <Filter>Parser</Filter> </ClInclude> + <ClInclude Include="Database\ConfigurationDatabase.h"> + <Filter>Database</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\IConfigurationDatabase.h"> + <Filter>Database\Schema</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\0_1\Interface.h"> + <Filter>Database\Schema\0_1</Filter> + </ClInclude> + <ClInclude Include="Filesystem.h"> + <Filter>Internals</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\0_1\SetInfoTable.h"> + <Filter>Database\Schema\0_1</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\0_1\UnitInfoTable.h"> + <Filter>Database\Schema\0_1</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" /> @@ -256,6 +292,15 @@ <Filter Include="Telemetry"> <UniqueIdentifier>{5a02f1a5-14f3-4a28-8bed-212f3e6b1a00}</UniqueIdentifier> </Filter> + <Filter Include="Database"> + <UniqueIdentifier>{c82c1df2-4ef3-4d54-9c18-a13ade2ab16a}</UniqueIdentifier> + </Filter> + <Filter Include="Database\Schema"> + <UniqueIdentifier>{6f544d8a-2c3f-4d26-9b53-84dbd2144d43}</UniqueIdentifier> + </Filter> + <Filter Include="Database\Schema\0_1"> + <UniqueIdentifier>{efb71f71-31e4-42db-9105-f10c2e89e1d5}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" /> diff --git 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 <atomic> +#include <chrono> #include <filesystem> #include <fstream> #include <functional> @@ -26,6 +27,7 @@ #include <memory> #include <mutex> #include <optional> +#include <queue> #include <sstream> #include <stack> #include <stdexcept> diff --git 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 file="OpenConfiguration.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -22,16 +22,33 @@ namespace Microsoft.WinGet.Configuration.Cmdlets.Common Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, - ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + ParameterSetName = Constants.ParameterSet.OpenConfigurationSetFromFile)] public string File { get; set; } /// <summary> + /// Gets or sets the configuration history item identifier. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSet.OpenConfigurationSetFromHistory)] + public string InstanceIdentifier { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether all configuration history items should be returned. + /// </summary> + [Parameter( + Mandatory = true, + ParameterSetName = Constants.ParameterSet.OpenAllConfigurationSetsFromHistory)] + public SwitchParameter All { get; set; } + + /// <summary> /// Gets or sets custom location to install modules. /// </summary> [Parameter( Position = 1, - ValueFromPipelineByPropertyName = true, - ParameterSetName = Constants.ParameterSet.OpenConfigurationSet)] + ValueFromPipelineByPropertyName = true)] public string ModulePath { get; set; } /// <summary> diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs b/src/PowerShell/Microsoft.WinGet.Configuration.Cmdlets/Cmdlets/ConvertToWinGetConfigurationYamlCmdlet.cs @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ConvertToWinGetConfigurationYamlCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// ConvertTo-WinGetConfigurationYaml + /// Serializes a PSConfigurationSet to a YAML string. + /// </summary> + [Cmdlet(VerbsData.ConvertTo, "WinGetConfigurationYaml")] + public sealed class ConvertToWinGetConfigurationYamlCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Converts the given set to a string. + /// </summary> + 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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="GetWinGetConfigurationCmdlet.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -14,7 +14,7 @@ namespace Microsoft.WinGet.Configuration.Cmdlets /// Get-WinGetConfiguration. /// Opens a configuration set. /// </summary> - [Cmdlet(VerbsCommon.Get, "WinGetConfiguration")] + [Cmdlet(VerbsCommon.Get, "WinGetConfiguration", DefaultParameterSetName = Helpers.Constants.ParameterSet.OpenConfigurationSetFromFile)] public sealed class GetWinGetConfigurationCmdlet : OpenConfiguration { /// <summary> @@ -23,11 +23,30 @@ namespace Microsoft.WinGet.Configuration.Cmdlets 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 @@ -0,0 +1,39 @@ +// ----------------------------------------------------------------------------- +// <copyright file="RemoveWinGetConfigurationHistoryCmdlet.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace Microsoft.WinGet.Configuration.Cmdlets +{ + using System.Management.Automation; + using Microsoft.WinGet.Configuration.Engine.Commands; + using Microsoft.WinGet.Configuration.Engine.PSObjects; + + /// <summary> + /// Remove-WinGetConfigurationHistory. + /// Removes the given configuration set from history. + /// </summary> + [Cmdlet(VerbsCommon.Remove, "WinGetConfigurationHistory")] + public sealed class RemoveWinGetConfigurationHistoryCmdlet : PSCmdlet + { + /// <summary> + /// Gets or sets the configuration set. + /// </summary> + [Parameter( + Position = 0, + Mandatory = true, + ValueFromPipeline = true, + ValueFromPipelineByPropertyName = true)] + public PSConfigurationSet Set { get; set; } + + /// <summary> + /// Removes the given set from history. + /// </summary> + 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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="Constants.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -14,7 +14,10 @@ namespace Microsoft.WinGet.Configuration.Helpers #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 @@ -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; @@ -96,10 +97,65 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands var runningTask = this.RunOnMTA<PSConfigurationSet>( async () => { + return (await this.OpenConfigurationSetAsync(openParams)) !; + }); + + this.Wait(runningTask); + this.Write(StreamType.Object, runningTask.Result); + } + + /// <summary> + /// Open a configuration set from history. + /// </summary> + /// <param name="instanceIdentifier">Instance identifier.</param> + /// <param name="modulePath">The module path to use.</param> + /// <param name="executionPolicy">Execution policy.</param> + /// <param name="canUseTelemetry">If telemetry can be used.</param> + 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<PSConfigurationSet?>( + async () => + { return await this.OpenConfigurationSetAsync(openParams); }); this.Wait(runningTask); + if (runningTask.Result != null) + { + this.Write(StreamType.Object, runningTask.Result); + } + } + + /// <summary> + /// Opens all configuration sets from history. + /// </summary> + /// <param name="modulePath">The module path to use.</param> + /// <param name="executionPolicy">Execution policy.</param> + /// <param name="canUseTelemetry">If telemetry can be used.</param> + public void GetAllFromHistory( + string modulePath, + ExecutionPolicy executionPolicy, + bool canUseTelemetry) + { + var openParams = new OpenConfigurationParameters( + this, modulePath, executionPolicy, canUseTelemetry); + + // Start task. + var runningTask = this.RunOnMTA<PSConfigurationSet[]>( + async () => + { + return await this.GetConfigurationSetHistoryAsync(openParams); + }); + + this.Wait(runningTask); this.Write(StreamType.Object, runningTask.Result); } @@ -272,6 +328,31 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands psConfigurationJob.StartCommand.Cancel(); } + /// <summary> + /// Removes a configuration set from history. + /// </summary> + /// <param name="psConfigurationSet">PSConfiguration set.</param> + public void Remove(PSConfigurationSet psConfigurationSet) + { + psConfigurationSet.Set.Remove(); + } + + /// <summary> + /// Serializes a configuration set and outputs the string. + /// </summary> + /// <param name="psConfigurationSet">PSConfiguration set.</param> + public void Serialize(PSConfigurationSet psConfigurationSet) + { + // Start task. + var result = this.RunOnMTA<string>( + () => + { + 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 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return factory; } - private async Task<PSConfigurationSet> OpenConfigurationSetAsync(OpenConfigurationParameters openParams) + private async Task<PSConfigurationSet?> 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<PSConfigurationSet[]> 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 @@ namespace Microsoft.WinGet.Configuration.Engine.Commands return psConfigurationSet; } + + /// <summary> + /// Serializes a configuration set and outputs the string. + /// </summary> + /// <param name="psConfigurationSet">PSConfiguration set.</param> + /// <returns>The string version of the set.</returns> + 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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="OpenConfigurationParameters.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -31,14 +31,44 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// <param name="modulePath">The module path to use.</param> /// <param name="executionPolicy">Execution policy.</param> /// <param name="canUseTelemetry">If telemetry can be used.</param> + /// <param name="fromHistory">If the configuration is from history; changes the meaning of `ConfigFile` to the instance identifier.</param> 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; + } + + /// <summary> + /// Initializes a new instance of the <see cref="OpenConfigurationParameters"/> class. + /// </summary> + /// <param name="pwshCmdlet">PowerShellCmdlet.</param> + /// <param name="modulePath">The module path to use.</param> + /// <param name="executionPolicy">Execution policy.</param> + /// <param name="canUseTelemetry">If telemetry can be used.</param> + 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 @@ namespace Microsoft.WinGet.Configuration.Engine.Helpers /// </summary> public bool CanUseTelemetry { get; } + /// <summary> + /// Gets a value indicating whether the configuration is from history. + /// </summary> + 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 @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="PSConfigurationSet.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -6,6 +6,7 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects { + using System; using Microsoft.Management.Configuration; /// <summary> @@ -40,6 +41,17 @@ namespace Microsoft.WinGet.Configuration.Engine.PSObjects } /// <summary> + /// Gets the instance identifier. + /// </summary> + public Guid InstanceIdentifier + { + get + { + return this.Set.InstanceIdentifier; + } + } + + /// <summary> /// Gets the origin. /// </summary> public string 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 @@ -268,6 +268,15 @@ namespace Microsoft.WinGet.Resources { } /// <summary> + /// Looks up a localized string similar to Reading configuration history. + /// </summary> + internal static string ConfigurationReadingConfigHistory { + get { + return ResourceManager.GetString("ConfigurationReadingConfigHistory", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Settings:. /// </summary> internal static string ConfigurationSettings { diff --git a/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx b/src/PowerShell/Microsoft.WinGet.Configuration.Engine/Resources/Resources.resx @@ -315,4 +315,7 @@ <data name="WinGetConfigUnitSettingConfigRoot" xml:space="preserve"> <value>A unit contains a setting that requires the config root.</value> </data> + <data name="ConfigurationReadingConfigHistory" xml:space="preserve"> + <value>Reading configuration history</value> + </data> </root> \ 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 @@ -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 @@ -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 @@ -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 +}