winget-cli

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

commit 3c604914c68c1321a261888b32348e250389bb96
parent 4505e94ef5541098014a4dc1002a051fc4a31e5c
Author: JohnMcPMS <johnmcp@microsoft.com>
Date:   Fri, 28 Jun 2024 13:35:10 -0700

Configuration apply queueing (#4590)

## Change
Adds a queue table to the configuration database and some code to
synchronize the application of configurations.

Every apply in the queue puts a row in the table, with its instance
identifier (it should also be in the history) and a named object that it
will keep alive as long as it is in the queue. This allows for other
queued processes to check for dead queue items.

A global named mutex must be held in order to apply, or even check if
one is at the front of the queue. If not at the front of the queue, the
waiting operation will release the mutex and wait for N * 100ms where N
is their perceived position in the queue. This should prevent repeated
contention on the global mutex as the queued items sort themselves via
the wait.
Diffstat:
Msrc/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h | 1+
Msrc/AppInstallerSharedLib/SQLiteStatementBuilder.cpp | 6++++++
Msrc/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorApplyTests.cs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/ConfigurationProcessor.cpp | 20+++++++++++++++++++-
Asrc/Microsoft.Management.Configuration/ConfigurationSequencer.cpp | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/ConfigurationSequencer.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp | 98++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h | 21+++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h | 7++++++-
Msrc/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp | 12++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_2/Interface.h | 29+++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_2/Interface_0_2.cpp | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.h | 32++++++++++++++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.cpp | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h | 25++++++++++++++++++++++++-
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj | 6++++++
Msrc/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters | 21+++++++++++++++++++++
Msrc/Microsoft.Management.Configuration/pch.h | 1+
19 files changed, 863 insertions(+), 19 deletions(-)

diff --git a/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h b/src/AppInstallerSharedLib/Public/winget/SQLiteStatementBuilder.h @@ -343,6 +343,7 @@ namespace AppInstaller::SQLite::Builder // Specify the ordering to use. StatementBuilder& OrderBy(std::string_view column); StatementBuilder& OrderBy(const QualifiedColumn& column); + StatementBuilder& OrderBy(std::initializer_list<std::string_view> columns); // Specify the ordering behavior. StatementBuilder& Ascending(); diff --git a/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp b/src/AppInstallerSharedLib/SQLiteStatementBuilder.cpp @@ -521,6 +521,12 @@ namespace AppInstaller::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::OrderBy(std::initializer_list<std::string_view> columns) + { + OutputColumns(m_stream, " ORDER BY ", columns); + return *this; + } + StatementBuilder& StatementBuilder::Ascending() { m_stream << " ASC"; diff --git a/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorApplyTests.cs b/src/Microsoft.Management.Configuration.UnitTests/Tests/ConfigurationProcessorApplyTests.cs @@ -456,6 +456,149 @@ namespace Microsoft.Management.Configuration.UnitTests.Tests this.VerifySummaryEvent(configurationSet, result, ConfigurationUnitResultSource.Precondition); } + /// <summary> + /// Ensures that multiple apply operations are sequenced. + /// </summary> + [Fact] + public void ApplySet_Sequenced() + { + ConfigurationSet configurationSet = this.ConfigurationSet(); + ConfigurationUnit configurationUnitApply = this.ConfigurationUnit().Assign(new { Intent = ConfigurationUnitIntent.Apply }); + configurationSet.Units = new ConfigurationUnit[] { configurationUnitApply }; + + ManualResetEvent startProcessing = new ManualResetEvent(true); + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + factory.CreateSetProcessorDelegate = (f, c) => + { + WaitOn(startProcessing); + return f.DefaultCreateSetProcessor(c); + }; + + TestConfigurationSetProcessor setProcessor = factory.CreateTestProcessor(configurationSet); + TestConfigurationUnitProcessor unitProcessorApply = setProcessor.CreateTestProcessor(configurationUnitApply); + unitProcessorApply.TestSettingsDelegate = () => new TestSettingsResultInstance(configurationUnitApply) { TestResult = ConfigurationTestResult.Negative }; + + ManualResetEvent applyEventWaiting = new ManualResetEvent(false); + ManualResetEvent completeApplyEvent = new ManualResetEvent(false); + unitProcessorApply.ApplySettingsDelegate = () => + { + applyEventWaiting.Set(); + WaitOn(completeApplyEvent); + return new ApplySettingsResultInstance(configurationUnitApply); + }; + + ConfigurationSet configurationSetThatWaits = this.ConfigurationSet(); + ConfigurationUnit configurationUnitThatWaits = this.ConfigurationUnit().Assign(new { Intent = ConfigurationUnitIntent.Apply }); + configurationSetThatWaits.Units = new ConfigurationUnit[] { configurationUnitThatWaits }; + + TestConfigurationSetProcessor setThatWaitsProcessor = factory.CreateTestProcessor(configurationSetThatWaits); + TestConfigurationUnitProcessor unitThatWaitsProcessor = setProcessor.CreateTestProcessor(configurationUnitThatWaits); + unitThatWaitsProcessor.TestSettingsDelegate = () => new TestSettingsResultInstance(configurationUnitApply) { TestResult = ConfigurationTestResult.Negative }; + + ManualResetEvent waitingUnitApply = new ManualResetEvent(false); + unitThatWaitsProcessor.ApplySettingsDelegate = () => + { + WaitOn(waitingUnitApply); + return new ApplySettingsResultInstance(configurationUnitThatWaits); + }; + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + var applySetOperation = processor.ApplySetAsync(configurationSet, ApplyConfigurationSetFlags.None); + WaitOn(applyEventWaiting); + + startProcessing.Reset(); + var waitingSetOperation = processor.ApplySetAsync(configurationSetThatWaits, ApplyConfigurationSetFlags.None); + AutoResetEvent waitingProgress = new AutoResetEvent(false); + ConfigurationSetState progressState = ConfigurationSetState.Unknown; + waitingSetOperation.Progress += (result, changeData) => + { + if (changeData.Change == ConfigurationSetChangeEventType.SetStateChanged) + { + progressState = changeData.SetState; + waitingProgress.Set(); + } + }; + + startProcessing.Set(); + WaitOn(waitingProgress); + Assert.Equal(ConfigurationSetState.Pending, progressState); + + completeApplyEvent.Set(); + WaitOn(waitingProgress); + Assert.Equal(ConfigurationSetState.InProgress, progressState); + + waitingUnitApply.Set(); + WaitOn(waitingProgress); + Assert.Equal(ConfigurationSetState.Completed, progressState); + } + + /// <summary> + /// Ensures that a consistency check apply is not blocked. + /// </summary> + [Fact] + public void ApplySet_ConsistencyCheckNotSequenced() + { + ConfigurationSet configurationSet = this.ConfigurationSet(); + ConfigurationUnit configurationUnitApply = this.ConfigurationUnit().Assign(new { Intent = ConfigurationUnitIntent.Apply }); + configurationSet.Units = new ConfigurationUnit[] { configurationUnitApply }; + + ManualResetEvent startProcessing = new ManualResetEvent(true); + TestConfigurationProcessorFactory factory = new TestConfigurationProcessorFactory(); + factory.CreateSetProcessorDelegate = (f, c) => + { + WaitOn(startProcessing); + return f.DefaultCreateSetProcessor(c); + }; + + TestConfigurationSetProcessor setProcessor = factory.CreateTestProcessor(configurationSet); + TestConfigurationUnitProcessor unitProcessorApply = setProcessor.CreateTestProcessor(configurationUnitApply); + unitProcessorApply.TestSettingsDelegate = () => new TestSettingsResultInstance(configurationUnitApply) { TestResult = ConfigurationTestResult.Negative }; + + ManualResetEvent applyEventWaiting = new ManualResetEvent(false); + ManualResetEvent completeApplyEvent = new ManualResetEvent(false); + unitProcessorApply.ApplySettingsDelegate = () => + { + applyEventWaiting.Set(); + WaitOn(completeApplyEvent); + return new ApplySettingsResultInstance(configurationUnitApply); + }; + + ConfigurationSet configurationSetThatWaits = this.ConfigurationSet(); + ConfigurationUnit configurationUnitThatWaits = this.ConfigurationUnit().Assign(new { Intent = ConfigurationUnitIntent.Apply }); + configurationSetThatWaits.Units = new ConfigurationUnit[] { configurationUnitThatWaits }; + + TestConfigurationSetProcessor setThatWaitsProcessor = factory.CreateTestProcessor(configurationSetThatWaits); + TestConfigurationUnitProcessor unitThatWaitsProcessor = setProcessor.CreateTestProcessor(configurationUnitThatWaits); + unitThatWaitsProcessor.TestSettingsDelegate = () => new TestSettingsResultInstance(configurationUnitApply) { TestResult = ConfigurationTestResult.Negative }; + + ManualResetEvent waitingUnitApply = new ManualResetEvent(false); + unitThatWaitsProcessor.ApplySettingsDelegate = () => + { + WaitOn(waitingUnitApply); + return new ApplySettingsResultInstance(configurationUnitThatWaits); + }; + + ConfigurationProcessor processor = this.CreateConfigurationProcessorWithDiagnostics(factory); + + var applySetOperation = processor.ApplySetAsync(configurationSet, ApplyConfigurationSetFlags.None); + WaitOn(applyEventWaiting); + + startProcessing.Reset(); + var waitingSetOperation = processor.ApplySetAsync(configurationSetThatWaits, ApplyConfigurationSetFlags.PerformConsistencyCheckOnly); + Assert.True(waitingSetOperation.AsTask().Wait(10000)); + + completeApplyEvent.Set(); + } + + private static void WaitOn(WaitHandle waitable) + { + if (!waitable.WaitOne(10000)) + { + throw new TimeoutException(); + } + } + private struct ExpectedConfigurationChangeData { public ConfigurationSetChangeEventType Change; diff --git a/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp b/src/Microsoft.Management.Configuration/ConfigurationProcessor.cpp @@ -19,6 +19,7 @@ #include "GetConfigurationUnitDetailsResult.h" #include "GetConfigurationSetDetailsResult.h" #include "DefaultSetGroupProcessor.h" +#include "ConfigurationSequencer.h" #include <AppInstallerErrors.h> #include <AppInstallerStrings.h> @@ -520,7 +521,24 @@ namespace winrt::Microsoft::Management::Configuration::implementation try { - // TODO: Send pending when blocked by another configuration run + ConfigurationSequencer sequencer{ m_database }; + + if (!WI_IsFlagSet(flags, ApplyConfigurationSetFlags::PerformConsistencyCheckOnly)) + { + if (sequencer.Enqueue(configurationSet)) + { + try + { + progress.Progress(implementation::ConfigurationSetChangeData::Create(ConfigurationSetState::Pending)); + } + CATCH_LOG(); + + sequencer.Wait(progress); + } + } + + progress.ThrowIfCancelled(); + try { progress.Progress(implementation::ConfigurationSetChangeData::Create(ConfigurationSetState::InProgress)); diff --git a/src/Microsoft.Management.Configuration/ConfigurationSequencer.cpp b/src/Microsoft.Management.Configuration/ConfigurationSequencer.cpp @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ConfigurationSequencer.h" +#include <AppInstallerStrings.h> + +using namespace std::chrono_literals; + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + ConfigurationSequencer::ConfigurationSequencer(ConfigurationDatabase& database) : m_database(database) {} + + ConfigurationSequencer::~ConfigurationSequencer() + { + // Best effort attempt to remove our queue row + try + { + m_database.RemoveQueueItem(m_queueItemObjectName); + } + CATCH_LOG(); + } + + // This function creates necessary objects and records this operation into the table. + // It then performs the equivalent of `Wait` with a timeout of 0. + bool ConfigurationSequencer::Enqueue(const Configuration::ConfigurationSet& configurationSet) + { + // Create an arbitrarily named object + std::wstring objectName = L"WinGetConfigQueue_" + AppInstaller::Utility::CreateNewGuidNameWString(); + m_queueItemObjectName = AppInstaller::Utility::ConvertToUTF8(objectName); + m_queueItemObject.create(wil::EventOptions::None, objectName.c_str()); + + m_database.AddQueueItem(configurationSet, m_queueItemObjectName); + + // Create shared mutex + constexpr PCWSTR applyMutexName = L"WinGetConfigQueueApplyMutex"; + + for (int i = 0; !m_applyMutex && i < 2; ++i) + { + if (!m_applyMutex.try_create(applyMutexName, 0, SYNCHRONIZE)) + { + m_applyMutex.try_open(applyMutexName, SYNCHRONIZE); + } + } + + THROW_LAST_ERROR_IF(!m_applyMutex); + + // Probe for an empty queue + DWORD status = 0; + m_applyMutexScope = m_applyMutex.acquire(&status, 0); + THROW_LAST_ERROR_IF(status == WAIT_FAILED); + + if (status == WAIT_TIMEOUT) + { + return true; + } + + if (GetQueuePosition() == 0) + { + m_database.SetActiveQueueItem(m_queueItemObjectName); + return false; + } + else + { + m_applyMutexScope.reset(); + return true; + } + } + + // The configuration queue consists of a table in the shared database and cooperative handling of said table. + // At any moment, the active processor must be holding a common named mutex. + // Each active queue entry also holds their own arbitrarily named object, recorded in the table. + // + // The general mechanism to wait is: + // 1. Wait on common named mutex + // 2. Check if first in queue, including probing arbitrary named objects of entries ahead of us + // 3. If not first, wait for X * queue position, where X is sufficiently high to prevent contention on main mutex + void ConfigurationSequencer::Wait(AppInstaller::WinRT::AsyncCancellation& cancellation) + { + THROW_HR_IF(E_NOT_VALID_STATE, !m_applyMutex); + + wil::unique_event cancellationEvent; + cancellationEvent.create(); + + HANDLE waitHandles[2]; + waitHandles[0] = cancellationEvent.get(); + waitHandles[1] = m_applyMutex.get(); + + cancellation.Callback([&]() { cancellationEvent.SetEvent(); }); + auto clearCancelCallback = wil::scope_exit([&cancellation]() { cancellation.Callback([]() {}); }); + + for (;;) + { + DWORD waitResult = WaitForMultipleObjects(ARRAYSIZE(waitHandles), waitHandles, FALSE, INFINITE); + THROW_LAST_ERROR_IF(waitResult == WAIT_FAILED); + + if (waitResult == WAIT_OBJECT_0) + { + // Cancellation + break; + } + else if (waitResult == WAIT_OBJECT_0 + 1 || waitResult == WAIT_ABANDONED_0 + 1) + { + // We now hold the apply mutex + wil::mutex_release_scope_exit applyMutexScope{ m_applyMutex.get() }; + + size_t queuePosition = GetQueuePosition(); + if (queuePosition == 0) + { + m_applyMutexScope = std::move(applyMutexScope); + m_database.SetActiveQueueItem(m_queueItemObjectName); + break; + } + else + { + applyMutexScope.reset(); + std::this_thread::sleep_for(queuePosition * 100ms); + } + } + } + } + + size_t ConfigurationSequencer::GetQueuePosition() + { + auto queueItems = m_database.GetQueueItems(); + + // If we get no queue items at all, we assume that the database doesn't support queueing. + if (queueItems.empty()) + { + return 0; + } + + size_t result = 0; + bool found = false; + + for (const auto& item : queueItems) + { + if (item.ObjectName == m_queueItemObjectName) + { + found = true; + break; + } + + std::wstring objectName = AppInstaller::Utility::ConvertToUTF16(item.ObjectName); + QueueObjectType itemObject; + if (itemObject.try_open(objectName.c_str(), SYNCHRONIZE)) + { + ++result; + } + else + { + // Best effort attempt to remove the dead queue row + try + { + m_database.RemoveQueueItem(item.ObjectName); + } + CATCH_LOG(); + } + } + + THROW_HR_IF(E_NOT_SET, !found); + + return result; + } +} diff --git a/src/Microsoft.Management.Configuration/ConfigurationSequencer.h b/src/Microsoft.Management.Configuration/ConfigurationSequencer.h @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Database/ConfigurationDatabase.h" +#include <winget/AsyncTokens.h> +#include <wil/resource.h> +#include <winrt/Microsoft.Management.Configuration.h> + + +namespace winrt::Microsoft::Management::Configuration::implementation +{ + // Allows for sequencing of configuration set applications. + struct ConfigurationSequencer + { + ConfigurationSequencer(ConfigurationDatabase& database); + + ConfigurationSequencer(const ConfigurationSequencer&) = delete; + ConfigurationSequencer& operator=(const ConfigurationSequencer&) = delete; + + ConfigurationSequencer(ConfigurationSequencer&&) = delete; + ConfigurationSequencer& operator=(ConfigurationSequencer&&) = delete; + + ~ConfigurationSequencer(); + + // Enters the current sequencer into the queue of operations. + // Returns true to indicate that this operation has been queued and must wait. + // Returns false to indicate that this operation is able to proceed (queued directly to the front). + bool Enqueue(const Configuration::ConfigurationSet& configurationSet); + + // Waits for this operation to reach the front of the queue. + // Registers a cancellation callback so that we can halt our waiting. + void Wait(AppInstaller::WinRT::AsyncCancellation& cancellation); + + private: + // Determines the effective queue position of this operation; removing queue entries that are not longer active. + // 0 is the front of the queue. + size_t GetQueuePosition(); + + using QueueObjectType = wil::unique_event; + + ConfigurationDatabase& m_database; + std::string m_queueItemObjectName; + QueueObjectType m_queueItemObject; + wil::unique_mutex m_applyMutex; + wil::mutex_release_scope_exit m_applyMutexScope; + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.cpp @@ -66,7 +66,7 @@ namespace winrt::Microsoft::Management::Configuration::implementation if (!m_database && std::filesystem::is_regular_file(databaseFile)) { m_connection = std::make_shared<SQLiteDynamicStorage>(databaseFile, SQLiteStorageBase::OpenDisposition::ReadWrite); - m_database = IConfigurationDatabase::CreateFor(m_connection); + m_database = IConfigurationDatabase::CreateFor(m_connection, true); } } #ifdef AICLI_DISABLE_TEST_HOOKS @@ -170,6 +170,102 @@ namespace winrt::Microsoft::Management::Configuration::implementation #endif } + + void ConfigurationDatabase::AddQueueItem(const Configuration::ConfigurationSet& configurationSet, const std::string& objectName) + { +#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("AddQueueItem"); + + m_database->AddQueueItem(configurationSet.InstanceIdentifier(), objectName); + m_connection->SetLastWriteTime(); + + transaction->Commit(); +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + void ConfigurationDatabase::SetActiveQueueItem(const std::string& objectName) + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + THROW_HR_IF_NULL(E_NOT_VALID_STATE, m_database); + + auto transaction = BeginTransaction("SetActiveQueueItem"); + + m_database->SetActiveQueueItem(objectName); + m_connection->SetLastWriteTime(); + + transaction->Commit(); +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); +#endif + } + + std::vector<ConfigurationDatabase::QueueItem> ConfigurationDatabase::GetQueueItems() const + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + THROW_HR_IF_NULL(E_NOT_VALID_STATE, m_database); + + auto transaction = BeginTransaction("GetQueueItems"); + + std::vector<ConfigurationDatabase::QueueItem> result; + auto queueItems = m_database->GetQueueItems(); + result.reserve(queueItems.size()); + + for (const auto& item : queueItems) + { + QueueItem resultItem; + std::tie(resultItem.SetInstanceIdentifier, resultItem.ObjectName, resultItem.QueuedAt, resultItem.Active) = item; + result.emplace_back(std::move(resultItem)); + } + + return result; +#ifdef AICLI_DISABLE_TEST_HOOKS + } + CATCH_LOG(); + + return {}; +#endif + } + + void ConfigurationDatabase::RemoveQueueItem(const std::string& objectName) + { +#ifdef AICLI_DISABLE_TEST_HOOKS + // While under development, treat errors escaping this function as a test hook. + try + { +#endif + THROW_HR_IF_NULL(E_NOT_VALID_STATE, m_database); + + auto transaction = BeginTransaction("RemoveQueueItem"); + + m_database->RemoveQueueItem(objectName); + 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); diff --git a/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h b/src/Microsoft.Management.Configuration/Database/ConfigurationDatabase.h @@ -41,6 +41,27 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Removes the given set from the database history if it is present. void RemoveSetHistory(const Configuration::ConfigurationSet& configurationSet); + // Adds a new queue item for the given configuration set and object name. + void AddQueueItem(const Configuration::ConfigurationSet& configurationSet, const std::string& objectName); + + // Sets the queue item with the given object name as active. + void SetActiveQueueItem(const std::string& objectName); + + // Data about a queue item. + struct QueueItem + { + GUID SetInstanceIdentifier{}; + std::string ObjectName; + std::chrono::system_clock::time_point QueuedAt; + bool Active = false; + }; + + // Gets all queue items in queue order (item at index 0 is active/next). + std::vector<QueueItem> GetQueueItems() const; + + // Removes the queue item with the given object name. + void RemoveQueueItem(const std::string& objectName); + private: std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> m_connection; mutable std::unique_ptr<IConfigurationDatabase> m_database; diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface.h @@ -9,6 +9,8 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: { Interface(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> storage); + const AppInstaller::SQLite::Version& GetSchemaVersion() override; + // Version 0.1 void InitializeDatabase() override; void AddSet(const Configuration::ConfigurationSet& configurationSet) override; @@ -17,7 +19,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: std::vector<ConfigurationSetPtr> GetSets() override; std::optional<AppInstaller::SQLite::rowid_t> GetSetRowId(const GUID& instanceIdentifier) override; - private: + // Version 0.2 + bool MigrateFrom(IConfigurationDatabase* current) override; + + protected: std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> m_storage; }; } diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_1/Interface_0_1.cpp @@ -10,10 +10,17 @@ using namespace AppInstaller::Utility; namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_1 { + static constexpr AppInstaller::SQLite::Version s_InterfaceVersion{ 0, 1 }; + Interface::Interface(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> storage) : m_storage(std::move(storage)) {} + const AppInstaller::SQLite::Version& Interface::GetSchemaVersion() + { + return s_InterfaceVersion; + } + void Interface::InitializeDatabase() { // Must enable WAL mode outside of a transaction @@ -71,4 +78,9 @@ namespace winrt::Microsoft::Management::Configuration::implementation::Database: SetInfoTable setInfoTable(*m_storage); return setInfoTable.GetSetRowId(instanceIdentifier); } + + bool Interface::MigrateFrom(IConfigurationDatabase* current) + { + return current->GetSchemaVersion() == s_InterfaceVersion; + } } diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/Interface.h b/src/Microsoft.Management.Configuration/Database/Schema/0_2/Interface.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Database/Schema/IConfigurationDatabase.h" +#include "Database/Schema/0_1/Interface.h" + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_2 +{ + struct Interface : public V0_1::Interface + { + using V0_1::Interface::Interface; + + const AppInstaller::SQLite::Version& GetSchemaVersion() override; + + // Version 0.1 + void InitializeDatabase() override; + + // Version 0.2 + bool MigrateFrom(IConfigurationDatabase* current) override; + void AddQueueItem(const GUID& instanceIdentifier, const std::string& objectName) override; + void SetActiveQueueItem(const std::string& objectName) override; + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> GetQueueItems() override; + void RemoveQueueItem(const std::string& objectName) override; + + private: + // Unconditionally attempts to migrate from the 0.1 base. + void MigrateFrom0_1(); + }; +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/Interface_0_2.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_2/Interface_0_2.cpp @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Interface.h" +#include "QueueTable.h" +#include <winget/SQLiteMetadataTable.h> + +using namespace AppInstaller::SQLite; +using namespace AppInstaller::Utility; + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_2 +{ + static constexpr AppInstaller::SQLite::Version s_InterfaceVersion{ 0, 2 }; + + const AppInstaller::SQLite::Version& Interface::GetSchemaVersion() + { + return s_InterfaceVersion; + } + + void Interface::InitializeDatabase() + { + V0_1::Interface::InitializeDatabase(); + MigrateFrom0_1(); + } + + bool Interface::MigrateFrom(IConfigurationDatabase* current) + { + auto currentSchemaVersion = current->GetSchemaVersion(); + if (currentSchemaVersion < s_InterfaceVersion) + { + if (V0_1::Interface::MigrateFrom(current)) + { + Savepoint savepoint = Savepoint::Create(*m_storage, "MigrateFrom0_1"); + + MigrateFrom0_1(); + s_InterfaceVersion.SetSchemaVersion(*m_storage); + + savepoint.Commit(); + + return true; + } + } + else if (currentSchemaVersion == s_InterfaceVersion) + { + return true; + } + + return false; + } + + void Interface::AddQueueItem(const GUID& instanceIdentifier, const std::string& objectName) + { + QueueTable queueTable(*m_storage); + queueTable.AddQueueItem(instanceIdentifier, objectName); + } + + void Interface::SetActiveQueueItem(const std::string& objectName) + { + QueueTable queueTable(*m_storage); + queueTable.SetActiveQueueItem(objectName); + } + + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> Interface::GetQueueItems() + { + QueueTable queueTable(*m_storage); + return queueTable.GetQueueItems(); + } + + void Interface::RemoveQueueItem(const std::string& objectName) + { + QueueTable queueTable(*m_storage); + queueTable.RemoveQueueItem(objectName); + } + + void Interface::MigrateFrom0_1() + { + QueueTable queueTable(*m_storage); + queueTable.Create(); + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.cpp @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "QueueTable.h" +#include <AppInstallerDateTime.h> +#include <winget/SQLiteStatementBuilder.h> + +using namespace AppInstaller::SQLite; +using namespace AppInstaller::SQLite::Builder; +using namespace AppInstaller::Utility; + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_2 +{ + namespace + { + constexpr std::string_view s_QueueTable_Table = "queue"sv; + + constexpr std::string_view s_QueueTable_Column_SetInstanceIdentifier = "set_instance_identifier"sv; + constexpr std::string_view s_QueueTable_Column_ObjectName = "object_name"sv; + constexpr std::string_view s_QueueTable_Column_QueuedAt = "queued_at"sv; + constexpr std::string_view s_QueueTable_Column_Active = "active"sv; + } + + QueueTable::QueueTable(Connection& connection) : m_connection(connection) {} + + void QueueTable::Create() + { + Savepoint savepoint = Savepoint::Create(m_connection, "QueueTable_Create_0_2"); + + StatementBuilder tableBuilder; + tableBuilder.CreateTable(s_QueueTable_Table).Columns({ + IntegerPrimaryKey(), + ColumnBuilder(s_QueueTable_Column_SetInstanceIdentifier, Type::Blob).NotNull(), + ColumnBuilder(s_QueueTable_Column_ObjectName, Type::Text).Unique().NotNull(), + ColumnBuilder(s_QueueTable_Column_QueuedAt, Type::Int64).NotNull(), + ColumnBuilder(s_QueueTable_Column_Active, Type::Bool).NotNull(), + }); + + tableBuilder.Execute(m_connection); + + savepoint.Commit(); + } + + void QueueTable::AddQueueItem(const GUID& instanceIdentifier, const std::string& objectName) + { + StatementBuilder builder; + builder.InsertInto(s_QueueTable_Table).Columns({ + s_QueueTable_Column_SetInstanceIdentifier, + s_QueueTable_Column_ObjectName, + s_QueueTable_Column_QueuedAt, + s_QueueTable_Column_Active + }).Values( + instanceIdentifier, + objectName, + GetCurrentUnixEpoch(), + false + ); + + builder.Execute(m_connection); + } + + void QueueTable::SetActiveQueueItem(const std::string& objectName) + { + StatementBuilder builder; + builder.Update(s_QueueTable_Table).Set().Column(s_QueueTable_Column_Active).Equals(true).Where(s_QueueTable_Column_ObjectName).Equals(objectName); + + builder.Execute(m_connection); + } + + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> QueueTable::GetQueueItems() + { + StatementBuilder builder; + builder.Select({ + s_QueueTable_Column_SetInstanceIdentifier, + s_QueueTable_Column_ObjectName, + s_QueueTable_Column_QueuedAt, + s_QueueTable_Column_Active + }).From(s_QueueTable_Table).OrderBy({ s_QueueTable_Column_QueuedAt, RowIDName }); + + Statement statement = builder.Prepare(m_connection); + + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> result; + + while (statement.Step()) + { + result.emplace_back(std::make_tuple(statement.GetColumn<GUID>(0), statement.GetColumn<std::string>(1), ConvertUnixEpochToSystemClock(statement.GetColumn<int64_t>(2)), statement.GetColumn<bool>(3))); + } + + return result; + } + + void QueueTable::RemoveQueueItem(const std::string& objectName) + { + StatementBuilder builder; + builder.DeleteFrom(s_QueueTable_Table).Where(s_QueueTable_Column_ObjectName).Equals(objectName); + + builder.Execute(m_connection); + } +} diff --git a/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.h b/src/Microsoft.Management.Configuration/Database/Schema/0_2/QueueTable.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include <winget/SQLiteWrapper.h> +#include <vector> +#include <tuple> + +namespace winrt::Microsoft::Management::Configuration::implementation::Database::Schema::V0_2 +{ + struct QueueTable + { + QueueTable(AppInstaller::SQLite::Connection& connection); + + // Creates the queue table. + void Create(); + + // Adds a new queue item for the given configuration set and object name. + void AddQueueItem(const GUID& instanceIdentifier, const std::string& objectName); + + // Sets the queue item with the given object name as active. + void SetActiveQueueItem(const std::string& objectName); + + // Gets all queue items in queue order (item at index 0 is active/next). + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> GetQueueItems(); + + // Removes the queue item with the given object name. + void RemoveQueueItem(const std::string& objectName); + + 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 @@ -4,34 +4,74 @@ #include "Database/Schema/IConfigurationDatabase.h" #include "Database/Schema/0_1/Interface.h" +#include "Database/Schema/0_2/Interface.h" namespace winrt::Microsoft::Management::Configuration::implementation { + namespace + { + std::unique_ptr<IConfigurationDatabase> CreateForVersion(const AppInstaller::SQLite::Version& version, const std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>& storage) + { + using StorageT = std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>; + + if (version.MajorVersion == 0) + { + constexpr std::array<std::unique_ptr<IConfigurationDatabase>(*)(const StorageT& s), 2> versionCreatorMap = + { + [](const StorageT& s) { return std::unique_ptr<IConfigurationDatabase>(std::make_unique<Database::Schema::V0_1::Interface>(s)); }, + [](const StorageT& s) { return std::unique_ptr<IConfigurationDatabase>(std::make_unique<Database::Schema::V0_2::Interface>(s)); }, + }; + + size_t minorVersion = static_cast<size_t>(version.MinorVersion); + if (minorVersion >= 1 && minorVersion <= versionCreatorMap.size()) + { + return versionCreatorMap[minorVersion - 1](storage); + } + } + + // We do not have the capacity to operate on this schema version + THROW_WIN32(ERROR_NOT_SUPPORTED); + } + } + AppInstaller::SQLite::Version IConfigurationDatabase::GetLatestVersion() { - return { 0, 1 }; + return { 0, 2 }; } - std::unique_ptr<IConfigurationDatabase> IConfigurationDatabase::CreateFor(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> storage) + std::unique_ptr<IConfigurationDatabase> IConfigurationDatabase::CreateFor(const std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>& storage, bool allowMigration) { using StorageT = std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>; const AppInstaller::SQLite::Version& version = storage->GetVersion(); - if (version.MajorVersion == 0) - { - constexpr std::array<std::unique_ptr<IConfigurationDatabase>(*)(StorageT&& s), 1> versionCreatorMap = - { - [](StorageT&& s) { return std::unique_ptr<IConfigurationDatabase>(std::make_unique<Database::Schema::V0_1::Interface>(std::move(s))); }, - }; + std::unique_ptr<IConfigurationDatabase> result = CreateForVersion(version, storage); - size_t minorVersion = static_cast<size_t>(version.MinorVersion); - if (minorVersion >= 1 && minorVersion <= versionCreatorMap.size()) - { - return versionCreatorMap[minorVersion - 1](std::move(storage)); - } + AppInstaller::SQLite::Version latestVersion = GetLatestVersion(); + if (allowMigration && version < latestVersion) + { + // Always migrate to the latest version until a reason comes along to not do that + std::unique_ptr<IConfigurationDatabase> latest = CreateForVersion(latestVersion, storage); + THROW_WIN32_IF(ERROR_NOT_SUPPORTED, !latest->MigrateFrom(result.get())); + result = std::move(latest); } - // We do not have the capacity to operate on this schema version - THROW_WIN32(ERROR_NOT_SUPPORTED); + return result; + } + + void IConfigurationDatabase::AddQueueItem(const GUID&, const std::string&) + { + } + + void IConfigurationDatabase::SetActiveQueueItem(const std::string&) + { + } + + std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> IConfigurationDatabase::GetQueueItems() + { + return {}; + } + + void IConfigurationDatabase::RemoveQueueItem(const std::string&) + { } } diff --git a/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h b/src/Microsoft.Management.Configuration/Database/Schema/IConfigurationDatabase.h @@ -8,6 +8,7 @@ #include <winget/SQLiteDynamicStorage.h> #include <wil/cppwinrt_wrl.h> #include <memory> +#include <tuple> namespace winrt::Microsoft::Management::Configuration::implementation { @@ -23,7 +24,10 @@ namespace winrt::Microsoft::Management::Configuration::implementation static AppInstaller::SQLite::Version GetLatestVersion(); // Creates the version appropriate database object for the given storage. - static std::unique_ptr<IConfigurationDatabase> CreateFor(std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage> storage); + static std::unique_ptr<IConfigurationDatabase> CreateFor(const std::shared_ptr<AppInstaller::SQLite::SQLiteDynamicStorage>& storage, bool allowMigration = false); + + // Gets the schema version from the current interface. + virtual const AppInstaller::SQLite::Version& GetSchemaVersion() = 0; // Version 0.1 @@ -44,5 +48,24 @@ namespace winrt::Microsoft::Management::Configuration::implementation // Gets the row id of the set with the given instance identifier, if present. virtual std::optional<AppInstaller::SQLite::rowid_t> GetSetRowId(const GUID& instanceIdentifier) = 0; + + // Version 0.2 + + // Migrates from the current interface given. + // Returns true if supported (or is already same schema version); false if not. + // Throws on errors that occur during an attempted migration. + virtual bool MigrateFrom(IConfigurationDatabase* current) = 0; + + // Adds a new queue item for the given configuration set and object name. + virtual void AddQueueItem(const GUID& instanceIdentifier, const std::string& objectName); + + // Sets the queue item with the given object name as active. + virtual void SetActiveQueueItem(const std::string& objectName); + + // Gets all queue items in queue order (item at index 0 is active/next). + virtual std::vector<std::tuple<GUID, std::string, std::chrono::system_clock::time_point, bool>> GetQueueItems(); + + // Removes the queue item with the given object name. + virtual void RemoveQueueItem(const std::string& objectName); }; } diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj @@ -207,6 +207,7 @@ <ClInclude Include="ConfigurationConflictSetting.h" /> <ClInclude Include="ConfigurationParameter.h" /> <ClInclude Include="ConfigurationProcessor.h" /> + <ClInclude Include="ConfigurationSequencer.h" /> <ClInclude Include="ConfigurationSet.h" /> <ClInclude Include="ConfigurationSetApplyProcessor.h" /> <ClInclude Include="ConfigurationSetChangeData.h" /> @@ -225,6 +226,8 @@ <ClInclude Include="Database\Schema\0_1\Interface.h" /> <ClInclude Include="Database\Schema\0_1\SetInfoTable.h" /> <ClInclude Include="Database\Schema\0_1\UnitInfoTable.h" /> + <ClInclude Include="Database\Schema\0_2\Interface.h" /> + <ClInclude Include="Database\Schema\0_2\QueueTable.h" /> <ClInclude Include="Database\Schema\IConfigurationDatabase.h" /> <ClInclude Include="DefaultSetGroupProcessor.h" /> <ClInclude Include="DiagnosticInformationInstance.h" /> @@ -255,6 +258,7 @@ <ClCompile Include="ConfigurationConflictSetting.cpp" /> <ClCompile Include="ConfigurationParameter.cpp" /> <ClCompile Include="ConfigurationProcessor.cpp" /> + <ClCompile Include="ConfigurationSequencer.cpp" /> <ClCompile Include="ConfigurationSet.cpp" /> <ClCompile Include="ConfigurationSetApplyProcessor.cpp" /> <ClCompile Include="ConfigurationSetChangeData.cpp" /> @@ -272,6 +276,8 @@ <ClCompile Include="Database\Schema\0_1\Interface_0_1.cpp" /> <ClCompile Include="Database\Schema\0_1\SetInfoTable.cpp" /> <ClCompile Include="Database\Schema\0_1\UnitInfoTable.cpp" /> + <ClCompile Include="Database\Schema\0_2\Interface_0_2.cpp" /> + <ClCompile Include="Database\Schema\0_2\QueueTable.cpp" /> <ClCompile Include="Database\Schema\IConfigurationDatabase.cpp" /> <ClCompile Include="DefaultSetGroupProcessor.cpp" /> <ClCompile Include="DiagnosticInformationInstance.cpp" /> diff --git a/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters b/src/Microsoft.Management.Configuration/Microsoft.Management.Configuration.vcxproj.filters @@ -129,6 +129,15 @@ <ClCompile Include="Database\Schema\0_1\UnitInfoTable.cpp"> <Filter>Database\Schema\0_1</Filter> </ClCompile> + <ClCompile Include="ConfigurationSequencer.cpp"> + <Filter>Internals</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\0_2\Interface_0_2.cpp"> + <Filter>Database\Schema\0_2</Filter> + </ClCompile> + <ClCompile Include="Database\Schema\0_2\QueueTable.cpp"> + <Filter>Database\Schema\0_2</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h" /> @@ -267,6 +276,15 @@ <ClInclude Include="Database\Schema\0_1\UnitInfoTable.h"> <Filter>Database\Schema\0_1</Filter> </ClInclude> + <ClInclude Include="ConfigurationSequencer.h"> + <Filter>Internals</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\0_2\Interface.h"> + <Filter>Database\Schema\0_2</Filter> + </ClInclude> + <ClInclude Include="Database\Schema\0_2\QueueTable.h"> + <Filter>Database\Schema\0_2</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <Midl Include="Microsoft.Management.Configuration.idl" /> @@ -301,6 +319,9 @@ <Filter Include="Database\Schema\0_1"> <UniqueIdentifier>{efb71f71-31e4-42db-9105-f10c2e89e1d5}</UniqueIdentifier> </Filter> + <Filter Include="Database\Schema\0_2"> + <UniqueIdentifier>{f214d0f3-3e9c-469b-91ae-213315d39a69}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <Natvis Include="$(MSBuildThisFileDirectory)..\..\natvis\wil.natvis" /> diff --git a/src/Microsoft.Management.Configuration/pch.h b/src/Microsoft.Management.Configuration/pch.h @@ -33,5 +33,6 @@ #include <stdexcept> #include <string> #include <string_view> +#include <tuple> #include <utility> #include <vector>