Skip to content

Commit

Permalink
(GH-47) Rollback when upgrade fails
Browse files Browse the repository at this point in the history
Allow the user to determine if they want the previous package files to
be put back when an upgrade fails or is canceled. Handle state of
install, upgrade or uninstall that may have previous package folder
still around (usually due to unforeseen errors or user ctrl+c during
choco run).
  • Loading branch information
ferventcoder committed Feb 2, 2015
1 parent da39da0 commit 4197b07
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 65 deletions.
1 change: 1 addition & 0 deletions src/chocolatey/infrastructure.app/ApplicationParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static class ApplicationParameters
public static readonly string ChocolateyCommunityFeedPushSource = "https://chocolatey.org/";
public static readonly string UserAgent = "Chocolatey Command Line";
public static readonly string RegistryValueInstallLocation = "InstallLocation";
public static readonly string RollbackPackageSuffix = "._.previous";

/// <summary>
/// Default is 45 minutes
Expand Down
10 changes: 9 additions & 1 deletion src/chocolatey/infrastructure.app/nuget/NugetCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,15 @@ public static IPackageManager GetPackageManager(ChocolateyConfiguration configur
if (chocoPathResolver != null)
{
chocoPathResolver.UseSideBySidePaths = !chocoPathResolver.UseSideBySidePaths;
packageManager.UninstallPackage(pkg, forceRemove: configuration.Force, removeDependencies: false);

// an unfound package folder can cause an endless loop.
// look for it and ignore it if doesn't line up with versioning
if (nugetPackagesFileSystem.DirectoryExists(chocoPathResolver.GetInstallPath(pkg)))
{
// this causes this to be called again, which should then call the uninstallSuccessAction below
packageManager.UninstallPackage(pkg, forceRemove: configuration.Force, removeDependencies: false);
}

chocoPathResolver.UseSideBySidePaths = configuration.AllowMultipleVersions;
}
}
Expand Down
102 changes: 65 additions & 37 deletions src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace chocolatey.infrastructure.app.services
using System.IO;
using System.Linq;
using System.Threading;
using commandline;
using configuration;
using domain;
using filesystem;
Expand Down Expand Up @@ -195,7 +196,7 @@ public void handle_package_result(PackageResult packageResult, ChocolateyConfigu
if (!packageResult.Success)
{
this.Log().Error(ChocolateyLoggers.Important, "{0} {1} not successful.".format_with(packageResult.Name, commandName.to_string()));
handle_unsuccessful_install(packageResult);
handle_unsuccessful_install(config, packageResult);

return;
}
Expand Down Expand Up @@ -297,29 +298,29 @@ public ConcurrentDictionary<string, PackageResult> uninstall_run(ChocolateyConfi
var packageUninstalls = _nugetService.uninstall_run(
config,
(packageResult) =>
{
if (!_fileSystem.directory_exists(packageResult.InstallLocation))
{
if (!_fileSystem.directory_exists(packageResult.InstallLocation))
{
packageResult.InstallLocation += ".{0}".format_with(packageResult.Package.Version.to_string());
}
packageResult.InstallLocation += ".{0}".format_with(packageResult.Package.Version.to_string());
}

_shimgenService.uninstall(config, packageResult);
_shimgenService.uninstall(config, packageResult);

if (!config.SkipPackageInstallProvider)
{
_powershellService.uninstall(config, packageResult);
}
if (!config.SkipPackageInstallProvider)
{
_powershellService.uninstall(config, packageResult);
}

_autoUninstallerService.run(packageResult, config);
_autoUninstallerService.run(packageResult, config);

if (packageResult.Success)
{
//todo: v2 clean up package information store for things no longer installed (call it compact?)
_packageInfoService.remove_package_information(packageResult.Package);
}
if (packageResult.Success)
{
//todo: v2 clean up package information store for things no longer installed (call it compact?)
_packageInfoService.remove_package_information(packageResult.Package);
}

//todo:prevent reboots
});
//todo:prevent reboots
});

