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