From b8180ac38413e3939611ca0b34e3ee5a86803fcc Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 16:32:35 -0600 Subject: [PATCH 01/19] (GH-15)(config) Add features section Adding this will allow feature flagging new features.c --- src/chocolatey/chocolatey.csproj | 5 ++- .../configuration/ConfigFileFeatureSetting.cs | 34 +++++++++++++++++++ .../configuration/ConfigFileSettings.cs | 5 ++- 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index cab8bd8de9..98e9560bd7 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -86,6 +86,7 @@ + @@ -214,7 +215,9 @@ - + + Designer + diff --git a/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs b/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs new file mode 100644 index 0000000000..58dda483db --- /dev/null +++ b/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs @@ -0,0 +1,34 @@ +// Copyright © 2011 - Present 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.configuration +{ + using System; + using System.Xml.Serialization; + + /// + /// XML config file features element + /// + [Serializable] + [XmlType("feature")] + public sealed class ConfigFileFeatureSetting + { + [XmlAttribute(AttributeName = "id")] + public string Id { get; set; } + + [XmlAttribute(AttributeName = "enabled")] + public bool Enabled { get; set; } + } +} \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/configuration/ConfigFileSettings.cs b/src/chocolatey/infrastructure.app/configuration/ConfigFileSettings.cs index 0893f45bc1..bb0eb58156 100644 --- a/src/chocolatey/infrastructure.app/configuration/ConfigFileSettings.cs +++ b/src/chocolatey/infrastructure.app/configuration/ConfigFileSettings.cs @@ -42,7 +42,10 @@ public class ConfigFileSettings public int CommandExecutionTimeoutSeconds { get; set; } [XmlArray("sources")] - public HashSet Sources { get; set; } + public HashSet Sources { get; set; } + + [XmlArray("features")] + public HashSet Features { get; set; } [XmlArray("apiKeys")] public HashSet ApiKeys { get; set; } From 7212f4932bc599566a31e3ce8fa13f9257fbb07b Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 16:33:33 -0600 Subject: [PATCH 02/19] (GH-15) autouninstaller feature default to disabled --- .../infrastructure.app/configuration/chocolatey.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/chocolatey/infrastructure.app/configuration/chocolatey.config b/src/chocolatey/infrastructure.app/configuration/chocolatey.config index aed3f1e645..e073fa95ca 100644 --- a/src/chocolatey/infrastructure.app/configuration/chocolatey.config +++ b/src/chocolatey/infrastructure.app/configuration/chocolatey.config @@ -8,4 +8,7 @@ + + + \ No newline at end of file From 27833259275938317f044981205619352c5ec84d Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 16:52:51 -0600 Subject: [PATCH 03/19] (GH-15)(config) set feature to configuration Require the feature and enabled to explicitly be set to true to allow a feature to be flagged as on. --- .../infrastructure.app/ApplicationParameters.cs | 5 +++++ .../builders/ConfigurationBuilder.cs | 5 +++++ .../configuration/ChocolateyConfiguration.cs | 12 ++++++++++++ .../configuration/ConfigFileFeatureSetting.cs | 4 ++-- .../configuration/chocolatey.config | 2 +- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 45e6a56f06..2aab742af0 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -59,6 +59,11 @@ public static class Tools public static readonly string ShimGenExe = _fileSystem.combine_paths(InstallLocation, "tools", "shimgen.exe"); } + public static class Features + { + public static readonly string AutoUnintaller = "autouninstaller"; + } + public static class Messages { public static readonly string ContinueChocolateyAction = "Moving forward with chocolatey actions."; diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 50391bf890..4cedb2e9e5 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -89,6 +89,11 @@ private static void set_file_configuration(ChocolateyConfiguration config, IFile } config.CommandExecutionTimeoutSeconds = configFileSettings.CommandExecutionTimeoutSeconds; + //refactor - as features grows, it will need it's own section. + config.Features.AutoUninstaller = false; + var feature = configFileSettings.Features.FirstOrDefault(f => f.Name.is_equal_to(ApplicationParameters.Features.AutoUnintaller)); + if (feature != null && feature.Enabled) config.Features.AutoUninstaller = true; + try { xmlService.serialize(configFileSettings, globalConfigPath); diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 1c007b9c4e..cfc48861ff 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -32,6 +32,7 @@ public ChocolateyConfiguration() RegularOuptut = true; PromptForConfirmation = true; Information = new InformationCommandConfiguration(); + Features = new FeaturesConfiguration(); NewCommand = new NewCommandConfiguration(); ListCommand = new ListCommandConfiguration(); SourceCommand = new SourcesCommandConfiguration(); @@ -151,6 +152,11 @@ private void output_tostring(StringBuilder propertyValues, IEnumerable public InformationCommandConfiguration Information { get; private set; } + /// + /// Configuration related to features and whether they are enabled. + /// + public FeaturesConfiguration Features { get; private set; } + /// /// Configuration related specifically to List command /// @@ -175,6 +181,7 @@ private void output_tostring(StringBuilder propertyValues, IEnumerable public PushCommandConfiguration PushCommand { get; private set; } + } public sealed class InformationCommandConfiguration @@ -187,6 +194,11 @@ public sealed class InformationCommandConfiguration public bool IsInteractive { get; set; } } + public sealed class FeaturesConfiguration + { + public bool AutoUninstaller { get; set; } + } + //todo: retrofit other command configs this way public sealed class ListCommandConfiguration diff --git a/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs b/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs index 58dda483db..f1ad665537 100644 --- a/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs +++ b/src/chocolatey/infrastructure.app/configuration/ConfigFileFeatureSetting.cs @@ -25,8 +25,8 @@ namespace chocolatey.infrastructure.app.configuration [XmlType("feature")] public sealed class ConfigFileFeatureSetting { - [XmlAttribute(AttributeName = "id")] - public string Id { get; set; } + [XmlAttribute(AttributeName = "name")] + public string Name { get; set; } [XmlAttribute(AttributeName = "enabled")] public bool Enabled { get; set; } diff --git a/src/chocolatey/infrastructure.app/configuration/chocolatey.config b/src/chocolatey/infrastructure.app/configuration/chocolatey.config index e073fa95ca..53976f3642 100644 --- a/src/chocolatey/infrastructure.app/configuration/chocolatey.config +++ b/src/chocolatey/infrastructure.app/configuration/chocolatey.config @@ -9,6 +9,6 @@ - + \ No newline at end of file From 461280096a332e0c756a185d6ff3bc0260f1abc9 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 16:54:28 -0600 Subject: [PATCH 04/19] (GH-15) hide autouninstaller behind feature flag Do not allow autouninstaller to run automatically. Since it is experimental, we want to lock it down and make folks explicitly opt into it. --- .../services/ChocolateyPackageService.cs | 134 ++++++++++-------- 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index f8fdb9bb6c..d3971953ca 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -313,80 +313,88 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi //todo run autouninstaller every time - to do this you must determine if the install path still exists. if (!powershellRan) { - var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); - - if (pkgInfo.RegistrySnapshot != null) + //todo:refactor this crap + if (config.Features.AutoUninstaller) { - //todo:refactor this crap - this.Log().Info(" Running AutoUninstaller..."); + var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); - foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + if (pkgInfo.RegistrySnapshot != null) { - this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); - // split on " /" and " -" for quite a bit more accuracy - IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); - var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); - this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); - uninstallArgs.Remove(uninstallExe); - - if (!key.HasQuietUninstall) - { - IInstaller installer = new CustomInstaller(); - - //refactor this to elsewhere - switch (key.InstallerType) - { - case InstallerType.Msi: - installer = new MsiInstaller(); - break; - case InstallerType.InnoSetup: - installer = new InnoSetupInstaller(); - break; - case InstallerType.Nsis: - installer = new NsisInstaller(); - break; - case InstallerType.InstallShield: - installer = new CustomInstaller(); - break; - default: - // skip - - break; - } - this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); + this.Log().Info(" Running AutoUninstaller..."); - uninstallArgs.Add(installer.build_uninstall_command_arguments()); - } - - this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.join(" "))); + foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + { + this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); + // split on " /" and " -" for quite a bit more accuracy + IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); + this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); + uninstallArgs.Remove(uninstallExe); + + if (!key.HasQuietUninstall) + { + IInstaller installer = new CustomInstaller(); - var exitCode = CommandExecutor.execute( - uninstallExe, uninstallArgs.join(" "), config.CommandExecutionTimeoutSeconds, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }, - (s, e) => + //refactor this to elsewhere + switch (key.InstallerType) { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }); + case InstallerType.Msi: + installer = new MsiInstaller(); + break; + case InstallerType.InnoSetup: + installer = new InnoSetupInstaller(); + break; + case InstallerType.Nsis: + installer = new NsisInstaller(); + break; + case InstallerType.InstallShield: + installer = new CustomInstaller(); + break; + default: + // skip + + break; + } + + this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); + + uninstallArgs.Add(installer.build_uninstall_command_arguments()); + } - if (exitCode != 0) - { - Environment.ExitCode = exitCode; - string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; - this.Log().Error(() => logMessage); - packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - } - else - { - this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine install".format_with(packageResult.Package.Id)); + this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.join(" "))); + + var exitCode = CommandExecutor.execute( + uninstallExe, uninstallArgs.join(" "), config.CommandExecutionTimeoutSeconds, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; + this.Log().Error(() => logMessage); + packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + } + else + { + this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine install".format_with(packageResult.Package.Id)); + } } } } + else + { + this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); + } } if (packageResult.Success) From 9b640e1fb1b619407627e6d6b1c2f3f63cfdf1ab Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 17:11:30 -0600 Subject: [PATCH 05/19] (GH-9)(refactor) autouninstaller code to a service Move the code for autouninstaller to its own service and remove the stipulation that powershell needed to have NOT run for the autouninstaller to run. --- src/chocolatey/chocolatey.csproj | 2 + .../registration/ContainerBinding.cs | 1 + .../services/AutomaticUninstallerService.cs | 120 ++++++++++++++++++ .../services/ChocolateyPackageService.cs | 95 +------------- .../services/IAutomaticUninstallerService.cs | 33 +++++ 5 files changed, 161 insertions(+), 90 deletions(-) create mode 100644 src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs create mode 100644 src/chocolatey/infrastructure.app/services/IAutomaticUninstallerService.cs diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 98e9560bd7..4db435631a 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -91,6 +91,7 @@ + @@ -125,6 +126,7 @@ + diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index c6d01335f7..b1a057bdd1 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -57,6 +57,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); //todo:refactor - this should be autowired container.Register>(() => diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs new file mode 100644 index 0000000000..d586594bc3 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -0,0 +1,120 @@ +// Copyright © 2011 - Present 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 System; + using System.Collections.Generic; + using System.Linq; + using configuration; + using domain; + using infrastructure.commands; + using results; + + public class AutomaticUninstallerService : IAutomaticUninstallerService + { + private readonly IChocolateyPackageInformationService _packageInfoService; + + public AutomaticUninstallerService(IChocolateyPackageInformationService packageInfoService) + { + _packageInfoService = packageInfoService; + } + + public void run(PackageResult packageResult, ChocolateyConfiguration config) + { + //todo run autouninstaller every time - to do this you must determine if the install path still exists. + + if (config.Features.AutoUninstaller) + { + var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); + + if (pkgInfo.RegistrySnapshot != null) + { + this.Log().Info(" Running AutoUninstaller..."); + + foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + { + this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); + // split on " /" and " -" for quite a bit more accuracy + IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); + this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); + uninstallArgs.Remove(uninstallExe); + + if (!key.HasQuietUninstall) + { + IInstaller installer = new CustomInstaller(); + + //refactor this to elsewhere + switch (key.InstallerType) + { + case InstallerType.Msi: + installer = new MsiInstaller(); + break; + case InstallerType.InnoSetup: + installer = new InnoSetupInstaller(); + break; + case InstallerType.Nsis: + installer = new NsisInstaller(); + break; + case InstallerType.InstallShield: + installer = new CustomInstaller(); + break; + default: + // skip + break; + } + + this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); + + uninstallArgs.Add(installer.build_uninstall_command_arguments()); + } + + this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.join(" "))); + + var exitCode = CommandExecutor.execute( + uninstallExe, uninstallArgs.join(" "), config.CommandExecutionTimeoutSeconds, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }, + (s, e) => + { + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; + this.Log().Error(() => logMessage); + packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + } + else + { + this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine install".format_with(packageResult.Package.Id)); + } + } + } + } + else + { + this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); + } + } + } +} \ 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 d3971953ca..d4197e6514 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -24,7 +24,6 @@ namespace chocolatey.infrastructure.app.services using configuration; using domain; using filesystem; - using infrastructure.commands; using logging; using platforms; using results; @@ -37,8 +36,9 @@ public class ChocolateyPackageService : IChocolateyPackageService private readonly IFileSystem _fileSystem; private readonly IRegistryService _registryService; private readonly IChocolateyPackageInformationService _packageInfoService; + private readonly IAutomaticUninstallerService _autoUninstallerService; - public ChocolateyPackageService(INugetService nugetService, IPowershellService powershellService, IShimGenerationService shimgenService, IFileSystem fileSystem, IRegistryService registryService, IChocolateyPackageInformationService packageInfoService) + public ChocolateyPackageService(INugetService nugetService, IPowershellService powershellService, IShimGenerationService shimgenService, IFileSystem fileSystem, IRegistryService registryService, IChocolateyPackageInformationService packageInfoService, IAutomaticUninstallerService autoUninstallerService) { _nugetService = nugetService; _powershellService = powershellService; @@ -46,6 +46,7 @@ public ChocolateyPackageService(INugetService nugetService, IPowershellService p _fileSystem = fileSystem; _registryService = registryService; _packageInfoService = packageInfoService; + _autoUninstallerService = autoUninstallerService; } public void list_noop(ChocolateyConfiguration config) @@ -304,98 +305,12 @@ public ConcurrentDictionary uninstall_run(ChocolateyConfi _shimgenService.uninstall(config, packageResult); - var powershellRan = false; if (!config.SkipPackageInstallProvider) { - powershellRan = _powershellService.uninstall(config, packageResult); + _powershellService.uninstall(config, packageResult); } - //todo run autouninstaller every time - to do this you must determine if the install path still exists. - if (!powershellRan) - { - //todo:refactor this crap - if (config.Features.AutoUninstaller) - { - var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); - - if (pkgInfo.RegistrySnapshot != null) - { - - this.Log().Info(" Running AutoUninstaller..."); - - foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) - { - this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); - // split on " /" and " -" for quite a bit more accuracy - IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); - var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); - this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); - uninstallArgs.Remove(uninstallExe); - - if (!key.HasQuietUninstall) - { - IInstaller installer = new CustomInstaller(); - - //refactor this to elsewhere - switch (key.InstallerType) - { - case InstallerType.Msi: - installer = new MsiInstaller(); - break; - case InstallerType.InnoSetup: - installer = new InnoSetupInstaller(); - break; - case InstallerType.Nsis: - installer = new NsisInstaller(); - break; - case InstallerType.InstallShield: - installer = new CustomInstaller(); - break; - default: - // skip - - break; - } - - this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); - - uninstallArgs.Add(installer.build_uninstall_command_arguments()); - } - - this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.join(" "))); - - var exitCode = CommandExecutor.execute( - uninstallExe, uninstallArgs.join(" "), config.CommandExecutionTimeoutSeconds, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }); - - if (exitCode != 0) - { - Environment.ExitCode = exitCode; - string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; - this.Log().Error(() => logMessage); - packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - } - else - { - this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine install".format_with(packageResult.Package.Id)); - } - } - } - } - else - { - this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); - } - } + _autoUninstallerService.run(packageResult, config); if (packageResult.Success) { diff --git a/src/chocolatey/infrastructure.app/services/IAutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/IAutomaticUninstallerService.cs new file mode 100644 index 0000000000..31ae030948 --- /dev/null +++ b/src/chocolatey/infrastructure.app/services/IAutomaticUninstallerService.cs @@ -0,0 +1,33 @@ +// Copyright © 2011 - Present 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 results; + + /// + /// The automagic uninstaller service + /// + public interface IAutomaticUninstallerService + { + /// + /// Runs to remove a application from the registry. + /// + /// The package result. + /// The configuration. + void run(PackageResult packageResult, ChocolateyConfiguration config); + } +} \ No newline at end of file From bbb83b7b7b1b2bce1467711dc3284d27316abac6 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Sun, 18 Jan 2015 17:21:31 -0600 Subject: [PATCH 06/19] (GH-9)(refactor) invert ifs to return early Remove nesting and return early. --- .../services/AutomaticUninstallerService.cs | 138 +++++++++--------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index d586594bc3..b8d83cb19d 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -36,84 +36,82 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) { //todo run autouninstaller every time - to do this you must determine if the install path still exists. - if (config.Features.AutoUninstaller) + if (!config.Features.AutoUninstaller) { - var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); + this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); + return; + } + + var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); - if (pkgInfo.RegistrySnapshot != null) + if (pkgInfo.RegistrySnapshot == null) return; + + this.Log().Info(" Running AutoUninstaller..."); + + foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + { + this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); + // split on " /" and " -" for quite a bit more accuracy + IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); + var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); + this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); + uninstallArgs.Remove(uninstallExe); + + if (!key.HasQuietUninstall) { - this.Log().Info(" Running AutoUninstaller..."); + IInstaller installer = new CustomInstaller(); - foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + //refactor this to elsewhere + switch (key.InstallerType) { - this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); - // split on " /" and " -" for quite a bit more accuracy - IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); - var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); - this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); - uninstallArgs.Remove(uninstallExe); - - if (!key.HasQuietUninstall) - { - IInstaller installer = new CustomInstaller(); - - //refactor this to elsewhere - switch (key.InstallerType) - { - case InstallerType.Msi: - installer = new MsiInstaller(); - break; - case InstallerType.InnoSetup: - installer = new InnoSetupInstaller(); - break; - case InstallerType.Nsis: - installer = new NsisInstaller(); - break; - case InstallerType.InstallShield: - installer = new CustomInstaller(); - break; - default: - // skip - break; - } - - this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); - - uninstallArgs.Add(installer.build_uninstall_command_arguments()); - } - - this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.join(" "))); - - var exitCode = CommandExecutor.execute( - uninstallExe, uninstallArgs.join(" "), config.CommandExecutionTimeoutSeconds, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }, - (s, e) => - { - if (string.IsNullOrWhiteSpace(e.Data)) return; - this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); - }); - - if (exitCode != 0) + case InstallerType.Msi: + installer = new MsiInstaller(); + break; + case InstallerType.InnoSetup: + installer = new InnoSetupInstaller(); + break; + case InstallerType.Nsis: + installer = new NsisInstaller(); + break; + case InstallerType.InstallShield: + installer = new CustomInstaller(); + break; + default: + // skip + break; + } + + this.Log().Debug(() => " Installer type is '{0}'".format_with(installer.GetType().Name)); + + uninstallArgs.Add(installer.build_uninstall_command_arguments()); + } + + this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.@join(" "))); + + var exitCode = CommandExecutor.execute( + uninstallExe, uninstallArgs.@join(" "), config.CommandExecutionTimeoutSeconds, + (s, e) => { - Environment.ExitCode = exitCode; - string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; - this.Log().Error(() => logMessage); - packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - } - else + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }, + (s, e) => { - this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine install".format_with(packageResult.Package.Id)); - } - } + if (string.IsNullOrWhiteSpace(e.Data)) return; + this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); + }); + + if (exitCode != 0) + { + Environment.ExitCode = exitCode; + string logMessage = " Auto uninstaller failed. Please remove machine installation manually."; + this.Log().Error(() => logMessage); + packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + } + else + { + this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine.".format_with(packageResult.Package.Id)); } - } - else - { - this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); } } } From 4cb62484e76dbfc0e79836e707cea363e82aa752 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 07:19:09 -0600 Subject: [PATCH 07/19] (refactor) Move other features to config section Move checksumFiles and virusCheckFiles to feature flags and mark them appropriately. --- .../ApplicationParameters.cs | 4 +++- .../builders/ConfigurationBuilder.cs | 23 ++++++++++++++----- .../configuration/ChocolateyConfiguration.cs | 5 ++-- .../configuration/ConfigFileSettings.cs | 6 ----- .../configuration/chocolatey.config | 7 +++--- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 2aab742af0..7eaf9c7263 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -61,7 +61,9 @@ public static class Tools public static class Features { - public static readonly string AutoUnintaller = "autouninstaller"; + public static readonly string CheckSumFiles = "checksumFiles"; + public static readonly string VirusCheckFiles = "virusCheckFiles"; + public static readonly string AutoUninstaller = "autoUninstaller"; } public static class Messages diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 4cedb2e9e5..c54e234dbc 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -79,8 +79,6 @@ private static void set_file_configuration(ChocolateyConfiguration config, IFile config.Sources = sources.Remove(sources.Length - 1, 1).ToString(); } - config.CheckSumFiles = configFileSettings.ChecksumFiles; - config.VirusCheckFiles = configFileSettings.VirusCheckFiles; config.CacheLocation = configFileSettings.CacheLocation; config.ContainsLegacyPackageInstalls = configFileSettings.ContainsLegacyPackageInstalls; if (configFileSettings.CommandExecutionTimeoutSeconds <= 0) @@ -89,13 +87,11 @@ private static void set_file_configuration(ChocolateyConfiguration config, IFile } config.CommandExecutionTimeoutSeconds = configFileSettings.CommandExecutionTimeoutSeconds; - //refactor - as features grows, it will need it's own section. - config.Features.AutoUninstaller = false; - var feature = configFileSettings.Features.FirstOrDefault(f => f.Name.is_equal_to(ApplicationParameters.Features.AutoUnintaller)); - if (feature != null && feature.Enabled) config.Features.AutoUninstaller = true; + set_feature_flags(config, configFileSettings); try { + // save so all updated configuration items get set to existing config xmlService.serialize(configFileSettings, globalConfigPath); } catch (Exception ex) @@ -104,6 +100,21 @@ private static void set_file_configuration(ChocolateyConfiguration config, IFile } } + private static void set_feature_flags(ChocolateyConfiguration config, ConfigFileSettings configFileSettings) + { + config.Features.CheckSumFiles = set_feature_flag(ApplicationParameters.Features.CheckSumFiles, config, configFileSettings); + config.Features.VirusCheckFiles = set_feature_flag(ApplicationParameters.Features.VirusCheckFiles, config, configFileSettings); + config.Features.AutoUninstaller = set_feature_flag(ApplicationParameters.Features.AutoUninstaller, config, configFileSettings); + } + + private static bool set_feature_flag(string featureName, ChocolateyConfiguration config, ConfigFileSettings configFileSettings) + { + var feature = configFileSettings.Features.FirstOrDefault(f => f.Name.is_equal_to(featureName)); + if (feature != null && feature.Enabled) return true; + + return false; + } + private static void set_global_options(IList args, ChocolateyConfiguration config) { ConfigurationOptions.parse_arguments_and_update_configuration( diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index cfc48861ff..512fef5b28 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -94,9 +94,6 @@ private void output_tostring(StringBuilder propertyValues, IEnumerable - false - true - false true - + + + \ No newline at end of file From 68f743a6fd578411e3b8b5d9cd59c4bf927fd72f Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 07:20:28 -0600 Subject: [PATCH 08/19] (GH-9) RegistryService.value_exists Return whether a registry value exists. --- .../infrastructure.app/services/IRegistryService.cs | 1 + .../infrastructure.app/services/RegistryService.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/chocolatey/infrastructure.app/services/IRegistryService.cs b/src/chocolatey/infrastructure.app/services/IRegistryService.cs index eaebc49f66..af451bcc01 100644 --- a/src/chocolatey/infrastructure.app/services/IRegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/IRegistryService.cs @@ -23,5 +23,6 @@ public interface IRegistryService Registry get_differences(Registry before, Registry after); void save_to_file(Registry snapshot, string filePath); Registry read_from_file(string filePath); + bool value_exists(string keyPath, string value); } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure.app/services/RegistryService.cs b/src/chocolatey/infrastructure.app/services/RegistryService.cs index 97650f76fa..bbf4ebb1dd 100644 --- a/src/chocolatey/infrastructure.app/services/RegistryService.cs +++ b/src/chocolatey/infrastructure.app/services/RegistryService.cs @@ -202,6 +202,11 @@ public void save_to_file(Registry snapshot, string filePath) _xmlService.serialize(snapshot, filePath); } + public bool value_exists(string keyPath, string value) + { + return Microsoft.Win32.Registry.GetValue(keyPath, value, null) != null; + } + public Registry read_from_file(string filePath) { if (!_fileSystem.file_exists(filePath)) From 9495d76194ab325bbb2d7d3e873118211761b398 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 07:22:42 -0600 Subject: [PATCH 09/19] (GH-9) Run AutoUninstaller every time If the registry key AND the install location still exist, run autouninstaller. Otherwise, skip and log the details. --- .../services/AutomaticUninstallerService.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index b8d83cb19d..ea3f792720 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -20,22 +20,25 @@ namespace chocolatey.infrastructure.app.services using System.Linq; using configuration; using domain; + using filesystem; using infrastructure.commands; using results; public class AutomaticUninstallerService : IAutomaticUninstallerService { private readonly IChocolateyPackageInformationService _packageInfoService; + private readonly IFileSystem _fileSystem; + private readonly IRegistryService _registryService; - public AutomaticUninstallerService(IChocolateyPackageInformationService packageInfoService) + public AutomaticUninstallerService(IChocolateyPackageInformationService packageInfoService, IFileSystem fileSystem, IRegistryService registryService) { _packageInfoService = packageInfoService; + _fileSystem = fileSystem; + _registryService = registryService; } public void run(PackageResult packageResult, ChocolateyConfiguration config) { - //todo run autouninstaller every time - to do this you must determine if the install path still exists. - if (!config.Features.AutoUninstaller) { this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); @@ -51,6 +54,15 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) { this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); + + if (!_fileSystem.directory_exists(key.InstallLocation) || !_registryService.value_exists(key.KeyPath,"InstallLocation")) + { + this.Log().Info(()=> "Skipping auto uninstall. The application appears to have been uninstalled already by other means."); + this.Log().Debug(() => "Searched for install path '{0}' - found? {1}".format_with(key.InstallLocation.escape_curly_braces(), _fileSystem.directory_exists(key.InstallLocation))); + this.Log().Debug(() => "Searched for registry key '{0}' value 'InstallLocation' - found? {1}".format_with(key.KeyPath.escape_curly_braces(), _registryService.value_exists(key.KeyPath, "InstallLocation"))); + continue; + } + // split on " /" and " -" for quite a bit more accuracy IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); From b17c2263a1d55a27c0be803b435b68666ba10fa1 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 07:24:27 -0600 Subject: [PATCH 10/19] (GH-9) delay cleanup on uninstall path Remove uninstall executable from uninstall arguments before cleaning it up for use by command executor. --- .../services/AutomaticUninstallerService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index ea3f792720..b8664bc470 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -65,9 +65,11 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) // split on " /" and " -" for quite a bit more accuracy IList uninstallArgs = key.UninstallString.to_string().Split(new[] {" /", " -"}, StringSplitOptions.RemoveEmptyEntries).ToList(); - var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault().remove_surrounding_quotes(); - this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); + var uninstallExe = uninstallArgs.DefaultIfEmpty(string.Empty).FirstOrDefault(); uninstallArgs.Remove(uninstallExe); + uninstallExe = uninstallExe.remove_surrounding_quotes(); + this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); + if (!key.HasQuietUninstall) { From 937a0288b0dfae4809358e6b3935fe371ce8ca68 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 09:06:39 -0600 Subject: [PATCH 11/19] (build) upgrade nunit to 2.6.4 --- .uppercut | 2 +- src/.nuget/packages.config | 4 ++++ src/chocolatey.sln | 5 +++++ .../chocolatey.tests.integration.csproj | 5 +++-- src/chocolatey.tests.integration/packages.config | 2 +- src/chocolatey.tests/chocolatey.tests.csproj | 5 +++-- src/chocolatey.tests/packages.config | 2 +- 7 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 src/.nuget/packages.config diff --git a/.uppercut b/.uppercut index ebb6b10233..5e354fe124 100644 --- a/.uppercut +++ b/.uppercut @@ -71,7 +71,7 @@ - + diff --git a/src/.nuget/packages.config b/src/.nuget/packages.config new file mode 100644 index 0000000000..7e62aa75e7 --- /dev/null +++ b/src/.nuget/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/chocolatey.sln b/src/chocolatey.sln index 039b034f42..b4b32a2e24 100644 --- a/src/chocolatey.sln +++ b/src/chocolatey.sln @@ -13,6 +13,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chocolatey.resources", "cho EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chocolatey.tests.integration", "chocolatey.tests.integration\chocolatey.tests.integration.csproj", "{12ECAD4F-D463-4D72-A792-A4FCCFE9C456}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{7A36F8E0-B170-4A51-A788-F1246513808A}" + ProjectSection(SolutionItems) = preProject + .nuget\packages.config = .nuget\packages.config + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj index 2cce3bcb9d..cf65a3da3f 100644 --- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj +++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj @@ -35,8 +35,9 @@ ..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll - - ..\packages\NUnit.2.5.10.11092\lib\nunit.framework.dll + + False + ..\packages\NUnit.2.6.4\lib\nunit.framework.dll ..\packages\Should.1.1.12.0\lib\Should.dll diff --git a/src/chocolatey.tests.integration/packages.config b/src/chocolatey.tests.integration/packages.config index 35c5329590..6316acc715 100644 --- a/src/chocolatey.tests.integration/packages.config +++ b/src/chocolatey.tests.integration/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 50dae1f024..887cc0753b 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -35,8 +35,9 @@ ..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll - - ..\packages\NUnit.2.5.10.11092\lib\nunit.framework.dll + + False + ..\packages\NUnit.2.6.4\lib\nunit.framework.dll ..\packages\Should.1.1.12.0\lib\Should.dll diff --git a/src/chocolatey.tests/packages.config b/src/chocolatey.tests/packages.config index c635152d4e..6741863a42 100644 --- a/src/chocolatey.tests/packages.config +++ b/src/chocolatey.tests/packages.config @@ -2,7 +2,7 @@ - + \ No newline at end of file From 1a1aa797bce37e357893186c5070afb8fda4310e Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 09:07:23 -0600 Subject: [PATCH 12/19] (maint) add nuget.core to chocolatey.tests --- src/chocolatey.tests/chocolatey.tests.csproj | 6 ++++++ src/chocolatey.tests/packages.config | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 887cc0753b..5eb4ef0209 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -32,9 +32,15 @@ 4 + + ..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll + ..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll + + ..\packages\NuGet.Core.2.8.2\lib\net40-Client\NuGet.Core.dll + False ..\packages\NUnit.2.6.4\lib\nunit.framework.dll diff --git a/src/chocolatey.tests/packages.config b/src/chocolatey.tests/packages.config index 6741863a42..585fd5cb33 100644 --- a/src/chocolatey.tests/packages.config +++ b/src/chocolatey.tests/packages.config @@ -1,7 +1,9 @@  + + From 67fbbcf5b4f2828e7c3910730e042b90181e0415 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 09:07:41 -0600 Subject: [PATCH 13/19] (maint) formatting --- .../infrastructure/commands/CommandExecutorSpecs.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs b/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs index c2c3a6e9d7..06363614d8 100644 --- a/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs +++ b/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs @@ -27,12 +27,12 @@ public class CommandExecutorSpecs { public abstract class CommandExecutorSpecsBase : TinySpec { - protected Mock file_system = new Mock(); + protected Mock fileSystem = new Mock(); protected Mock process = new Mock(); public override void Context() { - CommandExecutor.initialize_with(new Lazy(() => file_system.Object), () => process.Object); + CommandExecutor.initialize_with(new Lazy(() => fileSystem.Object), () => process.Object); } } From 02b57d5e5b5ac3ce87c515ff330f382249b18c8a Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Mon, 19 Jan 2015 09:29:55 -0600 Subject: [PATCH 14/19] (GH-9)(refactor) AutoUninstaller + Specs - Move CommandExecutor away from static methods for the most part. Implement ICommandExecutor and set it up with container binding. Set AutoUninstallerService to use ICommandExecutor instead of static method calls. - Fix null reference error in Registry ctor - Log why when auto uninstaller is not run --- .../commands/CommandExecutorSpecs.cs | 10 +- src/chocolatey.tests/TinySpec.cs | 1 - src/chocolatey.tests/chocolatey.tests.csproj | 1 + .../AutomaticUninstallerServiceSpecs.cs | 281 ++++++++++++++++++ .../commands/CommandExecutorSpecs.cs | 8 +- src/chocolatey/chocolatey.csproj | 1 + .../ApplicationParameters.cs | 1 + .../infrastructure.app/domain/Registry.cs | 9 +- .../registration/ContainerBinding.cs | 1 + .../services/AutomaticUninstallerService.cs | 46 ++- .../services/ShimGenerationService.cs | 6 +- .../services/WebPiService.cs | 6 +- .../commands/CommandExecutor.cs | 17 +- .../commands/ICommandExecutor.cs | 34 +++ 14 files changed, 388 insertions(+), 34 deletions(-) create mode 100644 src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs create mode 100644 src/chocolatey/infrastructure/commands/ICommandExecutor.cs diff --git a/src/chocolatey.tests.integration/infrastructure/commands/CommandExecutorSpecs.cs b/src/chocolatey.tests.integration/infrastructure/commands/CommandExecutorSpecs.cs index ea00635e9e..80b2868545 100644 --- a/src/chocolatey.tests.integration/infrastructure/commands/CommandExecutorSpecs.cs +++ b/src/chocolatey.tests.integration/infrastructure/commands/CommandExecutorSpecs.cs @@ -25,8 +25,13 @@ public class CommandExecutorSpecs { public abstract class CommandExecutorSpecsBase : TinySpec { + protected readonly IFileSystem fileSystem = new DotNetFileSystem(); + protected CommandExecutor commandExecutor; + public override void Context() { + commandExecutor = new CommandExecutor(fileSystem); + } } @@ -34,7 +39,6 @@ public class when_CommandExecutor_errors : CommandExecutorSpecsBase { private int result; private string errorOutput; - private readonly IFileSystem file_system = new DotNetFileSystem(); public override void Context() { @@ -43,7 +47,7 @@ public override void Context() public override void Because() { - result = CommandExecutor.execute("cmd.exe", "/c bob123123", ApplicationParameters.DefaultWaitForExitInSeconds, file_system.get_current_directory(), null, (s, e) => { errorOutput += e.Data; }, updateProcessPath: false); + result = CommandExecutor.execute("cmd.exe", "/c bob123123", ApplicationParameters.DefaultWaitForExitInSeconds, fileSystem.get_current_directory(), null, (s, e) => { errorOutput += e.Data; }, updateProcessPath: false); } [Fact] @@ -74,7 +78,7 @@ public override void Because() { try { - CommandExecutor.execute("noprocess.exe", "/c bob123123", ApplicationParameters.DefaultWaitForExitInSeconds, null, (s, e) => { errorOutput += e.Data; }); + commandExecutor.execute("noprocess.exe", "/c bob123123", ApplicationParameters.DefaultWaitForExitInSeconds, null, (s, e) => { errorOutput += e.Data; }); } catch (Exception e) { diff --git a/src/chocolatey.tests/TinySpec.cs b/src/chocolatey.tests/TinySpec.cs index 3873ce55f3..ad8f39d427 100644 --- a/src/chocolatey.tests/TinySpec.cs +++ b/src/chocolatey.tests/TinySpec.cs @@ -66,7 +66,6 @@ public virtual void AfterEachSpec() public void TearDown() { AfterObservations(); - MockLogger = new MockLogger(); } public virtual void AfterObservations() diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 5eb4ef0209..dd9ad62dea 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -69,6 +69,7 @@ + diff --git a/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs new file mode 100644 index 0000000000..0a089d1504 --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs @@ -0,0 +1,281 @@ +// Copyright © 2011 - Present 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.tests.infrastructure.app.services +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using Moq; + using NuGet; + using Should; + using chocolatey.infrastructure.adapters; + using chocolatey.infrastructure.app; + using chocolatey.infrastructure.app.configuration; + using chocolatey.infrastructure.app.domain; + using chocolatey.infrastructure.app.services; + using chocolatey.infrastructure.commands; + using chocolatey.infrastructure.results; + using IFileSystem = chocolatey.infrastructure.filesystem.IFileSystem; + + public class AutomaticUninstallerServiceSpecs + { + public abstract class AutomaticUninstallerServiceSpecsBase : TinySpec + { + protected AutomaticUninstallerService service; + protected Mock packageInfoService = new Mock(); + protected Mock fileSystem = new Mock(); + protected Mock process = new Mock(); + protected Mock registryService = new Mock(); + protected Mock commandExecutor = new Mock(); + protected ChocolateyConfiguration config = new ChocolateyConfiguration(); + protected Mock package = new Mock(); + protected ConcurrentDictionary packageResults = new ConcurrentDictionary(); + protected PackageResult packageResult; + protected ChocolateyPackageInformation packageInformation; + protected IList registryKeys = new List(); + protected IInstaller installerType = new CustomInstaller(); + + protected readonly string originalUninstallString = @"""C:\Program Files (x86)\WinDirStat\Uninstall.exe"""; + protected readonly string expectedUninstallString = @"C:\Program Files (x86)\WinDirStat\Uninstall.exe"; + + public override void Context() + { + CommandExecutor.initialize_with(new Lazy(() => fileSystem.Object), () => process.Object); + + service = new AutomaticUninstallerService(packageInfoService.Object, fileSystem.Object, registryService.Object, commandExecutor.Object); + config.Features.AutoUninstaller = true; + package.Setup(p => p.Id).Returns("regular"); + package.Setup(p => p.Version).Returns(new SemanticVersion("1.2.0")); + packageResult = new PackageResult(package.Object, null); + packageInformation = new ChocolateyPackageInformation(package.Object); + registryKeys.Add(new RegistryApplicationKey + { + InstallLocation = @"C:\Program Files (x86)\WinDirStat", + UninstallString = originalUninstallString, + HasQuietUninstall = false, + KeyPath = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\WinDirStat", + }); + packageInformation.RegistrySnapshot = new Registry("123", registryKeys); + packageInfoService.Setup(s => s.get_package_information(package.Object)).Returns(packageInformation); + packageResults.GetOrAdd("regular", packageResult); + + fileSystem.Setup(f => f.directory_exists(registryKeys.FirstOrDefault().InstallLocation)).Returns(true); + registryService.Setup(r => r.value_exists(registryKeys.FirstOrDefault().KeyPath, ApplicationParameters.RegistryValueInstallLocation)).Returns(true); + fileSystem.Setup(f => f.get_full_path(expectedUninstallString)).Returns(expectedUninstallString); + } + } + + public class when_autouninstall_feature_is_off : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + config.Features.AutoUninstaller = false; + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // // will pass by itself, but won't pass as part of the test suite. + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - AutoUninstaller feature is not enabled."), Times.Once); + //} + + [Fact] + public void should_not_get_package_information() + { + packageInfoService.Verify(s => s.get_package_information(It.IsAny()), Times.Never); + } + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_registry_snapshot_is_null : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + packageInformation.RegistrySnapshot = null; + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - No registry snapshot."), Times.Once); + //} + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_registry_keys_are_empty : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + packageInformation.RegistrySnapshot = new Registry("123", null); + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - No registry keys in snapshot."), Times.Once); + //} + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_install_location_does_not_exist : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + fileSystem.ResetCalls(); + fileSystem.Setup(f => f.directory_exists(registryKeys.FirstOrDefault().InstallLocation)).Returns(false); + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - The application appears to have been uninstalled already by other means."), Times.Once); + //} + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_registry_location_does_not_exist : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + registryService.ResetCalls(); + registryService.Setup(r => r.value_exists(registryKeys.FirstOrDefault().KeyPath, ApplicationParameters.RegistryValueInstallLocation)).Returns(false); + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - The application appears to have been uninstalled already by other means."), Times.Once); + //} + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_registry_location_and_install_location_both_do_not_exist : AutomaticUninstallerServiceSpecsBase + { + public override void Context() + { + base.Context(); + fileSystem.ResetCalls(); + fileSystem.Setup(f => f.directory_exists(registryKeys.FirstOrDefault().InstallLocation)).Returns(false); + registryService.ResetCalls(); + registryService.Setup(r => r.value_exists(registryKeys.FirstOrDefault().KeyPath, ApplicationParameters.RegistryValueInstallLocation)).Returns(false); + } + + public override void Because() + { + service.run(packageResult, config); + } + + //[Fact] + //public void should_log_why_it_skips_auto_uninstaller() + //{ + // MockLogger.Verify(l => l.Info(" Skipping auto uninstaller - The application appears to have been uninstalled already by other means."), Times.Once); + //} + + [Fact] + public void should_not_call_command_executor() + { + commandExecutor.Verify(c => c.execute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_AutomaticUninstallerService_is_run_normally : AutomaticUninstallerServiceSpecsBase + { + public override void Because() + { + service.run(packageResult, config); + } + + [Fact] + public void should_call_get_package_information() + { + packageInfoService.Verify(s => s.get_package_information(It.IsAny()), Times.Once); + } + + [Fact] + public void should_call_command_executor() + { + commandExecutor.Verify(c => c.execute(expectedUninstallString, installerType.build_uninstall_command_arguments().trim_safe(), It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + } + + public class when_AutomaticUninstallerService_is_run : AutomaticUninstallerServiceSpecsBase + { + private Action because; + + public override void Because() + { + because = () => service.run(packageResult, config); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs b/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs index 06363614d8..d13756d9c4 100644 --- a/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs +++ b/src/chocolatey.tests/infrastructure/commands/CommandExecutorSpecs.cs @@ -29,9 +29,11 @@ public abstract class CommandExecutorSpecsBase : TinySpec { protected Mock fileSystem = new Mock(); protected Mock process = new Mock(); + protected CommandExecutor commandExecutor; public override void Context() { + commandExecutor = new CommandExecutor(fileSystem.Object); CommandExecutor.initialize_with(new Lazy(() => fileSystem.Object), () => process.Object); } } @@ -49,7 +51,7 @@ public override void Context() public override void Because() { - result = CommandExecutor.execute("bob", "args", ApplicationParameters.DefaultWaitForExitInSeconds); + result = commandExecutor.execute("bob", "args", ApplicationParameters.DefaultWaitForExitInSeconds); } [Fact] @@ -108,7 +110,7 @@ public override void Context() public override void Because() { - result = CommandExecutor.execute("bob", "args", ApplicationParameters.DefaultWaitForExitInSeconds); + result = commandExecutor.execute("bob", "args", ApplicationParameters.DefaultWaitForExitInSeconds); } [Fact] @@ -136,7 +138,7 @@ public class when_CommandExecutor_does_not_wait_for_exit : CommandExecutorSpecsB public override void Because() { - result = CommandExecutor.execute("bob", "args", waitForExitInSeconds: 0); + result = commandExecutor.execute("bob", "args", waitForExitInSeconds: 0); } [Fact] diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index 4db435631a..8554e78060 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -148,6 +148,7 @@ + diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 7eaf9c7263..984266a7b8 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -47,6 +47,7 @@ public static class ApplicationParameters public static string ChocolateyPackageInfoStoreLocation = _fileSystem.combine_paths(InstallLocation, ".chocolatey"); public static readonly string ChocolateyCommunityFeedPushSource = "https://chocolatey.org/"; public static readonly string UserAgent = "Chocolatey Command Line"; + public static readonly string RegistryValueInstallLocation = "InstallLocation"; /// /// Default is 45 minutes diff --git a/src/chocolatey/infrastructure.app/domain/Registry.cs b/src/chocolatey/infrastructure.app/domain/Registry.cs index 832f9bbf67..a7055654f5 100644 --- a/src/chocolatey/infrastructure.app/domain/Registry.cs +++ b/src/chocolatey/infrastructure.app/domain/Registry.cs @@ -43,7 +43,14 @@ public Registry() public Registry(string user, IEnumerable keys) { User = user; - RegistryKeys = keys.ToList(); + if (keys != null) + { + RegistryKeys = keys.ToList(); + } + else + { + RegistryKeys = new List(); + } } [XmlElement(ElementName = "user")] diff --git a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs index b1a057bdd1..f2814c26d9 100644 --- a/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs +++ b/src/chocolatey/infrastructure.app/registration/ContainerBinding.cs @@ -58,6 +58,7 @@ public void RegisterComponents(Container container) container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); container.Register(Lifestyle.Singleton); + container.Register(Lifestyle.Singleton); //todo:refactor - this should be autowired container.Register>(() => diff --git a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs index b8664bc470..1cad92f1af 100644 --- a/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs +++ b/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs @@ -29,37 +29,50 @@ public class AutomaticUninstallerService : IAutomaticUninstallerService private readonly IChocolateyPackageInformationService _packageInfoService; private readonly IFileSystem _fileSystem; private readonly IRegistryService _registryService; + private readonly ICommandExecutor _commandExecutor; - public AutomaticUninstallerService(IChocolateyPackageInformationService packageInfoService, IFileSystem fileSystem, IRegistryService registryService) + public AutomaticUninstallerService(IChocolateyPackageInformationService packageInfoService, IFileSystem fileSystem, IRegistryService registryService, ICommandExecutor commandExecutor) { _packageInfoService = packageInfoService; _fileSystem = fileSystem; _registryService = registryService; + _commandExecutor = commandExecutor; } public void run(PackageResult packageResult, ChocolateyConfiguration config) { if (!config.Features.AutoUninstaller) { - this.Log().Info(() => "Skipping auto uninstaller due to feature not being enabled."); + this.Log().Info(" Skipping auto uninstaller - AutoUninstaller feature is not enabled."); return; } var pkgInfo = _packageInfoService.get_package_information(packageResult.Package); - if (pkgInfo.RegistrySnapshot == null) return; + if (pkgInfo.RegistrySnapshot == null) + { + this.Log().Info(" Skipping auto uninstaller - No registry snapshot."); + return; + } + + var registryKeys = pkgInfo.RegistrySnapshot.RegistryKeys; + if (registryKeys == null || registryKeys.Count == 0) + { + this.Log().Info(" Skipping auto uninstaller - No registry keys in snapshot."); + return; + } - this.Log().Info(" Running AutoUninstaller..."); + this.Log().Info(" Running auto uninstaller..."); - foreach (var key in pkgInfo.RegistrySnapshot.RegistryKeys.or_empty_list_if_null()) + foreach (var key in registryKeys.or_empty_list_if_null()) { this.Log().Debug(() => " Preparing uninstall key '{0}'".format_with(key.UninstallString)); - if (!_fileSystem.directory_exists(key.InstallLocation) || !_registryService.value_exists(key.KeyPath,"InstallLocation")) + if (!_fileSystem.directory_exists(key.InstallLocation) || !_registryService.value_exists(key.KeyPath, ApplicationParameters.RegistryValueInstallLocation)) { - this.Log().Info(()=> "Skipping auto uninstall. The application appears to have been uninstalled already by other means."); - this.Log().Debug(() => "Searched for install path '{0}' - found? {1}".format_with(key.InstallLocation.escape_curly_braces(), _fileSystem.directory_exists(key.InstallLocation))); - this.Log().Debug(() => "Searched for registry key '{0}' value 'InstallLocation' - found? {1}".format_with(key.KeyPath.escape_curly_braces(), _registryService.value_exists(key.KeyPath, "InstallLocation"))); + this.Log().Info(" Skipping auto uninstaller - The application appears to have been uninstalled already by other means."); + this.Log().Debug(() => " Searched for install path '{0}' - found? {1}".format_with(key.InstallLocation.escape_curly_braces(), _fileSystem.directory_exists(key.InstallLocation))); + this.Log().Debug(() => " Searched for registry key '{0}' value '{1}' - found? {2}".format_with(key.KeyPath.escape_curly_braces(), ApplicationParameters.RegistryValueInstallLocation, _registryService.value_exists(key.KeyPath, ApplicationParameters.RegistryValueInstallLocation))); continue; } @@ -70,12 +83,11 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) uninstallExe = uninstallExe.remove_surrounding_quotes(); this.Log().Debug(() => " Uninstaller path is '{0}'".format_with(uninstallExe)); - + //todo: ultimately we should merge keys with logging if (!key.HasQuietUninstall) { IInstaller installer = new CustomInstaller(); - //refactor this to elsewhere switch (key.InstallerType) { case InstallerType.Msi: @@ -102,16 +114,18 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) this.Log().Debug(() => " Args are '{0}'".format_with(uninstallArgs.@join(" "))); - var exitCode = CommandExecutor.execute( - uninstallExe, uninstallArgs.@join(" "), config.CommandExecutionTimeoutSeconds, + var exitCode = _commandExecutor.execute( + uninstallExe, + uninstallArgs.@join(" "), + config.CommandExecutionTimeoutSeconds, (s, e) => { - if (string.IsNullOrWhiteSpace(e.Data)) return; + if (e == null || string.IsNullOrWhiteSpace(e.Data)) return; this.Log().Debug(() => " [AutoUninstaller] {0}".format_with(e.Data)); }, (s, e) => { - if (string.IsNullOrWhiteSpace(e.Data)) return; + if (e == null || string.IsNullOrWhiteSpace(e.Data)) return; this.Log().Error(() => " [AutoUninstaller] {0}".format_with(e.Data)); }); @@ -124,7 +138,7 @@ public void run(PackageResult packageResult, ChocolateyConfiguration config) } else { - this.Log().Info(() => " AutoUninstaller has successfully uninstalled {0} from your machine.".format_with(packageResult.Package.Id)); + this.Log().Info(() => " Auto uninstaller has successfully uninstalled {0} from your machine.".format_with(packageResult.Package.Id)); } } } diff --git a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs index 1428477e93..3581401400 100644 --- a/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs +++ b/src/chocolatey/infrastructure.app/services/ShimGenerationService.cs @@ -26,6 +26,7 @@ namespace chocolatey.infrastructure.app.services public class ShimGenerationService : IShimGenerationService { private readonly IFileSystem _fileSystem; + private readonly ICommandExecutor _commandExecutor; private const string PATH_TOKEN = "{{path}}"; private const string ICON_PATH_TOKEN = "{{icon_path}}"; private const string OUTPUT_TOKEN = "{{output}}"; @@ -33,9 +34,10 @@ public class ShimGenerationService : IShimGenerationService private readonly IDictionary _shimGenArguments = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - public ShimGenerationService(IFileSystem fileSystem) + public ShimGenerationService(IFileSystem fileSystem,ICommandExecutor commandExecutor) { _fileSystem = fileSystem; + _commandExecutor = commandExecutor; set_shimgen_args_dictionary(); } @@ -92,7 +94,7 @@ public void install(ChocolateyConfiguration configuration, PackageResult package argsForPackage += " --gui"; } - var exitCode = CommandExecutor.execute( + var exitCode = _commandExecutor.execute( _shimGenExePath, argsForPackage, configuration.CommandExecutionTimeoutSeconds, (s, e) => { diff --git a/src/chocolatey/infrastructure.app/services/WebPiService.cs b/src/chocolatey/infrastructure.app/services/WebPiService.cs index e7b04480ee..134336af5b 100644 --- a/src/chocolatey/infrastructure.app/services/WebPiService.cs +++ b/src/chocolatey/infrastructure.app/services/WebPiService.cs @@ -31,13 +31,15 @@ public interface IWebPiService //todo this is the old nuget.exe installer code that needs cleaned up for webpi public class WebPiService : IWebPiService { + private readonly ICommandExecutor _commandExecutor; private const string PACKAGE_NAME_TOKEN = "{{packagename}}"; private readonly string _webPiExePath = "webpicmd"; //ApplicationParameters.Tools.NugetExe; private readonly IDictionary _webPiListArguments = new Dictionary(StringComparer.InvariantCultureIgnoreCase); private readonly IDictionary _webPiInstallArguments = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - public WebPiService() + public WebPiService(ICommandExecutor commandExecutor) { + _commandExecutor = commandExecutor; set_cmd_args_dictionaries(); } @@ -80,7 +82,7 @@ public ConcurrentDictionary install_run(ChocolateyConfigu foreach (var packageToInstall in configuration.PackageNames.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries)) { var argsForPackage = args.Replace(PACKAGE_NAME_TOKEN, packageToInstall); - var exitCode = CommandExecutor.execute( + var exitCode = _commandExecutor.execute( _webPiExePath, argsForPackage, configuration.CommandExecutionTimeoutSeconds, (s, e) => { diff --git a/src/chocolatey/infrastructure/commands/CommandExecutor.cs b/src/chocolatey/infrastructure/commands/CommandExecutor.cs index f557d6c492..6533976eb7 100644 --- a/src/chocolatey/infrastructure/commands/CommandExecutor.cs +++ b/src/chocolatey/infrastructure/commands/CommandExecutor.cs @@ -24,8 +24,13 @@ namespace chocolatey.infrastructure.commands using platforms; using Process = adapters.Process; - public sealed class CommandExecutor + public sealed class CommandExecutor : ICommandExecutor { + public CommandExecutor(IFileSystem fileSystem) + { + file_system_initializer = new Lazy(() => fileSystem); + } + private static Lazy file_system_initializer = new Lazy(() => new DotNetFileSystem()); private static IFileSystem file_system @@ -42,12 +47,12 @@ public static void initialize_with(Lazy file_system, Func initialize_process = process_initializer; } - public static int execute(string process, string arguments, int waitForExitInSeconds) + public int execute(string process, string arguments, int waitForExitInSeconds) { return execute(process, arguments, waitForExitInSeconds, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); } - public static int execute( + public int execute( string process, string arguments, int waitForExitInSeconds, @@ -65,7 +70,7 @@ Action stdErrAction ); } - public static int execute(string process, string arguments, int waitForExitInSeconds, string workingDirectory) + public int execute(string process, string arguments, int waitForExitInSeconds, string workingDirectory) { return execute(process, arguments, waitForExitInSeconds, workingDirectory, null, null, updateProcessPath: true); } @@ -143,12 +148,12 @@ bool updateProcessPath private static void log_output(object sender, DataReceivedEventArgs e) { - "chocolatey".Log().Info(e.Data); + if (e != null) "chocolatey".Log().Info(e.Data); } private static void log_error(object sender, DataReceivedEventArgs e) { - "chocolatey".Log().Error(e.Data); + if (e != null) "chocolatey".Log().Error(e.Data); } } } \ No newline at end of file diff --git a/src/chocolatey/infrastructure/commands/ICommandExecutor.cs b/src/chocolatey/infrastructure/commands/ICommandExecutor.cs new file mode 100644 index 0000000000..585122d1cd --- /dev/null +++ b/src/chocolatey/infrastructure/commands/ICommandExecutor.cs @@ -0,0 +1,34 @@ +// Copyright © 2011 - Present 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.commands +{ + using System; + using System.Diagnostics; + + public interface ICommandExecutor + { + int execute(string process, string arguments, int waitForExitInSeconds); + int execute(string process, string arguments, int waitForExitInSeconds, string workingDirectory); + + int execute( + string process, + string arguments, + int waitForExitInSeconds, + Action stdOutAction, + Action stdErrAction + ); + } +} \ No newline at end of file From 51fc5de28d153f8e5c7352c13fd9e19ebdaa5bb7 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 20 Jan 2015 09:05:49 -0600 Subject: [PATCH 15/19] (spec) MockLogger / TinySpec improvements --- src/chocolatey.tests/MockLogger.cs | 24 ++++++++++++++---------- src/chocolatey.tests/TinySpec.cs | 14 +++++++++++--- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/chocolatey.tests/MockLogger.cs b/src/chocolatey.tests/MockLogger.cs index 8097fbdc24..ae054b0622 100644 --- a/src/chocolatey.tests/MockLogger.cs +++ b/src/chocolatey.tests/MockLogger.cs @@ -32,6 +32,10 @@ public enum LogLevel public class MockLogger : Mock, ILog, ILog { + public MockLogger() + { + } + private readonly Lazy>> _messages = new Lazy>>(); public ConcurrentDictionary> Messages @@ -57,61 +61,61 @@ public void LogMessage(LogLevel logLevel, string message) public void Debug(string message, params object[] formatting) { LogMessage(LogLevel.Debug, message.format_with(formatting)); - Object.Debug(message, formatting); + Object.Debug(message.format_with(formatting)); } public void Debug(Func message) { LogMessage(LogLevel.Debug, message()); - Object.Debug(message); + Object.Debug(message()); } public void Info(string message, params object[] formatting) { LogMessage(LogLevel.Info, message.format_with(formatting)); - Object.Info(message, formatting); + Object.Info(message.format_with(formatting)); } public void Info(Func message) { LogMessage(LogLevel.Info, message()); - Object.Info(message); + Object.Info(message()); } public void Warn(string message, params object[] formatting) { LogMessage(LogLevel.Warn, message.format_with(formatting)); - Object.Warn(message, formatting); + Object.Warn(message.format_with(formatting)); } public void Warn(Func message) { LogMessage(LogLevel.Warn, message()); - Object.Warn(message); + Object.Warn(message()); } public void Error(string message, params object[] formatting) { LogMessage(LogLevel.Error, message.format_with(formatting)); - Object.Error(message, formatting); + Object.Error(message.format_with(formatting)); } public void Error(Func message) { LogMessage(LogLevel.Error, message()); - Object.Error(message); + Object.Error(message()); } public void Fatal(string message, params object[] formatting) { LogMessage(LogLevel.Fatal, message.format_with(formatting)); - Object.Fatal(message, formatting); + Object.Fatal(message.format_with(formatting)); } public void Fatal(Func message) { LogMessage(LogLevel.Fatal, message()); - Object.Fatal(message); + Object.Fatal(message()); } } } \ No newline at end of file diff --git a/src/chocolatey.tests/TinySpec.cs b/src/chocolatey.tests/TinySpec.cs index ad8f39d427..840d676ebf 100644 --- a/src/chocolatey.tests/TinySpec.cs +++ b/src/chocolatey.tests/TinySpec.cs @@ -27,13 +27,18 @@ namespace chocolatey.tests [TestFixture] public abstract class TinySpec { - public MockLogger MockLogger { get; set; } + private MockLogger _mockLogger; + + public MockLogger MockLogger + { + get { return _mockLogger; } + } [TestFixtureSetUp] public void Setup() { - MockLogger = new MockLogger(); - Log.InitializeWith(MockLogger); + _mockLogger = new MockLogger(); + Log.InitializeWith(_mockLogger); Context(); Because(); } @@ -65,7 +70,10 @@ public virtual void AfterEachSpec() [TestFixtureTearDown] public void TearDown() { + AfterObservations(); + _mockLogger = null; + Log.InitializeWith(new NullLog()); } public virtual void AfterObservations() From c45f5fcebcc66a0e18ab761d8a7dee7086d02bc4 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 20 Jan 2015 09:06:34 -0600 Subject: [PATCH 16/19] (installer) Add installertype to IInstaller Allows installer to report the InstallerType it implements. --- .../infrastructure.app/domain/CustomInstaller.cs | 5 +++++ src/chocolatey/infrastructure.app/domain/IInstaller.cs | 1 + .../infrastructure.app/domain/InnoSetupInstaller.cs | 5 +++++ .../infrastructure.app/domain/InstallShieldInstaller.cs | 5 +++++ src/chocolatey/infrastructure.app/domain/MsiInstaller.cs | 7 ++++++- src/chocolatey/infrastructure.app/domain/NsisInstaller.cs | 5 +++++ 6 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/chocolatey/infrastructure.app/domain/CustomInstaller.cs b/src/chocolatey/infrastructure.app/domain/CustomInstaller.cs index 3f99f3d394..e4b98242dc 100644 --- a/src/chocolatey/infrastructure.app/domain/CustomInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/CustomInstaller.cs @@ -35,6 +35,11 @@ public CustomInstaller() ValidExitCodes = new List {0}; } + public InstallerType InstallerType + { + get { return InstallerType.Custom; } + } + public string InstallExecutable { get; private set; } public string SilentInstall { get; private set; } public string NoReboot { get; private set; } diff --git a/src/chocolatey/infrastructure.app/domain/IInstaller.cs b/src/chocolatey/infrastructure.app/domain/IInstaller.cs index f5d44c3c69..273a88a852 100644 --- a/src/chocolatey/infrastructure.app/domain/IInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/IInstaller.cs @@ -19,6 +19,7 @@ namespace chocolatey.infrastructure.app.domain public interface IInstaller { + InstallerType InstallerType { get; } string InstallExecutable { get; } string SilentInstall { get; } string NoReboot { get; } diff --git a/src/chocolatey/infrastructure.app/domain/InnoSetupInstaller.cs b/src/chocolatey/infrastructure.app/domain/InnoSetupInstaller.cs index 16f1567434..1a5ec4d1e8 100644 --- a/src/chocolatey/infrastructure.app/domain/InnoSetupInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/InnoSetupInstaller.cs @@ -41,6 +41,11 @@ public InnoSetupInstaller() ValidExitCodes = new List {0}; } + public InstallerType InstallerType + { + get { return InstallerType.InnoSetup; } + } + public string InstallExecutable { get; private set; } public string SilentInstall { get; private set; } public string NoReboot { get; private set; } diff --git a/src/chocolatey/infrastructure.app/domain/InstallShieldInstaller.cs b/src/chocolatey/infrastructure.app/domain/InstallShieldInstaller.cs index 96e14087cb..8b009073b3 100644 --- a/src/chocolatey/infrastructure.app/domain/InstallShieldInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/InstallShieldInstaller.cs @@ -41,6 +41,11 @@ public InstallShieldInstaller() ValidExitCodes = new List {0}; } + public InstallerType InstallerType + { + get { return InstallerType.InstallShield; } + } + public string InstallExecutable { get; private set; } public string SilentInstall { get; private set; } public string NoReboot { get; private set; } diff --git a/src/chocolatey/infrastructure.app/domain/MsiInstaller.cs b/src/chocolatey/infrastructure.app/domain/MsiInstaller.cs index 732261ca4a..b082cde4e9 100644 --- a/src/chocolatey/infrastructure.app/domain/MsiInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/MsiInstaller.cs @@ -40,7 +40,7 @@ public MsiInstaller() // http://msdn.microsoft.com/en-us/library/aa372064.aspx CustomInstallLocation = "TARGETDIR=\"{0}\"".format_with(InstallTokens.CUSTOM_INSTALL_LOCATION); // http://msdn.microsoft.com/en-us/library/aa370856.aspx - Language = "ProductLanguage={LANGUAGE}".format_with(InstallTokens.LANGUAGE); + Language = "ProductLanguage={0}".format_with(InstallTokens.LANGUAGE); // http://msdn.microsoft.com/en-us/library/aa367559.aspx OtherInstallOptions = "ALLUSERS=1 DISABLEDESKTOPSHORTCUT=1 ADDDESKTOPICON=0 ADDSTARTMENU=0"; UninstallExecutable = "msiexec.exe"; @@ -50,6 +50,11 @@ public MsiInstaller() ValidExitCodes = new List {0, 3010}; } + public InstallerType InstallerType + { + get { return InstallerType.Msi; } + } + public string InstallExecutable { get; private set; } public string SilentInstall { get; private set; } public string NoReboot { get; private set; } diff --git a/src/chocolatey/infrastructure.app/domain/NsisInstaller.cs b/src/chocolatey/infrastructure.app/domain/NsisInstaller.cs index fc2748a0a4..a0b4f7c05f 100644 --- a/src/chocolatey/infrastructure.app/domain/NsisInstaller.cs +++ b/src/chocolatey/infrastructure.app/domain/NsisInstaller.cs @@ -43,6 +43,11 @@ public NsisInstaller() ValidExitCodes = new List {0}; } + public InstallerType InstallerType + { + get { return InstallerType.Nsis; } + } + public string InstallExecutable { get; private set; } public string SilentInstall { get; private set; } public string NoReboot { get; private set; } From b17cffbb061b69ea6c8ffe14a55a8b5b5337f9c2 Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 20 Jan 2015 09:07:13 -0600 Subject: [PATCH 17/19] (maint) format_with should not error on null string --- src/chocolatey/StringExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chocolatey/StringExtensions.cs b/src/chocolatey/StringExtensions.cs index 867177de4f..767e338258 100644 --- a/src/chocolatey/StringExtensions.cs +++ b/src/chocolatey/StringExtensions.cs @@ -31,6 +31,8 @@ public static class StringExtensions /// A formatted string. public static string format_with(this string input, params object[] formatting) { + if (string.IsNullOrWhiteSpace(input)) return string.Empty; + return string.Format(input, formatting); } From cab61052257c8f93d9fc17855bbf2282e3f5d69b Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 20 Jan 2015 09:07:59 -0600 Subject: [PATCH 18/19] (GH-9) uninstall switch specs Ensure uninstaller uses the right switches on uninstall based on HasQuietUninstall true or false. --- .../AutomaticUninstallerServiceSpecs.cs | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs index 0a089d1504..1538229930 100644 --- a/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs @@ -268,14 +268,100 @@ public void should_call_command_executor() } } - public class when_AutomaticUninstallerService_is_run : AutomaticUninstallerServiceSpecsBase + public class when_AutomaticUninstallerService_defines_uninstall_switches : AutomaticUninstallerServiceSpecsBase { private Action because; + private readonly string registryUninstallArgs = "bob"; public override void Because() { because = () => service.run(packageResult, config); } + + public void reset() + { + Context(); + } + + private void test_installertype(IInstaller installer,bool useInstallerDefaultArgs) + { + reset(); + registryKeys.Add(new RegistryApplicationKey + { + InstallLocation = @"C:\Program Files (x86)\WinDirStat", + UninstallString = "{0} {1}".format_with(originalUninstallString,registryUninstallArgs), + HasQuietUninstall = false, + KeyPath = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\WinDirStat", + InstallerType = installer.InstallerType, + }); + packageInformation.RegistrySnapshot = new Registry("123", registryKeys); + + because(); + + var uninstallArgs = useInstallerDefaultArgs ? installer.build_uninstall_command_arguments().trim_safe() : registryUninstallArgs.trim_safe(); + + commandExecutor.Verify(c => c.execute(expectedUninstallString, uninstallArgs, It.IsAny(), It.IsAny>(), It.IsAny>()), Times.Never); + } + + [Fact] + public void should_use_CustomInstaller_uninstall_args_when_installtype_is_unknown_and_has_quiet_uninstall_is_false() + { + test_installertype(new CustomInstaller(), useInstallerDefaultArgs: true); + } + + [Fact] + public void should_use_registry_uninstall_args_when_installtype_is_unknown_and_has_quiet_uninstall_is_true() + { + test_installertype(new CustomInstaller(), useInstallerDefaultArgs: false); + } + + [Fact] + public void should_use_MsiInstaller_uninstall_args_when_installtype_is_msi_and_has_quiet_uninstall_is_false() + { + test_installertype(new MsiInstaller(), useInstallerDefaultArgs: true); + } + + [Fact] + public void should_use_registry_uninstall_args_when_installtype_is_msi_and_has_quiet_uninstall_is_true() + { + test_installertype(new MsiInstaller(), useInstallerDefaultArgs: false); + } + + [Fact] + public void should_use_InnoSetupInstaller_uninstall_args_when_installtype_is_innosetup_and_has_quiet_uninstall_is_false() + { + test_installertype(new InnoSetupInstaller(), useInstallerDefaultArgs: true); + } + + [Fact] + public void should_use_registry_uninstall_args_when_installtype_is_innosetup_and_has_quiet_uninstall_is_true() + { + test_installertype(new InnoSetupInstaller(), useInstallerDefaultArgs: false); + } + + [Fact] + public void should_use_InstallShieldInstaller_uninstall_args_when_installtype_is_installshield_and_has_quiet_uninstall_is_false() + { + test_installertype(new InstallShieldInstaller(), useInstallerDefaultArgs: true); + } + + [Fact] + public void should_use_registry_uninstall_args_when_installtype_is_installshield_and_has_quiet_uninstall_is_true() + { + test_installertype(new InstallShieldInstaller(), useInstallerDefaultArgs: false); + } + + [Fact] + public void should_use_NsisInstaller_uninstall_args_when_installtype_is_nsis_and_has_quiet_uninstall_is_false() + { + test_installertype(new NsisInstaller(), useInstallerDefaultArgs: true); + } + + [Fact] + public void should_use_registry_uninstall_args_when_installtype_is_nsis_and_has_quiet_uninstall_is_true() + { + test_installertype(new NsisInstaller(), useInstallerDefaultArgs: false); + } } } } \ No newline at end of file From ffc5e6e778a8e178ea413184eb25d41c5dadbf2c Mon Sep 17 00:00:00 2001 From: Rob Reynolds Date: Tue, 20 Jan 2015 12:49:18 -0600 Subject: [PATCH 19/19] (maint) add note to address logging specs issues --- .../services/AutomaticUninstallerServiceSpecs.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs index 1538229930..85a0f384b6 100644 --- a/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/services/AutomaticUninstallerServiceSpecs.cs @@ -32,6 +32,8 @@ namespace chocolatey.tests.infrastructure.app.services using chocolatey.infrastructure.results; using IFileSystem = chocolatey.infrastructure.filesystem.IFileSystem; + //todo: come back and fix the logging tests - determine why it gets cycled out during test suite runs. + public class AutomaticUninstallerServiceSpecs { public abstract class AutomaticUninstallerServiceSpecsBase : TinySpec