From 2863cd18d670e400b5a0f29657ea611cd905a580 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Sat, 9 Mar 2019 11:42:48 +0000 Subject: [PATCH] (GH-1038) Stop operation on package reboot request There are now parameters and a feature flag which will allow halting/overriding the install/upgrade/uninstall of a package, when a reboot request is returned from one of it's dependencies. When this occurs, an ApplicationException will be thrown, along with a specific exit code, either 350 (pending reboot discovered prior to running), or 1604 (some work completed prior to restart request) will be returned. This could then be inspected to decide when a reboot should actually be performed, before continuing with the remainder of the installation. --- src/chocolatey.console/Program.cs | 2 +- .../ApplicationParameters.cs | 9 +- .../builders/ConfigurationBuilder.cs | 1 + .../commands/ChocolateyInstallCommand.cs | 15 ++ .../commands/ChocolateyUninstallCommand.cs | 15 ++ .../commands/ChocolateyUpgradeCommand.cs | 15 ++ .../configuration/ChocolateyConfiguration.cs | 1 + .../services/ChocolateyPackageService.cs | 152 ++++++++++++------ 8 files changed, 161 insertions(+), 49 deletions(-) diff --git a/src/chocolatey.console/Program.cs b/src/chocolatey.console/Program.cs index 3564241129..b1f715155b 100644 --- a/src/chocolatey.console/Program.cs +++ b/src/chocolatey.console/Program.cs @@ -158,7 +158,7 @@ private static void Main(string[] args) "chocolatey".Log().Error(ChocolateyLoggers.Important, () => "{0}".format_with(ex.Message)); } - Environment.ExitCode = 1; + if (Environment.ExitCode == 0) Environment.ExitCode = 1; } finally { diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 12d7843530..ee57e70a15 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -1,6 +1,6 @@ // Copyright © 2017 - 2018 Chocolatey Software, Inc // Copyright © 2011 - 2017 RealDimensions Software, LLC -// +// // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // @@ -127,6 +127,12 @@ public static class Environment /// public static readonly bool AllowPrompts = true; + public static class ExitCodes + { + public static readonly int ErrorFailNoActionReboot = 350; + public static readonly int ErrorInstallSuspend = 1604; + } + public static class Tools { //public static readonly string WebPiCmdExe = _fileSystem.combine_paths(InstallLocation, "nuget.exe"); @@ -170,6 +176,7 @@ public static class Features public static readonly string IgnoreUnfoundPackagesOnUpgradeOutdated = "ignoreUnfoundPackagesOnUpgradeOutdated"; public static readonly string RemovePackageInformationOnUninstall = "removePackageInformationOnUninstall"; public static readonly string LogWithoutColor = "logWithoutColor"; + public static readonly string ExitOnRebootDetected = "exitOnRebootDetected"; } public static class Messages diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 541467de53..1b371a55c0 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -303,6 +303,7 @@ private static void set_feature_flags(ChocolateyConfiguration config, ConfigFile config.Features.ScriptsCheckLastExitCode = set_feature_flag(ApplicationParameters.Features.ScriptsCheckLastExitCode, configFileSettings, defaultEnabled: false, description: "Scripts Check $LastExitCode (external commands) - Leave this off unless you absolutely need it while you fix your package scripts to use `throw 'error message'` or `Set-PowerShellExitCode #` instead of `exit #`. This behavior started in 0.9.10 and produced hard to find bugs. If the last external process exits successfully but with an exit code of not zero, this could cause hard to detect package failures. Available in 0.10.3+. Will be removed in 0.11.0."); config.PromptForConfirmation = !set_feature_flag(ApplicationParameters.Features.AllowGlobalConfirmation, configFileSettings, defaultEnabled: false, description: "Prompt for confirmation in scripts or bypass."); + config.Features.ExitOnRebootDetected = set_feature_flag(ApplicationParameters.Features.ExitOnRebootDetected, configFileSettings, defaultEnabled: false, description: "Exit On Reboot Detected - Stop running install, upgrade, or uninstall when a reboot request is detected. Requires '{0}' feature to be turned on. Will exit with either {1} or {2}. When it exits with {1}, it means pending reboot discovered prior to running operation. When it exits with {2}, it means some work completed prior to reboot request being detected. As this will affect upgrade all, it is normally recommended to leave this off. Available in 0.10.12+.".format_with(ApplicationParameters.Features.ExitOnRebootDetected, ApplicationParameters.ExitCodes.ErrorFailNoActionReboot, ApplicationParameters.ExitCodes.ErrorInstallSuspend)); } private static bool set_feature_flag(string featureName, ConfigFileSettings configFileSettings, bool defaultEnabled, string description) diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyInstallCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyInstallCommand.cs index 35ca0388c4..e1a8d5d5fb 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyInstallCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyInstallCommand.cs @@ -155,6 +155,21 @@ public virtual void configure_argument_parser(OptionSet optionSet, ChocolateyCon "Stop On First Package Failure - stop running install, upgrade or uninstall on first package failure instead of continuing with others. Overrides the default feature '{0}' set to '{1}'. Available in 0.10.4+.".format_with(ApplicationParameters.Features.StopOnFirstPackageFailure, configuration.Features.StopOnFirstPackageFailure.to_string()), option => configuration.Features.StopOnFirstPackageFailure = option != null ) + .Add("exitwhenrebootdetected|exit-when-reboot-detected", + "Exit When Reboot Detected - Stop running install, upgrade, or uninstall when a reboot request is detected. Requires '{0}' feature to be turned on. Will exit with either {1} or {2}. Overrides the default feature '{3}' set to '{4}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.UsePackageExitCodes, ApplicationParameters.ExitCodes.ErrorFailNoActionReboot, ApplicationParameters.ExitCodes.ErrorInstallSuspend, ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => configuration.Features.ExitOnRebootDetected = option != null + ) + .Add("ignoredetectedreboot|ignore-detected-reboot", + "Ignore Detected Reboot - Ignore any detected reboots if found. Overrides the default feature '{0}' set to '{1}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => + { + if (option != null) + { + configuration.Features.ExitOnRebootDetected = false; + } + }) ; //todo: package name can be a url / installertype diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyUninstallCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyUninstallCommand.cs index 3070a7f358..75a0dacd3d 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyUninstallCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyUninstallCommand.cs @@ -118,6 +118,21 @@ public virtual void configure_argument_parser(OptionSet optionSet, ChocolateyCon "Stop On First Package Failure - stop running install, upgrade or uninstall on first package failure instead of continuing with others. Overrides the default feature '{0}' set to '{1}'. Available in 0.10.4+.".format_with(ApplicationParameters.Features.StopOnFirstPackageFailure, configuration.Features.StopOnFirstPackageFailure.to_string()), option => configuration.Features.StopOnFirstPackageFailure = option != null ) + .Add("exitwhenrebootdetected|exit-when-reboot-detected", + "Exit When Reboot Detected - Stop running install, upgrade, or uninstall when a reboot request is detected. Requires '{0}' feature to be turned on. Will exit with either {1} or {2}. Overrides the default feature '{3}' set to '{4}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.UsePackageExitCodes, ApplicationParameters.ExitCodes.ErrorFailNoActionReboot, ApplicationParameters.ExitCodes.ErrorInstallSuspend, ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => configuration.Features.ExitOnRebootDetected = option != null + ) + .Add("ignoredetectedreboot|ignore-detected-reboot", + "Ignore Detected Reboot - Ignore any detected reboots if found. Overrides the default feature '{0}' set to '{1}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => + { + if (option != null) + { + configuration.Features.ExitOnRebootDetected = false; + } + }) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyUpgradeCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyUpgradeCommand.cs index 655d857f8f..b11727c5c5 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyUpgradeCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyUpgradeCommand.cs @@ -182,6 +182,21 @@ public virtual void configure_argument_parser(OptionSet optionSet, ChocolateyCon { if (option != null) configuration.Features.UseRememberedArgumentsForUpgrades = false; }) + .Add("exitwhenrebootdetected|exit-when-reboot-detected", + "Exit When Reboot Detected - Stop running install, upgrade, or uninstall when a reboot request is detected. Requires '{0}' feature to be turned on. Will exit with either {1} or {2}. Overrides the default feature '{3}' set to '{4}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.UsePackageExitCodes, ApplicationParameters.ExitCodes.ErrorFailNoActionReboot, ApplicationParameters.ExitCodes.ErrorInstallSuspend, ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => configuration.Features.ExitOnRebootDetected = option != null + ) + .Add("ignoredetectedreboot|ignore-detected-reboot", + "Ignore Detected Reboot - Ignore any detected reboots if found. Overrides the default feature '{0}' set to '{1}'. Available in 0.10.12+.".format_with + (ApplicationParameters.Features.ExitOnRebootDetected, configuration.Features.ExitOnRebootDetected.to_string()), + option => + { + if (option != null) + { + configuration.Features.ExitOnRebootDetected = false; + } + }) ; } diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 7b862f9539..939830ecdb 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -390,6 +390,7 @@ public sealed class FeaturesConfiguration public bool UseRememberedArgumentsForUpgrades { get; set; } public bool IgnoreUnfoundPackagesOnUpgradeOutdated { get; set; } public bool RemovePackageInformationOnUninstall { get; set; } + public bool ExitOnRebootDetected { get; set; } //todo remove in 0.11.0 public bool ScriptsCheckLastExitCode { get; set; } diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index c86313c3ea..2d9515031e 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -98,6 +98,11 @@ Did you know Pro / Business automatically syncs with Programs and private readonly string _shutdownExe = Environment.ExpandEnvironmentVariables("%systemroot%\\System32\\shutdown.exe"); + // Hold a list of exit codes that are known to be related to reboots + // 1641 - restart initiated + // 3010 - restart required + private readonly List _rebootExitCodes = new List { 1641, 3010 }; + public ChocolateyPackageService(INugetService nugetService, IPowershellService powershellService, IEnumerable sourceRunners, IShimGenerationService shimgenService, IFileSystem fileSystem, IRegistryService registryService, @@ -420,6 +425,18 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu remove_pending(packageResult, config); + if(_rebootExitCodes.Contains(packageResult.ExitCode)) + { + if(config.Features.ExitOnRebootDetected) + { + Environment.ExitCode = ApplicationParameters.ExitCodes.ErrorInstallSuspend; + this.Log().Warn(ChocolateyLoggers.Important, @"Chocolatey has detected a pending reboot after installing/upgrading +package '{0}' - stopping further execution".format_with(packageResult.Name)); + + throw new ApplicationException("Reboot required before continuing. Reboot and run same command again."); + } + } + if (!packageResult.Success) { this.Log().Error(ChocolateyLoggers.Important, "The {0} of {1} was NOT successful.".format_with(commandName.to_string(), packageResult.Name)); @@ -560,29 +577,34 @@ public ConcurrentDictionary install_run(ChocolateyConfigu get_environment_before(config, allowLogging: true); - foreach (var packageConfig in set_config_from_package_names_and_packages_config(config, packageInstalls).or_empty_list_if_null()) + try { - Action action = null; - if (packageConfig.SourceType == SourceType.normal) + foreach (var packageConfig in set_config_from_package_names_and_packages_config(config, packageInstalls).or_empty_list_if_null()) { - action = (packageResult) => handle_package_result(packageResult, packageConfig, CommandNameType.install); - } + Action action = null; + if (packageConfig.SourceType == SourceType.normal) + { + action = (packageResult) => handle_package_result(packageResult, packageConfig, CommandNameType.install); + } - var results = perform_source_runner_function(packageConfig, r => r.install_run(packageConfig, action)); + var results = perform_source_runner_function(packageConfig, r => r.install_run(packageConfig, action)); - foreach (var result in results) - { - packageInstalls.GetOrAdd(result.Key, result.Value); + foreach (var result in results) + { + packageInstalls.GetOrAdd(result.Key, result.Value); + } } } - - var installFailures = report_action_summary(packageInstalls, "installed"); - if (installFailures != 0 && Environment.ExitCode == 0) + finally { - Environment.ExitCode = 1; - } + var installFailures = report_action_summary(packageInstalls, "installed"); + if (installFailures != 0 && Environment.ExitCode == 0) + { + Environment.ExitCode = 1; + } - randomly_notify_about_pro_business(config); + randomly_notify_about_pro_business(config); + } return packageInstalls; } @@ -740,24 +762,36 @@ public ConcurrentDictionary upgrade_run(ChocolateyConfigu throw new ApplicationException("A packages.config file is only used with installs."); } - Action action = null; - if (config.SourceType == SourceType.normal) + var packageUpgrades = new ConcurrentDictionary(); + + try { - action = (packageResult) => handle_package_result(packageResult, config, CommandNameType.upgrade); - } + Action action = null; + if (config.SourceType == SourceType.normal) + { + action = (packageResult) => handle_package_result(packageResult, config, CommandNameType.upgrade); + } - get_environment_before(config, allowLogging: true); + get_environment_before(config, allowLogging: true); - var beforeUpgradeAction = new Action(packageResult => before_package_modify(packageResult, config)); - var packageUpgrades = perform_source_runner_function(config, r => r.upgrade_run(config, action, beforeUpgradeAction)); + var beforeUpgradeAction = new Action(packageResult => before_package_modify(packageResult, config)); + var results = perform_source_runner_function(config, r => r.upgrade_run(config, action, beforeUpgradeAction)); - var upgradeFailures = report_action_summary(packageUpgrades, "upgraded"); - if (upgradeFailures != 0 && Environment.ExitCode == 0) - { - Environment.ExitCode = 1; + foreach (var result in results) + { + packageUpgrades.GetOrAdd(result.Key, result.Value); + } } + finally + { + var upgradeFailures = report_action_summary(packageUpgrades, "upgraded"); + if (upgradeFailures != 0 && Environment.ExitCode == 0) + { + Environment.ExitCode = 1; + } - randomly_notify_about_pro_business(config); + randomly_notify_about_pro_business(config); + } return packageUpgrades; } @@ -796,30 +830,41 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi throw new ApplicationException("A packages.config file is only used with installs."); } - Action action = null; - if (config.SourceType == SourceType.normal) + var packageUninstalls = new ConcurrentDictionary(); + + try { - action = (packageResult) => handle_package_uninstall(packageResult, config); - } + Action action = null; + if (config.SourceType == SourceType.normal) + { + action = (packageResult) => handle_package_uninstall(packageResult, config); + } - var environmentBefore = get_environment_before(config); - var beforeUninstallAction = new Action(packageResult => before_package_modify(packageResult, config)); - var packageUninstalls = perform_source_runner_function(config, r => r.uninstall_run(config, action, beforeUninstallAction)); + var environmentBefore = get_environment_before(config); + var beforeUninstallAction = new Action(packageResult => before_package_modify(packageResult, config)); + var results = perform_source_runner_function(config, r => r.uninstall_run(config, action, beforeUninstallAction)); - // not handled in the uninstall handler - IEnumerable environmentChanges; - IEnumerable environmentRemovals; - get_log_environment_changes(config, environmentBefore, out environmentChanges, out environmentRemovals); + foreach (var result in results) + { + packageUninstalls.GetOrAdd(result.Key, result.Value); + } - var uninstallFailures = report_action_summary(packageUninstalls, "uninstalled"); - if (uninstallFailures != 0 && Environment.ExitCode == 0) - { - Environment.ExitCode = 1; + // not handled in the uninstall handler + IEnumerable environmentChanges; + IEnumerable environmentRemovals; + get_log_environment_changes(config, environmentBefore, out environmentChanges, out environmentRemovals); } - - if (uninstallFailures != 0) + finally { - this.Log().Warn(@" + var uninstallFailures = report_action_summary(packageUninstalls, "uninstalled"); + if (uninstallFailures != 0 && Environment.ExitCode == 0) + { + Environment.ExitCode = 1; + } + + if (uninstallFailures != 0) + { + this.Log().Warn(@" If a package uninstall is failing and/or you've already uninstalled the software outside of Chocolatey, you can attempt to run the command with `-n` to skip running a chocolateyUninstall script, additionally @@ -835,9 +880,10 @@ If a package is failing because it is a dependency of another package removed. Then delete the folder for the package. This option should only be used as a last resort. "); - } + } - randomly_notify_about_pro_business(config); + randomly_notify_about_pro_business(config); + } return packageUninstalls; } @@ -946,6 +992,18 @@ public void handle_package_uninstall(PackageResult packageResult, ChocolateyConf handle_unsuccessful_operation(config, packageResult, movePackageToFailureLocation: false, attemptRollback: false); } + if(_rebootExitCodes.Contains(packageResult.ExitCode)) + { + if(config.Features.ExitOnRebootDetected) + { + Environment.ExitCode = ApplicationParameters.ExitCodes.ErrorInstallSuspend; + this.Log().Warn(ChocolateyLoggers.Important, @"Chocolatey has detected a pending reboot after uninstalling +package '{0}' - stopping further execution".format_with(packageResult.Name)); + + throw new ApplicationException("Reboot required before continuing. Reboot and run same command again."); + } + } + if (!packageResult.Success) { // throw an error so that NuGet Service doesn't attempt to continue with package removal