Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updater rollback issue #117

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/NAppUpdate.Framework/Tasks/FileUpdateTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
/// To mitigate problems with the files being locked even though the application mutex has been released.
/// https://github.com/synhershko/NAppUpdate/issues/35
Expand Down
270 changes: 150 additions & 120 deletions src/NAppUpdate.Framework/Utils/FileSystem.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}

/// <summary>
/// Safely delete a folder recuresively
/// See http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502
/// </summary>
/// <param name="targetDir">Folder path to delete</param>
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<string> GetFiles(string path, string searchPattern, SearchOption searchOption)
{
string[] searchPatterns = searchPattern.Split('|');
var files = new List<string>();
foreach (string sp in searchPatterns)
files.AddRange(System.IO.Directory.GetFiles(path, sp, searchOption));
return files;
}

/// <summary>
/// Returns true if read/write lock exists on the file, otherwise false
/// From http://stackoverflow.com/a/937558
/// </summary>
/// <param name="file">The file to check for a lock</param>
/// <returns></returns>
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);
}
}

/// <summary>
/// Safely delete a folder recuresively
/// See http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502
/// </summary>
/// <param name="targetDir">Folder path to delete</param>
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<string> GetFiles(string path, string searchPattern, SearchOption searchOption)
{
string[] searchPatterns = searchPattern.Split('|');
var files = new List<string>();
foreach (string sp in searchPatterns)
files.AddRange(System.IO.Directory.GetFiles(path, sp, searchOption));
return files;
}

/// <summary>
/// Returns true if read/write lock exists on the file, otherwise false
/// From http://stackoverflow.com/a/937558
/// </summary>
/// <param name="file">The file to check for a lock</param>
/// <returns></returns>
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<byte> 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);
}
}
}
53 changes: 43 additions & 10 deletions src/NAppUpdate.Updater/AppStart.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
Expand Down Expand Up @@ -154,7 +155,11 @@ private static void PerformUpdates()

Log("Got {0} task objects", _dto.Tasks.Count);

var exceptions = new List<Exception>();
bool updateFailed = false;

// Perform the actual off-line update process
var completedTasks = new List<IUpdateTask>();
foreach (var t in _dto.Tasks)
{
Log("Task \"{0}\": {1}", t.Description, t.ExecutionStatus);
Expand All @@ -165,32 +170,57 @@ private static void PerformUpdates()
continue;
}

Exception exception = null;

try
{
Log("\tExecuting...");
t.ExecutionStatus = t.Execute(true);
}
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
Expand All @@ -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()
Expand Down