Skip to content

Commit

Permalink
(GH-313) Snapshot detection of locked files
Browse files Browse the repository at this point in the history
When a locked file is detected, use a special code for it instead of
file too big. When cleaning up the files during uninstall, provide
messages for the user to take action on those locked files as they will
be in the best place to determine whether they should stick around or
get removed.
  • Loading branch information
ferventcoder committed Jun 8, 2015
1 parent 8906ea8 commit 8a283d6
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="infrastructure.app\services\FilesServiceSpecs.cs" />
<Compile Include="infrastructure\commands\CommandExecutorSpecs.cs" />
<Compile Include="infrastructure\cryptography\CrytpoHashProviderSpecs.cs" />
<Compile Include="infrastructure\filesystem\DotNetFileSystemSpecs.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
namespace chocolatey.tests.integration.infrastructure.app.services
{
using System;
using System.IO;
using System.Linq;
using Moq;
using Should;
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.cryptography;
using chocolatey.infrastructure.filesystem;
using chocolatey.infrastructure.results;
using chocolatey.infrastructure.services;

public class FilesServiceSpecs
{
public abstract class FilesServiceSpecsBase : TinySpec
{
protected FilesService Service;
protected IFileSystem FileSystem = new DotNetFileSystem();

public override void Context()
{
Service = new FilesService(new XmlService(FileSystem), FileSystem, new CrytpoHashProvider(FileSystem, CryptoHashProviderType.Md5));
}
}

public class when_FilesService_encounters_locked_files : FilesServiceSpecsBase
{
private PackageFiles _result;
private readonly ChocolateyConfiguration _config = new ChocolateyConfiguration();
private PackageResult _packageResult;
private string _contextPath;
private string _theLockedFile;
private FileStream _fileStream;

public override void Context()
{
base.Context();
_contextPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "infrastructure", "filesystem");
_theLockedFile = Path.Combine(_contextPath, "Slipsum.txt");
_packageResult = new PackageResult("bob", "1.2.3", FileSystem.get_directory_name(_theLockedFile));
MockLogger.LogMessagesToConsole = true;

_fileStream = new FileStream(_theLockedFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
}

public override void AfterObservations()
{
base.AfterObservations();
_fileStream.Close();
}

public override void Because()
{
_result = Service.capture_package_files(_packageResult, _config);
}

[Fact]
public void should_not_error()
{
//nothing to see here
}

[Fact]
public void should_log_a_warning()
{
MockLogger.Verify(l => l.Warn(It.IsAny<string>()), Times.AtLeastOnce);
}

[Fact]
public void should_log_a_warning_about_locked_files()
{
bool lockedFiles = false;
foreach (var message in MockLogger.MessagesFor(LogLevel.Warn).or_empty_list_if_null())
{
if (message.Contains("The process cannot access the file")) lockedFiles = true;
}

lockedFiles.ShouldBeTrue();
}

[Fact]
public void should_return_a_special_code_for_locked_files()
{
_result.Files.FirstOrDefault(x => x.Path == _theLockedFile).Checksum.ShouldEqual(ApplicationParameters.HashProviderFileLocked);
}
}
}
}
1 change: 1 addition & 0 deletions src/chocolatey/infrastructure.app/ApplicationParameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static class ApplicationParameters
public static readonly string[] ConfigFileExtensions = new string[] {".autoconf",".config",".conf",".cfg",".jsc",".json",".jsonp",".ini",".xml",".yaml"};

public static string HashProviderFileTooBig = "UnableToDetectChanges_FileTooBig";
public static string HashProviderFileLocked = "UnableToDetectChanges_FileLocked";

