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