From 8a283d6162991298ca730306d519fcaf342ef20d Mon Sep 17 00:00:00 2001
From: Rob Reynolds <ferventcoder@gmail.com>
Date: Mon, 8 Jun 2015 08:59:50 -0500
Subject: [PATCH] (GH-313) Snapshot detection of locked files

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.
---
 .../chocolatey.tests.integration.csproj       |  1 +
 .../services/FilesServiceSpecs.cs             | 93 +++++++++++++++++++
 .../ApplicationParameters.cs                  |  1 +
 .../services/NugetService.cs                  |  9 +-
 .../cryptography/CrytpoHashProvider.cs        | 27 +++++-
 .../filesystem/DotNetFileSystem.cs            | 16 +++-
 6 files changed, 141 insertions(+), 6 deletions(-)
 create mode 100644 src/chocolatey.tests.integration/infrastructure.app/services/FilesServiceSpecs.cs

diff --git a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
index 74e6aa82d5..9e688e9abb 100644
--- a/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
+++ b/src/chocolatey.tests.integration/chocolatey.tests.integration.csproj
@@ -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" />
diff --git a/src/chocolatey.tests.integration/infrastructure.app/services/FilesServiceSpecs.cs b/src/chocolatey.tests.integration/infrastructure.app/services/FilesServiceSpecs.cs
new file mode 100644
index 0000000000..39e1369299
--- /dev/null
+++ b/src/chocolatey.tests.integration/infrastructure.app/services/FilesServiceSpecs.cs
@@ -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);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs
index 1ae066e192..3aa207dfe0 100644
--- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs
+++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs
@@ -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
         {
diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs
index 4a653cc32e..092431b58e 100644
--- a/src/chocolatey/infrastructure.app/services/NugetService.cs
+++ b/src/chocolatey/infrastructure.app/services/NugetService.cs
@@ -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)));
                     }
                 }
             }
diff --git a/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs b/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs
index 95f7c4c0f2..405adea131 100644
--- a/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs
+++ b/src/chocolatey/infrastructure/cryptography/CrytpoHashProvider.cs
@@ -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)
         {
@@ -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;
+        }
+
     }
 }
\ No newline at end of file
diff --git a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
index f2dc91eaec..8a529ae4ac 100644
--- a/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
+++ b/src/chocolatey/infrastructure/filesystem/DotNetFileSystem.cs
@@ -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
@@ -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