var uninstallFailures = packageUninstalls.Count(p => !p.Value.Success);
this.Log().Warn(() => @"{0}{1} uninstalled {2}/{3} packages. {4} packages failed.{0}See the log for details.".format_with(
Expand All @@ -346,25 +347,6 @@ public ConcurrentDictionary<string, PackageResult> uninstall_run(ChocolateyConfi
return packageUninstalls;
}

private void handle_unsuccessful_install(PackageResult packageResult)
{
foreach (var message in packageResult.Messages.Where(m => m.MessageType == ResultType.Error))
{
this.Log().Error(message.Message);
}

_fileSystem.create_directory_if_not_exists(ApplicationParameters.PackageFailuresLocation);
foreach (var file in _fileSystem.get_files(packageResult.InstallLocation, "*.*", SearchOption.AllDirectories))
{
var badFile = file.Replace(ApplicationParameters.PackagesLocation, ApplicationParameters.PackageFailuresLocation);
_fileSystem.create_directory_if_not_exists(_fileSystem.get_directory_name(badFile));
_fileSystem.move_file(file, badFile);
//_fileSystem.copy_file_unsafe(file, badFile,overwriteTheExistingFile:true);
}
Thread.Sleep(2000); // sleep for enough time that the for half a second to allow the folder to be cleared
_fileSystem.delete_directory(packageResult.InstallLocation, recursive: true);
}

private void ensure_bad_package_path_is_clean(ChocolateyConfiguration config, PackageResult packageResult)
{
try
Expand All @@ -387,5 +369,51 @@ private void ensure_bad_package_path_is_clean(ChocolateyConfiguration config, Pa
}
}
}

private void handle_unsuccessful_install(ChocolateyConfiguration config, PackageResult packageResult)
{
foreach (var message in packageResult.Messages.Where(m => m.MessageType == ResultType.Error))
{
this.Log().Error(message.Message);
}

move_bad_package_to_failure_location(packageResult);
rollback_previous_version(config, packageResult);
}

private void move_bad_package_to_failure_location(PackageResult packageResult)
{
_fileSystem.create_directory_if_not_exists(ApplicationParameters.PackageFailuresLocation);

_fileSystem.move_directory(packageResult.InstallLocation, packageResult.InstallLocation.Replace(ApplicationParameters.PackagesLocation, ApplicationParameters.PackageFailuresLocation));
_fileSystem.delete_directory(packageResult.InstallLocation, recursive: true);
}

private void rollback_previous_version(ChocolateyConfiguration config, PackageResult packageResult)
{
var rollbackDirectory = packageResult.InstallLocation + ApplicationParameters.RollbackPackageSuffix;
if (!_fileSystem.directory_exists(rollbackDirectory)) return;

var rollback = true;
if (config.PromptForConfirmation)
{
var selection = InteractivePrompt.prompt_for_confirmation(" Unsuccessful install of {0}.{1} Do you want to rollback to previous version (package files only)?".format_with(packageResult.Name,Environment.NewLine), new[] { "yes", "no" }, "yes", requireAnswer: true);
if (selection.is_equal_to("no")) rollback = false;
}

if (rollback)
{
_fileSystem.move_directory(rollbackDirectory, packageResult.InstallLocation);
}

try
{
_fileSystem.delete_directory_if_exists(rollbackDirectory, recursive: true);
}
catch (Exception ex)
{
this.Log().Warn("Attempted to remove '{0}' but had an error:{1} {2}".format_with(rollbackDirectory, Environment.NewLine, ex.Message));
}
}
}
}
73 changes: 64 additions & 9 deletions src/chocolatey/infrastructure.app/services/NugetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,11 @@ public ConcurrentDictionary<string, PackageResult> install_run(ChocolateyConfigu
else
{
//todo: get smarter about realizing multiple versions have been installed before and allowing that

remove_existing_rollback_directory(packageName);

IPackage installedPackage = packageManager.LocalRepository.FindPackage(packageName);

if (installedPackage != null && (version == null || version == installedPackage.Version) && !config.Force)
{
string logMessage = "{0} v{1} already installed.{2} Use --force to reinstall, specify a version to install, or try upgrade.".format_with(installedPackage.Id, installedPackage.Version, Environment.NewLine);
Expand Down Expand Up @@ -307,6 +310,19 @@ public ConcurrentDictionary<string, PackageResult> install_run(ChocolateyConfigu
return packageInstalls;
}

private void remove_existing_rollback_directory(string packageName)
{
var rollbackDirectory = _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, packageName) + ApplicationParameters.RollbackPackageSuffix;
try
{
_fileSystem.delete_directory_if_exists(rollbackDirectory, recursive: true);
}
catch (Exception ex)
{
this.Log().Warn("Attempted to remove '{0}' but had an error:{1} {2}".format_with(rollbackDirectory, Environment.NewLine, ex.Message));
}
}

public void upgrade_noop(ChocolateyConfiguration config, Action<PackageResult> continueAction)
{
config.Force = false;
Expand All @@ -324,23 +340,27 @@ public ConcurrentDictionary<string, PackageResult> upgrade_run(ChocolateyConfigu
var packageInstalls = new ConcurrentDictionary<string, PackageResult>();

SemanticVersion version = config.Version != null ? new SemanticVersion(config.Version) : null;
var packageManager = NugetCommon.GetPackageManager(config, _nugetLogger,
installSuccessAction: (e) =>
{
var pkg = e.Package;
var results = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath));
results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction));
var packageManager = NugetCommon.GetPackageManager(
config,
_nugetLogger,
installSuccessAction: (e) =>
{
var pkg = e.Package;
var results = packageInstalls.GetOrAdd(pkg.Id.to_lower(), new PackageResult(pkg, e.InstallPath));
results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.ContinueChocolateyAction));

if (continueAction != null) continueAction.Invoke(results);
},
uninstallSuccessAction: null);
if (continueAction != null) continueAction.Invoke(results);
},
uninstallSuccessAction: null);

