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

Add experimental feature for initiating reboot for single package installs #3631

Merged
merged 12 commits into from
Oct 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/spelling/allow.txt
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ etstandard
ETW
EULA
EVENTTAG
EWX
exe
executables
executionengine
Expand Down Expand Up @@ -294,6 +295,7 @@ LONGLONG
LPCGUID
LPCSTR
LPVOID
Luid
mailto
MAJORVERSION
makeappx
Expand Down Expand Up @@ -467,6 +469,7 @@ resetpins
resheader
resmimetype
RESOLVESOURCE
RESTARTAPPS
RESTSOURCE
resw
resx
Expand Down Expand Up @@ -509,6 +512,7 @@ SHELLEXECUTEINFOA
SHELLEXECUTEINFOW
shlobj
Shlwapi
SHTDN
shtypes
signtool
silentwithprogress
Expand Down
11 changes: 11 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,15 @@ You can enable the feature as shown below.
"experimentalFeatures": {
"windowsFeature": true
},
```

### reboot

This feature enables support for initiating a reboot.
You can enable the feature as shown below.

```json
"experimentalFeatures": {
"reboot": true
},
```
5 changes: 5 additions & 0 deletions schemas/JSON/settings/settings.schema.0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,11 @@
"description": "Enable support for enabling Windows Feature(s)",
"type": "boolean",
"default": false
},
"reboot": {
"description": "Enable support for initiating a reboot",
"type": "boolean",
"default": false
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/AppInstallerCLICore/Argument.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ namespace AppInstaller::CLI
return { type, "no-upgrade"_liv, ArgTypeCategory::CopyFlagToSubContext };
case Execution::Args::Type::SkipDependencies:
return { type, "skip-dependencies"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext };
case Execution::Args::Type::AllowReboot:
return { type, "allow-reboot"_liv, ArgTypeCategory::InstallerBehavior | ArgTypeCategory::CopyFlagToSubContext };

// Uninstall behavior
case Execution::Args::Type::Purge:
Expand Down Expand Up @@ -361,6 +363,8 @@ namespace AppInstaller::CLI
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 };
case Args::Type::AllowReboot:
return Argument{ type, Resource::String::AllowRebootArgumentDescription, ArgumentType::Flag, ExperimentalFeature::Feature::Reboot };
default:
THROW_HR(E_UNEXPECTED);
}
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Commands/InstallCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ namespace AppInstaller::CLI
Argument::ForType(Args::Type::Override),
Argument::ForType(Args::Type::InstallLocation),
Argument::ForType(Args::Type::HashOverride),
Argument::ForType(Args::Type::AllowReboot),
Argument::ForType(Args::Type::SkipDependencies),
Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan),
Argument::ForType(Args::Type::DependencySource),
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/Commands/UpgradeCommand.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace AppInstaller::CLI
Argument::ForType(Args::Type::InstallerType),
Argument::ForType(Args::Type::Locale),
Argument::ForType(Args::Type::HashOverride),
Argument::ForType(Args::Type::AllowReboot),
Argument::ForType(Args::Type::SkipDependencies),
Argument::ForType(Args::Type::IgnoreLocalArchiveMalwareScan),
Argument::ForType(Args::Type::AcceptPackageAgreements),
Expand Down
1 change: 1 addition & 0 deletions src/AppInstallerCLICore/ExecutionArgs.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ namespace AppInstaller::CLI::Execution
AcceptPackageAgreements, // Accept all license agreements for packages
Rename, // Renames the file of the executable. Only applies to the portable installerType
NoUpgrade, // Install flow should not try to convert to upgrade flow upon finding existing installed version
AllowReboot, // Allows the reboot flow to proceed if applicable

// Uninstall behavior
Purge, // Removes all files and directories related to a package during an uninstall. Only applies to the portable installerType.
Expand Down
4 changes: 3 additions & 1 deletion src/AppInstallerCLICore/ExecutionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ namespace AppInstaller::CLI::Execution
DisableInteractivity = 0x40,
BypassIsStoreClientBlockedPolicyCheck = 0x80,
InstallerDownloadOnly = 0x100,
Resume = 0x200
Resume = 0x200,
RebootRequired = 0x400,
RegisterResume = 0x800,
};

DEFINE_ENUM_FLAG_OPERATORS(ContextFlag);
Expand Down
3 changes: 3 additions & 0 deletions src/AppInstallerCLICore/Resources.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnabled);
WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnableDescription);
WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingHeader);
WINGET_DEFINE_RESOURCE_STRINGID(AllowRebootArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ArchitectureArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScan);
WINGET_DEFINE_RESOURCE_STRINGID(ArchiveFailedMalwareScanOverridden);
Expand Down Expand Up @@ -182,6 +183,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeature);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverridden);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToEnableWindowsFeatureOverrideRequired);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToInitiateReboot);
WINGET_DEFINE_RESOURCE_STRINGID(FailedToRefreshPathWarning);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledByAdminSettingMessage);
WINGET_DEFINE_RESOURCE_STRINGID(FeatureDisabledMessage);
Expand Down Expand Up @@ -229,6 +231,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IncludeUnknownInListArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(IncompatibleArgumentsProvided);
WINGET_DEFINE_RESOURCE_STRINGID(InitiatingReboot);
WINGET_DEFINE_RESOURCE_STRINGID(InstallAbandoned);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer1);
WINGET_DEFINE_RESOURCE_STRINGID(InstallationDisclaimer2);
Expand Down
85 changes: 54 additions & 31 deletions src/AppInstallerCLICore/Workflows/InstallFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "DownloadFlow.h"
#include "UninstallFlow.h"
#include "UpdateFlow.h"
#include "ResumeFlow.h"
#include "ShowFlow.h"
#include "Resources.h"
#include "ShellExecuteInstallerHandler.h"
Expand Down Expand Up @@ -466,47 +467,68 @@ namespace AppInstaller::CLI::Workflow
const auto& additionalSuccessCodes = context.Get<Execution::Data::Installer>()->InstallerSuccessCodes;
if (installResult != 0 && (std::find(additionalSuccessCodes.begin(), additionalSuccessCodes.end(), installResult) == additionalSuccessCodes.end()))
{
const auto& manifest = context.Get<Execution::Data::Manifest>();
Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, m_installerType, installResult);

if (m_isHResult)
{
context.Reporter.Error()
<< Resource::String::InstallerFailedWithCode(Utility::LocIndView{ GetUserPresentableMessage(installResult) })
<< std::endl;
}
else
{
context.Reporter.Error()
<< Resource::String::InstallerFailedWithCode(installResult)
<< std::endl;
}

// Show installer log path if exists
if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get<Execution::Data::LogPath>()))
{
auto installerLogPath = Utility::LocIndString{ context.Get<Execution::Data::LogPath>().u8string() };
context.Reporter.Info() << Resource::String::InstallerLogAvailable(installerLogPath) << std::endl;
}

// Show a specific message if we can identify the return code
HRESULT terminationHR = m_hr;
const auto& expectedReturnCodes = context.Get<Execution::Data::Installer>()->ExpectedReturnCodes;
auto expectedReturnCodeItr = expectedReturnCodes.find(installResult);
if (expectedReturnCodeItr != expectedReturnCodes.end() && expectedReturnCodeItr->second.ReturnResponseEnum != ExpectedReturnCodeEnum::Unknown)
{
auto returnCode = ExpectedReturnCode::GetExpectedReturnCode(expectedReturnCodeItr->second.ReturnResponseEnum);
context.Reporter.Error() << returnCode.Message << std::endl;
terminationHR = returnCode.HResult;

auto returnResponseUrl = expectedReturnCodeItr->second.ReturnResponseUrl;
if (!returnResponseUrl.empty())
switch (terminationHR)
{
context.Reporter.Error() << Resource::String::RelatedLink << ' ' << returnResponseUrl << std::endl;
case APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_FINISH:
// REBOOT_REQUIRED_TO_FINISH is treated as a success since installation has completed but is pending a reboot.
context.SetFlags(ContextFlag::RebootRequired);
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.
context.SetFlags(ContextFlag::RebootRequired);
// TODO: Add separate workflow to handle restart registration for resume.
context.SetFlags(ContextFlag::RegisterResume);
break;
}

AICLI_TERMINATE_CONTEXT(returnCode.HResult);
if (FAILED(terminationHR))
{
context.Reporter.Error() << returnCode.Message << std::endl;
auto returnResponseUrl = expectedReturnCodeItr->second.ReturnResponseUrl;
if (!returnResponseUrl.empty())
{
context.Reporter.Error() << Resource::String::RelatedLink << ' ' << returnResponseUrl << std::endl;
}
}
}

AICLI_TERMINATE_CONTEXT(m_hr);
if (FAILED(terminationHR))
{
const auto& manifest = context.Get<Execution::Data::Manifest>();
Logging::Telemetry().LogInstallerFailure(manifest.Id, manifest.Version, manifest.Channel, m_installerType, installResult);

if (m_isHResult)
{
context.Reporter.Error()
<< Resource::String::InstallerFailedWithCode(Utility::LocIndView{ GetUserPresentableMessage(installResult) })
<< std::endl;
}
else
{
context.Reporter.Error()
<< Resource::String::InstallerFailedWithCode(installResult)
<< std::endl;
}

// Show installer log path if exists
if (context.Contains(Execution::Data::LogPath) && std::filesystem::exists(context.Get<Execution::Data::LogPath>()))
{
auto installerLogPath = Utility::LocIndString{ context.Get<Execution::Data::LogPath>().u8string() };
context.Reporter.Info() << Resource::String::InstallerLogAvailable(installerLogPath) << std::endl;
}

AICLI_TERMINATE_CONTEXT(terminationHR);
}
}
else
{
Expand Down Expand Up @@ -574,7 +596,8 @@ namespace AppInstaller::CLI::Workflow
Workflow::CreateDependencySubContexts(Resource::String::PackageRequiresDependencies) <<
Workflow::InstallDependencies <<
Workflow::DownloadInstaller <<
Workflow::InstallPackageInstaller;
Workflow::InstallPackageInstaller <<
Workflow::InitiateRebootIfApplicable();
}

void EnsureSupportForInstall(Execution::Context& context)
Expand Down
29 changes: 29 additions & 0 deletions src/AppInstallerCLICore/Workflows/ResumeFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
#include "pch.h"
#include "ResumeFlow.h"
#include "winget/Reboot.h"

namespace AppInstaller::CLI::Workflow
{
Expand All @@ -14,4 +15,32 @@ namespace AppInstaller::CLI::Workflow

context.Checkpoint(m_checkpointName, m_contextData);
}

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

if (!context.Args.Contains(Execution::Args::Type::AllowReboot))
{
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);

if (Reboot::InitiateReboot())
{
context.Reporter.Warn() << Resource::String::InitiatingReboot << std::endl;
}
else
{
context.Reporter.Error() << Resource::String::FailedToInitiateReboot << std::endl;
}
}
}
}
14 changes: 14 additions & 0 deletions src/AppInstallerCLICore/Workflows/ResumeFlow.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
namespace AppInstaller::CLI::Workflow
{
// Applies a checkpoint to the context workflow.
// Required Args: None
// Inputs: Context data, command arguments, client version
// Outputs: None
struct Checkpoint : public WorkflowTask
{
Checkpoint(std::string_view checkpointName, std::vector<Execution::Data> contextData) :
Expand All @@ -19,4 +22,15 @@ namespace AppInstaller::CLI::Workflow
std::string_view m_checkpointName;
std::vector<Execution::Data> m_contextData;
};

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

void operator()(Execution::Context& context) const override;
};
}
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Workflows/WorkflowBase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::

AppInstaller::CLI::Execution::Context& operator<<(AppInstaller::CLI::Execution::Context& context, const AppInstaller::CLI::Workflow::WorkflowTask& task)
{
if (!context.IsTerminated())
if (!context.IsTerminated() || task.ExecuteAlways())
{
#ifndef AICLI_DISABLE_TEST_HOOKS
if (context.ShouldExecuteWorkflowTask(task))
Expand Down
4 changes: 3 additions & 1 deletion src/AppInstallerCLICore/Workflows/WorkflowBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ namespace AppInstaller::CLI::Workflow
using Func = void (*)(Execution::Context&);

WorkflowTask(Func f) : m_isFunc(true), m_func(f) {}
WorkflowTask(std::string_view name) : m_name(name) {}
WorkflowTask(std::string_view name, bool executeAlways = false) : m_name(name), m_executeAlways(executeAlways) {}

virtual ~WorkflowTask() = default;

Expand All @@ -66,11 +66,13 @@ namespace AppInstaller::CLI::Workflow
const std::string& GetName() const { return m_name; }
bool IsFunction() const { return m_isFunc; }
Func Function() const { return m_func; }
bool ExecuteAlways() const { return m_executeAlways; }

private:
bool m_isFunc = false;
Func m_func = nullptr;
std::string m_name;
bool m_executeAlways = false;
};

// Helper to determine installed source to use based on context input.
Expand Down
9 changes: 9 additions & 0 deletions src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2610,4 +2610,13 @@ Please specify one of them using the --source option to proceed.</value>
<data name="CommandDoesNotSupportResumeMessage" xml:space="preserve">
<value>This command does not support resuming.</value>
</data>
<data name="AllowRebootArgumentDescription" xml:space="preserve">
<value>Allows a reboot if applicable</value>
</data>
<data name="InitiatingReboot" xml:space="preserve">
<value>Initiating reboot to complete operation...</value>
</data>
<data name="FailedToInitiateReboot" xml:space="preserve">
<value>Failed to initiate a reboot.</value>
</data>
</root>
3 changes: 3 additions & 0 deletions src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,9 @@
<CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\UpdateFlowTest_ExpectedReturnCodes.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml">
<DeploymentContent>true</DeploymentContent>
</CopyFileToFolders>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,9 @@
<CopyFileToFolders Include="TestData\UpdateFlowTest_ExeDependencies.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\UpdateFlowTest_ExpectedReturnCodes.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
<CopyFileToFolders Include="TestData\UpdateFlowTest_Msix.yaml">
<Filter>TestData</Filter>
</CopyFileToFolders>
Expand Down