Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register restart for resume #3858

Merged
merged 25 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions schemas/JSON/settings/settings.schema.0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@
"description": "Default install location to use for packages that require it when not specified",
"type": "string",
"maxLength": "32767"
},
"maxResumes": {
"description": "The maximum number of resumes allowed for a single resume id. This is to prevent continuous reboots if an install that requires a reboot is not properly detected.",
"type": "integer",
"default": 3,
"minimum": 1
}
}
},
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/CheckpointManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ namespace AppInstaller::Checkpoints
automaticCheckpoint.SetMany(AutomaticCheckpointData::Arguments, argument, values);
}
}

automaticCheckpoint.Set(AutomaticCheckpointData::ResumeCount, {}, std::to_string(0));
}

void LoadCommandArgsFromAutomaticCheckpoint(CLI::Execution::Context& context, Checkpoint<AutomaticCheckpointData>& automaticCheckpoint)
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/CheckpointManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ namespace AppInstaller::Checkpoints
// Gets the file path of the checkpoint database.
static std::filesystem::path GetCheckpointDatabasePath(const std::string_view& resumeId, bool createCheckpointDirectory = false);

// Gets the resume id.
std::string GetResumeId() { return m_resumeId; };

// Gets the automatic checkpoint.
std::optional<Checkpoint<AutomaticCheckpointData>> GetAutomaticCheckpoint();

Expand Down
46 changes: 38 additions & 8 deletions src/AppInstallerCLICore/Command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <winget/UserSettings.h>
#include <AppInstallerRuntime.h>
#include <winget/Locale.h>
#include <winget/Reboot.h>

using namespace std::string_view_literals;
using namespace AppInstaller::Utility::literals;
Expand Down Expand Up @@ -870,16 +871,45 @@ namespace AppInstaller::CLI
ExecuteInternal(context);
}

if (context.Args.Contains(Execution::Args::Type::OpenLogs))
{
// TODO: Consider possibly adding functionality that if the context contains 'Execution::Args::Type::Log' to open the path provided for the log
// The above was omitted initially as a security precaution to ensure that user input to '--log' wouldn't be passed directly to ShellExecute
ShellExecute(NULL, NULL, Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation).wstring().c_str(), NULL, NULL, SW_SHOWNORMAL);
}
if (Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Reboot) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the context is terminated (not with an exception, but "normally"), this code will still run. That may or may not be correct, but I want to make sure it is considered. It feels ok, but then I worry that it won't happen for an exception. I would say that we haven't been strict on the precise method for failures to propagate, which means that this code is likely to not run in some situations when it probably should.

Since this is experimental, it is fine to live here for now. But it will be good to keep this in mind.

context.Args.Contains(Execution::Args::Type::AllowReboot) &&
WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RebootRequired))
{
context.Reporter.Warn() << Resource::String::InitiatingReboot << std::endl;

if (context.Args.Contains(Execution::Args::Type::Wait))
{
context.Reporter.PromptForEnter();
}

context.ClearFlags(Execution::ContextFlag::RebootRequired);

if (!Reboot::InitiateReboot())
{
context.Reporter.Error() << Resource::String::FailedToInitiateReboot << std::endl;
}
else if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RegisterResume))
{
context.ClearFlags(Execution::ContextFlag::RegisterResume);

if (context.Args.Contains(Execution::Args::Type::Wait))
// For Windows Error Reporting to restart this process, the process must be rebooted while it is still running.
// Workaround is to have the program sleep while the reboot process is kicked off. Only applies to resume.
Sleep(5000);
}
}
else
{
context.Reporter.PromptForEnter();
if (context.Args.Contains(Execution::Args::Type::OpenLogs))
{
// TODO: Consider possibly adding functionality that if the context contains 'Execution::Args::Type::Log' to open the path provided for the log
// The above was omitted initially as a security precaution to ensure that user input to '--log' wouldn't be passed directly to ShellExecute
ShellExecute(NULL, NULL, Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation).wstring().c_str(), NULL, NULL, SW_SHOWNORMAL);
}