set_package_names_if_all_is_specified(config, () => { config.IgnoreDependencies = true; });

foreach (string packageName in config.PackageNames.Split(new[] {ApplicationParameters.PackageNamesSeparator}, StringSplitOptions.RemoveEmptyEntries).or_empty_list_if_null())
{
//todo: get smarter about realizing multiple versions have been installed before and allowing that

remove_existing_rollback_directory(packageName);

IPackage installedPackage = packageManager.LocalRepository.FindPackage(packageName);

if (installedPackage == null)
Expand Down Expand Up @@ -419,6 +439,7 @@ public ConcurrentDictionary<string, PackageResult> upgrade_run(ChocolateyConfigu
packageName,
version == null ? null : version.ToString()))
{
backup_existing_version(config, installedPackage);
packageManager.UpdatePackage(availablePackage, updateDependencies: !config.IgnoreDependencies, allowPrereleaseVersions: config.Prerelease);
}
}
Expand All @@ -428,6 +449,29 @@ public ConcurrentDictionary<string, PackageResult> upgrade_run(ChocolateyConfigu
return packageInstalls;
}

public void backup_existing_version(ChocolateyConfiguration config, IPackage installedPackage)
{
var pathResolver = NugetCommon.GetPathResolver(config, NugetCommon.GetNuGetFileSystem(config, _nugetLogger));
var pkgInstallPath = pathResolver.GetInstallPath(installedPackage);
if (!_fileSystem.directory_exists(pkgInstallPath))
{
var chocoPathResolver = pathResolver as ChocolateyPackagePathResolver;
if (chocoPathResolver != null)
{
chocoPathResolver.UseSideBySidePaths = !chocoPathResolver.UseSideBySidePaths;
pkgInstallPath = chocoPathResolver.GetInstallPath(installedPackage);
}
}

if (_fileSystem.directory_exists(pkgInstallPath))
{
this.Log().Debug("Backing up existing {0} prior to upgrade.".format_with(installedPackage.Id));

var backupLocation = pkgInstallPath + ApplicationParameters.RollbackPackageSuffix;
_fileSystem.copy_directory(pkgInstallPath,backupLocation,overwriteExisting:true);
}
}

public void uninstall_noop(ChocolateyConfiguration config, Action<PackageResult> continueAction)
{
var results = uninstall_run(config, continueAction, performAction: false);
Expand Down Expand Up @@ -456,6 +500,7 @@ public ConcurrentDictionary<string, PackageResult> uninstall_run(ChocolateyConfi
"chocolatey".Log().Info(ChocolateyLoggers.Important, " {0} has been successfully uninstalled.".format_with(pkg.Id));
});

var loopCount = 0;
packageManager.PackageUninstalling += (s, e) =>
{
var pkg = e.Package;
Expand All @@ -467,10 +512,18 @@ public ConcurrentDictionary<string, PackageResult> uninstall_run(ChocolateyConfi
{
results.Messages.Add(new ResultMessage(ResultType.Debug, ApplicationParameters.Messages.NugetEventActionHeader));
"chocolatey".Log().Info(ChocolateyLoggers.Important, logMessage);
loopCount = 0;
}
else
{
"chocolatey".Log().Debug(ChocolateyLoggers.Important, "Another time through!{0}{1}".format_with(Environment.NewLine, logMessage));
loopCount += 1;
}

if (loopCount == 10)
{
this.Log().Warn("Loop detected. Attempting to break out. Check for issues with {0}".format_with(pkg.Id));
return;
}

// is this the latest version or have you passed --sxs? This is the only way you get through to the continue action.
Expand All @@ -496,6 +549,8 @@ public ConcurrentDictionary<string, PackageResult> uninstall_run(ChocolateyConfi

foreach (string packageName in config.PackageNames.Split(new[] {ApplicationParameters.PackageNamesSeparator}, StringSplitOptions.RemoveEmptyEntries).or_empty_list_if_null())
{
remove_existing_rollback_directory(packageName);

IList<IPackage> installedPackageVersions = new List<IPackage>();
if (string.IsNullOrWhiteSpace(config.Version))
{
Expand Down
Loading

0 comments on commit 4197b07

Please sign in to comment.