From 4bf3ef45b0ccf422b96e21220a9a6a748f36f12d Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Fri, 9 Sep 2016 01:05:27 +0200 Subject: [PATCH] Fixed: Auto-Updater rollback logic tries to restore unchanged files. --- src/LogentriesNLog/fastJSON/JSON.cs | 13 +-- src/LogentriesNLog/fastJSON/JsonSerializer.cs | 2 + .../DiskTests/DiskTransferServiceFixture.cs | 61 +++++++++++- .../Disk/DiskTransferService.cs | 97 ++++++++++++++++++- src/NzbDrone.Update/NzbDrone.Update.csproj | 1 + src/NzbDrone.Update/UpdateApp.cs | 2 - .../UpdateEngine/BackupAndRestore.cs | 7 +- .../UpdateEngine/BackupAppData.cs | 10 +- .../UpdateEngine/DetectExistingVersion.cs | 42 ++++++++ .../UpdateEngine/InstallUpdateService.cs | 32 ++++-- 10 files changed, 244 insertions(+), 23 deletions(-) create mode 100644 src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs diff --git a/src/LogentriesNLog/fastJSON/JSON.cs b/src/LogentriesNLog/fastJSON/JSON.cs index b40b7551d..aa730a947 100644 --- a/src/LogentriesNLog/fastJSON/JSON.cs +++ b/src/LogentriesNLog/fastJSON/JSON.cs @@ -24,6 +24,7 @@ private JSON() SerializeNullValues = false; UseOptimizedDatasetSchema = false; UsingGlobalTypes = false; + UseUTCDateTime = true; } public bool UseOptimizedDatasetSchema = true; public bool UseFastGuid = true; @@ -39,7 +40,7 @@ public string ToJSON(object obj) return ToJSON(obj, UseSerializerExtension, UseFastGuid, UseOptimizedDatasetSchema, SerializeNullValues); } - + public string ToJSON(object obj, bool enableSerializerExtensions, bool enableFastGuid, @@ -49,13 +50,13 @@ public string ToJSON(object obj, return new JSONSerializer(enableOptimizedDatasetSchema, enableFastGuid, enableSerializerExtensions, serializeNullValues, IndentOutput).ConvertToJSON(obj); } - + public T ToObject(string json) { return (T)ToObject(json, typeof(T)); } - + public object ToObject(string json, Type type) { var ht = new JsonParser(json).Decode() as Dictionary; @@ -320,7 +321,7 @@ internal List GetGetters(Type type) } } - + _getterscache.Add(type, getters); return getters; } @@ -448,7 +449,7 @@ private object ParseDictionary(Dictionary d, Dictionary)v, pi.pt, pi.GenericTypes, globaltypes); #endif @@ -817,4 +818,4 @@ DataTable CreateDataTable(Dictionary reader, Dictionary() .Verify(v => v.GetFileSize(_sourcePath), Times.Once()); @@ -688,6 +688,60 @@ public void should_throw_if_destination_is_readonly() Assert.Throws(() => Subject.TransferFile(_sourcePath, _targetPath, TransferMode.Copy)); } + [Test] + public void MirrorFolder_should_remove_additional_files() + { + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var source = new DirectoryInfo(GetTempFilePath()); + var destination = new DirectoryInfo(GetTempFilePath()); + + source.Create(); + Subject.TransferFolder(original.FullName, destination.FullName, TransferMode.Copy); + + var count = Subject.MirrorFolder(source.FullName, destination.FullName); + + count.Should().Equals(0); + destination.GetFileSystemInfos().Should().BeEmpty(); + } + + [Test] + public void MirrorFolder_should_add_new_files() + { + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var source = new DirectoryInfo(GetTempFilePath()); + var destination = new DirectoryInfo(GetTempFilePath()); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + var count = Subject.MirrorFolder(source.FullName, destination.FullName); + + count.Should().Equals(3); + VerifyCopyFolder(original.FullName, destination.FullName); + } + + [Test] + public void MirrorFolder_should_not_touch_equivalent_files() + { + WithRealDiskProvider(); + + var original = GetFilledTempFolder(); + var source = new DirectoryInfo(GetTempFilePath()); + var destination = new DirectoryInfo(GetTempFilePath()); + + Subject.TransferFolder(original.FullName, source.FullName, TransferMode.Copy); + + Subject.TransferFolder(original.FullName, destination.FullName, TransferMode.Copy); + + var count = Subject.MirrorFolder(source.FullName, destination.FullName); + + count.Should().Equals(0); + VerifyCopyFolder(original.FullName, destination.FullName); + } + public DirectoryInfo GetFilledTempFolder() { var tempFolder = GetTempFilePath(); @@ -807,7 +861,10 @@ private void WithRealDiskProvider() if (File.Exists(d) && o) File.Delete(d); File.Move(s, d); }); - + + Mocker.GetMock() + .Setup(v => v.OpenReadStream(It.IsAny())) + .Returns(s => new FileStream(s, FileMode.Open, FileAccess.Read)); } private void VerifyCopyFolder(string source, string destination) diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index 257dc1d22..2f5db90e1 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -15,6 +15,7 @@ public interface IDiskTransferService { TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode, bool verified = true); TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false, bool verified = true); + int MirrorFolder(string sourcePath, string targetPath); } public enum DiskTransferVerificationMode @@ -83,6 +84,100 @@ public TransferMode TransferFolder(string sourcePath, string targetPath, Transfe return result; } + public int MirrorFolder(string sourcePath, string targetPath) + { + var filesCopied = 0; + + Ensure.That(sourcePath, () => sourcePath).IsValidPath(); + Ensure.That(targetPath, () => targetPath).IsValidPath(); + + _logger.Debug("Mirror [{0}] > [{1}]", sourcePath, targetPath); + + if (!_diskProvider.FolderExists(targetPath)) + { + _diskProvider.CreateFolder(targetPath); + } + + var sourceFolders = _diskProvider.GetDirectoryInfos(sourcePath); + var targetFolders = _diskProvider.GetDirectoryInfos(targetPath); + + foreach (var subDir in targetFolders.Where(v => !sourceFolders.Any(d => d.Name == v.Name))) + { + _diskProvider.DeleteFolder(subDir.FullName, true); + } + + foreach (var subDir in sourceFolders) + { + filesCopied += MirrorFolder(subDir.FullName, Path.Combine(targetPath, subDir.Name)); + } + + var sourceFiles = _diskProvider.GetFileInfos(sourcePath); + var targetFiles = _diskProvider.GetFileInfos(targetPath); + + foreach (var targetFile in targetFiles.Where(v => !sourceFiles.Any(d => d.Name == v.Name))) + { + _diskProvider.DeleteFile(targetFile.FullName); + } + + foreach (var sourceFile in sourceFiles) + { + var targetFile = Path.Combine(targetPath, sourceFile.Name); + + if (CompareFiles(sourceFile.FullName, targetFile)) + { + continue; + } + + TransferFile(sourceFile.FullName, targetFile, TransferMode.Copy, true, true); + filesCopied++; + } + + return filesCopied; + } + + private bool CompareFiles(string sourceFile, string targetFile) + { + if (!_diskProvider.FileExists(sourceFile) || !_diskProvider.FileExists(targetFile)) + { + return false; + } + + if (_diskProvider.GetFileSize(sourceFile) != _diskProvider.GetFileSize(targetFile)) + { + return false; + } + + var sourceBuffer = new byte[64 * 1024]; + var targetBuffer = new byte[64 * 1024]; + using (var sourceStream = _diskProvider.OpenReadStream(sourceFile)) + using (var targetStream = _diskProvider.OpenReadStream(targetFile)) + { + while (true) + { + var sourceLength = sourceStream.Read(sourceBuffer, 0, sourceBuffer.Length); + var targetLength = targetStream.Read(targetBuffer, 0, targetBuffer.Length); + + if (sourceLength != targetLength) + { + return false; + } + + if (sourceLength == 0) + { + return true; + } + + for (var i = 0; i < sourceLength; i++) + { + if (sourceBuffer[i] != targetBuffer[i]) + { + return false; + } + } + } + } + } + public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false, bool verified = true) { var verificationMode = verified ? VerificationMode : DiskTransferVerificationMode.None; @@ -96,7 +191,7 @@ public TransferMode TransferFile(string sourcePath, string targetPath, TransferM Ensure.That(targetPath, () => targetPath).IsValidPath(); _logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath); - + var originalSize = _diskProvider.GetFileSize(sourcePath); if (sourcePath == targetPath) diff --git a/src/NzbDrone.Update/NzbDrone.Update.csproj b/src/NzbDrone.Update/NzbDrone.Update.csproj index ccccf87b2..0a58d6a68 100644 --- a/src/NzbDrone.Update/NzbDrone.Update.csproj +++ b/src/NzbDrone.Update/NzbDrone.Update.csproj @@ -58,6 +58,7 @@ + diff --git a/src/NzbDrone.Update/UpdateApp.cs b/src/NzbDrone.Update/UpdateApp.cs index f379ba99c..ca3542838 100644 --- a/src/NzbDrone.Update/UpdateApp.cs +++ b/src/NzbDrone.Update/UpdateApp.cs @@ -40,7 +40,6 @@ public static void Main(string[] args) _container = UpdateContainerBuilder.Build(startupArgument); - Logger.Info("Updating Sonarr to version {0}", BuildInfo.Version); _container.Resolve().Start(args); Logger.Info("Update completed successfully"); @@ -56,7 +55,6 @@ public void Start(string[] args) var startupContext = ParseArgs(args); var targetFolder = GetInstallationDirectory(startupContext); - Logger.Info("Starting update process. Target Path:{0}", targetFolder); _installUpdateService.Start(targetFolder, startupContext.ProcessId); } diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs index 6e800e0be..8bfcada5e 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAndRestore.cs @@ -27,13 +27,14 @@ public BackupAndRestore(IDiskTransferService diskTransferService, IAppFolderInfo public void Backup(string source) { _logger.Info("Creating backup of existing installation"); - _diskTransferService.TransferFolder(source, _appFolderInfo.GetUpdateBackUpFolder(), TransferMode.Copy, false); + _diskTransferService.MirrorFolder(source, _appFolderInfo.GetUpdateBackUpFolder()); } public void Restore(string target) { _logger.Info("Attempting to rollback upgrade"); - _diskTransferService.TransferFolder(_appFolderInfo.GetUpdateBackUpFolder(), target, TransferMode.Copy, false); + var count = _diskTransferService.MirrorFolder(_appFolderInfo.GetUpdateBackUpFolder(), target); + _logger.Info("Rolled back {0} files", count); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs index 5ab0d09f4..a93bca1f5 100644 --- a/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs +++ b/src/NzbDrone.Update/UpdateEngine/BackupAppData.cs @@ -34,7 +34,15 @@ public void Backup() _logger.Info("Backing up appdata (database/config)"); var backupFolderAppData = _appFolderInfo.GetUpdateBackUpAppDataFolder(); - _diskProvider.CreateFolder(backupFolderAppData); + if (_diskProvider.FolderExists(backupFolderAppData)) + { + _diskProvider.EmptyFolder(backupFolderAppData); + } + else + { + _diskProvider.CreateFolder(backupFolderAppData); + } + try { diff --git a/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs new file mode 100644 index 000000000..d27190f17 --- /dev/null +++ b/src/NzbDrone.Update/UpdateEngine/DetectExistingVersion.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using NLog; + +namespace NzbDrone.Update.UpdateEngine +{ + public interface IDetectExistingVersion + { + string GetExistingVersion(string targetFolder); + } + + public class DetectExistingVersion : IDetectExistingVersion + { + private readonly Logger _logger; + + public DetectExistingVersion(Logger logger) + { + _logger = logger; + } + + public string GetExistingVersion(string targetFolder) + { + try + { + var targetExecutable = Path.Combine(targetFolder, "NzbDrone.exe"); + + if (File.Exists(targetExecutable)) + { + var versionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(targetExecutable); + + return versionInfo.FileVersion; + } + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to get existing version from {0}", targetFolder); + } + + return "(unknown)"; + } + } +} diff --git a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs index f7ad19b28..9c2866330 100644 --- a/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs +++ b/src/NzbDrone.Update/UpdateEngine/InstallUpdateService.cs @@ -18,6 +18,7 @@ public class InstallUpdateService : IInstallUpdateService private readonly IDiskProvider _diskProvider; private readonly IDiskTransferService _diskTransferService; private readonly IDetectApplicationType _detectApplicationType; + private readonly IDetectExistingVersion _detectExistingVersion; private readonly ITerminateNzbDrone _terminateNzbDrone; private readonly IAppFolderInfo _appFolderInfo; private readonly IBackupAndRestore _backupAndRestore; @@ -29,6 +30,7 @@ public class InstallUpdateService : IInstallUpdateService public InstallUpdateService(IDiskProvider diskProvider, IDiskTransferService diskTransferService, IDetectApplicationType detectApplicationType, + IDetectExistingVersion detectExistingVersion, ITerminateNzbDrone terminateNzbDrone, IAppFolderInfo appFolderInfo, IBackupAndRestore backupAndRestore, @@ -40,6 +42,7 @@ public InstallUpdateService(IDiskProvider diskProvider, _diskProvider = diskProvider; _diskTransferService = diskTransferService; _detectApplicationType = detectApplicationType; + _detectExistingVersion = detectExistingVersion; _terminateNzbDrone = terminateNzbDrone; _appFolderInfo = appFolderInfo; _backupAndRestore = backupAndRestore; @@ -76,30 +79,42 @@ private void Verify(string targetFolder, int processId) public void Start(string installationFolder, int processId) { + _logger.Info("Installation Folder: {0}", installationFolder); + _logger.Info("Updating Sonarr from version {0} to version {1}", _detectExistingVersion.GetExistingVersion(installationFolder), BuildInfo.Version); + Verify(installationFolder, processId); var appType = _detectApplicationType.GetAppType(); + _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); + _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); + + if (OsInfo.IsWindows) + { + _terminateNzbDrone.Terminate(processId); + } + try { - _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME); - _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME); + _backupAndRestore.Backup(installationFolder); + _backupAppData.Backup(); if (OsInfo.IsWindows) { - _terminateNzbDrone.Terminate(processId); + if (_processProvider.Exists(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME) || _processProvider.Exists(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + { + _logger.Error("Sonarr was restarted prematurely by external process."); + return; + } } - _backupAndRestore.Backup(installationFolder); - _backupAppData.Backup(); - try { _logger.Info("Emptying installation folder"); _diskProvider.EmptyFolder(installationFolder); _logger.Info("Copying new files to target folder"); - _diskTransferService.TransferFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder, TransferMode.Copy, false); + _diskTransferService.MirrorFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder); // Set executable flag on Sonarr app if (OsInfo.IsOsx) @@ -109,8 +124,9 @@ public void Start(string installationFolder, int processId) } catch (Exception e) { - _logger.Fatal(e, "Failed to copy upgrade package to target folder."); + _logger.Error(e, "Failed to copy upgrade package to target folder."); _backupAndRestore.Restore(installationFolder); + throw; } } finally