winget-cli

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

commit a32e1140e2f40b620ddf1fd65988b33e789a9b52
parent dfe9a0fb17d18b61d0ad95ddbb7a38f8b805a54e
Author: Ryan <69221034+ryfu-msft@users.noreply.github.com>
Date:   Fri, 20 Oct 2023 19:32:55 -0700

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


Diffstat:
M.github/actions/spelling/allow.txt | 4++++
Mdoc/Settings.md | 11+++++++++++
Mschemas/JSON/settings/settings.schema.0.2.json | 5+++++
Msrc/AppInstallerCLICore/Argument.cpp | 4++++
Msrc/AppInstallerCLICore/Commands/InstallCommand.cpp | 1+
Msrc/AppInstallerCLICore/Commands/UpgradeCommand.cpp | 1+
Msrc/AppInstallerCLICore/ExecutionArgs.h | 1+
Msrc/AppInstallerCLICore/ExecutionContext.h | 4+++-
Msrc/AppInstallerCLICore/Resources.h | 3+++
Msrc/AppInstallerCLICore/Workflows/InstallFlow.cpp | 85++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/AppInstallerCLICore/Workflows/ResumeFlow.cpp | 29+++++++++++++++++++++++++++++
Msrc/AppInstallerCLICore/Workflows/ResumeFlow.h | 14++++++++++++++
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.cpp | 2+-
Msrc/AppInstallerCLICore/Workflows/WorkflowBase.h | 4+++-
Msrc/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw | 9+++++++++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj | 3+++
Msrc/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters | 3+++
Msrc/AppInstallerCLITests/ImportFlow.cpp | 3+++
Msrc/AppInstallerCLITests/InstallFlow.cpp | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestData/InstallFlowTest_ExpectedReturnCodes.yaml | 4++--
Asrc/AppInstallerCLITests/TestData/UpdateFlowTest_ExpectedReturnCodes.yaml | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCLITests/TestHooks.h | 21+++++++++++++++++++++
Msrc/AppInstallerCLITests/UpdateFlow.cpp | 50++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/AppInstallerCLITests/WorkflowCommon.cpp | 16++++++++++++++++
Msrc/AppInstallerCLITests/WorkflowCommon.h | 1+
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj | 2++
Msrc/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters | 6++++++
Msrc/AppInstallerCommonCore/ExperimentalFeature.cpp | 4++++
Msrc/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h | 1+
Asrc/AppInstallerCommonCore/Public/winget/Reboot.h | 9+++++++++
Msrc/AppInstallerCommonCore/Public/winget/UserSettings.h | 2++
Asrc/AppInstallerCommonCore/Reboot.cpp | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/AppInstallerCommonCore/UserSettings.cpp | 1+
33 files changed, 418 insertions(+), 38 deletions(-)

diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt @@ -166,6 +166,7 @@ etstandard ETW EULA EVENTTAG +EWX exe executables executionengine @@ -294,6 +295,7 @@ LONGLONG LPCGUID LPCSTR LPVOID +Luid mailto MAJORVERSION makeappx @@ -467,6 +469,7 @@ resetpins resheader resmimetype RESOLVESOURCE +RESTARTAPPS RESTSOURCE resw resx @@ -509,6 +512,7 @@ SHELLEXECUTEINFOA SHELLEXECUTEINFOW shlobj Shlwapi +SHTDN shtypes signtool silentwithprogress diff --git a/doc/Settings.md b/doc/Settings.md @@ -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 + }, ``` \ No newline at end of file diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json @@ -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 } } } diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -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: @@ -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); } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -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), diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp b/src/AppInstallerCLICore/Commands/UpgradeCommand.cpp @@ -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), diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h @@ -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. diff --git a/src/AppInstallerCLICore/ExecutionContext.h b/src/AppInstallerCLICore/ExecutionContext.h @@ -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); diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -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); @@ -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); @@ -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); diff --git a/src/AppInstallerCLICore/Workflows/InstallFlow.cpp b/src/AppInstallerCLICore/Workflows/InstallFlow.cpp @@ -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" @@ -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 { @@ -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) diff --git a/src/AppInstallerCLICore/Workflows/ResumeFlow.cpp b/src/AppInstallerCLICore/Workflows/ResumeFlow.cpp @@ -2,6 +2,7 @@ // Licensed under the MIT License. #include "pch.h" #include "ResumeFlow.h" +#include "winget/Reboot.h" namespace AppInstaller::CLI::Workflow { @@ -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; + } + } + } } diff --git a/src/AppInstallerCLICore/Workflows/ResumeFlow.h b/src/AppInstallerCLICore/Workflows/ResumeFlow.h @@ -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) : @@ -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; + }; } diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp b/src/AppInstallerCLICore/Workflows/WorkflowBase.cpp @@ -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)) diff --git a/src/AppInstallerCLICore/Workflows/WorkflowBase.h b/src/AppInstallerCLICore/Workflows/WorkflowBase.h @@ -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; @@ -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. diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -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> \ No newline at end of file diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -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> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -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> diff --git a/src/AppInstallerCLITests/ImportFlow.cpp b/src/AppInstallerCLITests/ImportFlow.cpp @@ -1,9 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #include "pch.h" +#include "TestHooks.h" #include "WorkflowCommon.h" #include <Commands/ImportCommand.h> +#include <winget/Settings.h> #include <Workflows/ImportExportFlow.h> +#include <Workflows/ShellExecuteInstallerHandler.h> using namespace TestCommon; using namespace AppInstaller::CLI; diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp @@ -1208,3 +1208,51 @@ TEST_CASE("InstallFlow_InstallAcquiresLock", "[InstallFlow][workflow]") REQUIRE(installResultStr.find("/custom") != std::string::npos); REQUIRE(installResultStr.find("/silentwithprogress") != std::string::npos); } + +TEST_CASE("InstallFlow_InstallWithReboot", "[InstallFlow][workflow][reboot]") +{ + TestCommon::TempFile installResultPath("TestExeInstalled.txt"); + TestCommon::TestUserSettings testSettings; + testSettings.Set<Setting::EFReboot>(true); + + std::ostringstream installOutput; + TestContext context{ installOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + + context.Args.AddArg(Execution::Args::Type::Manifest, TestDataFile("InstallFlowTest_ExpectedReturnCodes.yaml").GetPath().u8string()); + context.Args.AddArg(Execution::Args::Type::AllowReboot); + + context.Override({ ShellExecuteInstallImpl, [&](TestContext& context) + { + // APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_INSTALL (should be treated as an installer error) + context.Add<Data::OperationReturnCode>(10); + } }); + + SECTION("Reboot success") + { + TestHook::SetInitiateRebootResult_Override initiateRebootResultOverride(true); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(context.IsTerminated()); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::InitiatingReboot).get()) != std::string::npos); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::FailedToInitiateReboot).get()) != std::string::npos); + } + SECTION("Reboot failed") + { + TestHook::SetInitiateRebootResult_Override initiateRebootResultOverride(false); + + InstallCommand install({}); + install.Execute(context); + INFO(installOutput.str()); + + REQUIRE(context.IsTerminated()); + REQUIRE(!std::filesystem::exists(installResultPath.GetPath())); + REQUIRE_FALSE(installOutput.str().find(Resource::LocString(Resource::String::InitiatingReboot).get()) != std::string::npos); + REQUIRE(installOutput.str().find(Resource::LocString(Resource::String::FailedToInitiateReboot).get()) != std::string::npos); + } +} diff --git a/src/AppInstallerCLITests/TestData/InstallFlowTest_ExpectedReturnCodes.yaml b/src/AppInstallerCLITests/TestData/InstallFlowTest_ExpectedReturnCodes.yaml @@ -1,7 +1,7 @@ -PackageIdentifier: AppInstallerCliTest.TestInstaller +PackageIdentifier: AppInstallerCliTest.ExpectedReturnCodes PackageVersion: 1.0.0.0 PackageLocale: en-US -PackageName: AppInstaller Test Installer +PackageName: TestExeInstallerWithExpectedReturnCodes ShortDescription: AppInstaller Test Installer Publisher: Microsoft Corporation Moniker: AICLITestExe diff --git a/src/AppInstallerCLITests/TestData/UpdateFlowTest_ExpectedReturnCodes.yaml b/src/AppInstallerCLITests/TestData/UpdateFlowTest_ExpectedReturnCodes.yaml @@ -0,0 +1,48 @@ +# Same content with InstallFlowTest_ExpectedReturnCodes.yaml but with higher version +PackageIdentifier: AppInstallerCliTest.ExpectedReturnCodes +PackageVersion: 2.0.0.0 +PackageLocale: en-US +PackageName: TestExeInstallerWithExpectedReturnCodes +ShortDescription: AppInstaller Test Installer +Publisher: Microsoft Corporation +Moniker: AICLITestExe +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://ThisIsNotUsed + InstallerType: exe + InstallerSha256: 65DB2F2AC2686C7F2FD69D4A4C6683B888DC55BFA20A0E32CA9F838B51689A3B + ExpectedReturnCodes: + - InstallerReturnCode: 1 + ReturnResponse: packageInUse + - InstallerReturnCode: 2 + ReturnResponse: installInProgress + - InstallerReturnCode: 3 + ReturnResponse: fileInUse + - InstallerReturnCode: 4 + ReturnResponse: missingDependency + - InstallerReturnCode: 5 + ReturnResponse: diskFull + - InstallerReturnCode: 6 + ReturnResponse: insufficientMemory + - InstallerReturnCode: 7 + ReturnResponse: noNetwork + - InstallerReturnCode: 8 + ReturnResponse: contactSupport + ReturnResponseUrl: https://TestReturnResponseUrl + - InstallerReturnCode: 9 + ReturnResponse: rebootRequiredToFinish + - InstallerReturnCode: 10 + ReturnResponse: rebootRequiredForInstall + - InstallerReturnCode: 11 + ReturnResponse: rebootInitiated + - InstallerReturnCode: 12 + ReturnResponse: cancelledByUser + - InstallerReturnCode: 13 + ReturnResponse: alreadyInstalled + - InstallerReturnCode: 14 + ReturnResponse: downgrade + - InstallerReturnCode: 15 + ReturnResponse: blockedByPolicy +ManifestType: singleton +ManifestVersion: 1.2.0 diff --git a/src/AppInstallerCLITests/TestHooks.h b/src/AppInstallerCLITests/TestHooks.h @@ -66,6 +66,11 @@ namespace AppInstaller void TestHook_SetEnableWindowsFeatureResult_Override(std::optional<DWORD>&& result); void TestHook_SetDoesWindowsFeatureExistResult_Override(std::optional<DWORD>&& result); } + + namespace Reboot + { + void TestHook_SetInitiateRebootResult_Override(bool* status); + } } namespace TestHook @@ -156,4 +161,20 @@ namespace TestHook AppInstaller::CLI::Workflow::TestHook_SetDoesWindowsFeatureExistResult_Override({}); } }; + + struct SetInitiateRebootResult_Override + { + SetInitiateRebootResult_Override(bool status) : m_status(status) + { + AppInstaller::Reboot::TestHook_SetInitiateRebootResult_Override(&m_status); + } + + ~SetInitiateRebootResult_Override() + { + AppInstaller::Reboot::TestHook_SetInitiateRebootResult_Override(nullptr); + } + + private: + bool m_status; + }; } \ No newline at end of file diff --git a/src/AppInstallerCLITests/UpdateFlow.cpp b/src/AppInstallerCLITests/UpdateFlow.cpp @@ -7,6 +7,7 @@ #include <Commands/UninstallCommand.h> #include <Commands/UpgradeCommand.h> #include <winget/PathVariable.h> +#include <Workflows/ShellExecuteInstallerHandler.h> using namespace TestCommon; using namespace AppInstaller::CLI; @@ -1008,4 +1009,50 @@ TEST_CASE("UpdateFlow_UpdateMultiple_NotAllFound", "[UpdateFlow][workflow][Multi REQUIRE_TERMINATED_WITH(context, APPINSTALLER_CLI_ERROR_NOT_ALL_QUERIES_FOUND_SINGLE); } -}- \ No newline at end of file +} + +TEST_CASE("UpdateFlow_UpdateWithReboot", "[UpdateFlow][workflow][reboot]") +{ + TestCommon::TestUserSettings testSettings; + testSettings.Set<Setting::EFReboot>(true); + + std::ostringstream updateOutput; + TestContext context{ updateOutput, std::cin }; + auto previousThreadGlobals = context.SetForCurrentThread(); + OverrideForShellExecute(context); + OverrideForCompositeInstalledSource(context, CreateTestSource({ TSR::TestInstaller_Exe_ExpectedReturnCodes })); + + context.Args.AddArg(Execution::Args::Type::Query, TSR::TestInstaller_Exe_ExpectedReturnCodes.Query); + context.Args.AddArg(Execution::Args::Type::AllowReboot); + + context.Override({ AppInstaller::CLI::Workflow::ShellExecuteInstallImpl, [&](TestContext& context) + { + // APPINSTALLER_CLI_ERROR_INSTALL_REBOOT_REQUIRED_TO_FINISH (not treated as an installer error) + context.Add<Data::OperationReturnCode>(9); + } }); + + SECTION("Reboot success") + { + TestHook::SetInitiateRebootResult_Override initiateRebootResultOverride(true); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + REQUIRE_FALSE(context.IsTerminated()); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::InitiatingReboot).get()) != std::string::npos); + REQUIRE_FALSE(updateOutput.str().find(Resource::LocString(Resource::String::FailedToInitiateReboot).get()) != std::string::npos); + } + SECTION("Reboot failed") + { + TestHook::SetInitiateRebootResult_Override initiateRebootResultOverride(false); + + UpgradeCommand update({}); + update.Execute(context); + INFO(updateOutput.str()); + + REQUIRE_FALSE(context.IsTerminated()); + REQUIRE_FALSE(updateOutput.str().find(Resource::LocString(Resource::String::InitiatingReboot).get()) != std::string::npos); + REQUIRE(updateOutput.str().find(Resource::LocString(Resource::String::FailedToInitiateReboot).get()) != std::string::npos); + } +} diff --git a/src/AppInstallerCLITests/WorkflowCommon.cpp b/src/AppInstallerCLITests/WorkflowCommon.cpp @@ -204,6 +204,22 @@ namespace TestCommon PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.TestMSStoreInstaller"))); }); + const TestSourceResult TestInstaller_Exe_ExpectedReturnCodes( + "AppInstallerCliTest.ExpectedReturnCodes"sv, + [](std::vector<ResultMatch>& matches, std::weak_ptr<const ISource> source) { + auto manifest = YamlParser::CreateFromPath(TestDataFile("InstallFlowTest_ExpectedReturnCodes.yaml")); + auto manifest2 = YamlParser::CreateFromPath(TestDataFile("UpdateFlowTest_ExpectedReturnCodes.yaml")); + matches.emplace_back( + ResultMatch( + TestPackage::Make( + manifest, + TestPackage::MetadataMap{ { PackageVersionMetadata::InstalledType, "Exe" } }, + std::vector<Manifest>{ manifest2, manifest }, + source + ), + PackageMatchFilter(PackageMatchField::Id, MatchType::Exact, "AppInstallerCliTest.ExpectedReturnCodes"))); + }); + const TestSourceResult TestInstaller_Exe_UnknownVersion( "TestExeInstallerWithUnknownVersion"sv, [](std::vector<ResultMatch>& matches, std::weak_ptr<const ISource> source) { diff --git a/src/AppInstallerCLITests/WorkflowCommon.h b/src/AppInstallerCLITests/WorkflowCommon.h @@ -35,6 +35,7 @@ namespace TestCommon const extern TestSourceResult TestInstaller_Exe; const extern TestSourceResult TestInstaller_Exe_Dependencies; const extern TestSourceResult TestInstaller_Exe_DifferentInstallerType; + const extern TestSourceResult TestInstaller_Exe_ExpectedReturnCodes; const extern TestSourceResult TestInstaller_Exe_IncompatibleInstallerType; const extern TestSourceResult TestInstaller_Exe_LatestInstalled; const extern TestSourceResult TestInstaller_Exe_LicenseAgreement; diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj @@ -427,6 +427,7 @@ <ClInclude Include="Public\winget\Filesystem.h" /> <ClInclude Include="Public\winget\PackageDependenciesValidationUtil.h" /> <ClInclude Include="Public\winget\Pin.h" /> + <ClInclude Include="Public\winget\Reboot.h" /> <ClInclude Include="Public\winget\Regex.h" /> <ClInclude Include="Public\winget\PathVariable.h" /> <ClInclude Include="Public\winget\PortableARPEntry.h" /> @@ -483,6 +484,7 @@ <ClCompile Include="PackageDependenciesValidationUtil.cpp" /> <ClCompile Include="Pin.cpp" /> <ClCompile Include="Progress.cpp" /> + <ClCompile Include="Reboot.cpp" /> <ClCompile Include="Regex.cpp" /> <ClCompile Include="Runtime.cpp" /> <ClCompile Include="pch.cpp"> diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters @@ -177,6 +177,9 @@ <ClInclude Include="Public\winget\MSStore.h"> <Filter>Public\winget</Filter> </ClInclude> + <ClInclude Include="Public\winget\Reboot.h"> + <Filter>Public\winget</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -317,6 +320,9 @@ <ClCompile Include="MSStore.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Reboot.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp @@ -44,6 +44,8 @@ namespace AppInstaller::Settings return userSettings.Get<Setting::EFWindowsFeature>(); case ExperimentalFeature::Feature::Resume: return userSettings.Get<Setting::EFResume>(); + case ExperimentalFeature::Feature::Reboot: + return userSettings.Get<Setting::EFReboot>(); default: THROW_HR(E_UNEXPECTED); } @@ -77,6 +79,8 @@ namespace AppInstaller::Settings 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 }; + case Feature::Reboot: + return ExperimentalFeature{ "Reboot", "reboot", "https://aka.ms/winget-settings", Feature::Reboot }; default: THROW_HR(E_UNEXPECTED); } diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h @@ -25,6 +25,7 @@ namespace AppInstaller::Settings DirectMSI = 0x1, WindowsFeature = 0x2, Resume = 0x4, + Reboot = 0x8, 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/Reboot.h b/src/AppInstallerCommonCore/Public/winget/Reboot.h @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once + +namespace AppInstaller::Reboot +{ + bool InitiateReboot(); +}+ \ No newline at end of file diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h @@ -72,6 +72,7 @@ namespace AppInstaller::Settings EFDirectMSI, EFWindowsFeature, EFResume, + EFReboot, // Telemetry TelemetryDisable, // Install behavior @@ -149,6 +150,7 @@ namespace AppInstaller::Settings 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); + SETTINGMAPPING_SPECIALIZATION(Setting::EFReboot, bool, bool, false, ".experimentalFeatures.reboot"sv); // Telemetry SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv); // Install behavior diff --git a/src/AppInstallerCommonCore/Reboot.cpp b/src/AppInstallerCommonCore/Reboot.cpp @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "AppInstallerLogging.h" +#include "Public/winget/Reboot.h" +#include <Windows.h> + +namespace AppInstaller::Reboot +{ +#ifndef AICLI_DISABLE_TEST_HOOKS + static bool* s_InitiateRebootResult_TestHook_Override = nullptr; + + void TestHook_SetInitiateRebootResult_Override(bool* status) + { + s_InitiateRebootResult_TestHook_Override = status; + } +#endif + + bool InitiateReboot() + { +#ifndef AICLI_DISABLE_TEST_HOOKS + if (s_InitiateRebootResult_TestHook_Override) + { + return *s_InitiateRebootResult_TestHook_Override; + } +#endif + + wil::unique_handle hToken; + TOKEN_PRIVILEGES pTokenPrivileges; + + // Get a token for this process. + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) + { + AICLI_LOG(Core, Error, << "OpenProcessToken error: " << GetLastError()); + return false; + } + + // Shutdown privilege must be enabled for this process. + if (!LookupPrivilegeValueW(NULL, SE_SHUTDOWN_NAME, &pTokenPrivileges.Privileges[0].Luid)) + { + AICLI_LOG(Core, Error, << "LookupPrivilegeValue error: " << GetLastError()); + return false; + } + + pTokenPrivileges.PrivilegeCount = 1; + pTokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(hToken.get(), FALSE, &pTokenPrivileges, 0, (PTOKEN_PRIVILEGES)NULL, 0)) + { + AICLI_LOG(Core, Error, << "AdjustTokenPrivilege error: " << GetLastError()); + return false; + } + + AICLI_LOG(Core, Info, << "Initiating reboot."); + return ExitWindowsEx(EWX_RESTARTAPPS, SHTDN_REASON_MINOR_INSTALLATION); + } +} diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp @@ -261,6 +261,7 @@ namespace AppInstaller::Settings WINGET_VALIDATE_PASS_THROUGH(EFDirectMSI) WINGET_VALIDATE_PASS_THROUGH(EFWindowsFeature) WINGET_VALIDATE_PASS_THROUGH(EFResume) + WINGET_VALIDATE_PASS_THROUGH(EFReboot) WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay) WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable) WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable)