diff --git a/.teamcity/settings.kts b/.teamcity/settings.kts index 4ae9eedbfe..f4769c551f 100644 --- a/.teamcity/settings.kts +++ b/.teamcity/settings.kts @@ -381,6 +381,7 @@ object ChocolateyPosix : BuildType({ params { param("env.CAKE_NUGET_SOURCE", "") // The Cake version we use has issues with authing to our private source on Linux param("env.PRIMARY_NUGET_SOURCE", "") // As above there are issues with authing to our private source on Linux + param("env.NUGETDEVRESTORE_SOURCE", "") // As above there are issues with authing to our private source on Linux param("env.CHOCOLATEY_VERSION", "%dep.Chocolatey.build.number%") param("env.CHOCOLATEY_OFFICIAL_KEY", "%system.teamcity.build.checkoutDir%/chocolatey.official.snk") password("env.GITHUB_PAT", "%system.GitHubPAT%", display = ParameterDisplay.HIDDEN, readOnly = true) diff --git a/docs/legal/CREDITS.pdf b/docs/legal/CREDITS.pdf index 9627f8b67c..c39d541cca 100644 Binary files a/docs/legal/CREDITS.pdf and b/docs/legal/CREDITS.pdf differ diff --git a/src/chocolatey.tests.integration/Scenario.cs b/src/chocolatey.tests.integration/Scenario.cs index b6c24878fb..8a6dd8bb81 100644 --- a/src/chocolatey.tests.integration/Scenario.cs +++ b/src/chocolatey.tests.integration/Scenario.cs @@ -192,16 +192,22 @@ public static void InstallPackage(ChocolateyConfiguration config, string package NUnitSetup.MockLogger.Messages.Clear(); } - public static void AddFiles(IEnumerable> files) + public static void AddFiles(IEnumerable<(string name, string content)> files) { foreach (var file in files) { - if (_fileSystem.FileExists(file.Item1)) - { - _fileSystem.DeleteFile(file.Item1); - } - _fileSystem.WriteFile(file.Item1, file.Item2); + AddFile(file.name, file.content); + } + } + + public static void AddFile(string name, string content) + { + if (_fileSystem.FileExists(name)) + { + _fileSystem.DeleteFile(name); } + + _fileSystem.WriteFile(name, content); } public static void CreateDirectory(string directoryPath) diff --git a/src/chocolatey.tests.integration/scenarios/InstallScenarios.cs b/src/chocolatey.tests.integration/scenarios/InstallScenarios.cs index 20a2b1153f..1a8892c101 100644 --- a/src/chocolatey.tests.integration/scenarios/InstallScenarios.cs +++ b/src/chocolatey.tests.integration/scenarios/InstallScenarios.cs @@ -4208,6 +4208,79 @@ public void Should_have_executed_chocolateyInstall_script() } } + public class When_installing_a_package_with_corrupt_local_package_already_on_disk : ScenariosBase + { + private PackageResult _packageResult; + + public override void Context() + { + base.Context(); + + var corruptPackageFolder = Path.Combine(Scenario.GetPackageInstallPath(), Configuration.PackageNames); + Scenario.CreateDirectory(corruptPackageFolder); + + // Add a corrupt nuspec (full of null bytes) as an "already installed, but corrupted" package. + // The nupkg is intentionally missing here, since NuGet doesn't recognise the package as being "installed" + // even if files are present at the target install location, so it will blindly attempt to install a package. + var corruptNuspec = Path.Combine(corruptPackageFolder, Configuration.PackageNames + ".nuspec"); + Scenario.AddFile(corruptNuspec, new string((char)0, 1024)); + } + + public override void Because() + { + Results = Service.Install(Configuration); + _packageResult = Results.FirstOrDefault().Value; + } + + [Fact] + public void Should_not_install_where_install_location_reports() + { + Assert.That(_packageResult.InstallLocation ?? string.Empty, Is.Not.EqualTo(string.Empty)); + Assert.That(_packageResult.InstallLocation, Does.Not.Exist); + } + + [Fact] + public void Should_move_the_package_to_lib_bad_directory() + { + var packageDir = Path.Combine(Scenario.GetTopLevel(), "lib-bad", Configuration.PackageNames); + + Assert.That(packageDir, Does.Exist); + } + + + [Fact] + public void Should_contain_a_warning_message_that_it_did_not_install_successfully() + { + MockLogger.Messages.Should().ContainKey(LogLevel.Warn.ToStringSafe()) + .WhoseValue.Should().Contain(m => m.Contains("0/1")); + } + + + [Fact] + public void Should_not_have_a_successful_package_result() + { + _packageResult.Success.Should().BeFalse(); + } + + [Fact] + public void Should_not_have_inconclusive_package_result() + { + _packageResult.Inconclusive.Should().BeFalse(); + } + + [Fact] + public void Config_should_match_package_result_name() + { + _packageResult.Name.Should().Be(Configuration.PackageNames); + } + + [Fact] + public void Should_have_an_empty_version_string() + { + _packageResult.Version.Should().Be(string.Empty); + } + } + public class When_installing_a_package_with_non_normalized_version : ScenariosBase { private PackageResult _packageResult; diff --git a/src/chocolatey.tests.integration/scenarios/PackScenarios.cs b/src/chocolatey.tests.integration/scenarios/PackScenarios.cs index b0f2ddb49c..d086fe72a2 100644 --- a/src/chocolatey.tests.integration/scenarios/PackScenarios.cs +++ b/src/chocolatey.tests.integration/scenarios/PackScenarios.cs @@ -60,7 +60,7 @@ public override void BeforeEachSpec() protected void AddFile(string fileName, string fileContent) { - Scenario.AddFiles(new[] { new Tuple(fileName, fileContent) }); + Scenario.AddFile(fileName, fileContent); } } @@ -136,7 +136,7 @@ public override void Context() { Configuration = Scenario.Pack(); Scenario.Reset(Configuration); - Scenario.AddFiles(new[] { new Tuple("myPackage.nuspec", GetNuspecContent()) }); + Scenario.AddFile("myPackage.nuspec", GetNuspecContent()); if (!string.IsNullOrEmpty(ExpectedSubDirectory)) { @@ -600,7 +600,7 @@ public override void Context() Scenario.Reset(Configuration); Configuration.Version = "0.1.0"; Configuration.PackCommand.Properties.Add("commitId", "1234abcd"); - Scenario.AddFiles(new[] { new Tuple("myPackage.nuspec", NuspecContentWithVariables) }); + Scenario.AddFile("myPackage.nuspec", NuspecContentWithVariables); } public override void Because() diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index 14742a25a2..6db192d47e 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -1823,7 +1823,11 @@ private void MovePackageToFailedPackagesLocation(PackageResult packageResult) if (!string.IsNullOrWhiteSpace(packageResult.InstallLocation) && _fileSystem.DirectoryExists(packageResult.InstallLocation)) { - var normalizedVersion = new NuGetVersion(packageResult.Version).ToNormalizedStringChecked(); + // If there's no version information (certain edge cases on package install errors), fallback to an unknown version with a timestamp + // so this can't fail out unexpectedly. + var normalizedVersion = string.IsNullOrEmpty(packageResult.Version) + ? "unknown-{0}".FormatWith(DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-ss")) + : new NuGetVersion(packageResult.Version).ToNormalizedStringChecked(); var failuresFolder = _fileSystem.CombinePaths(ApplicationParameters.PackageFailuresLocation, packageResult.Name, normalizedVersion); if (_filesService.MovePackageUsingBackupStrategy(packageResult.InstallLocation, failuresFolder, restoreSource: false)) @@ -1835,7 +1839,7 @@ private void MovePackageToFailedPackagesLocation(PackageResult packageResult) private void RestorePreviousPackageVersion(ChocolateyConfiguration config, PackageResult packageResult) { - if (packageResult.InstallLocation == null) + if (string.IsNullOrEmpty(packageResult.InstallLocation) || string.IsNullOrEmpty(packageResult.Version)) { return; } diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 6313c337f5..6fe23d5dbe 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -50,6 +50,7 @@ using NuGet.Resolver; using NuGet.Versioning; using static chocolatey.StringResources; +using System.Xml; namespace chocolatey.infrastructure.app.services { @@ -954,7 +955,6 @@ Version was specified as '{0}'. It is possible that version } } - try { //TODO, do sanity check here. @@ -975,13 +975,18 @@ Version was specified as '{0}'. It is possible that version _nugetLogger, CancellationToken.None).GetAwaiter().GetResult()) { ValidatePackageHash(config, packageDependencyInfo, downloadResult); - - nugetProject.InstallPackageAsync( - packageDependencyInfo, - downloadResult, - projectContext, - CancellationToken.None).GetAwaiter().GetResult(); - + try + { + nugetProject.InstallPackageAsync( + packageDependencyInfo, + downloadResult, + projectContext, + CancellationToken.None).GetAwaiter().GetResult(); + } + catch (XmlException) + { + throw new ApplicationException("An unexpected error was encountered parsing a malformed nuspec file. This may occur if corrupt files are present in the package's install directory."); + } } var installedPath = nugetProject.GetInstalledPath(packageDependencyInfo); @@ -1042,7 +1047,16 @@ Version was specified as '{0}'. It is possible that version var logMessage = "{0} not installed. An error occurred during installation:{1} {2}".FormatWith(packageDependencyInfo.Id, Environment.NewLine, message); this.Log().Error(ChocolateyLoggers.Important, logMessage); - var errorResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id, new PackageResult(packageDependencyInfo.Id, version.ToFullStringChecked(), null)); + + // Set this install path if the folder does exist, to ensure otherwise unrecoverable scenarios still move corrupt files + // to lib-bad if they happen to be present, when the continueAction is set up to do so. + var packageInstallPath = _fileSystem.CombinePaths(nugetProject.Root, pathResolver.GetPackageDirectoryName(packageDependencyInfo)); + if (!_fileSystem.DirectoryExists(packageInstallPath)) + { + packageInstallPath = null; + } + + var errorResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id, new PackageResult(packageDependencyInfo.Id, version.ToFullStringChecked(), packageInstallPath)); errorResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); if (errorResult.ExitCode == 0) { @@ -1801,11 +1815,18 @@ public virtual ConcurrentDictionary Upgrade(ChocolateyCon { ValidatePackageHash(config, packageDependencyInfo, downloadResult); - nugetProject.InstallPackageAsync( - packageDependencyInfo, - downloadResult, - projectContext, - CancellationToken.None).GetAwaiter().GetResult(); + try + { + nugetProject.InstallPackageAsync( + packageDependencyInfo, + downloadResult, + projectContext, + CancellationToken.None).GetAwaiter().GetResult(); + } + catch (XmlException) + { + throw new ApplicationException("An unexpected error was encountered parsing a malformed nuspec file. This may occur if corrupt files are present in the package's install directory."); + } } var installedPath = nugetProject.GetInstalledPath(packageDependencyInfo);