if (context.Args.Contains(Execution::Args::Type::Wait))
{
context.Reporter.PromptForEnter();
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/AppInstallerCLICore/Commands/ResumeCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ namespace AppInstaller::CLI
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_CLIENT_VERSION_MISMATCH);
}

const auto& resumeCountString = automaticCheckpoint.Get(AutomaticCheckpointData::ResumeCount, {});
int resumeCount = std::stoi(resumeCountString);
if (resumeCount >= Settings::User().Get<Settings::Setting::MaxResumes>())
{
context.Reporter.Error() << Resource::String::ResumeLimitExceeded(Utility::LocIndView{ resumeCountString }) << std::endl;
ryfu-msft marked this conversation as resolved.
Show resolved Hide resolved
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_RESUME_LIMIT_EXCEEDED);
}
else
{
automaticCheckpoint.Update(AutomaticCheckpointData::ResumeCount, {}, std::to_string(resumeCount + 1));
}

const auto& checkpointCommand = automaticCheckpoint.Get(AutomaticCheckpointData::Command, {});
AICLI_LOG(CLI, Info, << "Resuming command: " << checkpointCommand);
std::unique_ptr<Command> commandToResume = FindCommandToResume(checkpointCommand);
Expand Down
9 changes: 7 additions & 2 deletions src/AppInstallerCLICore/ExecutionContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ namespace AppInstaller::CLI::Execution

Context::~Context()
{
if (Settings::ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Resume) && !IsTerminated())
if (Settings::ExperimentalFeature::IsEnabled(ExperimentalFeature::Feature::Resume))
{
if (m_checkpointManager)
if (m_checkpointManager && (!IsTerminated() || GetTerminationHR() != APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL))
{
m_checkpointManager->CleanUpDatabase();
}
Expand Down Expand Up @@ -427,6 +427,11 @@ namespace AppInstaller::CLI::Execution
}
#endif

std::string Context::GetResumeId()
{
return m_checkpointManager->GetResumeId();
}

std::optional<Checkpoint<AutomaticCheckpointData>> Context::LoadCheckpoint(const std::string& resumeId)
{
m_checkpointManager = std::make_unique<AppInstaller::Checkpoints::CheckpointManager>(resumeId);
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/ExecutionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ namespace AppInstaller::CLI::Execution
bool ShouldExecuteWorkflowTask(const Workflow::WorkflowTask& task);
#endif

// Returns the resume id.
std::string GetResumeId();

// Called by the resume command. Loads the checkpoint manager with the resume id and returns the automatic checkpoint.
std::optional<AppInstaller::Checkpoints::Checkpoint<AppInstaller::Checkpoints::AutomaticCheckpointData>> LoadCheckpoint(const std::string& resumeId);

Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverrideRequired);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToInitiateReboot);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToRefreshPathWarning);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToRegisterReboot);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage);
WINGET_DEFINE_RESOURCE_STRINGID(FeaturesCommandLongDescription);
Expand Down Expand Up @@ -425,6 +426,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(ResumeCommandShortDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ResumeIdArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ResumeIdNotFoundError);
WINGET_DEFINE_RESOURCE_STRINGID(ResumeLimitExceeded);
WINGET_DEFINE_RESOURCE_STRINGID(ResumeStateDataNotFoundError);
WINGET_DEFINE_RESOURCE_STRINGID(RetroArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(SearchCommandLongDescription);
Expand Down
4 changes: 3 additions & 1 deletion src/AppInstallerCLICore/Workflows/DependenciesFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ namespace AppInstaller::CLI::Workflow
else
{
context.Reporter.Error() << Resource::String::RebootRequiredToEnableWindowsFeatureOverrideRequired << std::endl;
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL);
context.SetFlags(Execution::ContextFlag::RegisterResume);
context.SetFlags(Execution::ContextFlag::RebootRequired);
AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL);
}
}
else
Expand Down
8 changes: 4 additions & 4 deletions src/AppInstallerCLICore/Workflows/InstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ namespace AppInstaller::CLI::Workflow
case ExpectedReturnCodeEnum::RebootRequiredToFinish:
return ExpectedReturnCode(returnCode, APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_FINISH, Resource::String::InstallFlowReturnCodeRebootRequiredToFinish);
case ExpectedReturnCodeEnum::RebootRequiredForInstall:
return ExpectedReturnCode(returnCode, APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL, Resource::String::InstallFlowReturnCodeRebootRequiredForInstall);
return ExpectedReturnCode(returnCode, APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL, Resource::String::InstallFlowReturnCodeRebootRequiredForInstall);
case ExpectedReturnCodeEnum::RebootInitiated:
return ExpectedReturnCode(returnCode, APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_INITIATED, Resource::String::InstallFlowReturnCodeRebootInitiated);
case ExpectedReturnCodeEnum::CancelledByUser:
Expand Down Expand Up @@ -484,8 +484,8 @@ namespace AppInstaller::CLI::Workflow
context.Reporter.Warn() << returnCode.Message << std::endl;
terminationHR = S_OK;
break;
case APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL:
// REBOOT_REQUIRED_TO_INSTALL is treated as an error since installation has not yet completed.
case APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL:
// REBOOT_REQUIRED_FOR_INSTALL is treated as an error since installation has not yet completed.
context.SetFlags(ContextFlag::RebootRequired);
// TODO: Add separate workflow to handle restart registration for resume.
context.SetFlags(ContextFlag::RegisterResume);
Expand Down Expand Up @@ -599,7 +599,7 @@ namespace AppInstaller::CLI::Workflow
Workflow::InstallDependencies <<
Workflow::DownloadInstaller <<
Workflow::InstallPackageInstaller <<
Workflow::InitiateRebootIfApplicable();
Workflow::RegisterForReboot();
}

