Add resume command and support saving the argument state.#3508
Conversation
JohnMcPMS
left a comment
There was a problem hiding this comment.
We should probably talk again.
Trenly
left a comment
There was a problem hiding this comment.
Will be extended in a future PR to handle more complex context data
I presume this relates to sub-contexts, multi-installs, etc?
| case Args::Type::InstallerType: | ||
| return Argument{ type, Resource::String::InstallerTypeArgumentDescription, ArgumentType::Standard, Argument::Visibility::Help, false }; | ||
| case Args::Type::ResumeGuid: | ||
| return Argument{ type, Resource::String::ResumeGuidArgumentDescription, ArgumentType::Standard, true }; |
There was a problem hiding this comment.
I'd like to understand the design of this feature a bit more; What was the reason for making this a standard argument and not a positional argument? What happens if the argument is not provided?
|
|
||
| checkpointManager.Checkpoint(resumeContext, Execution::CheckpointFlag::CommandArguments); | ||
|
|
||
| commandToResume->Execute(resumeContext); |
There was a problem hiding this comment.
Because the command to be resumed is actually calling the command itself, I presume this already handles a change in state on the machine? i.e - User started a command with experimental feature for dependencies enabled, first dependency spawned a reboot, upon reboot, dependencies experimental feature was magically disabled by some script that ran during the reboot, and then the winget resume happens; Would it fail due to a change in state? Proceed but just not install further dependencies? What if an argument was used originally that was behind an experimental feature and then was disabled? I think that would cause it to error out and show the "need to enable experimental feature for this", but would that also remove the resume from the checkpoint index?
| using namespace SQLite; | ||
| using namespace std::string_view_literals; | ||
|
|
||
| static constexpr std::string_view s_CheckpointArgumentsTable_Table_Name = "CheckpointArguments"sv; |
There was a problem hiding this comment.
Instead of having a column for each argument type, wouldn't it make more sense to have an ArgumentName table which associates a unique numerical ID to each argument type, then use a many-to-one architecture with dynamic typing for storing the arguments? It would reduce the amount of space needed in memory to not have to have empty columns for every argument, especially when arguments aren't supported by certain commands
|
Hello @ryfu-msft, This pull request has been automatically marked as stale because it has been marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within 7 days of this comment. Template: msftbot/noRecentActivity |
This comment has been minimized.
This comment has been minimized.
|
/azp run |
| } | ||
|
|
||
| // Sets a single value for the a data type. | ||
| void Set(T dataType, std::string value) |
There was a problem hiding this comment.
I think we should force all fields to have a name considered. The caller can still use empty string as the field name, although I would definitely make a comment on that PR.
| } | ||
|
|
||
| // Gets a single value for a data type. | ||
| std::string Get(T dataType) |
There was a problem hiding this comment.
Field naming comment from above applies here as well. I'm assuming that this is due to using unique enum values per field in the automatic data case. I think it better to have those pass in empty strings as they are special cases rather than expose the standard use cases to "should I name the field or not?".
|
|
||
| // A representation of a row in the Checkpoint table. | ||
| template <typename T> | ||
| struct Checkpoint |
There was a problem hiding this comment.
This should be in the repository core, created by the database object. It can have a templated function to retrieve one.
There was a problem hiding this comment.
Moved to repository core
|
|
||
| StatementBuilder createTableBuilder; | ||
| createTableBuilder.CreateTable(s_CheckpointTable_Table_Name).BeginColumns(); | ||
| createTableBuilder.Column(ColumnBuilder(s_CheckpointTable_Name_Column, Type::Text).Unique()); |
There was a problem hiding this comment.
Any time that you are going to use the row id as a foreign key in another table, you must specify it explicitly as a column or it can be changed on you when the table is rebalanced. See the index code for the way that you do it (it has to be one of a few specific names and with specific properties).
There was a problem hiding this comment.
Removed Guid and just made the resume id a string. Also removed the argument validation so that it simply checks if we can find the checkpoint database with the provided resume id string.
| createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_ContextData_Column, Type::Int)); | ||
| createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Name_Column, Type::Text)); | ||
| createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Value_Column, Type::Text)); | ||
| createTableBuilder.Column(ColumnBuilder(s_CheckpointDataTable_Index_Column, Type::Int)); |
There was a problem hiding this comment.
Table should probably have a primary key on { checkpoint, data, name, index }. Also, I don't think you want to allow nulls in any of the columns except for value, and then I think it would be better to have the design be that you simply don't insert the value rather than putting in a null one.
There was a problem hiding this comment.
Changed to suggested
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
| } | ||
| else | ||
| { | ||
| THROW_HR_IF(ERROR_CANNOT_MAKE, !std::filesystem::is_directory(checkpointsDirectory)); |
There was a problem hiding this comment.
| THROW_HR_IF(ERROR_CANNOT_MAKE, !std::filesystem::is_directory(checkpointsDirectory)); | |
| THROW_HR_IF(MAKE_HRESULT_FROM_WIN32(ERROR_CANNOT_MAKE), !std::filesystem::is_directory(checkpointsDirectory)); |
|
|
||
| std::filesystem::path CheckpointManager::GetCheckpointDatabasePath(const std::string_view& resumeId, bool createCheckpointDirectory) | ||
| { | ||
|
|
| auto checkpointIds = m_checkpointDatabase->GetCheckpointIds(); | ||
|
|
||
| // Remove the last checkpoint (automatic) | ||
| checkpointIds.pop_back(); |
There was a problem hiding this comment.
In GetAutomaticCheckpoint you check for empty, but not here.
| } | ||
|
|
||
| const auto& checkpointDatabaseParentDirectory = checkpointDatabasePath.parent_path(); | ||
| if (std::filesystem::remove(checkpointDatabaseParentDirectory, error)) |
There was a problem hiding this comment.
I feel like I've left this comment before, but why not simply delete the directory with a recursive delete method?
| AICLI_LOG(CLI, Info, << "Resuming command: " << checkpointCommand); | ||
| std::unique_ptr<Command> commandToResume = FindCommandToResume(checkpointCommand); | ||
|
|
||
| for (const auto& fieldName : automaticCheckpoint.GetFieldNames(AutomaticCheckpointData::Arguments)) |
There was a problem hiding this comment.
For future maintainability, it would be better if this code that reads the values out were next to the code that writes the values in. Even if that means a standalone helper, I would much rather have them in the same file (and even have the functions next to each other).
| <value>A unit contains a setting that requires the config root.</value> | ||
| </data> | ||
| <data name="ResumeCommandLongDescription" xml:space="preserve"> | ||
| <value>Resumes execution of a previously saved command by passing in the unique guid identifier of the saved command. This is used to resume an executed command that may have been terminated due to a reboot.</value> |
There was a problem hiding this comment.
| <value>Resumes execution of a previously saved command by passing in the unique guid identifier of the saved command. This is used to resume an executed command that may have been terminated due to a reboot.</value> | |
| <value>Resumes execution of a previously saved command by passing in the unique identifier of the saved command. This is used to resume an executed command that may have been terminated due to a reboot.</value> | |
| void Command::Resume(Execution::Context& context) const | ||
| { | ||
| context.Reporter.Error() << Resource::String::CommandDoesNotSupportResumeMessage << std::endl; | ||
| THROW_HR(E_NOTIMPL); |
There was a problem hiding this comment.
Terminate the context rather than throwing I think...
| 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; |
There was a problem hiding this comment.
At some point I think we will need to have something going through and cleaning out old resume context data. Doesn't need to be now.
| std::string_view ConvertBoolToString(bool value); | ||
|
|
||
| // Converts the given GUID value to a string. | ||
| std::string ConvertGuidToString(GUID value); |
There was a problem hiding this comment.
| std::string ConvertGuidToString(GUID value); | |
| std::string ConvertGuidToString(const GUID& value); |

Related to:
#3165
Changes:
Add a new
Resumecommand that takes in a--resume-id; -gargument that points to a resume state.Created a Checkpoint Index that creates/interacts with the following tables:
Resumefunction that an individual command can override if they have command-specific steps for resuming.Tests:
Add unit tests to verify the following scenarios:
Added E2E tests to verify:
Microsoft Reviewers: codeflow:open?pullrequest=#3508