winget-cli

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

commit 85951aef927a010a7726bdf614afa4cde00344ac
parent 7ca4297448064d97e874ac24a4d3d6be39fa5098
Author: Ryan <69221034+ryfu-msft@users.noreply.github.com>
Date:   Wed, 11 Oct 2023 22:38:52 -0400

Add `resume` command and support saving the argument state. (#3508)


Diffstat:
M.github/actions/spelling/allow.txt | 1+
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj | 6++++++
Msrc/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters | 22++++++++++++++++++++--
Msrc/AppInstallerCLICore/Argument.cpp | 6++++++
Asrc/AppInstallerCLICore/CheckpointManager.cpp | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/CheckpointManager.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Command.cpp | 8+++++++-
Msrc/AppInstallerCLICore/Command.h | 3+++
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 15+++++++++++++--
Msrc/AppInstallerCLICore/Commands/InstallCommand.h | 2++
Asrc/AppInstallerCLICore/Commands/ResumeCommand.cpp | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLICore/Commands/ResumeCommand.h | 22++++++++++++++++++++++
Msrc/AppInstallerCLICore/Commands/RootCommand.cpp | 2++
Msrc/AppInstallerCLICore/ExecutionArgs.h | 3+++
Msrc/AppInstallerCLICore/ExecutionContext.cpp | 44++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/ExecutionContext.h | 16++++++++++++++++
Msrc/AppInstallerCLICore/Resources.h | 7+++++++
Asrc/AppInstallerCLICore/Workflows/ResumeFlow.cpp | 17+++++++++++++++++
Asrc/AppInstallerCLICore/Workflows/ResumeFlow.h | 22++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 1-
Msrc/AppInstallerCLIE2ETests/Constants.cs | 7+++++++
Msrc/AppInstallerCLIE2ETests/FeaturesCommand.cs | 1+
Msrc/AppInstallerCLIE2ETests/Helpers/TestCommon.cs | 16++++++++++++++++
Msrc/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs | 5++++-
Asrc/AppInstallerCLIE2ETests/ResumeCommand.cs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 23+++++++++++++++++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 2++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 6++++++
Asrc/AppInstallerCLITests/CheckpointDatabase.cpp | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerCLITests/ResumeFlow.cpp | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/Strings.cpp | 8++++++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/AppInstallerRuntime.h | 2++
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1+
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Msrc/AppInstallerCommonCore/Runtime.cpp | 9+++++++++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj | 12+++++++++++-
Msrc/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters | 35++++++++++++++++++++++++++++++++++-
Asrc/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.h | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.cpp | 210+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDataTable.h | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface.h | 24++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointDatabaseInterface_1_0.cpp | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/Checkpoint_1_0/CheckpointTable.h | 25+++++++++++++++++++++++++
Asrc/AppInstallerRepositoryCore/Microsoft/Schema/ICheckpointDatabase.h | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Version.cpp | 22++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/Microsoft/Schema/Version.h | 9++++-----
Asrc/AppInstallerRepositoryCore/Public/winget/Checkpoint.h | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerRepositoryCore/SQLiteStatementBuilder.cpp | 15+++++++++++++++
Msrc/AppInstallerRepositoryCore/SQLiteStatementBuilder.h | 5+++++
Msrc/AppInstallerSharedLib/AppInstallerStrings.cpp | 7+++++++
Msrc/AppInstallerSharedLib/Errors.cpp | 5+++++
Msrc/AppInstallerSharedLib/Public/AppInstallerErrors.h | 5++++-
Msrc/AppInstallerSharedLib/Public/AppInstallerStrings.h | 2++
57 files changed, 1919 insertions(+), 15 deletions(-)

diff --git 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 @@ -350,6 +350,7 @@ <ItemGroup> <ClInclude Include="Argument.h" /> <ClInclude Include="ChannelStreams.h" /> + <ClInclude Include="CheckpointManager.h" /> <ClInclude Include="Command.h" /> <ClInclude Include="Commands\COMCommand.h" /> <ClInclude Include="Commands\CompleteCommand.h" /> @@ -371,6 +372,7 @@ <ClInclude Include="Commands\ShowCommand.h" /> <ClInclude Include="Commands\InstallCommand.h" /> <ClInclude Include="Commands\RootCommand.h" /> + <ClInclude Include="Commands\ResumeCommand.h" /> <ClInclude Include="Commands\SourceCommand.h" /> <ClInclude Include="Commands\TestCommand.h" /> <ClInclude Include="Commands\UninstallCommand.h" /> @@ -414,6 +416,7 @@ <ClInclude Include="Workflows\ShellExecuteInstallerHandler.h" /> <ClInclude Include="Workflows\InstallFlow.h" /> <ClInclude Include="Workflows\ManifestComparator.h" /> + <ClInclude Include="Workflows\ResumeFlow.h" /> <ClInclude Include="Workflows\ShowFlow.h" /> <ClInclude Include="Workflows\SourceFlow.h" /> <ClInclude Include="Workflows\UninstallFlow.h" /> @@ -422,6 +425,7 @@ </ItemGroup> <ItemGroup> <ClCompile Include="COMContext.cpp" /> + <ClCompile Include="CheckpointManager.cpp" /> <ClCompile Include="Commands\COMCommand.cpp" /> <ClCompile Include="Commands\ConfigureCommand.cpp" /> <ClCompile Include="Commands\ConfigureShowCommand.cpp" /> @@ -453,6 +457,7 @@ <ClCompile Include="Commands\ShowCommand.cpp" /> <ClCompile Include="Commands\InstallCommand.cpp" /> <ClCompile Include="Commands\RootCommand.cpp" /> + <ClCompile Include="Commands\ResumeCommand.cpp" /> <ClCompile Include="Commands\SourceCommand.cpp" /> <ClCompile Include="Commands\UninstallCommand.cpp" /> <ClCompile Include="Commands\UpgradeCommand.cpp" /> @@ -484,6 +489,7 @@ <ClCompile Include="Workflows\ShellExecuteInstallerHandler.cpp" /> <ClCompile Include="Workflows\InstallFlow.cpp" /> <ClCompile Include="Workflows\ManifestComparator.cpp" /> + <ClCompile Include="Workflows\ResumeFlow.cpp" /> <ClCompile Include="Workflows\ShowFlow.cpp" /> <ClCompile Include="Workflows\SourceFlow.cpp" /> <ClCompile Include="Workflows\UninstallFlow.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -233,6 +233,15 @@ <ClInclude Include="Commands\ErrorCommand.h"> <Filter>Commands</Filter> </ClInclude> + <ClInclude Include="Commands\ResumeCommand.h"> + <Filter>Commands</Filter> + </ClInclude> + <ClInclude Include="Workflows\ResumeFlow.h"> + <Filter>Workflows</Filter> + </ClInclude> + <ClInclude Include="CheckpointManager.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -424,10 +433,19 @@ <ClCompile Include="Commands\TestCommand.cpp"> <Filter>Commands</Filter> </ClCompile> - <ClCompile Include="Commands\DownloadCommand.cpp"> + <ClCompile Include="ConfigurationCommon.cpp"> <Filter>Source Files</Filter> </ClCompile> - <ClCompile Include="ConfigurationCommon.cpp"> + <ClInclude Include="Commands\DownloadCommand.cpp"> + <Filter>Commands</Filter> + </ClInclude> + <ClCompile Include="Commands\ResumeCommand.cpp"> + <Filter>Commands</Filter> + </ClCompile> + <ClCompile Include="Workflows\ResumeFlow.cpp"> + <Filter>Workflows</Filter> + </ClCompile> + <ClCompile Include="CheckpointManager.cpp"> <Filter>Source Files</Filter> </ClCompile> <ClCompile Include="Commands\ErrorCommand.cpp"> diff --git 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 @@ -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 <AppInstallerRuntime.h> + +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<AutomaticCheckpointData> 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<int>(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<AutomaticCheckpointData>& 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<Execution::Args::Type>(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<Checkpoint<AutomaticCheckpointData>> CheckpointManager::GetAutomaticCheckpoint() + { + const auto& checkpointIds = m_checkpointDatabase->GetCheckpointIds(); + if (checkpointIds.empty()) + { + return {}; + } + + CheckpointDatabase::IdType automaticCheckpointId = checkpointIds.back(); + return Checkpoint<AutomaticCheckpointData>{ m_checkpointDatabase, automaticCheckpointId }; + } + + Checkpoint<CLI::Execution::Data> CheckpointManager::CreateCheckpoint(std::string_view checkpointName) + { + CheckpointDatabase::IdType checkpointId = m_checkpointDatabase->AddCheckpoint(checkpointName); + Checkpoint<CLI::Execution::Data> checkpoint{ m_checkpointDatabase, checkpointId }; + return checkpoint; + } + + std::vector<Checkpoint<CLI::Execution::Data>> CheckpointManager::GetCheckpoints() + { + auto checkpointIds = m_checkpointDatabase->GetCheckpointIds(); + if (checkpointIds.empty()) + { + return {}; + } + + // Remove the last checkpoint (automatic) + checkpointIds.pop_back(); + + std::vector<Checkpoint<CLI::Execution::Data>> checkpoints; + for (const auto& checkpointId : checkpointIds) + { + checkpoints.emplace_back(Checkpoint<CLI::Execution::Data>{ 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 @@ -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 <guiddef.h> + +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<AutomaticCheckpointData>& 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<Checkpoint<AutomaticCheckpointData>> GetAutomaticCheckpoint(); + + // Creates an automatic checkpoint using the provided context. + void CreateAutomaticCheckpoint(CLI::Execution::Context& context); + + // Gets all context data checkpoints. + std::vector<Checkpoint<CLI::Execution::Data>> GetCheckpoints(); + + // Creates a new context data checkpoint. + Checkpoint<CLI::Execution::Data> CreateCheckpoint(std::string_view checkpointName); + + // Cleans up the checkpoint database. + void CleanUpDatabase(); + + private: + std::string m_resumeId; + std::shared_ptr<AppInstaller::Repository::Microsoft::CheckpointDatabase> m_checkpointDatabase; + }; +}+ \ No newline at end of file diff --git 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 @@ -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 <typename Container> diff --git 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 @@ -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 @@ -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<Command> FindCommandToResume(const std::string& commandFullName) + { + std::unique_ptr<Command> commandToResume = std::make_unique<RootCommand>(); + + 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<Argument> 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<Checkpoint<AutomaticCheckpointData>> 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<AutomaticCheckpointData> 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<Command> 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 @@ -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<Argument> GetArguments() const override; + + Resource::LocString ShortDescription() const override; + Resource::LocString LongDescription() const override; + + Utility::LocIndView HelpLink() const override; + + protected: + void ExecuteInternal(Execution::Context& context) const override; + }; +} diff --git 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<ConfigureCommand>(FullName()), std::make_unique<DownloadCommand>(FullName()), std::make_unique<ErrorCommand>(FullName()), + std::make_unique<ResumeCommand>(FullName()), #if _DEBUG std::make_unique<DebugCommand>(FullName()), #endif diff --git 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 @@ -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> Context::CreateSubContext() { auto clone = std::make_unique<Context>(Reporter, m_threadGlobals); @@ -407,4 +426,29 @@ namespace AppInstaller::CLI::Execution return SignalTerminationHandler::Instance().WaitForAppShutdownEvent(); } #endif + + std::optional<Checkpoint<AutomaticCheckpointData>> Context::LoadCheckpoint(const std::string& resumeId) + { + m_checkpointManager = std::make_unique<AppInstaller::Checkpoints::CheckpointManager>(resumeId); + return m_checkpointManager->GetAutomaticCheckpoint(); + } + + std::vector<AppInstaller::Checkpoints::Checkpoint<Execution::Data>> Context::GetCheckpoints() + { + return m_checkpointManager->GetCheckpoints(); + } + + void Context::Checkpoint(std::string_view checkpointName, std::vector<Execution::Data> contextData) + { + UNREFERENCED_PARAMETER(checkpointName); + UNREFERENCED_PARAMETER(contextData); + + if (!m_checkpointManager) + { + m_checkpointManager = std::make_unique<AppInstaller::Checkpoints::CheckpointManager>(); + m_checkpointManager->CreateAutomaticCheckpoint(*this); + } + + // TODO: Capture context data for checkpoint. + } } diff --git 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 <string_view> @@ -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<Context> 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<AppInstaller::Checkpoints::Checkpoint<AppInstaller::Checkpoints::AutomaticCheckpointData>> LoadCheckpoint(const std::string& resumeId); + + // Returns data checkpoints in the order of latest checkpoint to earliest. + std::vector<AppInstaller::Checkpoints::Checkpoint<Execution::Data>> GetCheckpoints(); + + // Creates a checkpoint for the provided context data. + void Checkpoint(std::string_view checkpointName, std::vector<Execution::Data> 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<AppInstaller::Checkpoints::CheckpointManager> m_checkpointManager; }; } diff --git 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 @@ -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 @@ -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<Execution::Data> 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<Execution::Data> m_contextData; + }; +} diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -10,7 +10,6 @@ #include <string> #include <string_view> - namespace AppInstaller::CLI::Execution { struct Context; diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -66,6 +66,8 @@ namespace AppInstallerCLIE2ETests 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 @@ namespace AppInstallerCLIE2ETests 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 @@ -54,6 +54,7 @@ namespace AppInstallerCLIE2ETests 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 @@ -315,6 +315,22 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> + /// Gets the checkpoints directory based on whether the command is invoked in desktop package or not. + /// </summary> + /// <returns>The default checkpoints directory.</returns> + 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); + } + } + + /// <summary> /// Verify portable package. /// </summary> /// <param name="installDir">Install dir.</param> diff --git a/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs b/src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs @@ -42,6 +42,8 @@ namespace AppInstallerCLIE2ETests.Helpers { "experimentalArg", false }, { "experimentalCmd", false }, { "directMSI", false }, + { "windowsFeature", false }, + { "resume", false }, } }, { @@ -206,7 +208,8 @@ namespace AppInstallerCLIE2ETests.Helpers 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 @@ -0,0 +1,102 @@ +// ----------------------------------------------------------------------------- +// <copyright file="ResumeCommand.cs" company="Microsoft Corporation"> +// Copyright (c) Microsoft Corporation. Licensed under the MIT License. +// </copyright> +// ----------------------------------------------------------------------------- + +namespace AppInstallerCLIE2ETests +{ + using System.IO; + using System.Linq; + using AppInstallerCLIE2ETests.Helpers; + using NUnit.Framework; + + /// <summary> + /// Test download command. + /// </summary> + public class ResumeCommand : BaseCommand + { + /// <summary> + /// One time setup. + /// </summary> + [OneTimeSetUp] + public void OneTimeSetup() + { + WinGetSettingsHelper.ConfigureFeature("resume", true); + } + + /// <summary> + /// One time teardown. + /// </summary> + [OneTimeTearDown] + public void OneTimeTearDown() + { + WinGetSettingsHelper.ConfigureFeature("resume", false); + } + + /// <summary> + /// Installs a test exe installer and verifies that the checkpoint index is cleaned up. + /// </summary> + [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); + } + + /// <summary> + /// Verifies that an error message is shown when a resume id does not exist. + /// </summary> + [Test] + public void ResumeIdNotFound() + { + var resumeResult = TestCommon.RunAICLICommand("resume", "-g invalidResumeId"); + Assert.AreEqual(Constants.ErrorCode.ERROR_RESUME_ID_NOT_FOUND, resumeResult.ExitCode); + } + + /// <summary> + /// Verifies that a checkpoint record persists after a failed install. + /// </summary> + [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 @@ -2587,4 +2587,27 @@ Please specify one of them using the --source option to proceed.</value> <data name="EnableWindowsFeaturesSuccess" xml:space="preserve"> <value>Successfully enabled Windows Features dependencies</value> </data> + <data name="ResumeCommandLongDescription" xml:space="preserve"> + <value>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.</value> + </data> + <data name="ResumeCommandShortDescription" xml:space="preserve"> + <value>Resumes execution of a previously saved command.</value> + </data> + <data name="ResumeIdArgumentDescription" xml:space="preserve"> + <value>The unique identifier of the saved state to resume</value> + </data> + <data name="ClientVersionMismatchError" xml:space="preserve"> + <value>Resuming the state from a different client version is not supported: {0}</value> + <comment>{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.</comment> + </data> + <data name="ResumeIdNotFoundError" xml:space="preserve"> + <value>The resume state does not exist: {0}</value> + <comment>{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.</comment> + </data> + <data name="ResumeStateDataNotFoundError" xml:space="preserve"> + <value>No data found in the resume state.</value> + </data> + <data name="CommandDoesNotSupportResumeMessage" xml:space="preserve"> + <value>This command does not support resuming.</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -198,6 +198,7 @@ <ClCompile Include="Argument.cpp" /> <ClCompile Include="ARPChanges.cpp" /> <ClCompile Include="Certificates.cpp" /> + <ClCompile Include="CheckpointDatabase.cpp" /> <ClCompile Include="Command.cpp" /> <ClCompile Include="Completion.cpp" /> <ClCompile Include="CompositeSource.cpp" /> @@ -246,6 +247,7 @@ <ClCompile Include="RestInterface_1_4.cpp" /> <ClCompile Include="RestInterface_1_5.cpp" /> <ClCompile Include="RestInterface_1_6.cpp" /> + <ClCompile Include="ResumeFlow.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="SearchRequestSerializer.cpp" /> <ClCompile Include="ShowFlow.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -182,6 +182,9 @@ <ClCompile Include="RestInterface_1_1.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> + <ClCompile Include="ResumeFlow.cpp"> + <Filter>Source Files\CLI</Filter> + </ClCompile> <ClCompile Include="SearchRequestSerializer.cpp"> <Filter>Source Files\Repository</Filter> </ClCompile> @@ -317,6 +320,9 @@ <ClCompile Include="Errors.cpp"> <Filter>Source Files\Common</Filter> </ClCompile> + <ClCompile Include="CheckpointDatabase.cpp"> + <Filter>Source Files\Repository</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/CheckpointDatabase.cpp 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 <Microsoft/CheckpointDatabase.h> +#include <Public/winget/Checkpoint.h> + +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<CheckpointDatabase> database = CheckpointDatabase::CreateNew(tempFile, Schema::Version::Latest()); + versionCreated = database->GetVersion(); + } + + // Reopen the database + { + INFO("Trying with ReadWrite"); + std::shared_ptr<CheckpointDatabase> 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<CheckpointDatabase> 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<CheckpointDatabase> 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<CheckpointDatabase> 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<CheckpointDatabase> 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<CheckpointDatabase> database = CheckpointDatabase::CreateNew(tempFile, { 1, 0 }); + database->AddCheckpoint("firstCheckpoint"sv); + database->AddCheckpoint("secondCheckpoint"sv); + database->AddCheckpoint("thirdCheckpoint"sv); + } + + { + std::shared_ptr<CheckpointDatabase> 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 @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "WorkflowCommon.h" +#include "TestHooks.h" +#include <Commands/InstallCommand.h> +#include <Commands/ResumeCommand.h> +#include <AppInstallerRuntime.h> +#include <AppInstallerStrings.h> +#include <AppInstallerVersions.h> +#include <CheckpointManager.h> + +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<CheckpointDatabase> 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<Setting::EFResume>(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<std::filesystem::path> 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<Setting::EFResume>(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<std::filesystem::path> 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 @@ -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 @@ -42,6 +42,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFDirectMSI>(); case ExperimentalFeature::Feature::WindowsFeature: return userSettings.Get<Setting::EFWindowsFeature>(); + case ExperimentalFeature::Feature::Resume: + return userSettings.Get<Setting::EFResume>(); 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('$(SolutionDir)\packages\Microsoft.Windows.CppWinRT.2.0.210505.3\build\native\Microsoft.Windows.CppWinRT.props')" /> <PropertyGroup Label="Globals"> @@ -353,6 +353,7 @@ <ClInclude Include="Microsoft\PredefinedInstalledSourceFactory.h" /> <ClInclude Include="Microsoft\PredefinedWriteableSourceFactory.h" /> <ClInclude Include="Microsoft\PreIndexedPackageSourceFactory.h" /> + <ClInclude Include="Microsoft\CheckpointDatabase.h" /> <ClInclude Include="Microsoft\Schema\1_0\ChannelTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\CommandsTable.h" /> <ClInclude Include="Microsoft\Schema\1_0\IdTable.h" /> @@ -388,12 +389,16 @@ <ClInclude Include="Microsoft\Schema\1_7\Interface.h" /> <ClInclude Include="Microsoft\Schema\IPinningIndex.h" /> <ClInclude Include="Microsoft\Schema\IPortableIndex.h" /> + <ClInclude Include="Microsoft\Schema\ICheckpointDatabase.h" /> <ClInclude Include="Microsoft\Schema\ISQLiteIndex.h" /> <ClInclude Include="Microsoft\Schema\Pinning_1_0\PinningIndexInterface.h" /> <ClInclude Include="Microsoft\Schema\Pinning_1_0\PinTable.h" /> <ClInclude Include="Microsoft\Schema\Portable_1_0\PortableIndexInterface.h" /> <ClInclude Include="Microsoft\Schema\Portable_1_0\PortableTable.h" /> <ClInclude Include="Microsoft\Schema\MetadataTable.h" /> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDataTable.h" /> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDatabaseInterface.h" /> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointTable.h" /> <ClInclude Include="Microsoft\Schema\Version.h" /> <ClInclude Include="Microsoft\SQLiteIndex.h" /> <ClInclude Include="Microsoft\SQLiteIndexSource.h" /> @@ -403,6 +408,7 @@ <ClInclude Include="PackageTrackingCatalogSourceFactory.h" /> <ClInclude Include="pch.h" /> <ClInclude Include="Public\winget\ARPCorrelation.h" /> + <ClInclude Include="Public\winget\Checkpoint.h" /> <ClInclude Include="Public\winget\IconExtraction.h" /> <ClInclude Include="Public\winget\InstalledFilesCorrelation.h" /> <ClInclude Include="Public\winget\InstallerMetadataCollectionContext.h" /> @@ -472,6 +478,7 @@ <ClCompile Include="Microsoft\PredefinedInstalledSourceFactory.cpp" /> <ClCompile Include="Microsoft\PredefinedWriteableSourceFactory.cpp" /> <ClCompile Include="Microsoft\PreIndexedPackageSourceFactory.cpp" /> + <ClCompile Include="Microsoft\CheckpointDatabase.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\Interface_1_0.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\ManifestTable.cpp" /> <ClCompile Include="Microsoft\Schema\1_0\OneToManyTable.cpp" /> @@ -495,6 +502,9 @@ <ClCompile Include="Microsoft\Schema\Pinning_1_0\PinTable.cpp" /> <ClCompile Include="Microsoft\Schema\Portable_1_0\PortableIndexInterface_1_0.cpp" /> <ClCompile Include="Microsoft\Schema\Portable_1_0\PortableTable.cpp" /> + <ClCompile Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDataTable.cpp" /> + <ClCompile Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDatabaseInterface_1_0.cpp" /> + <ClCompile Include="Microsoft\Schema\Checkpoint_1_0\CheckpointTable.cpp" /> <ClCompile Include="Microsoft\Schema\Version.cpp" /> <ClCompile Include="Microsoft\SQLiteIndex.cpp" /> <ClCompile Include="Microsoft\SQLiteIndexSource.cpp" /> diff --git a/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters b/src/AppInstallerRepositoryCore/AppInstallerRepositoryCore.vcxproj.filters @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <Filter Include="Source Files"> @@ -94,6 +94,9 @@ <Filter Include="Microsoft\Schema\1_7"> <UniqueIdentifier>{f610927a-6f1d-42c5-9ad9-b59790091944}</UniqueIdentifier> </Filter> + <Filter Include="Microsoft\Schema\Checkpoint_1_0"> + <UniqueIdentifier>{a3f9c7ed-f487-40d6-9ee7-e9a052e55c29}</UniqueIdentifier> + </Filter> </ItemGroup> <ItemGroup> <ClInclude Include="pch.h"> @@ -393,6 +396,24 @@ <ClInclude Include="Microsoft\Schema\1_7\Interface.h"> <Filter>Microsoft\Schema\1_7</Filter> </ClInclude> + <ClInclude Include="Microsoft\Schema\ICheckpointDatabase.h"> + <Filter>Microsoft\Schema</Filter> + </ClInclude> + <ClInclude Include="Microsoft\CheckpointDatabase.h"> + <Filter>Microsoft</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDataTable.h"> + <Filter>Microsoft\Schema\Checkpoint_1_0</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDatabaseInterface.h"> + <Filter>Microsoft\Schema\Checkpoint_1_0</Filter> + </ClInclude> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointTable.h"> + <Filter>Microsoft\Schema\Checkpoint_1_0</Filter> + </ClInclude> + <ClInclude Include="Public\winget\Checkpoint.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -623,6 +644,18 @@ <ClCompile Include="Microsoft\Schema\1_7\Interface_1_7.cpp"> <Filter>Microsoft\Schema\1_7</Filter> </ClCompile> + <ClCompile Include="Microsoft\CheckpointDatabase.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDataTable.cpp"> + <Filter>Microsoft\Schema\Checkpoint_1_0</Filter> + </ClCompile> + <ClCompile Include="Microsoft\Schema\Checkpoint_1_0\CheckpointDatabaseInterface_1_0.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClInclude Include="Microsoft\Schema\Checkpoint_1_0\CheckpointTable.cpp"> + <Filter>Microsoft\Schema\Checkpoint_1_0</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerRepositoryCore/Microsoft/CheckpointDatabase.cpp 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> 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<CheckpointDatabase>(std::move(result)); + } + + bool CheckpointDatabase::IsEmpty() + { + return m_interface->IsEmpty(m_dbconn); + } + + CheckpointDatabase::IdType CheckpointDatabase::AddCheckpoint(std::string_view checkpointName) + { + std::lock_guard<std::mutex> 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::IdType> 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<int> CheckpointDatabase::GetDataTypes(IdType checkpointId) + { + return m_interface->GetCheckpointDataTypes(m_dbconn, checkpointId); + } + + std::vector<std::string> 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<std::string>& values) + { + std::lock_guard<std::mutex> 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<std::string> 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<Schema::ICheckpointDatabase> CheckpointDatabase::CreateICheckpointDatabase() const + { + if (m_version == Schema::Version{ 1, 0 } || + m_version.MajorVersion == 1 || + m_version.IsLatest()) + { + return std::make_unique<Schema::Checkpoint_V1_0::CheckpointDatabaseInterface>(); + } + + 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 @@ -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 <winget/ManagedFile.h> + +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<CheckpointDatabase> CreateNew(const std::string& filePath, Schema::Version version = Schema::Version::Latest()); + + // Opens an existing checkpoint database. + static std::shared_ptr<CheckpointDatabase> Open(const std::string& filePath, OpenDisposition disposition = OpenDisposition::ReadWrite, Utility::ManagedFile&& indexFile = {}) + { + CheckpointDatabase result{ filePath, disposition, std::move(indexFile) }; + return std::make_shared<CheckpointDatabase>(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<IdType> 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<int> GetDataTypes(IdType checkpointId); + + // Returns the available field names for a checkpoint data. + std::vector<std::string> 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<std::string>& 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<std::string> 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<Schema::ICheckpointDatabase> CreateICheckpointDatabase() const; + + std::unique_ptr<Schema::ICheckpointDatabase> 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 @@ -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<std::string>(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<int>(0) == 0); + } + + std::vector<int> 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<int> availableData; + + while (select.Step()) + { + availableData.emplace_back(select.GetColumn<int>(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<int>(0) == 0); + } + + std::vector<std::string> 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<std::string> fields; + + while (select.Step()) + { + fields.emplace_back(select.GetColumn<std::string>(0)); + } + + return fields; + } + + std::vector<std::string> 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<std::string> values; + + while (select.Step()) + { + values.emplace_back(select.GetColumn<std::string>(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<std::string>(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 @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include <string_view> +#include <vector> + +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<int> 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<std::string> 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<std::string> 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 @@ -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<SQLite::rowid_t> GetCheckpointIds(SQLite::Connection& connection) override; + std::vector<int> GetCheckpointDataTypes(SQLite::Connection& connection, SQLite::rowid_t checkpointId) override; + std::vector<std::string> 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<std::string>& values) override; + std::optional<std::vector<std::string>> 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 @@ -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<SQLite::rowid_t> CheckpointDatabaseInterface::GetCheckpointIds(SQLite::Connection& connection) + { + return CheckpointTable::GetCheckpointIds(connection); + } + + std::vector<int> CheckpointDatabaseInterface::GetCheckpointDataTypes(SQLite::Connection& connection, SQLite::rowid_t checkpointId) + { + return CheckpointDataTable::GetAvailableData(connection, checkpointId); + } + + std::vector<std::string> 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<std::string>& 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<std::vector<std::string>> 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 @@ -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<SQLite::rowid_t> 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<SQLite::rowid_t> checkpoints; + + while (select.Step()) + { + checkpoints.emplace_back(select.GetColumn<SQLite::rowid_t>(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 @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "SQLiteWrapper.h" +#include <vector> +#include <optional> + +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<SQLite::rowid_t> 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 @@ -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<SQLite::rowid_t> 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<int> 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<std::string>& values) = 0; + + // Gets all field names for a checkpoint data type. + virtual std::vector<std::string> GetCheckpointDataFields(SQLite::Connection& connection, SQLite::rowid_t checkpointId, int dataType) = 0; + + // Gets all field values for a checkpoint data type. + virtual std::optional<std::vector<std::string>> 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 @@ -4,6 +4,8 @@ #include "Version.h" #include "MetadataTable.h" +#include <limits> + 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<uint32_t>::max(), std::numeric_limits<uint32_t>::max() }; + } + + Version Version::LatestForMajor(uint32_t majorVersion) + { + return { majorVersion, std::numeric_limits<uint32_t>::max() }; + } + + bool Version::IsLatest() const + { + return (MajorVersion == std::numeric_limits<uint32_t>::max() && MinorVersion == std::numeric_limits<uint32_t>::max()); + } + + bool Version::IsLatestForMajor(uint32_t majorVersion) const + { + return (MajorVersion == majorVersion && MinorVersion == std::numeric_limits<uint32_t>::max()); + } } diff --git 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 <limits> #include <memory> 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<uint32_t>::max(), std::numeric_limits<uint32_t>::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<uint32_t>::max() }; } + static Version LatestForMajor(uint32_t majorVersion); // Determines if this version represents the latest schema. - bool IsLatest() const { return (MajorVersion == std::numeric_limits<uint32_t>::max() && MinorVersion == std::numeric_limits<uint32_t>::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<uint32_t>::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 @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "Microsoft/CheckpointDatabase.h" +#include <guiddef.h> + +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 <typename T> + struct Checkpoint + { + static_assert(std::is_enum<T>::value); + friend CheckpointManager; + + std::vector<T> 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<std::string> 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<std::string>& 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<std::string> GetMany(T dataType, const std::string& fieldName) + { + return m_checkpointDatabase->GetDataFieldMultiValue(m_checkpointId, dataType, fieldName); + } + + private: + Checkpoint(std::shared_ptr<AppInstaller::Repository::Microsoft::CheckpointDatabase> checkpointDatabase, AppInstaller::Repository::Microsoft::CheckpointDatabase::IdType checkpointId) : + m_checkpointDatabase(checkpointDatabase), m_checkpointId(checkpointId){}; + + AppInstaller::Repository::Microsoft::CheckpointDatabase::IdType m_checkpointId; + std::shared_ptr<CheckpointDatabase> m_checkpointDatabase; + }; +}+ \ No newline at end of file diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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); }