void EnsureSupportForInstall(Execution::Context& context)
Expand Down
28 changes: 12 additions & 16 deletions src/AppInstallerCLICore/Workflows/ResumeFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,26 @@ namespace AppInstaller::CLI::Workflow
context.Checkpoint(m_checkpointName, m_contextData);
}

void InitiateRebootIfApplicable::operator()(Execution::Context& context) const
void RegisterForReboot::operator()(Execution::Context& context) const
{
if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Reboot))
if (!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Resume) ||
!Settings::ExperimentalFeature::IsEnabled(Settings::ExperimentalFeature::Feature::Reboot))
{
return;
}

if (!context.Args.Contains(Execution::Args::Type::AllowReboot))
if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RegisterResume))
{
AICLI_LOG(CLI, Info, << "No reboot flag found; skipping reboot flow.");
return;
}

if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RebootRequired))
{
context.ClearFlags(Execution::ContextFlag::RebootRequired);
std::string commandLineArg = "resume -g " + context.GetResumeId();

if (Reboot::InitiateReboot())
if (!Reboot::RegisterApplicationForReboot(commandLineArg))
{
context.Reporter.Warn() << Resource::String::InitiatingReboot << std::endl;
}
else
{
context.Reporter.Error() << Resource::String::FailedToInitiateReboot << std::endl;
context.Reporter.Error() << Resource::String::FailedToRegisterReboot << std::endl;

if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RebootRequired))
{
context.ClearFlags(Execution::ContextFlag::RebootRequired);
}
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/AppInstallerCLICore/Workflows/ResumeFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ namespace AppInstaller::CLI::Workflow
std::vector<Execution::Data> m_contextData;
};

