From 36af792c4359b85bc7bb9e3bd3c92c336c990cd0 Mon Sep 17 00:00:00 2001 From: Gary Ewan Park Date: Sat, 9 Mar 2019 11:42:21 +0000 Subject: [PATCH] (GH-1038) Stop execution if pending reboot When executing Chocolatey commands, it is well known that a pending reboot requirement can prevent a succesful operation. This adds the ability to enable detection of a pending reboot (implemented as a global validation), which will then halt execution of the command. This addition doesn't require that the usePackageExitCodes feature be enabled, but rather, it inspecting the current state of the system, via the registry to determine if a reboot is required. These checks will only be performed when running on the Windows Operating System. --- src/chocolatey/chocolatey.csproj | 3 + .../registration/ContainerBinding.cs | 2 + .../services/IPendingRebootService.cs | 31 +++ .../services/PendingRebootService.cs | 180 ++++++++++++++++++ .../validations/SystemStateValidation.cs | 107 +++++++++++ 5 files changed, 323 insertions(+) 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/SystemStateValidation.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 31a3ff31ae..97dd3a9447 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -121,10 +121,13 @@ + + + diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index 8d7a24a8fa..67ffc551b5 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -63,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); @@ -135,6 +136,7 @@ public void RegisterComponents(Container container) var list = new List { new GlobalConfigurationValidation(), + new SystemStateValidation(container.GetInstance()) }; return list.AsReadOnly(); 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/SystemStateValidation.cs b/src/chocolatey/infrastructure.app/validations/SystemStateValidation.cs new file mode 100644 index 0000000000..91fca03657 --- /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 (exit code {0}).".format_with(ApplicationParameters.ExitCodes.ErrorFailNoActionReboot), + Status = ValidationStatus.Error, + ExitCode = ApplicationParameters.ExitCodes.ErrorFailNoActionReboot + }); + } + } + } + } +}