From 2349488261389c17379e903b330cb80397532372 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Wed, 6 Mar 2019 12:39:29 +0000 Subject: [PATCH] (GH-1038) Add ability to stop operation on reboot 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. The initial implementation of this ability followed closely to how the stoponfirstfailure parameter and feature flag are implemented, and then subsequent changes added to the implementation. --- src/chocolatey.console/Program.cs | 2 +- src/chocolatey/chocolatey.csproj | 7 + .../ApplicationParameters.cs | 9 +- .../builders/ConfigurationBuilder.cs | 1 + .../commands/ChocolateyInstallCommand.cs | 15 ++ .../commands/ChocolateyUninstallCommand.cs | 15 ++ .../commands/ChocolateyUpgradeCommand.cs | 15 ++ .../configuration/ChocolateyConfiguration.cs | 1 + .../registration/ContainerBinding.cs | 16 ++ .../runners/ConsoleApplication.cs | 63 +++++- .../services/ChocolateyPackageService.cs | 140 +++++++++----- .../services/IPendingRebootService.cs | 31 +++ .../services/PendingRebootService.cs | 180 ++++++++++++++++++ .../GlobalConfigurationValidation.cs | 81 ++++++++ .../validations/SystemStateValidation.cs | 107 +++++++++++ .../infrastructure/validations/IValidation.cs | 34 ++++ .../validations/ValidationResult.cs | 30 +++ .../validations/ValidationStatus.cs | 25 +++ 18 files changed, 722 insertions(+), 50 deletions(-) create mode 100644 src/chocolatey/infrastructure.app/services/IPendingRebootService.cs create mode 100644 src/chocolatey/infrastructure.app/services/PendingRebootService.cs create mode 100644 src/chocolatey/infrastructure.app/validations/GlobalConfigurationValidation.cs create mode 100644 src/chocolatey/infrastructure.app/validations/SystemStateValidation.cs create mode 100644 src/chocolatey/infrastructure/validations/IValidation.cs create mode 100644 src/chocolatey/infrastructure/validations/ValidationResult.cs create mode 100644 src/chocolatey/infrastructure/validations/ValidationStatus.cs 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/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 7eb772368a..97dd3a9447 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -102,6 +102,7 @@ + @@ -120,10 +121,13 @@ + + + @@ -317,6 +321,9 @@ + + + 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..f3f191dfb0 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 - If a reboot request is detected during a Chocolatey operation, then ignore it. 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..11e5e78331 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 - If a reboot request is detected during a Chocolatey operation, then ignore it. 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..67258b6d88 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 - If a reboot request is detected during a Chocolatey operation, then ignore it. 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/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index f8983fb834..67ffc551b5 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -27,9 +27,11 @@ namespace chocolatey.infrastructure.app.registration using infrastructure.commands; using infrastructure.configuration; using infrastructure.services; + using infrastructure.validations; using nuget; using services; using tasks; + using validations; using CryptoHashProvider = cryptography.CryptoHashProvider; using IFileSystem = filesystem.IFileSystem; using IHashProvider = cryptography.IHashProvider; @@ -61,6 +63,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(() => new CryptoHashProvider(container.GetInstance()), Lifestyle.Singleton); @@ -126,6 +129,19 @@ public void RegisterComponents(Container container) return list.AsReadOnly(); }, Lifestyle.Singleton); + + container.Register>( + () => + { + var list = new List + { + new GlobalConfigurationValidation(), + new SystemStateValidation(container.GetInstance()) + }; + + return list.AsReadOnly(); + }, + Lifestyle.Singleton); } } diff --git a/src/chocolatey/infrastructure.app/runners/ConsoleApplication.cs b/src/chocolatey/infrastructure.app/runners/ConsoleApplication.cs index 591dc6326b..e361a23933 100644 --- a/src/chocolatey/infrastructure.app/runners/ConsoleApplication.cs +++ b/src/chocolatey/infrastructure.app/runners/ConsoleApplication.cs @@ -18,10 +18,13 @@ namespace chocolatey.infrastructure.app.runners { using System; using System.Collections.Generic; + using System.Linq; using SimpleInjector; using configuration; + using infrastructure.validations; using logging; using utility; + using validations; /// /// Console application responsible for running chocolatey @@ -85,9 +88,67 @@ public void run(string[] args, ChocolateyConfiguration config, Container contain } } }, - () => command.handle_validation(config), + () => { + command.handle_validation(config); + + var validationResults = new List(); + var validationChecks = container.GetAllInstances(); + + foreach (var validationCheck in validationChecks) + { + validationResults.AddRange(validationCheck.validate(config)); + } + + var validationErrors = report_validation_summary(validationResults, config); + + if (validationErrors != 0) + { + // NOTE: This is intentionally left blank, as the reason for throwing is + // documented in the report_validation_summary above, and a duplication + // is not required in the exception. + throw new ApplicationException(""); + } + }, () => command.help_message(config)); }); } + + private int report_validation_summary(IList validationResults, ChocolateyConfiguration config) + { + var successes = validationResults.Count(v => v.Status == ValidationStatus.Success); + var warnings = validationResults.Count(v => v.Status == ValidationStatus.Warning); + var errors = validationResults.Count(v => v.Status == ValidationStatus.Error); + + if (config.RegularOutput) + { + this.Log().Info(errors + warnings == 0 ? ChocolateyLoggers.LogFileOnly : ChocolateyLoggers.Important, () => "{0} validations performed. {1} success(es), {2} warning(s), and {3} error(s).".format_with( + validationResults.Count, + successes, + warnings, + errors)); + + if (warnings != 0) + { + this.Log().Info(""); + this.Log().Warn("Validation Warnings:"); + foreach (var warning in validationResults.Where(p => p.Status == ValidationStatus.Warning).or_empty_list_if_null()) + { + this.Log().Warn(" - {0}.".format_with(warning.Message)); + } + } + + if (errors != 0) + { + this.Log().Info(""); + this.Log().Error("Validation Errors:"); + foreach (var error in validationResults.Where(p => p.Status == ValidationStatus.Error).or_empty_list_if_null()) + { + this.Log().Error(" - {0} (exit code {1}).".format_with(error.Message, error.ExitCode)); + } + } + } + + return errors; + } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index c86313c3ea..bc3fd5136e 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -420,6 +420,23 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu remove_pending(packageResult, config); + // Test to see if exit code is either: + // 1641 - restart initiated + // 3010 - restart required + var rebootExitCodes = new List { 1641, 3010 }; + + 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; } diff --git a/src/chocolatey/infrastructure.app/services/IPendingRebootService.cs b/src/chocolatey/infrastructure.app/services/IPendingRebootService.cs new file mode 100644 index 0000000000..f5cb4eeab2 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/IPendingRebootService.cs @@ -0,0 +1,31 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.services +{ + using configuration; + + /// + /// Test to see if there are any known situations that require + /// a System reboot. + /// + /// The current Chocolatey Configuration + /// true if reboot is required; otherwise false. + public interface IPendingRebootService + { + bool is_pending_reboot(ChocolateyConfiguration config); + } +} diff --git a/src/chocolatey/infrastructure.app/services/PendingRebootService.cs b/src/chocolatey/infrastructure.app/services/PendingRebootService.cs new file mode 100644 index 0000000000..96cbfe3ae9 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/PendingRebootService.cs @@ -0,0 +1,180 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.services +{ + using configuration; + using Microsoft.Win32; + using platforms; + + /// + /// Service to check for System level pending reboot request + /// + /// Based on code from https://github.com/puppetlabs/puppetlabs-reboot + public class PendingRebootService : IPendingRebootService + { + private readonly IRegistryService _registryService; + + public PendingRebootService(IRegistryService registryService) + { + _registryService = registryService; + } + + /// + /// Test to see if there are any known situations that require + /// a System reboot. + /// + /// The current Chocolatey Configuration + /// true if reboot is required; otherwise false. + public bool is_pending_reboot(ChocolateyConfiguration config) + { + if (config.Information.PlatformType != PlatformType.Windows) + { + return false; + } + + this.Log().Debug("Performing reboot requirement checks..."); + + return is_pending_computer_rename() || + is_pending_component_based_servicing() || + is_pending_windows_auto_update() || + is_pending_file_rename_operations() || + is_pending_package_installer() || + is_pending_package_installer_syswow64(); + } + + private bool is_pending_computer_rename() + { + var path = "SYSTEM\\CurrentControlSet\\Control\\ComputerName\\{0}"; + var activeName = get_registry_key_string_value(path.format_with("ActiveComputerName"), "ComputerName"); + var pendingName = get_registry_key_string_value(path.format_with("ComputerName"), "ComputerName"); + + bool result = !string.IsNullOrWhiteSpace(activeName) && + !string.IsNullOrWhiteSpace(pendingName) && + activeName != pendingName; + + this.Log().Debug("PendingComputerRename: {0}".format_with(result)); + + return result; + } + + private bool is_pending_component_based_servicing() + { + if (!is_vista_sp1_or_later()) + { + this.Log().Debug("Not using Windows Vista SP1 or earlier, so no check for Component Based Servicing can be made."); + return false; + } + + var path = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending"; + var key = _registryService.get_key(RegistryHive.LocalMachine, path); + var result = key != null; + + this.Log().Debug("PendingComponentBasedServicing: {0}".format_with(result)); + + return result; + } + + private bool is_pending_windows_auto_update() + { + var path = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired"; + var key = _registryService.get_key(RegistryHive.LocalMachine, path); + var result = key != null; + + this.Log().Debug("PendingWindowsAutoUpdate: {0}".format_with(result)); + + return result; + } + + private bool is_pending_file_rename_operations() + { + var path = "SYSTEM\\CurrentControlSet\\Control\\Session Manager"; + var value = get_registry_key_value(path, "PendingFileRenameOperations"); + + var result = false; + + if (value != null && value is string[]) + { + result = (value as string[]).Length != 0; + } + + this.Log().Debug("PendingFileRenameOperations: {0}".format_with(result)); + + return result; + } + + private bool is_pending_package_installer() + { + // http://support.microsoft.com/kb/832475 + // 0x00000000 (0) No pending restart. + var path = "SOFTWARE\\Microsoft\\Updates"; + var value = get_registry_key_string_value(path, "UpdateExeVolatile"); + + var result = !string.IsNullOrWhiteSpace(value) && value != "0"; + + this.Log().Debug("PendingPackageInstaller: {0}".format_with(result)); + + return result; + } + + private bool is_pending_package_installer_syswow64() + { + // http://support.microsoft.com/kb/832475 + // 0x00000000 (0) No pending restart. + var path = "SOFTWARE\\Wow6432Node\\Microsoft\\Updates"; + var value = get_registry_key_string_value(path, "UpdateExeVolatile"); + + var result = !string.IsNullOrWhiteSpace(value) && value != "0"; + + this.Log().Debug("PendingPackageInstallerSysWow64: {0}".format_with(result)); + + return result; + } + + private string get_registry_key_string_value(string path, string value) + { + var key = _registryService.get_key(RegistryHive.LocalMachine, path); + + if (key == null) + { + return string.Empty; + } + + return key.GetValue(value, string.Empty).to_string(); + } + + private object get_registry_key_value(string path, string value) + { + var key = _registryService.get_key(RegistryHive.LocalMachine, path); + + if (key == null) + { + return null; + } + + return key.GetValue(value, null); + } + + private bool is_vista_sp1_or_later() + { + var versionNumber = Platform.get_version(); + + this.Log().Debug("Operating System Version Number: {0}".format_with(versionNumber)); + + return versionNumber.Build >= 6001; + } + } +} diff --git a/src/chocolatey/infrastructure.app/validations/GlobalConfigurationValidation.cs b/src/chocolatey/infrastructure.app/validations/GlobalConfigurationValidation.cs new file mode 100644 index 0000000000..9f940e7cf6 --- /dev/null +++ b/src/chocolatey/infrastructure.app/validations/GlobalConfigurationValidation.cs @@ -0,0 +1,81 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.validations +{ + using System.Collections.Generic; + using configuration; + using infrastructure.validations; + + /// + /// Performs top level validation checks against the current + /// Chocolatey Configuration object to ensure that everything is + /// set up properly. Any errors that are returned will halt + /// the current operation. + /// + public class GlobalConfigurationValidation : IValidation + { + public ICollection validate(ChocolateyConfiguration config) + { + var validationResults = new List(); + + check_usage_of_package_exit_code(config, validationResults); + + if (validationResults.Count == 0) + { + validationResults.Add(new ValidationResult + { + Message = "Global Chocolatey Configuration is valid", + Status = ValidationStatus.Success, + ExitCode = 0 + }); + } + + return validationResults; + } + + private void check_usage_of_package_exit_code(ChocolateyConfiguration config, ICollection validationResults) + { + // In order for a Chocolatey execution to correctly halt + // on a detected reboot, it is necessary for package exit + // codes to be used. Otherwise, the codes (1641, and 3010) + // can't be detected. + if (config.Features.ExitOnRebootDetected && + !config.Features.UsePackageExitCodes) + { + var validationResult = new ValidationResult + { + Message = @"When attempting to halt execution of a Chocolatey command based on a + request for a system reboot, it is necessary to have the + usePackageExitCodes feature enabled. Currently, this is disabled. + This can be re-enabled using the command: + choco feature enable -name={0} +".format_with(ApplicationParameters.Features.UsePackageExitCodes), + Status = ValidationStatus.Error, + ExitCode = 1 + }; + + var commandsToErrorOn = new List {"install", "uninstall", "upgrade"}; + if (!commandsToErrorOn.Contains(config.CommandName.ToLowerInvariant())) + { + validationResult.Status = ValidationStatus.Warning; + } + + validationResults.Add(validationResult); + } + } + } +} diff --git a/src/chocolatey/infrastructure.app/validations/SystemStateValidation.cs b/src/chocolatey/infrastructure.app/validations/SystemStateValidation.cs new file mode 100644 index 0000000000..608dc6e289 --- /dev/null +++ b/src/chocolatey/infrastructure.app/validations/SystemStateValidation.cs @@ -0,0 +1,107 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.validations +{ + using System; + using System.Collections.Generic; + using configuration; + using infrastructure.validations; + using services; + + /// + /// Performs validation against the current System State. This + /// includes things like pending reboot requirement. Any errors + /// that are returned will halt the current operation. + /// + public class SystemStateValidation : IValidation + { + private readonly IPendingRebootService _pendingRebootService; + + public SystemStateValidation(IPendingRebootService pendingRebootService) + { + _pendingRebootService = pendingRebootService; + } + + public ICollection validate(ChocolateyConfiguration config) + { + var validationResults = new List(); + + check_system_pending_reboot(config, validationResults); + + if (validationResults.Count == 0) + { + validationResults.Add(new ValidationResult + { + Message = "System State is valid", + Status = ValidationStatus.Success, + ExitCode = 0 + }); + } + + return validationResults; + } + + private void check_system_pending_reboot(ChocolateyConfiguration config, ICollection validationResults) + { + var result = _pendingRebootService.is_pending_reboot(config); + + if (result) + { + var commandsToErrorOn = new List {"install", "uninstall", "upgrade"}; + + if (!commandsToErrorOn.Contains(config.CommandName.ToLowerInvariant())) + { + validationResults.Add(new ValidationResult + { + Message = @"A pending system reboot request has been detected, however, this is + being ignored due to the current command being used '{0}'. + It is recommended that you reboot at your earliest convenience +".format_with(config.CommandName), + Status = ValidationStatus.Warning, + ExitCode = 0 + }); + } + else if (!config.Features.ExitOnRebootDetected) + { + validationResults.Add(new ValidationResult + { + Message = @"A pending system reboot request has been detected, however, this is + being ignored due to the current Chocolatey configuration. If you + want to halt when this occurs, then either set the global feature + using: + choco feature enable -name={0} + or, pass the option --exit-when-reboot-detected +".format_with(ApplicationParameters.Features.ExitOnRebootDetected), + Status = ValidationStatus.Warning, + ExitCode = 0 + }); + } + else + { + Environment.ExitCode = ApplicationParameters.ExitCodes.ErrorFailNoActionReboot; + + validationResults.Add(new ValidationResult + { + Message = "A pending system reboot has been detected", + Status = ValidationStatus.Error, + ExitCode = ApplicationParameters.ExitCodes.ErrorFailNoActionReboot + }); + } + } + } + } +} diff --git a/src/chocolatey/infrastructure/validations/IValidation.cs b/src/chocolatey/infrastructure/validations/IValidation.cs new file mode 100644 index 0000000000..130ddfad7d --- /dev/null +++ b/src/chocolatey/infrastructure/validations/IValidation.cs @@ -0,0 +1,34 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.validations +{ + using System.Collections.Generic; + using app.configuration; + + /// + /// Interface for all validations + /// + public interface IValidation + { + /// + /// Performs the required validation logic + /// + /// The current Chocolatey Configuration + /// The validation results + ICollection validate(ChocolateyConfiguration config); + } +} diff --git a/src/chocolatey/infrastructure/validations/ValidationResult.cs b/src/chocolatey/infrastructure/validations/ValidationResult.cs new file mode 100644 index 0000000000..f56653f284 --- /dev/null +++ b/src/chocolatey/infrastructure/validations/ValidationResult.cs @@ -0,0 +1,30 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.validations +{ + /// + /// Outcome of validation + /// + public sealed class ValidationResult + { + public ValidationStatus Status { get; set; } + + public string Message { get; set; } + + public int ExitCode { get; set; } + } +} diff --git a/src/chocolatey/infrastructure/validations/ValidationStatus.cs b/src/chocolatey/infrastructure/validations/ValidationStatus.cs new file mode 100644 index 0000000000..17f7c8d757 --- /dev/null +++ b/src/chocolatey/infrastructure/validations/ValidationStatus.cs @@ -0,0 +1,25 @@ +// 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. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.validations +{ + public enum ValidationStatus + { + Success, + Error, + Warning + } +}