// Initiates a reboot if applicable. This task always executes even if context terminates.
// Registers the resume command to execute upon reboot if applicable. This task always executes even if context terminates.
// Required Args: None
// Inputs: None
// Outputs: None
struct InitiateRebootIfApplicable : public WorkflowTask
struct RegisterForReboot : public WorkflowTask
{
InitiateRebootIfApplicable() : WorkflowTask("InitiateRebootIfApplicable", /* executeAlways */true) {}
RegisterForReboot() : WorkflowTask("RegisterForReboot", /* executeAlways*/ true) {}

void operator()(Execution::Context& context) const override;
void operator()(Execution::Context & context) const override;
};
}
2 changes: 1 addition & 1 deletion src/AppInstallerCLIE2ETests/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public class ErrorCode
public const int ERROR_INSTALL_NO_NETWORK = unchecked((int)0x8A150107);
public const int ERROR_INSTALL_CONTACT_SUPPORT = unchecked((int)0x8A150108);
public const int ERROR_INSTALL_REBOOT_REQUIRED_TO_FINISH = unchecked((int)0x8A150109);
public const int ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL = unchecked((int)0x8A15010A);
public const int ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL = unchecked((int)0x8A15010A);
public const int ERROR_INSTALL_REBOOT_INITIATED = unchecked((int)0x8A15010B);
public const int ERROR_INSTALL_CANCELLED_BY_USER = unchecked((int)0x8A15010C);
public const int ERROR_INSTALL_ALREADY_INSTALLED = unchecked((int)0x8A15010D);
Expand Down
2 changes: 2 additions & 0 deletions src/AppInstallerCLIE2ETests/Helpers/WinGetSettingsHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static void InitializeWingetSettings()
{ "directMSI", false },
{ "windowsFeature", false },
{ "resume", false },
{ "reboot", false },
}
},
{
Expand Down Expand Up @@ -210,6 +211,7 @@ public static void InitializeAllFeatures(bool status)
ConfigureFeature("directMSI", status);
ConfigureFeature("windowsFeature", status);
ConfigureFeature("resume", status);
ConfigureFeature("reboot", status);
}

private static JObject GetJsonSettingsObject(string objectName)
Expand Down
47 changes: 35 additions & 12 deletions src/AppInstallerCLIE2ETests/ResumeCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,46 @@ public void ResumeIdNotFound()
Assert.AreEqual(Constants.ErrorCode.ERROR_RESUME_ID_NOT_FOUND, resumeResult.ExitCode);
}

/// <summary>
/// Verifies that a checkpoint record persists after a failed install.
/// <summary>
/// Test install a package that returns REBOOT_REQUIRED_TO_FINISH and verify that the checkpoint database is properly cleaned up.
/// </summary>
[Test]
public void InstallRequiresRebootToFinish()
{
var checkpointsDir = TestCommon.GetCheckpointsDirectory();
int initialCheckpointsCount = Directory.Exists(checkpointsDir) ? Directory.GetDirectories(checkpointsDir).Length : 0;

var installDir = TestCommon.GetRandomTestDir();
var result = TestCommon.RunAICLICommand("install", $"TestRebootRequired --custom \"/ExitCode 9\" -l {installDir}");

// REBOOT_REQUIRED_TO_FINISH is treated as a success.
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Restart your PC to finish installation."));
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir));

int actualCheckpointsCount = Directory.GetDirectories(checkpointsDir).Length;

// Checkpoint database should be cleaned up since resume is not needed to complete installation.
Assert.AreEqual(initialCheckpointsCount, actualCheckpointsCount);
}

/// <summary>
/// Test install a package that returns REBOOT_REQUIRED_FOR_INSTALL and verify that resume command can be called successfully.
/// </summary>
[Test]
public void ResumeRecordPreserved()
{
public void InstallRequiresRebootForInstall()
{
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"));
var installDir = TestCommon.GetRandomTestDir();
var result = TestCommon.RunAICLICommand("install", $"TestRebootRequired --custom \"/ExitCode 10\" -l {installDir}");
Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL, result.ExitCode);
Assert.True(result.StdOut.Contains("Your PC will restart to finish installation."));
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir));

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);
Expand All @@ -95,8 +117,9 @@ public void ResumeRecordPreserved()

// 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"));
Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALL_REBOOT_REQUIRED_FOR_INSTALL, resumeResult.ExitCode);
Assert.True(resumeResult.StdOut.Contains("Your PC will restart to finish installation."));
Assert.True(TestCommon.VerifyTestExeInstalledAndCleanup(installDir));
}
}
}
Loading
Loading