diff --git a/src/NAppUpdate.Framework/Tasks/FileUpdateTask.cs b/src/NAppUpdate.Framework/Tasks/FileUpdateTask.cs index 3830f2bf..d2c76a31 100644 --- a/src/NAppUpdate.Framework/Tasks/FileUpdateTask.cs +++ b/src/NAppUpdate.Framework/Tasks/FileUpdateTask.cs @@ -151,14 +151,21 @@ public override bool Rollback() { if (string.IsNullOrEmpty(_destinationFile)) return true; + if (!File.Exists(_backupFile)) + return false; // Copy the backup copy back to its original position if (File.Exists(_destinationFile)) + { + if (FileSystem.AreFileContentsEqual(_backupFile, _destinationFile)) + return true; File.Delete(_destinationFile); + } File.Copy(_backupFile, _destinationFile, true); return true; } + /// /// To mitigate problems with the files being locked even though the application mutex has been released. /// https://github.com/synhershko/NAppUpdate/issues/35 diff --git a/src/NAppUpdate.Framework/Utils/FileSystem.cs b/src/NAppUpdate.Framework/Utils/FileSystem.cs index 550cb91a..146d489a 100644 --- a/src/NAppUpdate.Framework/Utils/FileSystem.cs +++ b/src/NAppUpdate.Framework/Utils/FileSystem.cs @@ -1,125 +1,155 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; using System.Security.AccessControl; using System.Security.Principal; -namespace NAppUpdate.Framework.Utils -{ - public static class FileSystem - { - public static void CreateDirectoryStructure(string path) - { - CreateDirectoryStructure(path, true); - } - - public static void CreateDirectoryStructure(string path, bool pathIncludeFile) - { - string[] paths = path.Split(Path.DirectorySeparatorChar); - - // ignore the last split because its the filename - int loopCount = paths.Length; - if (pathIncludeFile) - loopCount--; - - for (int ix = 0; ix < loopCount; ix++) - { - string newPath = paths[0] + @"\"; - for (int add = 1; add <= ix; add++) - newPath = Path.Combine(newPath, paths[add]); - if (!Directory.Exists(newPath)) - Directory.CreateDirectory(newPath); - } - } - - /// - /// Safely delete a folder recuresively - /// See http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502 - /// - /// Folder path to delete - public static void DeleteDirectory(string targetDir) - { - string[] files = Directory.GetFiles(targetDir); - string[] dirs = Directory.GetDirectories(targetDir); - - foreach (string file in files) - { - File.SetAttributes(file, FileAttributes.Normal); - File.Delete(file); - } - - foreach (string dir in dirs) - { - DeleteDirectory(dir); - } - - File.SetAttributes(targetDir, FileAttributes.Normal); - Directory.Delete(targetDir, false); - } - - public static IEnumerable GetFiles(string path, string searchPattern, SearchOption searchOption) - { - string[] searchPatterns = searchPattern.Split('|'); - var files = new List(); - foreach (string sp in searchPatterns) - files.AddRange(System.IO.Directory.GetFiles(path, sp, searchOption)); - return files; - } - - /// - /// Returns true if read/write lock exists on the file, otherwise false - /// From http://stackoverflow.com/a/937558 - /// - /// The file to check for a lock - /// - public static bool IsFileLocked(FileInfo file) - { - FileStream stream = null; - - try - { - stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None); - } - catch (IOException) - { - //the file is unavailable because it is: - //still being written to - //or being processed by another thread - //or does not exist (has already been processed) - return true; - } - finally - { - if (stream != null) - stream.Close(); - } - - //file is not locked - return false; +namespace NAppUpdate.Framework.Utils +{ + public static class FileSystem + { + public static void CreateDirectoryStructure(string path) + { + CreateDirectoryStructure(path, true); } - public static void CopyAccessControl(FileInfo src, FileInfo dst) - { - FileSecurity fs = src.GetAccessControl(); - - bool hasInheritanceRules = fs.GetAccessRules(false, true, typeof(SecurityIdentifier)).Count > 0; - if (hasInheritanceRules) - { - fs.SetAccessRuleProtection(false, false); - } - else - { - fs.SetAccessRuleProtection(true, true); - } - - dst.SetAccessControl(fs); - } - - public static string GetFullPath(string localPath) - { - var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; - return Path.Combine(currentDirectory, localPath); - } - } -} + public static void CreateDirectoryStructure(string path, bool pathIncludeFile) + { + string[] paths = path.Split(Path.DirectorySeparatorChar); + + // ignore the last split because its the filename + int loopCount = paths.Length; + if (pathIncludeFile) + loopCount--; + + for (int ix = 0; ix < loopCount; ix++) + { + string newPath = paths[0] + @"\"; + for (int add = 1; add <= ix; add++) + newPath = Path.Combine(newPath, paths[add]); + if (!Directory.Exists(newPath)) + Directory.CreateDirectory(newPath); + } + } + + /// + /// Safely delete a folder recuresively + /// See http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502 + /// + /// Folder path to delete + public static void DeleteDirectory(string targetDir) + { + string[] files = Directory.GetFiles(targetDir); + string[] dirs = Directory.GetDirectories(targetDir); + + foreach (string file in files) + { + File.SetAttributes(file, FileAttributes.Normal); + File.Delete(file); + } + + foreach (string dir in dirs) + { + DeleteDirectory(dir); + } + + File.SetAttributes(targetDir, FileAttributes.Normal); + Directory.Delete(targetDir, false); + } + + public static IEnumerable GetFiles(string path, string searchPattern, SearchOption searchOption) + { + string[] searchPatterns = searchPattern.Split('|'); + var files = new List(); + foreach (string sp in searchPatterns) + files.AddRange(System.IO.Directory.GetFiles(path, sp, searchOption)); + return files; + } + + /// + /// Returns true if read/write lock exists on the file, otherwise false + /// From http://stackoverflow.com/a/937558 + /// + /// The file to check for a lock + /// + public static bool IsFileLocked(FileInfo file) + { + FileStream stream = null; + + try + { + stream = file.Open(FileMode.Open, FileAccess.ReadWrite, FileShare.None); + } + catch (IOException) + { + //the file is unavailable because it is: + //still being written to + //or being processed by another thread + //or does not exist (has already been processed) + return true; + } + finally + { + if (stream != null) + stream.Close(); + } + + //file is not locked + return false; + } + + public static void CopyAccessControl(FileInfo src, FileInfo dst) + { + FileSecurity fs = src.GetAccessControl(); + + bool hasInheritanceRules = fs.GetAccessRules(false, true, typeof(SecurityIdentifier)).Count > 0; + if (hasInheritanceRules) + { + fs.SetAccessRuleProtection(false, false); + } + else + { + fs.SetAccessRuleProtection(true, true); + } + + dst.SetAccessControl(fs); + } + + public static bool AreFileContentsEqual(string fileA, string fileB) + { + const int bufferSize = sizeof(Int64) << 10; + using (var streamA = new FileStream(fileA, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize)) + using (var streamB = new FileStream(fileB, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize)) + { + if (streamA.Length != streamB.Length) + return false; + var seqA = streamA.AsIenumerable(bufferSize); + var seqB = streamB.AsIenumerable(bufferSize); + return seqA.Zip(seqB, (bA, bB) => bA == bB).All(b => b); + } + } + + private static IEnumerable AsIenumerable(this Stream stream, int bufferSize) + { + var buffer = new byte[bufferSize]; + while (stream.Position < stream.Length) + { + bufferSize = Math.Min((int)(stream.Length - stream.Position), bufferSize); + var read = stream.Read(buffer, 0, bufferSize); + for (int i = 0; i < read; i++) + { + yield return buffer[i]; + } + if (read < bufferSize) + yield break; + } + + public static string GetFullPath(string localPath) + { + var currentDirectory = AppDomain.CurrentDomain.BaseDirectory; + return Path.Combine(currentDirectory, localPath); + } + } +} diff --git a/src/NAppUpdate.Updater/AppStart.cs b/src/NAppUpdate.Updater/AppStart.cs index 825206db..8918b760 100644 --- a/src/NAppUpdate.Updater/AppStart.cs +++ b/src/NAppUpdate.Updater/AppStart.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; @@ -154,7 +155,11 @@ private static void PerformUpdates() Log("Got {0} task objects", _dto.Tasks.Count); + var exceptions = new List(); + bool updateFailed = false; + // Perform the actual off-line update process + var completedTasks = new List(); foreach (var t in _dto.Tasks) { Log("Task \"{0}\": {1}", t.Description, t.ExecutionStatus); @@ -165,8 +170,6 @@ private static void PerformUpdates() continue; } - Exception exception = null; - try { Log("\tExecuting..."); @@ -174,23 +177,50 @@ private static void PerformUpdates() } catch (Exception ex) { + Log(Logger.SeverityLevel.Error, "\tFailed: " + ex.Message); t.ExecutionStatus = TaskExecutionStatus.Failed; - exception = ex; + exceptions.Add(new Exception(string.Format("Update failed, task execution failed, description: {0}, execution status: {1}", t.Description, t.ExecutionStatus), ex)); } + completedTasks.Add(t); if (t.ExecutionStatus != TaskExecutionStatus.Successful) { - string taskFailedMessage = string.Format("Update failed, task execution failed, description: {0}, execution status: {1}", t.Description, t.ExecutionStatus); - throw new Exception(taskFailedMessage, exception); + updateFailed = true; + break; } } - Log("Finished successfully"); - Log("Removing backup folder"); + bool rollbackFailed = false; + if (!updateFailed) + { + Log("Finished successfully"); + } + else + { + Log("\tRollback..."); + foreach (var task in completedTasks) + { + try + { + task.Rollback(); + } + catch (Exception ex) + { + Log(Logger.SeverityLevel.Error, "\t\tRollback failed: " + ex.Message); + exceptions.Add(ex); + rollbackFailed = true; + } + } + } - if (Directory.Exists(_dto.Configs.BackupFolder)) + if (!updateFailed || !rollbackFailed) { - FileSystem.DeleteDirectory(_dto.Configs.BackupFolder); + Log("Removing backup folder"); + + if (Directory.Exists(_dto.Configs.BackupFolder)) + { + FileSystem.DeleteDirectory(_dto.Configs.BackupFolder); + } } // Start the application only if requested to do so @@ -215,9 +245,12 @@ private static void PerformUpdates() } catch (Exception ex) { - throw new UpdateProcessFailedException("Unable to relaunch application and/or send DTO", ex); + exceptions.Add(new UpdateProcessFailedException("Unable to relaunch application and/or send DTO", ex)); } } + + if(updateFailed) + throw new AggregateException("Unable to update", exceptions); } private static void Teardown()