public static class Tools
{
Expand Down
9 changes: 8 additions & 1 deletion src/chocolatey/infrastructure.app/services/NugetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -959,11 +959,18 @@ public void remove_installation_files(IPackage removedPackage, ChocolateyPackage
foreach (var file in _fileSystem.get_files(installDir, "*.*", SearchOption.AllDirectories).or_empty_list_if_null())
{
var fileSnapshot = pkgInfo.FilesSnapshot.Files.FirstOrDefault(f => f.Path.is_equal_to(file));
if (fileSnapshot != null && fileSnapshot.Checksum == _filesService.get_package_file(file).Checksum)
if (fileSnapshot == null) continue;

if (fileSnapshot.Checksum == _filesService.get_package_file(file).Checksum)
{
FaultTolerance.try_catch_with_logging_exception(
() => _fileSystem.delete_file(file),
"Error deleting file");
}

if (fileSnapshot.Checksum == ApplicationParameters.HashProviderFileLocked)
{
this.Log().Warn(()=> "Snapshot for '{0}' was attempted when file was locked.{1} Please inspect and manually remove file{1} at '{2}'".format_with(_fileSystem.get_file_name(file), Environment.NewLine, _fileSystem.get_directory_name(file)));
}
}
}
Expand Down
27 changes: 24 additions & 3 deletions src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,23 @@
namespace chocolatey.infrastructure.cryptography
{
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using adapters;
using app;
using filesystem;
using platforms;
using Environment = System.Environment;
using HashAlgorithm = adapters.HashAlgorithm;


public sealed class CrytpoHashProvider : IHashProvider
{
private readonly IFileSystem _fileSystem;
private readonly IHashAlgorithm _hashAlgorithm;
private const int ERROR_LOCK_VIOLATION = 33;
private const int ERROR_SHARING_VIOLATION = 32;

public CrytpoHashProvider(IFileSystem fileSystem, CryptoHashProviderType providerType)
{
Expand Down Expand Up @@ -69,11 +73,28 @@ public string hash_file(string filePath)
}
catch (IOException ex)
{
this.Log().Warn(() => "Error computing hash for '{0}'{1} Captured error:{1} {2}".format_with(filePath, Environment.NewLine, ex.Message));
this.Log().Warn(() => "Error computing hash for '{0}'{1} Hash will be special code for locked file or file too big instead.{1} Captured error:{1} {2}".format_with(filePath, Environment.NewLine, ex.Message));

if (file_is_locked(ex))
{
return ApplicationParameters.HashProviderFileLocked;
}

//IO.IO_FileTooLong2GB (over Int32.MaxValue)
return ApplicationParameters.HashProviderFileTooBig;
return "UnableToDetectChanges_FileTooBig";
}
}

private static bool file_is_locked(Exception exception)
{
var errorCode = 0;

var hresult = Marshal.GetHRForException(exception);

errorCode = hresult & ((1 << 16) - 1);

return errorCode == ERROR_SHARING_VIOLATION || errorCode == ERROR_LOCK_VIOLATION;
}

}
}
16 changes: 14 additions & 2 deletions src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,23 @@ public void copy_file(string sourceFilePath, string destinationFilePath, bool ov

public bool copy_file_unsafe(string sourceFilePath, string destinationFilePath, bool overwriteExisting)
{
if (Platform.get_platform() != PlatformType.Windows)
{
copy_file(sourceFilePath, destinationFilePath, overwriteExisting);
return true;
}

this.Log().Debug(() => "Attempting to copy from \"{0}\" to \"{1}\".".format_with(sourceFilePath, destinationFilePath));
create_directory_if_not_exists(get_directory_name(destinationFilePath), ignoreError: true);

//Private Declare Function apiCopyFile Lib "kernel32" Alias "CopyFileA" _
int success = CopyFileW(sourceFilePath, destinationFilePath, overwriteExisting ? 0 : 1);
return success == 0;
//if (success == 0)
//{
// var error = Marshal.GetLastWin32Error();

//}
return success != 0;
}

// ReSharper disable InconsistentNaming
Expand All @@ -207,7 +219,7 @@ _In_ BOOL bFailIfExists
);
*/

[DllImport("kernel32")]
[DllImport("kernel32", SetLastError = true)]
private static extern int CopyFileW(string lpExistingFileName, string lpNewFileName, int bFailIfExists);

// ReSharper restore InconsistentNaming
Expand Down

0 comments on commit 8a283d6

Please sign in to comment.