From 85951aef927a010a7726bdf614afa4cde00344ac Mon Sep 17 00:00:00 2001 From: Ryan <69221034+ryfu-msft@users.noreply.github.com> Date: Wed, 11 Oct 2023 22:38:52 -0400 Subject: [PATCH] Add `resume` command and support saving the argument state. (#3508) --- .github/actions/spelling/allow.txt | 1 + .../AppInstallerCLICore.vcxproj | 6 + .../AppInstallerCLICore.vcxproj.filters | 22 +- src/AppInstallerCLICore/Argument.cpp | 6 + src/AppInstallerCLICore/CheckpointManager.cpp | 166 ++++++++++++++ src/AppInstallerCLICore/CheckpointManager.h | 52 +++++ src/AppInstallerCLICore/Command.cpp | 8 +- src/AppInstallerCLICore/Command.h | 3 + .../Commands/InstallCommand.cpp | 15 +- .../Commands/InstallCommand.h | 2 + .../Commands/ResumeCommand.cpp | 118 ++++++++++ .../Commands/ResumeCommand.h | 22 ++ .../Commands/RootCommand.cpp | 2 + src/AppInstallerCLICore/ExecutionArgs.h | 3 + src/AppInstallerCLICore/ExecutionContext.cpp | 44 ++++ src/AppInstallerCLICore/ExecutionContext.h | 16 ++ src/AppInstallerCLICore/Resources.h | 7 + .../Workflows/ResumeFlow.cpp | 17 ++ .../Workflows/ResumeFlow.h | 22 ++ .../Workflows/WorkflowBase.h | 1 - src/AppInstallerCLIE2ETests/Constants.cs | 7 + .../FeaturesCommand.cs | 1 + .../Helpers/TestCommon.cs | 16 ++ .../Helpers/WinGetSettingsHelper.cs | 5 +- src/AppInstallerCLIE2ETests/ResumeCommand.cs | 102 +++++++++ .../Shared/Strings/en-us/winget.resw | 23 ++ .../AppInstallerCLITests.vcxproj | 2 + .../AppInstallerCLITests.vcxproj.filters | 6 + .../CheckpointDatabase.cpp | 129 +++++++++++ src/AppInstallerCLITests/ResumeFlow.cpp | 197 +++++++++++++++++ src/AppInstallerCLITests/Strings.cpp | 8 + .../ExperimentalFeature.cpp | 4 + .../Public/AppInstallerRuntime.h | 2 + .../Public/winget/ExperimentalFeature.h | 1 + .../Public/winget/UserSettings.h | 2 + src/AppInstallerCommonCore/Runtime.cpp | 9 + src/AppInstallerCommonCore/UserSettings.cpp | 1 + .../AppInstallerRepositoryCore.vcxproj | 12 +- ...AppInstallerRepositoryCore.vcxproj.filters | 35 ++- .../Microsoft/CheckpointDatabase.cpp | 129 +++++++++++ .../Microsoft/CheckpointDatabase.h | 71 ++++++ .../Checkpoint_1_0/CheckpointDataTable.cpp | 209 ++++++++++++++++++ .../Checkpoint_1_0/CheckpointDataTable.h | 42 ++++ .../CheckpointDatabaseInterface.h | 23 ++ .../CheckpointDatabaseInterface_1_0.cpp | 77 +++++++ .../Schema/Checkpoint_1_0/CheckpointTable.cpp | 66 ++++++ .../Schema/Checkpoint_1_0/CheckpointTable.h | 24 ++ .../Microsoft/Schema/ICheckpointDatabase.h | 41 ++++ .../Microsoft/Schema/Version.cpp | 22 ++ .../Microsoft/Schema/Version.h | 9 +- .../Public/winget/Checkpoint.h | 75 +++++++ .../SQLiteStatementBuilder.cpp | 15 ++ .../SQLiteStatementBuilder.h | 5 + .../AppInstallerStrings.cpp | 7 + src/AppInstallerSharedLib/Errors.cpp | 5 + .../Public/AppInstallerErrors.h | 5 +- .../Public/AppInstallerStrings.h | 2 + 57 files changed, 1907 insertions(+), 15 deletions(-) create mode 100644 src/AppInstallerCLICore/CheckpointManager.cpp create mode 100644 src/AppInstallerCLICore/CheckpointManager.h create mode 100644 src/AppInstallerCLICore/Commands/ResumeCommand.cpp create mode 100644 src/AppInstallerCLICore/Commands/ResumeCommand.h create mode 100644 src/AppInstallerCLICore/Workflows/ResumeFlow.cpp create mode 100644 src/AppInstallerCLICore/Workflows/ResumeFlow.h create mode 100644 src/AppInstallerCLIE2ETests/ResumeCommand.cs create mode 100644 src/AppInstallerCLITests/CheckpointDatabase.cpp create mode 100644 src/AppInstallerCLITests/ResumeFlow.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface_1_0.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.cpp create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h create mode 100644 src/AppInstallerRepositoryCore/Microsoft/Schema/ICheckpointDatabase.h create mode 100644 src/AppInstallerRepositoryCore/Public/winget/Checkpoint.h diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 620d4eb9c7..e20cc0ed32 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -233,6 +233,7 @@ https HWND Hyperlink IAppx +ICheckpoint IConfiguration icu IDX diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj index f52f3ac068..af557baf27 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -350,6 +350,7 @@ + @@ -371,6 +372,7 @@ + @@ -414,6 +416,7 @@ + @@ -422,6 +425,7 @@ + @@ -453,6 +457,7 @@ + @@ -484,6 +489,7 @@ + diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters index 3e3b894eb3..de40ebe314 100644 --- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters +++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -233,6 +233,15 @@ Commands + + Commands + + + Workflows + + + Header Files + @@ -424,10 +433,19 @@ Commands - + Source Files - + + Commands + + + Commands + + + Workflows + + Source Files diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp index 52c353e392..4b25532125 100644 --- a/src/AppInstallerCLICore/Argument.cpp +++ b/src/AppInstallerCLICore/Argument.cpp @@ -180,6 +180,10 @@ namespace AppInstaller::CLI case Execution::Args::Type::ErrorInput: return { type, "input"_liv, ArgTypeCategory::None }; + // Resume command + case Execution::Args::Type::ResumeId: + return { type, "resume-id"_liv, 'g', ArgTypeCategory::None }; + // Configuration commands case Execution::Args::Type::ConfigurationFile: return { type, "file"_liv, 'f' }; @@ -355,6 +359,8 @@ namespace AppInstaller::CLI return Argument{ type, Resource::String::DownloadDirectoryArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; case Args::Type::InstallerType: return Argument{ type, Resource::String::InstallerTypeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; + case Args::Type::ResumeId: + return Argument{ type, Resource::String::ResumeIdArgumentDescription, ArgumentType::Standard, true }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCLICore/CheckpointManager.cpp b/src/AppInstallerCLICore/CheckpointManager.cpp new file mode 100644 index 0000000000..65e1f7396d --- /dev/null +++ b/src/AppInstallerCLICore/CheckpointManager.cpp @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "CheckpointManager.h" +#include "Command.h" +#include "ExecutionContextData.h" +#include + +namespace AppInstaller::Checkpoints +{ + using namespace AppInstaller::CLI; + using namespace AppInstaller::Repository::Microsoft; + using namespace AppInstaller::Repository::SQLite; + + // This checkpoint name is reserved for the starting checkpoint which captures the automatic metadata. + constexpr std::string_view s_AutomaticCheckpoint = "automatic"sv; + constexpr std::string_view s_CheckpointsFileName = "checkpoints.db"sv; + + std::filesystem::path CheckpointManager::GetCheckpointDatabasePath(const std::string_view& resumeId, bool createCheckpointDirectory) + { + const auto checkpointsDirectory = Runtime::GetPathTo(Runtime::PathName::CheckpointsLocation) / resumeId; + + if (createCheckpointDirectory) + { + if (!std::filesystem::exists(checkpointsDirectory)) + { + AICLI_LOG(Repo, Info, << "Creating checkpoint database directory: " << checkpointsDirectory); + std::filesystem::create_directories(checkpointsDirectory); + } + else + { + THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANNOT_MAKE), !std::filesystem::is_directory(checkpointsDirectory)); + } + } + + auto recordPath = checkpointsDirectory / s_CheckpointsFileName; + return recordPath; + } + + CheckpointManager::CheckpointManager() + { + GUID resumeId; + std::ignore = CoCreateGuid(&resumeId); + m_resumeId = Utility::ConvertGuidToString(resumeId); + const auto& checkpointDatabasePath = GetCheckpointDatabasePath(m_resumeId, true); + m_checkpointDatabase = CheckpointDatabase::CreateNew(checkpointDatabasePath.u8string()); + } + + CheckpointManager::CheckpointManager(const std::string& resumeId) + { + m_resumeId = resumeId; + const auto& checkpointDatabasePath = GetCheckpointDatabasePath(m_resumeId); + m_checkpointDatabase = CheckpointDatabase::Open(checkpointDatabasePath.u8string()); + } + + void CheckpointManager::CreateAutomaticCheckpoint(CLI::Execution::Context& context) + { + CheckpointDatabase::IdType startCheckpointId = m_checkpointDatabase->AddCheckpoint(s_AutomaticCheckpoint); + Checkpoint automaticCheckpoint{ m_checkpointDatabase, startCheckpointId }; + + automaticCheckpoint.Set(AutomaticCheckpointData::ClientVersion, {}, AppInstaller::Runtime::GetClientVersion()); + + const auto& executingCommand = context.GetExecutingCommand(); + if (executingCommand != nullptr) + { + automaticCheckpoint.Set(AutomaticCheckpointData::Command, {}, std::string{ executingCommand->FullName() }); + } + + const auto& argTypes = context.Args.GetTypes(); + for (auto type : argTypes) + { + const auto& argument = std::to_string(static_cast(type)); + auto argumentType = Argument::ForType(type).Type(); + + if (argumentType == ArgumentType::Flag) + { + automaticCheckpoint.Set(AutomaticCheckpointData::Arguments, argument, {}); + } + else + { + const auto& values = *context.Args.GetArgs(type); + automaticCheckpoint.SetMany(AutomaticCheckpointData::Arguments, argument, values); + } + } + } + + void LoadCommandArgsFromAutomaticCheckpoint(CLI::Execution::Context& context, Checkpoint& automaticCheckpoint) + { + for (const auto& fieldName : automaticCheckpoint.GetFieldNames(AutomaticCheckpointData::Arguments)) + { + // Command arguments are represented as integer strings in the checkpoint record. + Execution::Args::Type type = static_cast(std::stoi(fieldName)); + auto argumentType = Argument::ForType(type).Type(); + if (argumentType == ArgumentType::Flag) + { + context.Args.AddArg(type); + } + else + { + const auto& values = automaticCheckpoint.GetMany(AutomaticCheckpointData::Arguments, fieldName); + for (const auto& value : values) + { + context.Args.AddArg(type, value); + } + } + } + } + + std::optional> CheckpointManager::GetAutomaticCheckpoint() + { + const auto& checkpointIds = m_checkpointDatabase->GetCheckpointIds(); + if (checkpointIds.empty()) + { + return {}; + } + + CheckpointDatabase::IdType automaticCheckpointId = checkpointIds.back(); + return Checkpoint{ m_checkpointDatabase, automaticCheckpointId }; + } + + Checkpoint CheckpointManager::CreateCheckpoint(std::string_view checkpointName) + { + CheckpointDatabase::IdType checkpointId = m_checkpointDatabase->AddCheckpoint(checkpointName); + Checkpoint checkpoint{ m_checkpointDatabase, checkpointId }; + return checkpoint; + } + + std::vector> CheckpointManager::GetCheckpoints() + { + auto checkpointIds = m_checkpointDatabase->GetCheckpointIds(); + if (checkpointIds.empty()) + { + return {}; + } + + // Remove the last checkpoint (automatic) + checkpointIds.pop_back(); + + std::vector> checkpoints; + for (const auto& checkpointId : checkpointIds) + { + checkpoints.emplace_back(Checkpoint{ m_checkpointDatabase, checkpointId }); + } + + return checkpoints; + } + + void CheckpointManager::CleanUpDatabase() + { + if (m_checkpointDatabase) + { + m_checkpointDatabase.reset(); + } + + if (!m_resumeId.empty()) + { + const auto& checkpointDatabasePath = GetCheckpointDatabasePath(m_resumeId); + if (std::filesystem::exists(checkpointDatabasePath)) + { + const auto& checkpointDatabaseParentDirectory = checkpointDatabasePath.parent_path(); + AICLI_LOG(CLI, Info, << "Deleting Checkpoint database directory: " << checkpointDatabaseParentDirectory); + std::filesystem::remove_all(checkpointDatabaseParentDirectory); + } + } + } +} diff --git a/src/AppInstallerCLICore/CheckpointManager.h b/src/AppInstallerCLICore/CheckpointManager.h new file mode 100644 index 0000000000..2268e84ff2 --- /dev/null +++ b/src/AppInstallerCLICore/CheckpointManager.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContextData.h" +#include "ExecutionContext.h" +#include "Public/winget/Checkpoint.h" +#include + +namespace AppInstaller::Repository::Microsoft +{ + struct CheckpointDatabase; +} + +namespace AppInstaller::Checkpoints +{ + // Reads the command arguments from the automatic checkpoint and populates the context. + void LoadCommandArgsFromAutomaticCheckpoint(CLI::Execution::Context& context, Checkpoint& automaticCheckpoint); + + // Owns the lifetime of a checkpoint data base and creates the checkpoints. + struct CheckpointManager + { + // Constructor that generates a new resume id and creates the checkpoint database. + CheckpointManager(); + + // Constructor that loads the resume id and opens an existing checkpoint database. + CheckpointManager(const std::string& resumeId); + + ~CheckpointManager() = default; + + // Gets the file path of the checkpoint database. + static std::filesystem::path GetCheckpointDatabasePath(const std::string_view& resumeId, bool createCheckpointDirectory = false); + + // Gets the automatic checkpoint. + std::optional> GetAutomaticCheckpoint(); + + // Creates an automatic checkpoint using the provided context. + void CreateAutomaticCheckpoint(CLI::Execution::Context& context); + + // Gets all context data checkpoints. + std::vector> GetCheckpoints(); + + // Creates a new context data checkpoint. + Checkpoint CreateCheckpoint(std::string_view checkpointName); + + // Cleans up the checkpoint database. + void CleanUpDatabase(); + + private: + std::string m_resumeId; + std::shared_ptr m_checkpointDatabase; + }; +} \ No newline at end of file diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp index b7ec94f4b7..e7eb7943ad 100644 --- a/src/AppInstallerCLICore/Command.cpp +++ b/src/AppInstallerCLICore/Command.cpp @@ -840,7 +840,7 @@ namespace AppInstaller::CLI void Command::Complete(Execution::Context&, Execution::Args::Type) const { - // Derived commands must suppy context sensitive argument values. + // Derived commands must supply context sensitive argument values. } void Command::Execute(Execution::Context& context) const @@ -883,6 +883,12 @@ namespace AppInstaller::CLI } } + void Command::Resume(Execution::Context& context) const + { + context.Reporter.Error() << Resource::String::CommandDoesNotSupportResumeMessage << std::endl; + AICLI_TERMINATE_CONTEXT(E_NOTIMPL); + } + void Command::SelectCurrentCommandIfUnrecognizedSubcommandFound(bool value) { m_selectCurrentCommandIfUnrecognizedSubcommandFound = value; diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h index 0f9a7d14eb..15804ab86f 100644 --- a/src/AppInstallerCLICore/Command.h +++ b/src/AppInstallerCLICore/Command.h @@ -112,6 +112,8 @@ namespace AppInstaller::CLI virtual void Execute(Execution::Context& context) const; + virtual void Resume(Execution::Context& context) const; + protected: void SelectCurrentCommandIfUnrecognizedSubcommandFound(bool value); @@ -127,6 +129,7 @@ namespace AppInstaller::CLI Settings::TogglePolicy::Policy m_groupPolicy; CommandOutputFlags m_outputFlags; bool m_selectCurrentCommandIfUnrecognizedSubcommandFound = false; + std::string m_commandArguments; }; template diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp index 977c711823..e543423572 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.cpp +++ b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -1,15 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "AppInstallerRuntime.h" +#include "CheckpointManager.h" #include "InstallCommand.h" #include "Workflows/CompletionFlow.h" #include "Workflows/InstallFlow.h" #include "Workflows/UpdateFlow.h" #include "Workflows/MultiQueryFlow.h" +#include "Workflows/ResumeFlow.h" #include "Workflows/WorkflowBase.h" #include "Resources.h" - namespace AppInstaller::CLI { using namespace AppInstaller::CLI::Execution; @@ -103,6 +105,12 @@ namespace AppInstaller::CLI Argument::ValidateCommonArguments(execArgs); } + void InstallCommand::Resume(Context& context) const + { + // TODO: Load context data from checkpoint for install command. + ExecuteInternal(context); + } + void InstallCommand::ExecuteInternal(Context& context) const { context.SetFlags(ContextFlag::ShowSearchResultsOnPartialFailure); @@ -114,6 +122,7 @@ namespace AppInstaller::CLI Workflow::GetManifestFromArg << Workflow::SelectInstaller << Workflow::EnsureApplicableInstaller << + Workflow::Checkpoint("exampleCheckpoint", {}) << // TODO: Checkpoint example Workflow::InstallSinglePackage; } else @@ -140,7 +149,9 @@ namespace AppInstaller::CLI } else { - context << Workflow::InstallOrUpgradeSinglePackage(OperationType::Install); + context << + Workflow::Checkpoint("exampleCheckpoint", {}) << // TODO: Checkpoint example + Workflow::InstallOrUpgradeSinglePackage(OperationType::Install); } } } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.h b/src/AppInstallerCLICore/Commands/InstallCommand.h index ab41da9bff..190c984661 100644 --- a/src/AppInstallerCLICore/Commands/InstallCommand.h +++ b/src/AppInstallerCLICore/Commands/InstallCommand.h @@ -16,6 +16,8 @@ namespace AppInstaller::CLI void Complete(Execution::Context& context, Execution::Args::Type valueType) const override; + void Resume(Execution::Context& context) const override; + Utility::LocIndView HelpLink() const override; protected: diff --git a/src/AppInstallerCLICore/Commands/ResumeCommand.cpp b/src/AppInstallerCLICore/Commands/ResumeCommand.cpp new file mode 100644 index 0000000000..e620e6278e --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ResumeCommand.cpp @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AppInstallerRuntime.h" +#include "Resources.h" +#include "ResumeCommand.h" +#include "RootCommand.h" +#include "CheckpointManager.h" +#include "Workflows/ResumeFlow.h" + +using namespace AppInstaller::Checkpoints; + +namespace AppInstaller::CLI +{ + namespace + { + std::unique_ptr FindCommandToResume(const std::string& commandFullName) + { + std::unique_ptr commandToResume = std::make_unique(); + + for (const auto& commandPart : Utility::Split(commandFullName, ':')) + { + bool commandFound = false; + if (Utility::CaseInsensitiveEquals(commandPart, commandToResume->Name())) + { + // Since we always expect to start at the 'root' command, skip and check the next command part. + continue; + } + + for (auto& command : commandToResume->GetCommands()) + { + if (Utility::CaseInsensitiveEquals(commandPart, command->Name())) + { + commandFound = true; + commandToResume = std::move(command); + break; + } + } + + if (!commandFound) + { + THROW_HR_MSG(E_UNEXPECTED, "Command to resume not found."); + } + } + + return std::move(commandToResume); + } + } + + using namespace std::string_view_literals; + using namespace Execution; + + std::vector ResumeCommand::GetArguments() const + { + return { + Argument::ForType(Execution::Args::Type::ResumeId), + }; + } + + Resource::LocString ResumeCommand::ShortDescription() const + { + return { Resource::String::ResumeCommandShortDescription }; + } + + Resource::LocString ResumeCommand::LongDescription() const + { + return { Resource::String::ResumeCommandLongDescription }; + } + + Utility::LocIndView ResumeCommand::HelpLink() const + { + return "https://aka.ms/winget-command-resume"_liv; + } + + void ResumeCommand::ExecuteInternal(Execution::Context& context) const + { + const auto& resumeId = context.Args.GetArg(Execution::Args::Type::ResumeId); + + if (!std::filesystem::exists(Checkpoints::CheckpointManager::GetCheckpointDatabasePath(resumeId))) + { + context.Reporter.Error() << Resource::String::ResumeIdNotFoundError(Utility::LocIndView{ resumeId }) << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND); + } + + Execution::Context resumeContext = context.CreateEmptyContext(); + std::optional> foundAutomaticCheckpoint = resumeContext.LoadCheckpoint(std::string{ resumeId }); + if (!foundAutomaticCheckpoint.has_value()) + { + context.Reporter.Error() << Resource::String::ResumeStateDataNotFoundError << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE); + } + + Checkpoint automaticCheckpoint = foundAutomaticCheckpoint.value(); + + const auto& checkpointClientVersion = automaticCheckpoint.Get(AutomaticCheckpointData::ClientVersion, {}); + if (checkpointClientVersion != AppInstaller::Runtime::GetClientVersion().get()) + { + context.Reporter.Error() << Resource::String::ClientVersionMismatchError(Utility::LocIndView{ checkpointClientVersion }) << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH); + } + + const auto& checkpointCommand = automaticCheckpoint.Get(AutomaticCheckpointData::Command, {}); + AICLI_LOG(CLI, Info, << "Resuming command: " << checkpointCommand); + std::unique_ptr commandToResume = FindCommandToResume(checkpointCommand); + + LoadCommandArgsFromAutomaticCheckpoint(resumeContext, automaticCheckpoint); + + resumeContext.SetExecutingCommand(commandToResume.get()); + + // TODO: Ensure telemetry is properly handled for resume context. + resumeContext.SetFlags(Execution::ContextFlag::Resume); + + auto previousThreadGlobals = resumeContext.SetForCurrentThread(); + resumeContext.EnableSignalTerminationHandler(); + commandToResume->Resume(resumeContext); + context.SetTerminationHR(resumeContext.GetTerminationHR()); + } +} diff --git a/src/AppInstallerCLICore/Commands/ResumeCommand.h b/src/AppInstallerCLICore/Commands/ResumeCommand.h new file mode 100644 index 0000000000..79c915df6d --- /dev/null +++ b/src/AppInstallerCLICore/Commands/ResumeCommand.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Command.h" + +namespace AppInstaller::CLI +{ + struct ResumeCommand final : public Command + { + ResumeCommand(std::string_view parent) : Command("resume", {}, parent, Visibility::Hidden, Settings::ExperimentalFeature::Feature::Resume) {} + + 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; + }; +} diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp index 1a19e22704..3cbe1950fc 100644 --- a/src/AppInstallerCLICore/Commands/RootCommand.cpp +++ b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -25,6 +25,7 @@ #include "TestCommand.h" #include "DownloadCommand.h" #include "ErrorCommand.h" +#include "ResumeCommand.h" #include "Resources.h" #include "TableOutput.h" @@ -182,6 +183,7 @@ namespace AppInstaller::CLI std::make_unique(FullName()), std::make_unique(FullName()), std::make_unique(FullName()), + std::make_unique(FullName()), #if _DEBUG std::make_unique(FullName()), #endif diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h index 398c260fb3..1a65219c51 100644 --- a/src/AppInstallerCLICore/ExecutionArgs.h +++ b/src/AppInstallerCLICore/ExecutionArgs.h @@ -109,6 +109,9 @@ namespace AppInstaller::CLI::Execution // Error command ErrorInput, + // Resume Command + ResumeId, + // Configuration ConfigurationFile, ConfigurationAcceptWarning, diff --git a/src/AppInstallerCLICore/ExecutionContext.cpp b/src/AppInstallerCLICore/ExecutionContext.cpp index 2dbaf3cbf6..9918a77c24 100644 --- a/src/AppInstallerCLICore/ExecutionContext.cpp +++ b/src/AppInstallerCLICore/ExecutionContext.cpp @@ -5,6 +5,11 @@ #include "COMContext.h" #include "Argument.h" #include "winget/UserSettings.h" +#include "AppInstallerRuntime.h" +#include "Command.h" +#include "Public/winget/Checkpoint.h" + +using namespace AppInstaller::Checkpoints; namespace AppInstaller::CLI::Execution { @@ -250,12 +255,26 @@ namespace AppInstaller::CLI::Execution Context::~Context() { + if (Settings::ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Resume) && !IsTerminated()) + { + if (m_checkpointManager) + { + m_checkpointManager->CleanUpDatabase(); + } + } + if (m_disableSignalTerminationHandlerOnExit) { EnableSignalTerminationHandler(false); } } + Context Context::CreateEmptyContext() + { + AppInstaller::ThreadLocalStorage::WingetThreadGlobals threadGlobals; + return Context(Reporter, threadGlobals); + } + std::unique_ptr Context::CreateSubContext() { auto clone = std::make_unique(Reporter, m_threadGlobals); @@ -407,4 +426,29 @@ namespace AppInstaller::CLI::Execution return SignalTerminationHandler::Instance().WaitForAppShutdownEvent(); } #endif + + std::optional> Context::LoadCheckpoint(const std::string& resumeId) + { + m_checkpointManager = std::make_unique(resumeId); + return m_checkpointManager->GetAutomaticCheckpoint(); + } + + std::vector> Context::GetCheckpoints() + { + return m_checkpointManager->GetCheckpoints(); + } + + void Context::Checkpoint(std::string_view checkpointName, std::vector contextData) + { + UNREFERENCED_PARAMETER(checkpointName); + UNREFERENCED_PARAMETER(contextData); + + if (!m_checkpointManager) + { + m_checkpointManager = std::make_unique(); + m_checkpointManager->CreateAutomaticCheckpoint(*this); + } + + // TODO: Capture context data for checkpoint. + } } diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h index 295f89a35d..906063b7af 100644 --- a/src/AppInstallerCLICore/ExecutionContext.h +++ b/src/AppInstallerCLICore/ExecutionContext.h @@ -6,6 +6,8 @@ #include "ExecutionArgs.h" #include "ExecutionContextData.h" #include "CompletionData.h" +#include "CheckpointManager.h" +#include "Public/winget/Checkpoint.h" #include @@ -66,6 +68,7 @@ namespace AppInstaller::CLI::Execution DisableInteractivity = 0x40, BypassIsStoreClientBlockedPolicyCheck = 0x80, InstallerDownloadOnly = 0x100, + Resume = 0x200 }; DEFINE_ENUM_FLAG_OPERATORS(ContextFlag); @@ -96,6 +99,9 @@ namespace AppInstaller::CLI::Execution // The arguments given to execute with. Args Args; + // Creates a empty context, inheriting + Context CreateEmptyContext(); + // Creates a child of this context. virtual std::unique_ptr CreateSubContext(); @@ -161,6 +167,15 @@ namespace AppInstaller::CLI::Execution bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task); #endif + // Called by the resume command. Loads the checkpoint manager with the resume id and returns the automatic checkpoint. + std::optional> LoadCheckpoint(const std::string& resumeId); + + // Returns data checkpoints in the order of latest checkpoint to earliest. + std::vector> GetCheckpoints(); + + // Creates a checkpoint for the provided context data. + void Checkpoint(std::string_view checkpointName, std::vector contextData); + protected: // Copies the args that are also needed in a sub-context. E.g., silent void CopyArgsToSubContext(Context* subContext); @@ -179,5 +194,6 @@ namespace AppInstaller::CLI::Execution Workflow::ExecutionStage m_executionStage = Workflow::ExecutionStage::Initial; AppInstaller::ThreadLocalStorage::WingetThreadGlobals m_threadGlobals; AppInstaller::CLI::Command* m_executingCommand = nullptr; + std::unique_ptr m_checkpointManager; }; } diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h index a55a5ef52b..7fc0ad9dfe 100644 --- a/src/AppInstallerCLICore/Resources.h +++ b/src/AppInstallerCLICore/Resources.h @@ -43,8 +43,10 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(Cancelled); WINGET_DEFINE_RESOURCE_STRINGID(CancellingOperation); WINGET_DEFINE_RESOURCE_STRINGID(ChannelArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ClientVersionMismatchError); WINGET_DEFINE_RESOURCE_STRINGID(Command); WINGET_DEFINE_RESOURCE_STRINGID(CommandArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(CommandDoesNotSupportResumeMessage); WINGET_DEFINE_RESOURCE_STRINGID(CommandLineArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(CommandRequiresAdmin); WINGET_DEFINE_RESOURCE_STRINGID(CompleteCommandLongDescription); @@ -414,6 +416,11 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(ReportIdentityFound); WINGET_DEFINE_RESOURCE_STRINGID(RequiredArgError); WINGET_DEFINE_RESOURCE_STRINGID(ReservedFilenameError); + WINGET_DEFINE_RESOURCE_STRINGID(ResumeCommandLongDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ResumeCommandShortDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ResumeIdArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(ResumeIdNotFoundError); + WINGET_DEFINE_RESOURCE_STRINGID(ResumeStateDataNotFoundError); WINGET_DEFINE_RESOURCE_STRINGID(RetroArgumentDescription); WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandLongDescription); WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandShortDescription); diff --git a/src/AppInstallerCLICore/Workflows/ResumeFlow.cpp b/src/AppInstallerCLICore/Workflows/ResumeFlow.cpp new file mode 100644 index 0000000000..7ce25678de --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ResumeFlow.cpp @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "ResumeFlow.h" + +namespace AppInstaller::CLI::Workflow +{ + void Checkpoint::operator()(Execution::Context& context) const + { + if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Resume)) + { + return; + } + + context.Checkpoint(m_checkpointName, m_contextData); + } +} diff --git a/src/AppInstallerCLICore/Workflows/ResumeFlow.h b/src/AppInstallerCLICore/Workflows/ResumeFlow.h new file mode 100644 index 0000000000..d4c8307793 --- /dev/null +++ b/src/AppInstallerCLICore/Workflows/ResumeFlow.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +namespace AppInstaller::CLI::Workflow +{ + // Applies a checkpoint to the context workflow. + struct Checkpoint : public WorkflowTask + { + Checkpoint(std::string_view checkpointName, std::vector contextData) : + WorkflowTask("Checkpoint"), + m_checkpointName(checkpointName), + m_contextData(std::move(contextData)) {} + + void operator()(Execution::Context& context) const override; + + private: + std::string_view m_checkpointName; + std::vector m_contextData; + }; +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h index dbaaa911b6..14b7b9db26 100644 --- a/src/AppInstallerCLICore/Workflows/WorkflowBase.h +++ b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -10,7 +10,6 @@ #include #include - namespace AppInstaller::CLI::Execution { struct Context; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs index 03792028ec..5c5e793278 100644 --- a/src/AppInstallerCLIE2ETests/Constants.cs +++ b/src/AppInstallerCLIE2ETests/Constants.cs @@ -66,6 +66,8 @@ public class Constants public const string WinGetUtil = "WinGetUtil"; public const string E2ETestLogsPathPackaged = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\DiagOutputDir"; public const string E2ETestLogsPathUnpackaged = @"WinGet\defaultState"; + public const string CheckpointDirectoryPackaged = @"Packages\WinGetDevCLI_8wekyb3d8bbwe\LocalState\Checkpoints"; + public const string CheckpointDirectoryUnpackaged = @"Microsoft\WinGet\State\defaultState\Checkpoints"; // Installer filename public const string TestCommandExe = "testCommand.exe"; @@ -253,6 +255,11 @@ public class ErrorCode public const int ERROR_APPTERMINATION_RECEIVED = unchecked((int)0x8A15006A); public const int ERROR_DOWNLOAD_DEPENDENCIES = unchecked((int)0x8A15006B); public const int ERROR_DOWNLOAD_COMMAND_PROHIBITED = unchecked((int)0x8A15006C); + public const int ERROR_SERVICE_UNAVAILABLE = unchecked((int)0x8A15006D); + public const int ERROR_RESUME_ID_NOT_FOUND = unchecked((int)0x8A15006E); + public const int ERROR_CLIENT_VERSION_MISMATCH = unchecked((int)0x8A15006F); + public const int ERROR_INVALID_RESUME_STATE = unchecked((int)0x8A150070); + public const int ERROR_CANNOT_OPEN_CHECKPOINT_INDEX = unchecked((int)0x8A150071); public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); diff --git a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs index 04770c4690..5c0d47bd2d 100644 --- a/src/AppInstallerCLIE2ETests/FeaturesCommand.cs +++ b/src/AppInstallerCLIE2ETests/FeaturesCommand.cs @@ -54,6 +54,7 @@ public void EnableExperimentalFeatures() WinGetSettingsHelper.ConfigureFeature("experimentalCmd", true); WinGetSettingsHelper.ConfigureFeature("directMSI", true); WinGetSettingsHelper.ConfigureFeature("windowsFeature", true); + WinGetSettingsHelper.ConfigureFeature("resume", true); var result = TestCommon.RunAICLICommand("features", string.Empty); Assert.True(result.StdOut.Contains("Enabled")); } diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs index 451020ce31..e437d02215 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -314,6 +314,22 @@ public static string GetDefaultDownloadDirectory() return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads"); } + /// + /// Gets the checkpoints directory based on whether the command is invoked in desktop package or not. + /// + /// The default checkpoints directory. + public static string GetCheckpointsDirectory() + { + if (TestSetup.Parameters.PackagedContext) + { + return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), Constants.CheckpointDirectoryPackaged); + } + else + { + return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), Constants.CheckpointDirectoryUnpackaged); + } + } + /// /// Verify portable package. /// diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs index 8207b09941..b3f4be3f63 100644 --- a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs +++ b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -42,6 +42,8 @@ public static void InitializeWingetSettings() { "experimentalArg", false }, { "experimentalCmd", false }, { "directMSI", false }, + { "windowsFeature", false }, + { "resume", false }, } }, { @@ -206,7 +208,8 @@ public static void InitializeAllFeatures(bool status) ConfigureFeature("experimentalArg", status); ConfigureFeature("experimentalCmd", status); ConfigureFeature("directMSI", status); - ConfigureFeature("pinning", status); + ConfigureFeature("windowsFeature", status); + ConfigureFeature("resume", status); } private static JObject GetJsonSettingsObject(string objectName) diff --git a/src/AppInstallerCLIE2ETests/ResumeCommand.cs b/src/AppInstallerCLIE2ETests/ResumeCommand.cs new file mode 100644 index 0000000000..ab6b84303f --- /dev/null +++ b/src/AppInstallerCLIE2ETests/ResumeCommand.cs @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using System.Linq; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// + /// Test download command. + /// + public class ResumeCommand : BaseCommand + { + /// + /// One time setup. + /// + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("resume", true); + } + + /// + /// One time teardown. + /// + [OneTimeTearDown] + public void OneTimeTearDown() + { + WinGetSettingsHelper.ConfigureFeature("resume", false); + } + + /// + /// Installs a test exe installer and verifies that the checkpoint index is cleaned up. + /// + [Test] + public void InstallExe_VerifyIndexDoesNotExist() + { + var checkpointsDir = TestCommon.GetCheckpointsDirectory(); + + // If the checkpoints directory does not yet exist, set to 0. The directory should be created when the command is invoked. + int initialCheckpointsCount = Directory.Exists(checkpointsDir) ? Directory.GetFiles(checkpointsDir).Length : 0; + + var installDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestExeInstaller --silent -l {installDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Successfully installed")); + Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir, "/execustom")); + + int actualCheckpointsCount = Directory.GetFiles(checkpointsDir).Length; + + // The checkpoints count should not change as the index file should be cleaned up after a successful install. + Assert.AreEqual(initialCheckpointsCount, actualCheckpointsCount); + } + + /// + /// Verifies that an error message is shown when a resume id does not exist. + /// + [Test] + public void ResumeIdNotFound() + { + var resumeResult = TestCommon.RunAICLICommand("resume", "-g invalidResumeId"); + Assert.AreEqual(Constants.ErrorCode.ERROR_RESUME_ID_NOT_FOUND, resumeResult.ExitCode); + } + + /// + /// Verifies that a checkpoint record persists after a failed install. + /// + [Test] + public void ResumeRecordPreserved() + { + var checkpointsDir = TestCommon.GetCheckpointsDirectory(); + + int initialCheckpointsCount = Directory.Exists(checkpointsDir) ? Directory.GetDirectories(checkpointsDir).Length : 0; + + // TODO: Refine test case to more accurately reflect usage once resume is fully implemented. + var result = TestCommon.RunAICLICommand("install", $"AppInstallerTest.TestZipInvalidRelativePath"); + Assert.AreNotEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("Invalid relative file path to the nested installer; path points to a location outside of the install directory")); + + int actualCheckpointsCount = Directory.GetDirectories(checkpointsDir).Length; + + // One new checkpoint record should be created after running the install command. + Assert.AreEqual(initialCheckpointsCount + 1, actualCheckpointsCount); + + var checkpointsDirectoryInfo = new DirectoryInfo(checkpointsDir); + + var checkpoint = checkpointsDirectoryInfo.GetDirectories() + .OrderByDescending(f => f.LastWriteTime) + .First(); + + // Resume output should be the same as the install result. + var resumeResult = TestCommon.RunAICLICommand("resume", $"-g {checkpoint.Name}"); + Assert.AreNotEqual(Constants.ErrorCode.S_OK, resumeResult.ExitCode); + Assert.True(resumeResult.StdOut.Contains("Invalid relative file path to the nested installer; path points to a location outside of the install directory")); + } + } +} \ No newline at end of file diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw index 78e1b0daac..e0a2a3f98b 100644 --- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw +++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -2587,4 +2587,27 @@ Please specify one of them using the --source option to proceed. Successfully enabled Windows Features dependencies + + Resumes execution of a previously saved command by passing in the unique identifier of the saved command. This is used to resume an executed command that may have been terminated due to a reboot. + + + Resumes execution of a previously saved command. + + + The unique identifier of the saved state to resume + + + Resuming the state from a different client version is not supported: {0} + {Locked= "{0}"} Message displayed to inform the user that the client version of the resume state does not match the current client version. {0} is a placeholder for the client version that created the resume state. + + + The resume state does not exist: {0} + {Locked="{0}" Error message displayed when the user provides a guid that does not correspond to a valid saved state. {0} is a placeholder replaced by the provided guid string. + + + No data found in the resume state. + + + This command does not support resuming. + \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj index 301840825d..6ed7ea3ed6 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -198,6 +198,7 @@ + @@ -246,6 +247,7 @@ + diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters index c497f7c264..cb51464dbc 100644 --- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters +++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -182,6 +182,9 @@ Source Files\Repository + + Source Files\CLI + Source Files\Repository @@ -317,6 +320,9 @@ Source Files\Common + + Source Files\Repository + diff --git a/src/AppInstallerCLITests/CheckpointDatabase.cpp b/src/AppInstallerCLITests/CheckpointDatabase.cpp new file mode 100644 index 0000000000..fb13ae282e --- /dev/null +++ b/src/AppInstallerCLITests/CheckpointDatabase.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include +#include + +using namespace std::string_literals; +using namespace TestCommon; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Repository::SQLite; +using namespace AppInstaller::Repository::Microsoft::Schema; +using namespace AppInstaller::Checkpoints; + +TEST_CASE("CheckpointDatabaseCreateLatestAndReopen", "[checkpointDatabase]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + Schema::Version versionCreated; + + // Create the database + { + std::shared_ptr database = CheckpointDatabase::CreateNew(tempFile, Schema::Version::Latest()); + versionCreated = database->GetVersion(); + } + + // Reopen the database + { + INFO("Trying with ReadWrite"); + std::shared_ptr database = CheckpointDatabase::Open(tempFile, SQLiteStorageBase::OpenDisposition::ReadWrite); + Schema::Version versionRead = database->GetVersion(); + REQUIRE(versionRead == versionCreated); + } +} + +TEST_CASE("CheckpointDatabase_WriteMetadata", "[checkpointDatabase]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string_view testCheckpointName = "testCheckpoint"sv; + std::string testCommand = "install"; + std::string testClientVersion = "1.20.1234"; + + { + std::shared_ptr database = CheckpointDatabase::CreateNew(tempFile, { 1, 0 }); + CheckpointDatabase::IdType checkpointId = database->AddCheckpoint(testCheckpointName); + database->SetDataValue(checkpointId, AutomaticCheckpointData::Command, {}, { testCommand }); + database->SetDataValue(checkpointId, AutomaticCheckpointData::ClientVersion, {}, { testClientVersion }); + } + + { + std::shared_ptr database = CheckpointDatabase::Open(tempFile); + const auto& checkpointIds = database->GetCheckpointIds(); + REQUIRE_FALSE(checkpointIds.empty()); + + auto checkpointId = checkpointIds[0]; + REQUIRE(testCommand == database->GetDataFieldSingleValue(checkpointId, AutomaticCheckpointData::Command, {})); + REQUIRE(testClientVersion == database->GetDataFieldSingleValue(checkpointId, AutomaticCheckpointData::ClientVersion, {})); + } +} + +TEST_CASE("CheckpointDatabase_WriteContextData", "[checkpointDatabase]") +{ + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + std::string_view testCheckpoint = "testCheckpoint"sv; + + std::string fieldName1 = "field1"; + std::string fieldName2 = "field2"; + + std::string testValue1 = "value1"; + std::string testValue2 = "value2"; + std::string testValue3 = "value3"; + + { + std::shared_ptr database = CheckpointDatabase::CreateNew(tempFile, { 1, 0 }); + CheckpointDatabase::IdType checkpointId = database->AddCheckpoint(testCheckpoint); + + // Add multiple fields. + database->SetDataValue(checkpointId, AutomaticCheckpointData::Arguments, fieldName1, { testValue1 }); + database->SetDataValue(checkpointId, AutomaticCheckpointData::Arguments, fieldName2, { testValue2, testValue3 }); + } + + { + std::shared_ptr database = CheckpointDatabase::Open(tempFile); + const auto& checkpointIds = database->GetCheckpointIds(); + REQUIRE_FALSE(checkpointIds.empty()); + + auto automaticCheckpointId = checkpointIds.back(); + + const auto& fieldNames = database->GetDataFieldNames(automaticCheckpointId, AutomaticCheckpointData::Arguments); + + REQUIRE(fieldNames[0] == fieldName1); + REQUIRE(fieldNames[1] == fieldName2); + + REQUIRE(testValue1 == database->GetDataFieldSingleValue(automaticCheckpointId, AutomaticCheckpointData::Arguments, fieldName1)); + + const auto& multiValues = database->GetDataFieldMultiValue(automaticCheckpointId, AutomaticCheckpointData::Arguments, fieldName2); + + REQUIRE(testValue2 == multiValues[0]); + REQUIRE(testValue3 == multiValues[1]); + } +} + +TEST_CASE("CheckpointDatabase_CheckpointOrder", "[checkpointDatabase]") +{ + // Verifies that the checkpoints are shown in reverse order (latest first). + TempFile tempFile{ "repolibtest_tempdb"s, ".db"s }; + INFO("Using temporary file named: " << tempFile.GetPath()); + + { + std::shared_ptr database = CheckpointDatabase::CreateNew(tempFile, { 1, 0 }); + database->AddCheckpoint("firstCheckpoint"sv); + database->AddCheckpoint("secondCheckpoint"sv); + database->AddCheckpoint("thirdCheckpoint"sv); + } + + { + std::shared_ptr database = CheckpointDatabase::Open(tempFile); + const auto& checkpointIds = database->GetCheckpointIds(); + REQUIRE(checkpointIds.size() == 3); + REQUIRE(checkpointIds[0] == 3); + REQUIRE(checkpointIds[1] == 2); + REQUIRE(checkpointIds[2] == 1); + } +} diff --git a/src/AppInstallerCLITests/ResumeFlow.cpp b/src/AppInstallerCLITests/ResumeFlow.cpp new file mode 100644 index 0000000000..a9cf8f314f --- /dev/null +++ b/src/AppInstallerCLITests/ResumeFlow.cpp @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "WorkflowCommon.h" +#include "TestHooks.h" +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; +using namespace AppInstaller::CLI; +using namespace AppInstaller::Repository::Microsoft; +using namespace AppInstaller::Settings; +using namespace AppInstaller::Runtime; +using namespace TestCommon; +using namespace AppInstaller::Checkpoints; + +constexpr std::string_view s_AutomaticCheckpoint = "automatic"sv; +constexpr std::string_view s_CheckpointsFileName = "checkpoints.db"sv; + +TEST_CASE("ResumeFlow_IndexNotFound", "[Resume]") +{ + std::string tempGuidString = "{ec3a098c-a815-4d52-8866-946c03093a37}"; + + std::ostringstream resumeOutput; + TestContext context{ resumeOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::ResumeId, tempGuidString); + + ResumeCommand resume({}); + context.SetExecutingCommand(&resume); + resume.Execute(context); + INFO(resumeOutput.str()); + + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND); + auto expectedMessage = Resource::String::ResumeIdNotFoundError(AppInstaller::Utility::LocIndString(tempGuidString)); + REQUIRE(resumeOutput.str().find(Resource::LocString(expectedMessage).get()) != std::string::npos); +} + +TEST_CASE("ResumeFlow_InvalidClientVersion", "[Resume]") +{ + TestCommon::TempDirectory tempCheckpointRecordDirectory("TempCheckpointRecordDirectory", true); + + const auto& tempCheckpointRecordDirectoryPath = tempCheckpointRecordDirectory.GetPath(); + TestHook_SetPathOverride(PathName::CheckpointsLocation, tempCheckpointRecordDirectoryPath); + + // Create temp guid and populate with invalid client version. + std::string tempGuidString = "{615339e9-3ac5-4e86-a5ab-c246657aca25}"; + auto tempRecordPath = tempCheckpointRecordDirectoryPath / tempGuidString / s_CheckpointsFileName; + std::string_view invalidClientVersion = "1.2.3.4"sv; + + INFO("Using temporary file named: " << tempRecordPath); + + { + // Manually set invalid client version + std::filesystem::create_directories(tempRecordPath.parent_path()); + std::shared_ptr checkpointRecord = CheckpointDatabase::CreateNew(tempRecordPath.u8string()); + CheckpointDatabase::IdType checkpointId = checkpointRecord->AddCheckpoint(s_AutomaticCheckpoint); + checkpointRecord->SetDataValue(checkpointId, AutomaticCheckpointData::ClientVersion, {}, { "1.2.3.4" }); + } + + std::ostringstream resumeOutput; + TestContext context{ resumeOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::ResumeId, tempGuidString); + + ResumeCommand resume({}); + context.SetExecutingCommand(&resume); + resume.Execute(context); + INFO(resumeOutput.str()); + + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH); + auto expectedMessage = Resource::String::ClientVersionMismatchError(AppInstaller::Utility::LocIndString(invalidClientVersion)); + REQUIRE(resumeOutput.str().find(Resource::LocString(expectedMessage).get()) != std::string::npos); +} + +TEST_CASE("ResumeFlow_EmptyIndex", "[Resume]") +{ + TestCommon::TempDirectory tempCheckpointRecordDirectory("TempCheckpointRecordDirectory", true); + + const auto& tempCheckpointRecordDirectoryPath = tempCheckpointRecordDirectory.GetPath(); + TestHook_SetPathOverride(PathName::CheckpointsLocation, tempCheckpointRecordDirectoryPath); + + std::string tempGuidString = "{43ca664c-3eae-4f73-99ee-18cf83912c02}"; + auto tempRecordPath = tempCheckpointRecordDirectoryPath / tempGuidString / s_CheckpointsFileName; + + INFO("Using temporary file named: " << tempRecordPath); + + { + std::filesystem::create_directories(tempRecordPath.parent_path()); + CheckpointDatabase::CreateNew(tempRecordPath.u8string()); + } + + std::ostringstream resumeOutput; + TestContext context{ resumeOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + context.Args.AddArg(Execution::Args::Type::ResumeId, tempGuidString); + + ResumeCommand resume({}); + context.SetExecutingCommand(&resume); + resume.Execute(context); + INFO(resumeOutput.str()); + + REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE); + REQUIRE(resumeOutput.str().find(Resource::LocString(Resource::String::ResumeStateDataNotFoundError).get()) != std::string::npos); +} + +TEST_CASE("ResumeFlow_InstallSuccess", "[Resume]") +{ + TestCommon::TempDirectory tempCheckpointRecordDirectory("TempCheckpointRecordDirectory", false); + + const auto& tempCheckpointRecordDirectoryPath = tempCheckpointRecordDirectory.GetPath(); + TestHook_SetPathOverride(PathName::CheckpointsLocation, tempCheckpointRecordDirectoryPath); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + + { + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + + const auto& testManifestPath = TestDataFile("InstallFlowTest_Exe.yaml").GetPath().u8string(); + context.Args.AddArg(Execution::Args::Type::Manifest, testManifestPath); + + InstallCommand install({}); + context.SetExecutingCommand(&install); + install.Execute(context); + INFO(installOutput.str()); + } + + // Verify Installer is called and parameters are passed in. + REQUIRE(std::filesystem::exists(installResultPath.GetPath())); + std::ifstream installResultFile(installResultPath.GetPath()); + REQUIRE(installResultFile.is_open()); + std::string installResultStr; + std::getline(installResultFile, installResultStr); + REQUIRE(installResultStr.find("/custom") != std::string::npos); + REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); + + // The checkpoint index should not exist if the context succeeded. + std::vector checkpointFiles; + for (const auto& entry : std::filesystem::directory_iterator(tempCheckpointRecordDirectoryPath)) + { + checkpointFiles.emplace_back(entry.path()); + } + + REQUIRE(checkpointFiles.size() == 0); +} + +// TODO: This test will need to be updated once saving the resume state is restricted to certain HRs. +TEST_CASE("ResumeFlow_InstallFailure", "[Resume]") +{ + TestCommon::TempDirectory tempCheckpointRecordDirectory("TempCheckpointRecordDirectory", false); + + const auto& tempCheckpointRecordDirectoryPath = tempCheckpointRecordDirectory.GetPath(); + TestHook_SetPathOverride(PathName::CheckpointsLocation, tempCheckpointRecordDirectoryPath); + + TestCommon::TestUserSettings testSettings; + testSettings.Set(true); + + { + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + + const auto& testManifestPath = TestDataFile("InstallFlowTest_UnsupportedArguments.yaml").GetPath().u8string(); + context.Args.AddArg(Execution::Args::Type::Manifest, testManifestPath); + context.Args.AddArg(Execution::Args::Type::InstallLocation, "installLocation"sv); + + InstallCommand install({}); + context.SetExecutingCommand(&install); + install.Execute(context); + INFO(installOutput.str()); + + // Verify unsupported arguments error message is shown + REQUIRE(context.GetTerminationHR() == APPINSTALLER_CLI_ERROR_UNSUPPORTED_ARGUMENT); + } + + // Only one checkpoint file should be created. + std::vector checkpointFiles; + for (const auto& entry : std::filesystem::directory_iterator(tempCheckpointRecordDirectoryPath)) + { + checkpointFiles.emplace_back(entry.path()); + } + + REQUIRE(checkpointFiles.size() == 1); + + std::filesystem::path checkpointRecordPath = checkpointFiles[0]; + REQUIRE(std::filesystem::exists(checkpointRecordPath)); +} diff --git a/src/AppInstallerCLITests/Strings.cpp b/src/AppInstallerCLITests/Strings.cpp index 95311cdab2..345fb9a499 100644 --- a/src/AppInstallerCLITests/Strings.cpp +++ b/src/AppInstallerCLITests/Strings.cpp @@ -279,3 +279,11 @@ TEST_CASE("SplitWithSeparator", "[string]") REQUIRE(test3.size() == 1); REQUIRE(test3[0] == "test"); } + +TEST_CASE("ConvertGuid", "[string]") +{ + std::string validGuidString = "{4d1e55b2-f16f-11cf-88cb-001111000030}"; + GUID guid = { 0x4d1e55b2, 0xf16f, 0x11cf, 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }; + + REQUIRE(CaseInsensitiveEquals(ConvertGuidToString(guid), validGuidString)); +} diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp index 066c380d00..2d0896e811 100644 --- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp +++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -42,6 +42,8 @@ namespace AppInstaller::Settings return userSettings.Get(); case ExperimentalFeature::Feature::WindowsFeature: return userSettings.Get(); + case ExperimentalFeature::Feature::Resume: + return userSettings.Get(); default: THROW_HR(E_UNEXPECTED); } @@ -73,6 +75,8 @@ namespace AppInstaller::Settings return ExperimentalFeature{ "Direct MSI Installation", "directMSI", "https://aka.ms/winget-settings", Feature::DirectMSI }; case Feature::WindowsFeature: return ExperimentalFeature{ "Windows Feature Dependencies", "windowsFeature", "https://aka.ms/winget-settings", Feature::WindowsFeature }; + case Feature::Resume: + return ExperimentalFeature{ "Resume", "resume", "https://aka.ms/winget-settings", Feature::Resume }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h index 3ce763fa28..5dfd4f2567 100644 --- a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h +++ b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h @@ -49,6 +49,8 @@ namespace AppInstaller::Runtime SelfPackageRoot, // The location where user downloads are stored. UserProfileDownloads, + // The location where checkpoints are stored. + CheckpointsLocation, // Always one more than the last path; for being able to iterate paths in tests. Max }; diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h index 73b28ff650..2204e15eb1 100644 --- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h +++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -24,6 +24,7 @@ namespace AppInstaller::Settings // Before making DirectMSI non-experimental, it should be part of manifest validation. DirectMSI = 0x1, WindowsFeature = 0x2, + Resume = 0x4, Max, // This MUST always be after all experimental features // Features listed after Max will not be shown with the features command diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h index 9c17f50f10..bb7e2d984f 100644 --- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h +++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -71,6 +71,7 @@ namespace AppInstaller::Settings EFExperimentalArg, EFDirectMSI, EFWindowsFeature, + EFResume, // Telemetry TelemetryDisable, // Install behavior @@ -147,6 +148,7 @@ namespace AppInstaller::Settings SETTINGMAPPING_SPECIALIZATION(Setting::EFExperimentalArg, bool, bool, false, ".experimentalFeatures.experimentalArg"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFDirectMSI, bool, bool, false, ".experimentalFeatures.directMSI"sv); SETTINGMAPPING_SPECIALIZATION(Setting::EFWindowsFeature, bool, bool, false, ".experimentalFeatures.windowsFeature"sv); + SETTINGMAPPING_SPECIALIZATION(Setting::EFResume, bool, bool, false, ".experimentalFeatures.resume"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp index 5841c8bf39..9af6f62d3f 100644 --- a/src/AppInstallerCommonCore/Runtime.cpp +++ b/src/AppInstallerCommonCore/Runtime.cpp @@ -31,6 +31,7 @@ namespace AppInstaller::Runtime constexpr std::string_view s_PortablePackageRoot = "WinGet"sv; constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv; constexpr std::string_view s_LinksDirectory = "Links"sv; + constexpr std::string_view s_CheckpointsDirectory = "Checkpoints"sv; constexpr std::string_view s_DevModeSubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"sv; constexpr std::string_view s_AllowDevelopmentWithoutDevLicense = "AllowDevelopmentWithoutDevLicense"sv; #ifndef WINGET_DISABLE_FOR_FUZZING @@ -467,6 +468,10 @@ namespace AppInstaller::Runtime result.Path = GetPackagePath(); result.Create = false; break; + case PathName::CheckpointsLocation: + result = GetPathDetailsForPackagedContext(PathName::LocalState, forDisplay); + result.Path /= s_CheckpointsDirectory; + break; default: THROW_HR(E_UNEXPECTED); } @@ -553,6 +558,10 @@ namespace AppInstaller::Runtime result.Path = GetBinaryDirectoryPath(); result.Create = false; break; + case PathName::CheckpointsLocation: + result = GetPathDetailsForUnpackagedContext(PathName::LocalState, forDisplay); + result.Path /= s_CheckpointsDirectory; + break; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp index bc841557fd..6938e9c4bf 100644 --- a/src/AppInstallerCommonCore/UserSettings.cpp +++ b/src/AppInstallerCommonCore/UserSettings.cpp @@ -260,6 +260,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFExperimentalArg) WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) + WINGET_VALIDATE_PASS_THROUGH(EFResume) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable) diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj index 33d79302b6..ed017e7898 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj @@ -1,4 +1,4 @@ - + @@ -353,6 +353,7 @@ + @@ -388,12 +389,16 @@ + + + + @@ -403,6 +408,7 @@ + @@ -472,6 +478,7 @@ + @@ -495,6 +502,9 @@ + + + diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters index 950f82d2de..5d7830e334 100644 --- a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters +++ b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -1,4 +1,4 @@ - + @@ -94,6 +94,9 @@ {f610927a-6f1d-42c5-9ad9-b59790091944} + + {a3f9c7ed-f487-40d6-9ee7-e9a052e55c29} + @@ -393,6 +396,24 @@ Microsoft\Schema\1_7 + + Microsoft\Schema + + + Microsoft + + + Microsoft\Schema\Checkpoint_1_0 + + + Microsoft\Schema\Checkpoint_1_0 + + + Microsoft\Schema\Checkpoint_1_0 + + + Public\winget + @@ -623,6 +644,18 @@ Microsoft\Schema\1_7 + + Source Files + + + Microsoft\Schema\Checkpoint_1_0 + + + Source Files + + + Microsoft\Schema\Checkpoint_1_0 + diff --git a/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp b/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp new file mode 100644 index 0000000000..4910b1cfeb --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "CheckpointDatabase.h" +#include "Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h" + +namespace AppInstaller::Repository::Microsoft +{ + std::shared_ptr CheckpointDatabase::CreateNew(const std::string& filePath, Schema::Version version) + { + AICLI_LOG(Repo, Info, << "Creating new Checkpoint database with version [" << version << "] at '" << filePath << "'"); + CheckpointDatabase result{ filePath, version }; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(result.m_dbconn, "CheckpointDatabase_CreateNew"); + + // Use calculated version, as incoming version could be 'latest' + result.m_version.SetSchemaVersion(result.m_dbconn); + + result.m_interface->CreateTables(result.m_dbconn); + + result.SetLastWriteTime(); + + savepoint.Commit(); + + return std::make_shared(std::move(result)); + } + + bool CheckpointDatabase::IsEmpty() + { + return m_interface->IsEmpty(m_dbconn); + } + + CheckpointDatabase::IdType CheckpointDatabase::AddCheckpoint(std::string_view checkpointName) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Adding checkpoint [" << checkpointName << "]"); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "CheckpointDatabase_addCheckpoint"); + + IdType result = m_interface->AddCheckpoint(m_dbconn, checkpointName); + + SetLastWriteTime(); + savepoint.Commit(); + return result; + } + + std::vector CheckpointDatabase::GetCheckpointIds() + { + return m_interface->GetCheckpointIds(m_dbconn); + } + + bool CheckpointDatabase::HasDataField(IdType checkpointId, int type, const std::string& name) + { + return m_interface->GetCheckpointDataFieldValues(m_dbconn, checkpointId, type, name).has_value(); + } + + std::vector CheckpointDatabase::GetDataTypes(IdType checkpointId) + { + return m_interface->GetCheckpointDataTypes(m_dbconn, checkpointId); + } + + std::vector CheckpointDatabase::GetDataFieldNames(IdType checkpointId, int dataType) + { + return m_interface->GetCheckpointDataFields(m_dbconn, checkpointId, dataType); + } + + void CheckpointDatabase::SetDataValue(IdType checkpointId, int dataType, const std::string& field, const std::vector& values) + { + std::lock_guard lockInterface{ *m_interfaceLock }; + AICLI_LOG(Repo, Verbose, << "Setting checkpoint data [" << dataType << "]"); + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(m_dbconn, "CheckpointDatabase_setDataValue"); + + m_interface->SetCheckpointDataValues(m_dbconn, checkpointId, dataType, field, values); + + SetLastWriteTime(); + savepoint.Commit(); + } + + std::string CheckpointDatabase::GetDataFieldSingleValue(IdType checkpointId, int dataType, const std::string& field) + { + const auto& values = m_interface->GetCheckpointDataFieldValues(m_dbconn, checkpointId, dataType, field); + + if (!values.has_value()) + { + THROW_HR(E_UNEXPECTED); + } + + return values.value()[0]; + } + + std::vector CheckpointDatabase::GetDataFieldMultiValue(IdType checkpointId, int dataType, const std::string& field) + { + const auto& values = m_interface->GetCheckpointDataFieldValues(m_dbconn, checkpointId, dataType, field); + + if (!values.has_value()) + { + THROW_HR(E_UNEXPECTED); + } + + return values.value(); + } + + std::unique_ptr CheckpointDatabase::CreateICheckpointDatabase() const + { + if (m_version == Schema::Version{ 1, 0 } || + m_version.MajorVersion == 1 || + m_version.IsLatest()) + { + return std::make_unique(); + } + + THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); + } + + CheckpointDatabase::CheckpointDatabase(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile) : + SQLiteStorageBase(target, disposition, std::move(indexFile)) + { + AICLI_LOG(Repo, Info, << "Opened Checkpoint Index with version [" << m_version << "], last write [" << GetLastWriteTime() << "]"); + m_interface = CreateICheckpointDatabase(); + THROW_HR_IF(APPINSTALLER_CLI_ERROR_CANNOT_WRITE_TO_UPLEVEL_INDEX, disposition == SQLiteStorageBase::OpenDisposition::ReadWrite && m_version != m_interface->GetVersion()); + } + + CheckpointDatabase::CheckpointDatabase(const std::string& target, Schema::Version version) : SQLiteStorageBase(target, version) + { + m_interface = CreateICheckpointDatabase(); + m_version = m_interface->GetVersion(); + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.h b/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.h new file mode 100644 index 0000000000..262a479f64 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.h @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include "Microsoft/Schema/ICheckpointDatabase.h" +#include "Microsoft/SQLiteStorageBase.h" +#include + +namespace AppInstaller::Repository::Microsoft +{ + struct CheckpointDatabase : SQLiteStorageBase + { + // An id that refers to a specific Checkpoint. + using IdType = SQLite::rowid_t; + + CheckpointDatabase(const CheckpointDatabase&) = delete; + CheckpointDatabase& operator=(const CheckpointDatabase&) = delete; + + CheckpointDatabase(CheckpointDatabase&&) = default; + CheckpointDatabase& operator=(CheckpointDatabase&&) = default; + + // Create a new checkpoint database. + static std::shared_ptr CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest()); + + // Opens an existing checkpoint database. + static std::shared_ptr Open(const std::string& filePath, OpenDisposition disposition = OpenDisposition::ReadWrite, Utility::ManagedFile&& indexFile = {}) + { + CheckpointDatabase result{ filePath, disposition, std::move(indexFile) }; + return std::make_shared(std::move(result)); + } + + // Returns a value indicating whether the database is empty. + bool IsEmpty(); + + // Adds a new checkpoint name to the checkpoint table. + IdType AddCheckpoint(std::string_view checkpointName); + + // Returns all checkpoint ids in descending (newest at the front) order. + std::vector GetCheckpointIds(); + + // Returns a boolean value indicating a field exists for a checkpoint data type. + bool HasDataField(IdType checkpointId, int type, const std::string& name); + + // Returns the available data types for a checkpoint id. + std::vector GetDataTypes(IdType checkpointId); + + // Returns the available field names for a checkpoint data. + std::vector GetDataFieldNames(IdType checkpointId, int dataType); + + // Sets the value(s) for a data type and field. + void SetDataValue(IdType checkpointId, int dataType, const std::string& field, const std::vector& values); + + // Gets a single value for a data type field. + std::string GetDataFieldSingleValue(IdType checkpointId, int dataType, const std::string& field); + + // Gets multiple values for a data type field. + std::vector GetDataFieldMultiValue(IdType checkpointId, int dataType, const std::string& field); + + private: + // Constructor used to open an existing index. + CheckpointDatabase(const std::string& target, SQLiteStorageBase::OpenDisposition disposition, Utility::ManagedFile&& indexFile); + + // Constructor used to create a new index. + CheckpointDatabase(const std::string& target, Schema::Version version); + + // Creates the ICheckpointDatabase interface object for this version. + std::unique_ptr CreateICheckpointDatabase() const; + + std::unique_ptr m_interface; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.cpp new file mode 100644 index 0000000000..aac10d1e28 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.cpp @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "CheckpointDataTable.h" +#include "SQLiteStatementBuilder.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + using namespace SQLite; + using namespace std::string_view_literals; + static constexpr std::string_view s_CheckpointDataTable_Table_Name = "CheckpointData"sv; + static constexpr std::string_view s_CheckpointDataTable_CheckpointId_Column = "CheckpointId"sv; + static constexpr std::string_view s_CheckpointDataTable_ContextData_Column = "ContextData"sv; + static constexpr std::string_view s_CheckpointDataTable_Name_Column = "Name"sv; + static constexpr std::string_view s_CheckpointDataTable_Value_Column = "Value"sv; + static constexpr std::string_view s_CheckpointDataTable_Index_Column = "Index"sv; + + namespace + { + SQLite::rowid_t SetNamedValue(SQLite::Connection& connection, std::string_view name, std::string_view value) + { + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_CheckpointDataTable_Table_Name) + .Columns({ s_CheckpointDataTable_CheckpointId_Column, + s_CheckpointDataTable_ContextData_Column, + s_CheckpointDataTable_Name_Column, + s_CheckpointDataTable_Value_Column, + s_CheckpointDataTable_Index_Column}) + .Values(name, value); + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } + + std::string GetNamedValue(SQLite::Connection& connection, std::string_view name) + { + SQLite::Builder::StatementBuilder builder; + builder.Select({ s_CheckpointDataTable_Value_Column }) + .From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_Name_Column).Equals(name); + + SQLite::Statement statement = builder.Prepare(connection); + THROW_HR_IF(E_NOT_SET, !statement.Step()); + return statement.GetColumn(0); + } + } + + std::string_view CheckpointDataTable::TableName() + { + return s_CheckpointDataTable_Table_Name; + } + + void CheckpointDataTable::Create(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createCheckpointDataTable_v1_0"); + + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_CheckpointDataTable_Table_Name).BeginColumns(); + createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_CheckpointId_Column, Type::Int).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_ContextData_Column, Type::Int).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Name_Column, Type::Text).NotNull()); + createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Value_Column, Type::Text)); + createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Index_Column, Type::Int).NotNull()); + + PrimaryKeyBuilder pkBuilder; + pkBuilder.Column(s_CheckpointDataTable_CheckpointId_Column); + pkBuilder.Column(s_CheckpointDataTable_ContextData_Column); + pkBuilder.Column(s_CheckpointDataTable_Name_Column); + pkBuilder.Column(s_CheckpointDataTable_Index_Column); + + createTableBuilder.Column(pkBuilder).EndColumns(); + createTableBuilder.Execute(connection); + savepoint.Commit(); + } + + bool CheckpointDataTable::IsEmpty(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::Builder::RowCount).From(s_CheckpointDataTable_Table_Name); + + SQLite::Statement countStatement = builder.Prepare(connection); + + THROW_HR_IF(E_UNEXPECTED, !countStatement.Step()); + + return (countStatement.GetColumn(0) == 0); + } + + std::vector CheckpointDataTable::GetAvailableData(SQLite::Connection& connection, SQLite::rowid_t checkpointId) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(s_CheckpointDataTable_ContextData_Column).From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column); + builder.Equals(checkpointId); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector availableData; + + while (select.Step()) + { + availableData.emplace_back(select.GetColumn(0)); + } + + return availableData; + } + + SQLite::rowid_t CheckpointDataTable::AddCheckpointData(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData, std::string_view name, std::string_view value, int index) + { + SQLite::Builder::StatementBuilder builder; + + if (value.empty()) + { + builder.InsertInto(s_CheckpointDataTable_Table_Name) + .Columns({ s_CheckpointDataTable_CheckpointId_Column, + s_CheckpointDataTable_ContextData_Column, + s_CheckpointDataTable_Name_Column, + s_CheckpointDataTable_Index_Column }) + .Values(checkpointId, contextData, name, index); + } + else + { + builder.InsertInto(s_CheckpointDataTable_Table_Name) + .Columns({ s_CheckpointDataTable_CheckpointId_Column, + s_CheckpointDataTable_ContextData_Column, + s_CheckpointDataTable_Name_Column, + s_CheckpointDataTable_Value_Column, + s_CheckpointDataTable_Index_Column }) + .Values(checkpointId, contextData, name, value, index); + } + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } + + bool CheckpointDataTable::HasDataField(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type, std::string_view name) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::Builder::RowCount).From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column); + builder.Equals(checkpointId).And(s_CheckpointDataTable_ContextData_Column).Equals(type).And(s_CheckpointDataTable_Name_Column).Equals(name); + + SQLite::Statement countStatement = builder.Prepare(connection); + + THROW_HR_IF(E_UNEXPECTED, !countStatement.Step()); + + return (countStatement.GetColumn(0) == 0); + } + + std::vector CheckpointDataTable::GetDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(s_CheckpointDataTable_Name_Column).From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column); + builder.Equals(checkpointId).And(s_CheckpointDataTable_ContextData_Column).Equals(type); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector fields; + + while (select.Step()) + { + fields.emplace_back(select.GetColumn(0)); + } + + return fields; + } + + std::vector CheckpointDataTable::GetDataValuesByFieldName(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData, std::string_view name) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(s_CheckpointDataTable_Value_Column).From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column); + builder.Equals(checkpointId).And(s_CheckpointDataTable_ContextData_Column).Equals(contextData).And(s_CheckpointDataTable_Name_Column).Equals(name); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector values; + + while (select.Step()) + { + values.emplace_back(select.GetColumn(0)); + } + + return values; + } + + std::string CheckpointDataTable::GetDataValue(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(s_CheckpointDataTable_Value_Column).From(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column); + builder.Equals(checkpointId).And(s_CheckpointDataTable_ContextData_Column).Equals(type); + + SQLite::Statement select = builder.Prepare(connection); + + if (select.Step()) + { + return select.GetColumn(0); + } + else + { + return {}; + } + } + + void CheckpointDataTable::RemoveData(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData) + { + SQLite::Builder::StatementBuilder builder; + builder.DeleteFrom(s_CheckpointDataTable_Table_Name).Where(s_CheckpointDataTable_CheckpointId_Column).Equals(checkpointId) + .And(s_CheckpointDataTable_ContextData_Column).Equals(contextData); + builder.Execute(connection); + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h new file mode 100644 index 0000000000..0fa70b0d8f --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include +#include + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + struct CheckpointDataTable + { + // Get the table name. + static std::string_view TableName(); + + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + // Gets a value indicating whether the table is empty. + static bool IsEmpty(SQLite::Connection& connection); + + // Get all available context data. + static std::vector GetAvailableData(SQLite::Connection& connection, SQLite::rowid_t checkpointId); + + // Adds a context data for a checkpoint. Index is used to represent the item number if the context data has more than one value. + static SQLite::rowid_t AddCheckpointData(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData, std::string_view name, std::string_view value, int index = 1); + + // Gets all fields for a context data. + static std::vector GetDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type); + + // Gets the context data values by property name from a checkpoint id. + static std::vector GetDataValuesByFieldName(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData, std::string_view name); + + // Returns a boolean value indicating whether a field exists for a context data. + static bool HasDataField(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type, std::string_view name); + + // Removes the context data by checkpoint id. + static void RemoveData(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int contextData); + + // Gets a single data value for a context data. + static std::string GetDataValue(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int type); + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h new file mode 100644 index 0000000000..c619c318d4 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/Schema/ICheckpointDatabase.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + struct CheckpointDatabaseInterface : public ICheckpointDatabase + { + // Version 1.0 + Schema::Version GetVersion() const override; + void CreateTables(SQLite::Connection& connection) override; + + private: + bool IsEmpty(SQLite::Connection& connection) override; + SQLite::rowid_t AddCheckpoint(SQLite::Connection& connection, std::string_view checkpointName) override; + std::vector GetCheckpointIds(SQLite::Connection& connection) override; + std::vector GetCheckpointDataTypes(SQLite::Connection& connection, SQLite::rowid_t checkpointId) override; + std::vector GetCheckpointDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType) override; + void SetCheckpointDataValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name, const std::vector& values) override; + std::optional> GetCheckpointDataFieldValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name) override; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface_1_0.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface_1_0.cpp new file mode 100644 index 0000000000..0653c03d3c --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface_1_0.cpp @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h" +#include "Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h" +#include "Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + Schema::Version CheckpointDatabaseInterface::GetVersion() const + { + return { 1, 0 }; + } + + void CheckpointDatabaseInterface::CreateTables(SQLite::Connection& connection) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createCheckpointTables_v1_0"); + Checkpoint_V1_0::CheckpointTable::Create(connection); + Checkpoint_V1_0::CheckpointDataTable::Create(connection); + savepoint.Commit(); + } + + bool CheckpointDatabaseInterface::IsEmpty(SQLite::Connection& connection) + { + return CheckpointDataTable::IsEmpty(connection); + } + + SQLite::rowid_t CheckpointDatabaseInterface::AddCheckpoint(SQLite::Connection& connection, std::string_view checkpointName) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "addCheckpoint_v1_0"); + SQLite::rowid_t checkpointId = CheckpointTable::AddCheckpoint(connection, checkpointName); + savepoint.Commit(); + return checkpointId; + } + + std::vector CheckpointDatabaseInterface::GetCheckpointIds(SQLite::Connection& connection) + { + return CheckpointTable::GetCheckpointIds(connection); + } + + std::vector CheckpointDatabaseInterface::GetCheckpointDataTypes(SQLite::Connection& connection, SQLite::rowid_t checkpointId) + { + return CheckpointDataTable::GetAvailableData(connection, checkpointId); + } + + std::vector CheckpointDatabaseInterface::GetCheckpointDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType) + { + return CheckpointDataTable::GetDataFields(connection, checkpointId, dataType); + } + + void CheckpointDatabaseInterface::SetCheckpointDataValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name, const std::vector& values) + { + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "setCheckpointData_v1_0"); + + if (values.empty()) + { + CheckpointDataTable::AddCheckpointData(connection, checkpointId, dataType, name, {}, 0); + } + else + { + int index = 0; + + for (const auto& value : values) + { + CheckpointDataTable::AddCheckpointData(connection, checkpointId, dataType, name, value, index); + index++; + } + } + + savepoint.Commit(); + } + + std::optional> CheckpointDatabaseInterface::GetCheckpointDataFieldValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name) + { + return CheckpointDataTable::GetDataValuesByFieldName(connection, checkpointId, dataType, name); + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.cpp new file mode 100644 index 0000000000..6fdb60dd75 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.cpp @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "CheckpointTable.h" +#include "SQLiteStatementBuilder.h" + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + using namespace std::string_view_literals; + static constexpr std::string_view s_CheckpointTable_Table_Name = "Checkpoints"sv; + static constexpr std::string_view s_CheckpointTable_Index_Name = "Checkpoints_pkindex"sv; + static constexpr std::string_view s_CheckpointTable_Name_Column = "Name"; + static constexpr std::string_view s_CheckpointTable_CreationTime_Column = "CreationTime"; + + std::string_view CheckpointTable::TableName() + { + return s_CheckpointTable_Table_Name; + } + + void CheckpointTable::Create(SQLite::Connection& connection) + { + using namespace SQLite::Builder; + + SQLite::Savepoint savepoint = SQLite::Savepoint::Create(connection, "createCheckpointTable_v1_0"); + + StatementBuilder createTableBuilder; + createTableBuilder.CreateTable(s_CheckpointTable_Table_Name).Columns({ + ColumnBuilder(s_CheckpointTable_Index_Name, Type::Integer).PrimaryKey(), + ColumnBuilder(s_CheckpointTable_Name_Column, Type::Text).NotNull(), + ColumnBuilder(s_CheckpointTable_CreationTime_Column, Type::Int64).NotNull() + }); + + createTableBuilder.Execute(connection); + + savepoint.Commit(); + } + + std::vector CheckpointTable::GetCheckpointIds(SQLite::Connection& connection) + { + SQLite::Builder::StatementBuilder builder; + builder.Select(SQLite::RowIDName).From(s_CheckpointTable_Table_Name).OrderBy(SQLite::RowIDName).Descending(); + + SQLite::Statement select = builder.Prepare(connection); + + std::vector checkpoints; + + while (select.Step()) + { + checkpoints.emplace_back(select.GetColumn(0)); + } + + return checkpoints; + } + + SQLite::rowid_t CheckpointTable::AddCheckpoint(SQLite::Connection& connection, std::string_view checkpointName) + { + SQLite::Builder::StatementBuilder builder; + builder.InsertInto(s_CheckpointTable_Table_Name) + .Columns({ s_CheckpointTable_Name_Column, + s_CheckpointTable_CreationTime_Column }) + .Values(checkpointName, Utility::GetCurrentUnixEpoch()); + + builder.Execute(connection); + return connection.GetLastInsertRowID(); + } +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h new file mode 100644 index 0000000000..6b7e2edd93 --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include +#include + +namespace AppInstaller::Repository::Microsoft::Schema::Checkpoint_V1_0 +{ + struct CheckpointTable + { + // Get the table name. + static std::string_view TableName(); + + // Creates the table with named indices. + static void Create(SQLite::Connection& connection); + + // Returns all checkpoint ids in descending (newest at the front) order. + static std::vector GetCheckpointIds(SQLite::Connection& connection); + + // Adds a checkpoint. + static SQLite::rowid_t AddCheckpoint(SQLite::Connection& connection, std::string_view checkpointName); + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/ICheckpointDatabase.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/ICheckpointDatabase.h new file mode 100644 index 0000000000..b5ad6461ee --- /dev/null +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/ICheckpointDatabase.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include "Microsoft/Schema/Version.h" + +namespace AppInstaller::Repository::Microsoft::Schema +{ + struct ICheckpointDatabase + { + virtual ~ICheckpointDatabase() = default; + + // Gets the schema version that this index interface is built for. + virtual Schema::Version GetVersion() const = 0; + + // Creates all of the version dependent tables within the database. + virtual void CreateTables(SQLite::Connection& connection) = 0; + + // Version 1.0 + // Returns a bool value indicating whether all checkpoint tables are empty. + virtual bool IsEmpty(SQLite::Connection& connection) = 0; + + // Returns all checkpoint ids in descending (newest at the front) order. + virtual std::vector GetCheckpointIds(SQLite::Connection& connection) = 0; + + // Adds a new checkpoint to the Checkpoint table. + virtual SQLite::rowid_t AddCheckpoint(SQLite::Connection& connection, std::string_view checkpointName) = 0; + + // Gets the data types associated with a checkpoint id. + virtual std::vector GetCheckpointDataTypes(SQLite::Connection& connection, SQLite::rowid_t checkpointId) = 0; + + // Sets the field values for a checkpoint data type. + virtual void SetCheckpointDataValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name, const std::vector& values) = 0; + + // Gets all field names for a checkpoint data type. + virtual std::vector GetCheckpointDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType) = 0; + + // Gets all field values for a checkpoint data type. + virtual std::optional> GetCheckpointDataFieldValues(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType, std::string_view name) = 0; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp index f5a3049547..e845a2ac8f 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp @@ -4,6 +4,8 @@ #include "Version.h" #include "MetadataTable.h" +#include + namespace AppInstaller::Repository::Microsoft::Schema { Version Version::GetSchemaVersion(SQLite::Connection& connection) @@ -39,4 +41,24 @@ namespace AppInstaller::Repository::Microsoft::Schema return out << version.MajorVersion << '.' << version.MinorVersion; } } + + Version Version::Latest() + { + return { std::numeric_limits::max(), std::numeric_limits::max() }; + } + + Version Version::LatestForMajor(uint32_t majorVersion) + { + return { majorVersion, std::numeric_limits::max() }; + } + + bool Version::IsLatest() const + { + return (MajorVersion == std::numeric_limits::max() && MinorVersion == std::numeric_limits::max()); + } + + bool Version::IsLatestForMajor(uint32_t majorVersion) const + { + return (MajorVersion == majorVersion && MinorVersion == std::numeric_limits::max()); + } } diff --git a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h index f435c08b62..d179dab481 100644 --- a/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h +++ b/src/AppInstallerRepositoryCore/Microsoft/Schema/Version.h @@ -2,7 +2,6 @@ // Licensed under the MIT License. #pragma once #include "SQLiteWrapper.h" -#include #include namespace AppInstaller::Repository::Microsoft::Schema @@ -35,16 +34,16 @@ namespace AppInstaller::Repository::Microsoft::Schema } // Gets a version that represents the latest schema known to the implementation. - static constexpr Version Latest() { return { std::numeric_limits::max(), std::numeric_limits::max() }; } + static Version Latest(); // Gets a version that represents the latest schema known to the implementation for the given major version. - static constexpr Version LatestForMajor(uint32_t majorVersion) { return { majorVersion, std::numeric_limits::max() }; } + static Version LatestForMajor(uint32_t majorVersion); // Determines if this version represents the latest schema. - bool IsLatest() const { return (MajorVersion == std::numeric_limits::max() && MinorVersion == std::numeric_limits::max()); } + bool IsLatest() const; // Determines if this version represents the latest schema of the given major version. - bool IsLatestForMajor(uint32_t majorVersion) const { return (MajorVersion == majorVersion && MinorVersion == std::numeric_limits::max()); } + bool IsLatestForMajor(uint32_t majorVersion) const; // Determines the schema version of the opened index. static Version GetSchemaVersion(SQLite::Connection& connection); diff --git a/src/AppInstallerRepositoryCore/Public/winget/Checkpoint.h b/src/AppInstallerRepositoryCore/Public/winget/Checkpoint.h new file mode 100644 index 0000000000..924c30145e --- /dev/null +++ b/src/AppInstallerRepositoryCore/Public/winget/Checkpoint.h @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/CheckpointDatabase.h" +#include + +using namespace AppInstaller::Repository::Microsoft; + +namespace AppInstaller::Checkpoints +{ + enum AutomaticCheckpointData + { + ClientVersion, + Command, + Arguments + }; + + struct CheckpointManager; + + // A representation of a row in the Checkpoint table. + template + struct Checkpoint + { + static_assert(std::is_enum::value); + friend CheckpointManager; + + std::vector GetCheckpointDataTypes() + { + return m_checkpointDatabase->GetDataTypes(m_checkpointId); + } + + // Returns a boolean value indicating whether the field name exists. + bool Has(T dataType, const std::string& fieldName) + { + return m_checkpointDatabase->HasDataField(m_checkpointId, dataType, fieldName); + } + + // Gets all available field names. + std::vector GetFieldNames(T dataType) + { + return m_checkpointDatabase->GetDataFieldNames(m_checkpointId, dataType); + } + + // Sets a single field value for a data type. + void Set(T dataType, const std::string& fieldName, const std::string& value) + { + m_checkpointDatabase->SetDataValue(m_checkpointId, dataType, fieldName, { value }); + } + + // Sets multiple field values for a data type. + void SetMany(T dataType, const std::string& fieldName, const std::vector& values) + { + m_checkpointDatabase->SetDataValue(m_checkpointId, dataType, fieldName, values); + } + + // Gets a single field value for a data type. + std::string Get(T dataType, const std::string& fieldName) + { + return m_checkpointDatabase->GetDataFieldSingleValue(m_checkpointId, dataType, fieldName); + } + + // Gets multiple field values for a data type. + std::vector GetMany(T dataType, const std::string& fieldName) + { + return m_checkpointDatabase->GetDataFieldMultiValue(m_checkpointId, dataType, fieldName); + } + + private: + Checkpoint(std::shared_ptr checkpointDatabase, AppInstaller::Repository::Microsoft::CheckpointDatabase::IdType checkpointId) : + m_checkpointDatabase(checkpointDatabase), m_checkpointId(checkpointId){}; + + AppInstaller::Repository::Microsoft::CheckpointDatabase::IdType m_checkpointId; + std::shared_ptr m_checkpointDatabase; + }; +} \ No newline at end of file diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp index 21eb2d25fe..8bc78170d3 100644 --- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp +++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp @@ -142,6 +142,9 @@ namespace AppInstaller::Repository::SQLite::Builder case Type::Blob: out << "BLOB"; break; + case Type::Integer: + out << "INTEGER"; + break; default: THROW_HR(E_UNEXPECTED); } @@ -504,6 +507,18 @@ namespace AppInstaller::Repository::SQLite::Builder return *this; } + StatementBuilder& StatementBuilder::Ascending() + { + m_stream << " ASC"; + return *this; + } + + StatementBuilder& StatementBuilder::Descending() + { + m_stream << " DESC"; + return *this; + } + StatementBuilder& StatementBuilder::InsertInto(std::string_view table) { OutputOperationAndTable(m_stream, "INSERT INTO", table); diff --git a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h index 6324e1d5b5..1084086464 100644 --- a/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h +++ b/src/AppInstallerRepositoryCore/SQLiteStatementBuilder.h @@ -111,6 +111,7 @@ namespace AppInstaller::Repository::SQLite::Builder RowId = Int64, Text, Blob, + Integer, // Type for specifying a primary key column as a row id alias. }; // Aggregate functions. @@ -297,6 +298,10 @@ namespace AppInstaller::Repository::SQLite::Builder StatementBuilder& OrderBy(std::string_view column); StatementBuilder& OrderBy(const QualifiedColumn& column); + // Specify the ordering behavior. + StatementBuilder& Ascending(); + StatementBuilder& Descending(); + // Limits the result set to the given number of rows. StatementBuilder& Limit(size_t rowCount); diff --git a/src/AppInstallerSharedLib/AppInstallerStrings.cpp b/src/AppInstallerSharedLib/AppInstallerStrings.cpp index 566d740967..6e77f25030 100644 --- a/src/AppInstallerSharedLib/AppInstallerStrings.cpp +++ b/src/AppInstallerSharedLib/AppInstallerStrings.cpp @@ -860,4 +860,11 @@ namespace AppInstaller::Utility { return value ? "true"sv : "false"sv; } + + std::string ConvertGuidToString(const GUID& value) + { + wchar_t buffer[256]; + THROW_HR_IF(E_UNEXPECTED, !StringFromGUID2(value, buffer, ARRAYSIZE(buffer))); + return ConvertToUTF8(buffer); + } } diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp index ccaa2083a8..082985cab3 100644 --- a/src/AppInstallerSharedLib/Errors.cpp +++ b/src/AppInstallerSharedLib/Errors.cpp @@ -196,6 +196,11 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES, "Failed to download package dependencies."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED, "Failed to download package. Download for offline installation is prohibited."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE, "A required service is busy or unavailable. Try again later."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND, "The guid provided does not correspond to a valid resume state."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH, "The current client version did not match the client version of the saved state."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE, "The resume state data is invalid."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX, "Unable to open the checkpoint database."), + // Install errors. WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE, "Application is currently running. Exit the application then try again."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h index e24d7d4851..6702ab5227 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -126,7 +126,10 @@ #define APPINSTALLER_CLI_ERROR_DOWNLOAD_DEPENDENCIES ((HRESULT)0x8A15006B) #define APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED ((HRESULT)0x8A15006C) #define APPINSTALLER_CLI_ERROR_SERVICE_UNAVAILABLE ((HRESULT)0x8A15006D) - +#define APPINSTALLER_CLI_ERROR_RESUME_ID_NOT_FOUND ((HRESULT)0x8A15006E) +#define APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH ((HRESULT)0x8A15006F) +#define APPINSTALLER_CLI_ERROR_INVALID_RESUME_STATE ((HRESULT)0x8A150070) +#define APPINSTALLER_CLI_ERROR_CANNOT_OPEN_CHECKPOINT_INDEX ((HRESULT)0x8A150071) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) #define APPINSTALLER_CLI_ERROR_INSTALL_INSTALL_IN_PROGRESS ((HRESULT)0x8A150102) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h index 850b8e130a..5c7a3a39b7 100644 --- a/src/AppInstallerSharedLib/Public/AppInstallerStrings.h +++ b/src/AppInstallerSharedLib/Public/AppInstallerStrings.h @@ -266,4 +266,6 @@ namespace AppInstaller::Utility // Converts the given boolean value to a string. std::string_view ConvertBoolToString(bool value); + // Converts the given GUID value to a string. + std::string ConvertGuidToString(const GUID& value); }