Merge pull request #180 from Sonarr/updatecheck-fixes

Updatecheck fixes
pull/172/merge
Taloth 10 years ago
commit 5750f012cb

@ -1,6 +1,9 @@
using System.IO; using System.Collections.Generic;
using System.IO;
using FluentAssertions; using FluentAssertions;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Authentication; using NzbDrone.Core.Authentication;
@ -13,6 +16,8 @@ namespace NzbDrone.Common.Test
public class ConfigFileProviderTest : TestBase<ConfigFileProvider> public class ConfigFileProviderTest : TestBase<ConfigFileProvider>
{ {
private string _configFileContents;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
@ -20,8 +25,24 @@ namespace NzbDrone.Common.Test
var configFile = Mocker.Resolve<IAppFolderInfo>().GetConfigPath(); var configFile = Mocker.Resolve<IAppFolderInfo>().GetConfigPath();
if (File.Exists(configFile)) _configFileContents = null;
File.Delete(configFile);
WithMockConfigFile(configFile);
}
protected void WithMockConfigFile(string configFile)
{
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.FileExists(configFile))
.Returns<string>(p => _configFileContents != null);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.ReadAllText(configFile))
.Returns<string>(p => _configFileContents);
Mocker.GetMock<IDiskProvider>()
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
.Callback<string, string>((p, t) => _configFileContents = t);
} }
[Test] [Test]
@ -142,8 +163,28 @@ namespace NzbDrone.Common.Test
Subject.SaveConfigDictionary(dic); Subject.SaveConfigDictionary(dic);
Subject.Port.Should().Be(port);
}
[Test]
public void SaveDictionary_should_only_save_specified_values()
{
int port = 20555;
int origSslPort = 20551;
int sslPort = 20552;
var dic = Subject.GetConfigDictionary();
dic["Port"] = port;
dic["SslPort"] = origSslPort;
Subject.SaveConfigDictionary(dic);
dic = new Dictionary<string, object>();
dic["SslPort"] = sslPort;
Subject.SaveConfigDictionary(dic);
Subject.Port.Should().Be(port); Subject.Port.Should().Be(port);
Subject.SslPort.Should().Be(sslPort);
} }
} }

@ -8,7 +8,7 @@ using NzbDrone.Test.Common;
namespace NzbDrone.Common.Test.DiskTests namespace NzbDrone.Common.Test.DiskTests
{ {
public class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider public abstract class DiskProviderFixtureBase<TSubject> : TestBase<TSubject> where TSubject : class, IDiskProvider
{ {
public DirectoryInfo GetFilledTempFolder() public DirectoryInfo GetFilledTempFolder()
{ {
@ -46,6 +46,38 @@ namespace NzbDrone.Common.Test.DiskTests
Subject.FolderExists(@"C:\ThisBetterNotExist\".AsOsAgnostic()).Should().BeFalse(); Subject.FolderExists(@"C:\ThisBetterNotExist\".AsOsAgnostic()).Should().BeFalse();
} }
protected abstract void SetWritePermissions(string path, bool writable);
[Test]
public void FolderWritable_should_return_true_for_writable_directory()
{
var tempFolder = GetTempFilePath();
Directory.CreateDirectory(tempFolder);
var result = Subject.FolderWritable(tempFolder);
result.Should().BeTrue();
}
[Test]
public void FolderWritable_should_return_false_for_unwritable_directory()
{
var tempFolder = GetTempFilePath();
Directory.CreateDirectory(tempFolder);
SetWritePermissions(tempFolder, false);
try
{
var result = Subject.FolderWritable(tempFolder);
result.Should().BeFalse();
}
finally
{
SetWritePermissions(tempFolder, true);
}
}
[Test] [Test]
public void MoveFile_should_overwrite_existing_file() public void MoveFile_should_overwrite_existing_file()
{ {

@ -109,6 +109,25 @@ namespace NzbDrone.Common.Disk
} }
} }
public bool FolderWritable(string path)
{
Ensure.That(path, () => path).IsValidPath();
try
{
var testPath = Path.Combine(path, "sonarr_write_test.txt");
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
File.WriteAllText(testPath, testContent);
File.Delete(testPath);
return true;
}
catch (Exception e)
{
Logger.Trace("Directory '{0}' isn't writable. {1}", path, e.Message);
return false;
}
}
public string[] GetDirectories(string path) public string[] GetDirectories(string path)
{ {
Ensure.That(path, () => path).IsValidPath(); Ensure.That(path, () => path).IsValidPath();

@ -19,6 +19,7 @@ namespace NzbDrone.Common.Disk
bool FolderExists(string path); bool FolderExists(string path);
bool FileExists(string path); bool FileExists(string path);
bool FileExists(string path, StringComparison stringComparison); bool FileExists(string path, StringComparison stringComparison);
bool FolderWritable(string path);
string[] GetDirectories(string path); string[] GetDirectories(string path);
string[] GetFiles(string path, SearchOption searchOption); string[] GetFiles(string path, SearchOption searchOption);
long GetFolderSize(string path); long GetFolderSize(string path);

@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
{ {
private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted"; private const string DRONE_FACTORY_FOLDER = @"C:\Test\Unsorted";
private void GivenDroneFactoryFolder(bool exists = false) private void GivenDroneFactoryFolder(bool exists = false, bool writable = true)
{ {
Mocker.GetMock<IConfigService>() Mocker.GetMock<IConfigService>()
.SetupGet(s => s.DownloadedEpisodesFolder) .SetupGet(s => s.DownloadedEpisodesFolder)
@ -22,6 +22,10 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER)) .Setup(s => s.FolderExists(DRONE_FACTORY_FOLDER))
.Returns(exists); .Returns(exists);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.FolderWritable(It.IsAny<String>()))
.Returns(exists && writable);
} }
[Test] [Test]
@ -35,11 +39,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
[Test] [Test]
public void should_return_error_when_unable_to_write_to_drone_factory_folder() public void should_return_error_when_unable_to_write_to_drone_factory_folder()
{ {
GivenDroneFactoryFolder(true); GivenDroneFactoryFolder(true, false);
Mocker.GetMock<IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>()))
.Throws<Exception>();
Subject.Check().ShouldBeError(); Subject.Check().ShouldBeError();
} }

@ -21,9 +21,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Setup(s => s.StartUpFolder) .Setup(s => s.StartUpFolder)
.Returns(@"C:\NzbDrone"); .Returns(@"C:\NzbDrone");
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<NzbDrone.Common.Disk.IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>())) .Setup(c => c.FolderWritable(Moq.It.IsAny<string>()))
.Throws<Exception>(); .Returns(false);
Subject.Check().ShouldBeError(); Subject.Check().ShouldBeError();
} }
@ -41,9 +41,9 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Setup(s => s.StartUpFolder) .Setup(s => s.StartUpFolder)
.Returns(@"/opt/nzbdrone"); .Returns(@"/opt/nzbdrone");
Mocker.GetMock<IDiskProvider>() Mocker.GetMock<NzbDrone.Common.Disk.IDiskProvider>()
.Setup(s => s.WriteAllText(It.IsAny<String>(), It.IsAny<String>())) .Setup(c => c.FolderWritable(Moq.It.IsAny<string>()))
.Throws<Exception>(); .Returns(false);
Subject.Check().ShouldBeError(); Subject.Check().ShouldBeError();
} }

@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.UpdateTests
{ {
const string branch = "master"; const string branch = "master";
UseRealHttp(); UseRealHttp();
var recent = Subject.GetRecentUpdates(branch); var recent = Subject.GetRecentUpdates(branch, new Version(2, 0));
recent.Should().NotBeEmpty(); recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace()); recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
@ -11,6 +12,7 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Model; using NzbDrone.Common.Model;
using NzbDrone.Common.Processes; using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Core.Exceptions;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Update; using NzbDrone.Core.Update;
using NzbDrone.Core.Update.Commands; using NzbDrone.Core.Update.Commands;
@ -59,6 +61,14 @@ namespace NzbDrone.Core.Test.UpdateTests
Mocker.GetMock<IProcessProvider>().Setup(c => c.GetCurrentProcess()).Returns(new ProcessInfo { Id = 12 }); Mocker.GetMock<IProcessProvider>().Setup(c => c.GetCurrentProcess()).Returns(new ProcessInfo { Id = 12 });
Mocker.GetMock<IRuntimeInfo>().Setup(c => c.ExecutingApplication).Returns(@"C:\Test\NzbDrone.exe"); Mocker.GetMock<IRuntimeInfo>().Setup(c => c.ExecutingApplication).Returns(@"C:\Test\NzbDrone.exe");
Mocker.GetMock<IConfigFileProvider>()
.SetupGet(s => s.UpdateAutomatically)
.Returns(true);
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(true);
_sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder(); _sandboxFolder = Mocker.GetMock<IAppFolderInfo>().Object.GetUpdateSandboxFolder();
} }
@ -259,6 +269,47 @@ namespace NzbDrone.Core.Test.UpdateTests
ExceptionVerification.ExpectedErrors(1); ExceptionVerification.ExpectedErrors(1);
} }
[Test]
public void should_log_error_when_startup_folder_is_not_writable()
{
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(false);
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_log_when_install_cannot_be_started()
{
Mocker.GetMock<IDiskProvider>()
.Setup(c => c.FolderWritable(It.IsAny<string>()))
.Returns(false);
var updateArchive = Path.Combine(_sandboxFolder, _updatePackage.FileName);
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IHttpClient>().Verify(c => c.DownloadFile(_updatePackage.Url, updateArchive), Times.Never());
ExceptionVerification.ExpectedErrors(1);
}
[Test]
public void should_switch_to_branch_specified_in_updatepackage()
{
_updatePackage.Branch = "fake";
Subject.Execute(new ApplicationUpdateCommand());
Mocker.GetMock<IConfigFileProvider>()
.Verify(v => v.SaveConfigDictionary(It.Is<Dictionary<string, object>>(d => d.ContainsKey("Branch") && (string)d["Branch"] == "fake")), Times.Once());
}
[TearDown] [TearDown]
public void TearDown() public void TearDown()
{ {

@ -339,7 +339,7 @@ namespace NzbDrone.Core.Configuration
{ {
if (_diskProvider.FileExists(_configFile)) if (_diskProvider.FileExists(_configFile))
{ {
return XDocument.Load(_configFile); return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
} }
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes")); var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));

@ -122,30 +122,8 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
protected override void Test(List<ValidationFailure> failures) protected override void Test(List<ValidationFailure> failures)
{ {
failures.AddIfNotNull(TestWrite(Settings.NzbFolder, "NzbFolder")); failures.AddIfNotNull(TestFolder(Settings.NzbFolder, "NzbFolder"));
failures.AddIfNotNull(TestWrite(Settings.StrmFolder, "StrmFolder")); failures.AddIfNotNull(TestFolder(Settings.StrmFolder, "StrmFolder"));
}
private ValidationFailure TestWrite(String folder, String propertyName)
{
if (!_diskProvider.FolderExists(folder))
{
return new ValidationFailure(propertyName, "Folder does not exist");
}
try
{
var testPath = Path.Combine(folder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new ValidationFailure(propertyName, "Unable to write to folder");
}
return null;
} }
private String WriteStrmFile(String title, String nzbFile) private String WriteStrmFile(String title, String nzbFile)

@ -98,26 +98,17 @@ namespace NzbDrone.Core.Download
{ {
return new NzbDroneValidationFailure(propertyName, "Folder does not exist") return new NzbDroneValidationFailure(propertyName, "Folder does not exist")
{ {
DetailedDescription = "The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account that is used to execute NzbDrone." DetailedDescription = string.Format("The folder you specified does not exist or is inaccessible. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName)
}; };
} }
if (mustBeWritable) if (mustBeWritable && !_diskProvider.FolderWritable(folder))
{ {
try _logger.Error("Folder '{0}' is not writable.", folder);
return new NzbDroneValidationFailure(propertyName, "Unable to write to folder")
{ {
var testPath = Path.Combine(folder, "drone_test.txt"); DetailedDescription = string.Format("The folder you specified is not writable. Please verify the folder permissions for the user account '{0}', which is used to execute Sonarr.", Environment.UserName)
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); };
_diskProvider.DeleteFile(testPath);
}
catch (Exception ex)
{
_logger.ErrorException(ex.Message, ex);
return new NzbDroneValidationFailure(propertyName, "Unable to write to folder")
{
DetailedDescription = "The folder you specified is not writable. Please verify the folder permissions for the user account that is used to execute NzbDrone."
};
}
} }
return null; return null;

@ -31,13 +31,7 @@ namespace NzbDrone.Core.HealthCheck.Checks
return new HealthCheck(GetType(), HealthCheckResult.Error, "Drone factory folder does not exist"); return new HealthCheck(GetType(), HealthCheckResult.Error, "Drone factory folder does not exist");
} }
try if (!_diskProvider.FolderWritable(droneFactoryFolder))
{
var testPath = Path.Combine(droneFactoryFolder, "drone_test.txt");
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString());
_diskProvider.DeleteFile(testPath);
}
catch (Exception)
{ {
return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to write to drone factory folder"); return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to write to drone factory folder");
} }

@ -29,15 +29,11 @@ namespace NzbDrone.Core.HealthCheck.Checks
{ {
if (OsInfo.IsWindows || _configFileProvider.UpdateAutomatically) if (OsInfo.IsWindows || _configFileProvider.UpdateAutomatically)
{ {
try if (!_diskProvider.FolderWritable(_appFolderInfo.StartUpFolder))
{ {
var testPath = Path.Combine(_appFolderInfo.StartUpFolder, "drone_test.txt"); return new HealthCheck(GetType(), HealthCheckResult.Error,
_diskProvider.WriteAllText(testPath, DateTime.Now.ToString()); string.Format("Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", _appFolderInfo.StartUpFolder, Environment.UserName),
_diskProvider.DeleteFile(testPath); "Cannot install update because startup folder is not writable by the user");
}
catch (Exception)
{
return new HealthCheck(GetType(), HealthCheckResult.Error, "Unable to update, running from write-protected folder");
} }
} }

@ -94,7 +94,7 @@ namespace NzbDrone.Core.HealthCheck
public void Execute(CheckHealthCommand message) public void Execute(CheckHealthCommand message)
{ {
PerformHealthCheck(c => c.CheckOnSchedule); PerformHealthCheck(c => message.Manual || c.CheckOnSchedule);
} }
} }
} }

@ -60,21 +60,21 @@ namespace NzbDrone.Core.Messaging.Commands
SetMessage("Starting"); SetMessage("Starting");
} }
public void Failed(Exception exception) public void Failed(Exception exception, string message = "Failed")
{ {
_stopWatch.Stop(); _stopWatch.Stop();
StateChangeTime = DateTime.UtcNow; StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Failed; State = CommandStatus.Failed;
Exception = exception; Exception = exception;
SetMessage("Failed"); SetMessage(message);
} }
public void Completed() public void Completed(string message = "Completed")
{ {
_stopWatch.Stop(); _stopWatch.Stop();
StateChangeTime = DateTime.UtcNow; StateChangeTime = DateTime.UtcNow;
State = CommandStatus.Completed; State = CommandStatus.Completed;
SetMessage("Completed"); SetMessage(message);
} }
public void SetMessage(string message) public void SetMessage(string message)

@ -129,7 +129,11 @@ namespace NzbDrone.Core.Messaging.Commands
} }
handler.Execute((TCommand)command); handler.Execute((TCommand)command);
_trackCommands.Completed(command);
if (command.State == CommandStatus.Running)
{
_trackCommands.Completed(command);
}
} }
catch (Exception e) catch (Exception e)
{ {

@ -853,11 +853,12 @@
<Compile Include="Tv\SeriesTypes.cs" /> <Compile Include="Tv\SeriesTypes.cs" />
<Compile Include="Tv\ShouldRefreshSeries.cs" /> <Compile Include="Tv\ShouldRefreshSeries.cs" />
<Compile Include="Update\Commands\ApplicationUpdateCommand.cs" /> <Compile Include="Update\Commands\ApplicationUpdateCommand.cs" />
<Compile Include="Update\Commands\InstallUpdateCommand.cs" />
<Compile Include="Update\InstallUpdateService.cs" /> <Compile Include="Update\InstallUpdateService.cs" />
<Compile Include="Update\RecentUpdateProvider.cs" /> <Compile Include="Update\RecentUpdateProvider.cs" />
<Compile Include="Update\UpdateAbortedException.cs" />
<Compile Include="Update\UpdateChanges.cs" /> <Compile Include="Update\UpdateChanges.cs" />
<Compile Include="Update\UpdateCheckService.cs" /> <Compile Include="Update\UpdateCheckService.cs" />
<Compile Include="Update\UpdateFolderNotWritableException.cs" />
<Compile Include="Update\UpdateMechanism.cs" /> <Compile Include="Update\UpdateMechanism.cs" />
<Compile Include="Update\UpdatePackage.cs" /> <Compile Include="Update\UpdatePackage.cs" />
<Compile Include="Update\UpdatePackageAvailable.cs" /> <Compile Include="Update\UpdatePackageAvailable.cs" />

@ -1,17 +0,0 @@
using NzbDrone.Core.Messaging.Commands;
namespace NzbDrone.Core.Update.Commands
{
public class InstallUpdateCommand : Command
{
public UpdatePackage UpdatePackage { get; set; }
public override bool SendUpdatesToClient
{
get
{
return true;
}
}
}
}

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common; using NzbDrone.Common;
using NzbDrone.Common.Disk; using NzbDrone.Common.Disk;
@ -15,8 +17,7 @@ using NzbDrone.Core.Update.Commands;
namespace NzbDrone.Core.Update namespace NzbDrone.Core.Update
{ {
public class InstallUpdateService : IExecute<ApplicationUpdateCommand>
public class InstallUpdateService : IExecute<ApplicationUpdateCommand>, IExecute<InstallUpdateCommand>
{ {
private readonly ICheckUpdateService _checkUpdateService; private readonly ICheckUpdateService _checkUpdateService;
private readonly Logger _logger; private readonly Logger _logger;
@ -63,58 +64,80 @@ namespace NzbDrone.Core.Update
private void InstallUpdate(UpdatePackage updatePackage) private void InstallUpdate(UpdatePackage updatePackage)
{ {
try EnsureAppDataSafety();
{
EnsureAppDataSafety();
var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder();
var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName); if (OsInfo.IsWindows || _configFileProvider.UpdateMechanism != UpdateMechanism.Script)
{
if (_diskProvider.FolderExists(updateSandboxFolder)) if (!_diskProvider.FolderWritable(_appFolderInfo.StartUpFolder))
{ {
_logger.Info("Deleting old update files"); throw new UpdateFolderNotWritableException("Cannot install update because startup folder '{0}' is not writable by the user '{1}'.", _appFolderInfo.StartUpFolder, Environment.UserName);
_diskProvider.DeleteFolder(updateSandboxFolder, true);
} }
}
_logger.ProgressInfo("Downloading update {0}", updatePackage.Version); var updateSandboxFolder = _appFolderInfo.GetUpdateSandboxFolder();
_logger.Debug("Downloading update package from [{0}] to [{1}]", updatePackage.Url, packageDestination);
_httpClient.DownloadFile(updatePackage.Url, packageDestination);
_logger.ProgressInfo("Verifying update package"); var packageDestination = Path.Combine(updateSandboxFolder, updatePackage.FileName);
if (!_updateVerifier.Verify(updatePackage, packageDestination)) if (_diskProvider.FolderExists(updateSandboxFolder))
{ {
_logger.Error("Update package is invalid"); _logger.Info("Deleting old update files");
throw new UpdateVerificationFailedException("Update file '{0}' is invalid", packageDestination); _diskProvider.DeleteFolder(updateSandboxFolder, true);
} }
_logger.Info("Update package verified successfully"); _logger.ProgressInfo("Downloading update {0}", updatePackage.Version);
_logger.Debug("Downloading update package from [{0}] to [{1}]", updatePackage.Url, packageDestination);
_httpClient.DownloadFile(updatePackage.Url, packageDestination);
_logger.ProgressInfo("Extracting Update package"); _logger.ProgressInfo("Verifying update package");
_archiveService.Extract(packageDestination, updateSandboxFolder);
_logger.Info("Update package extracted successfully");
_backupService.Backup(BackupType.Update); if (!_updateVerifier.Verify(updatePackage, packageDestination))
{
_logger.Error("Update package is invalid");
throw new UpdateVerificationFailedException("Update file '{0}' is invalid", packageDestination);
}
if (OsInfo.IsNotWindows && _configFileProvider.UpdateMechanism == UpdateMechanism.Script) _logger.Info("Update package verified successfully");
{
InstallUpdateWithScript(updateSandboxFolder); _logger.ProgressInfo("Extracting Update package");
return; _archiveService.Extract(packageDestination, updateSandboxFolder);
} _logger.Info("Update package extracted successfully");
_logger.Info("Preparing client"); EnsureValidBranch(updatePackage);
_diskProvider.MoveFolder(_appFolderInfo.GetUpdateClientFolder(),
updateSandboxFolder);
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath()); _backupService.Backup(BackupType.Update);
_logger.ProgressInfo("NzbDrone will restart shortly.");
_processProvider.Start(_appFolderInfo.GetUpdateClientExePath(), GetUpdaterArgs(updateSandboxFolder)); if (OsInfo.IsNotWindows && _configFileProvider.UpdateMechanism == UpdateMechanism.Script)
{
InstallUpdateWithScript(updateSandboxFolder);
return;
} }
catch (Exception ex)
_logger.Info("Preparing client");
_diskProvider.MoveFolder(_appFolderInfo.GetUpdateClientFolder(),
updateSandboxFolder);
_logger.Info("Starting update client {0}", _appFolderInfo.GetUpdateClientExePath());
_logger.ProgressInfo("Sonarr will restart shortly.");
_processProvider.Start(_appFolderInfo.GetUpdateClientExePath(), GetUpdaterArgs(updateSandboxFolder));
}
private void EnsureValidBranch(UpdatePackage package)
{
var currentBranch = _configFileProvider.Branch;
if (package.Branch != currentBranch)
{ {
_logger.ErrorException("Update process failed", ex); try
{
_logger.Info("Branch [{0}] is being redirected to [{1}]]", currentBranch, package.Branch);
var config = new Dictionary<string, object>();
config["Branch"] = package.Branch;
_configFileProvider.SaveConfigDictionary(config);
}
catch (Exception e)
{
_logger.ErrorException(string.Format("Couldn't change the branch from [{0}] to [{1}].", currentBranch, package.Branch), e);
}
} }
} }
@ -124,13 +147,12 @@ namespace NzbDrone.Core.Update
if (scriptPath.IsNullOrWhiteSpace()) if (scriptPath.IsNullOrWhiteSpace())
{ {
throw new ArgumentException("Update Script has not been defined"); throw new UpdateFailedException("Update Script has not been defined");
} }
if (!_diskProvider.FileExists(scriptPath, StringComparison.Ordinal)) if (!_diskProvider.FileExists(scriptPath, StringComparison.Ordinal))
{ {
var message = String.Format("Update Script: '{0}' does not exist", scriptPath); throw new UpdateFailedException("Update Script: '{0}' does not exist", scriptPath);
throw new FileNotFoundException(message, scriptPath);
} }
_logger.Info("Removing NzbDrone.Update"); _logger.Info("Removing NzbDrone.Update");
@ -153,24 +175,49 @@ namespace NzbDrone.Core.Update
if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) || if (_appFolderInfo.StartUpFolder.IsParentPath(_appFolderInfo.AppDataFolder) ||
_appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder)) _appFolderInfo.StartUpFolder.PathEquals(_appFolderInfo.AppDataFolder))
{ {
throw new NotSupportedException("Update will cause AppData to be deleted, correct you configuration before proceeding"); throw new UpdateFailedException("Your Sonarr configuration '{0}' is being stored in application folder '{1}' which will cause data lost during the upgrade. Please remove any symlinks or redirects before trying again.", _appFolderInfo.AppDataFolder, _appFolderInfo.StartUpFolder);
} }
} }
public void Execute(ApplicationUpdateCommand message) public void Execute(ApplicationUpdateCommand message)
{ {
_logger.ProgressDebug("Checking for updates"); _logger.ProgressDebug("Checking for updates");
var latestAvailable = _checkUpdateService.AvailableUpdate(); var latestAvailable = _checkUpdateService.AvailableUpdate();
if (latestAvailable != null) if (latestAvailable == null)
{ {
InstallUpdate(latestAvailable); _logger.ProgressDebug("No update available.");
return;
} }
}
public void Execute(InstallUpdateCommand message) if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically && !message.Manual)
{ {
InstallUpdate(message.UpdatePackage); _logger.ProgressDebug("Auto-update not enabled, not installing available update.");
return;
}
try
{
InstallUpdate(latestAvailable);
message.Completed("Restarting Sonarr to apply updates");
}
catch (UpdateFolderNotWritableException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex, string.Format("Startup folder not writable by user '{0}'", Environment.UserName));
}
catch (UpdateVerificationFailedException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex, "Downloaded update package is corrupt");
}
catch (UpdateFailedException ex)
{
_logger.ErrorException("Update process failed", ex);
message.Failed(ex);
}
} }
} }
} }

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
namespace NzbDrone.Core.Update namespace NzbDrone.Core.Update
@ -23,7 +24,7 @@ namespace NzbDrone.Core.Update
public List<UpdatePackage> GetRecentUpdatePackages() public List<UpdatePackage> GetRecentUpdatePackages()
{ {
var branch = _configFileProvider.Branch; var branch = _configFileProvider.Branch;
return _updatePackageProvider.GetRecentUpdates(branch); return _updatePackageProvider.GetRecentUpdates(branch, BuildInfo.Version);
} }
} }
} }

@ -0,0 +1,17 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Update
{
public class UpdateFailedException : NzbDroneException
{
public UpdateFailedException(string message, params object[] args)
: base(message, args)
{
}
public UpdateFailedException(string message)
: base(message)
{
}
}
}

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Instrumentation.Extensions;
@ -30,34 +31,7 @@ namespace NzbDrone.Core.Update
public UpdatePackage AvailableUpdate() public UpdatePackage AvailableUpdate()
{ {
if (OsInfo.IsNotWindows && !_configFileProvider.UpdateAutomatically) return _updatePackageProvider.GetLatestUpdate(_configFileProvider.Branch, BuildInfo.Version);
{
return null;
}
var latestAvailable = _updatePackageProvider.GetLatestUpdate(_configFileProvider.Branch, BuildInfo.Version);
if (latestAvailable == null)
{
_logger.ProgressDebug("No update available.");
}
else if (latestAvailable.Branch != _configFileProvider.Branch)
{
try
{
_logger.Info("Branch [{0}] is being redirected to [{1}]]", _configFileProvider.Branch, latestAvailable.Branch);
var config = _configFileProvider.GetConfigDictionary();
config["Branch"] = latestAvailable.Branch;
_configFileProvider.SaveConfigDictionary(config);
}
catch (Exception e)
{
_logger.ErrorException("Couldn't save the branch redirect.", e);
}
}
return latestAvailable;
} }
} }
} }

@ -0,0 +1,17 @@
using NzbDrone.Common.Exceptions;
namespace NzbDrone.Core.Update
{
public class UpdateFolderNotWritableException : UpdateFailedException
{
public UpdateFolderNotWritableException(string message, params object[] args)
: base(message, args)
{
}
public UpdateFolderNotWritableException(string message)
: base(message)
{
}
}
}

@ -9,7 +9,7 @@ namespace NzbDrone.Core.Update
public interface IUpdatePackageProvider public interface IUpdatePackageProvider
{ {
UpdatePackage GetLatestUpdate(string branch, Version currentVersion); UpdatePackage GetLatestUpdate(string branch, Version currentVersion);
List<UpdatePackage> GetRecentUpdates(string branch); List<UpdatePackage> GetRecentUpdates(string branch, Version currentVersion);
} }
public class UpdatePackageProvider : IUpdatePackageProvider public class UpdatePackageProvider : IUpdatePackageProvider
@ -37,9 +37,10 @@ namespace NzbDrone.Core.Update
return update.UpdatePackage; return update.UpdatePackage;
} }
public List<UpdatePackage> GetRecentUpdates(string branch) public List<UpdatePackage> GetRecentUpdates(string branch, Version currentVersion)
{ {
var request = _requestBuilder.Build("/update/{branch}/changes"); var request = _requestBuilder.Build("/update/{branch}/changes");
request.UriBuilder.SetQueryParam("version", currentVersion);
request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()); request.UriBuilder.SetQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant());
request.AddSegment("branch", branch); request.AddSegment("branch", branch);

@ -2,13 +2,15 @@
namespace NzbDrone.Core.Update namespace NzbDrone.Core.Update
{ {
public class UpdateVerificationFailedException : NzbDroneException public class UpdateVerificationFailedException : UpdateFailedException
{ {
public UpdateVerificationFailedException(string message, params object[] args) : base(message, args) public UpdateVerificationFailedException(string message, params object[] args)
: base(message, args)
{ {
} }
public UpdateVerificationFailedException(string message) : base(message) public UpdateVerificationFailedException(string message)
: base(message)
{ {
} }
} }

@ -1,4 +1,6 @@
using NUnit.Framework; using System;
using Mono.Unix;
using NUnit.Framework;
using NzbDrone.Common.Test.DiskTests; using NzbDrone.Common.Test.DiskTests;
namespace NzbDrone.Mono.Test.DiskProviderTests namespace NzbDrone.Mono.Test.DiskProviderTests
@ -11,5 +13,26 @@ namespace NzbDrone.Mono.Test.DiskProviderTests
{ {
MonoOnly(); MonoOnly();
} }
protected override void SetWritePermissions(string path, bool writable)
{
if (Environment.UserName == "root")
{
Assert.Inconclusive("Need non-root user to test write permissions.");
}
// Remove Write permissions, we're still owner so we can clean it up, but we'll have to do that explicitly.
var entry = UnixFileSystemInfo.GetFileSystemEntry(path);
if (writable)
{
entry.FileAccessPermissions |= FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite;
}
else
{
entry.FileAccessPermissions &= ~(FileAccessPermissions.UserWrite | FileAccessPermissions.GroupWrite | FileAccessPermissions.OtherWrite);
}
}
} }
} }

@ -58,6 +58,10 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\FluentAssertions.3.2.1\lib\net40\FluentAssertions.Core.dll</HintPath> <HintPath>..\packages\FluentAssertions.3.2.1\lib\net40\FluentAssertions.Core.dll</HintPath>
</Reference> </Reference>
<Reference Include="Mono.Posix, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\Libraries\Mono.Posix.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL"> <Reference Include="nunit.framework, Version=2.6.3.13283, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath> <HintPath>..\packages\NUnit.2.6.3\lib\nunit.framework.dll</HintPath>

@ -6,6 +6,7 @@ using Moq;
using NLog; using NLog;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Common.Cache; using NzbDrone.Common.Cache;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;

@ -1,4 +1,7 @@
using NUnit.Framework; using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using NUnit.Framework;
using NzbDrone.Common.Test.DiskTests; using NzbDrone.Common.Test.DiskTests;
namespace NzbDrone.Windows.Test.DiskProviderTests namespace NzbDrone.Windows.Test.DiskProviderTests
@ -11,5 +14,26 @@ namespace NzbDrone.Windows.Test.DiskProviderTests
{ {
WindowsOnly(); WindowsOnly();
} }
protected override void SetWritePermissions(string path, bool writable)
{
// Remove Write permissions, we're owner and have Delete permissions, so we can still clean it up.
var owner = WindowsIdentity.GetCurrent().Owner;
var accessControlType = writable ? AccessControlType.Allow : AccessControlType.Deny;
if (Directory.Exists(path))
{
var ds = Directory.GetAccessControl(path);
ds.SetAccessRule(new FileSystemAccessRule(owner, FileSystemRights.Write, accessControlType));
Directory.SetAccessControl(path, ds);
}
else
{
var fs = File.GetAccessControl(path);
fs.SetAccessRule(new FileSystemAccessRule(owner, FileSystemRights.Write, accessControlType));
File.SetAccessControl(path, fs);
}
}
} }
} }

@ -15,12 +15,15 @@ module.exports = Marionette.ItemView.extend({
id : this.model.id, id : this.model.id,
hideAfter : 0 hideAfter : 0
}; };
var isManual = this.model.get('manual');
switch (this.model.get('state')) { switch (this.model.get('state')) {
case 'completed': case 'completed':
message.hideAfter = 4; message.hideAfter = 4;
break; break;
case 'failed': case 'failed':
message.hideAfter = 4; message.hideAfter = isManual ? 10 : 4;
message.type = 'error'; message.type = 'error';
break; break;
default: default:

@ -12,21 +12,21 @@ var StatusModel = require('./StatusModel');
module.exports = Marionette.Layout.extend({ module.exports = Marionette.Layout.extend({
template : 'System/SystemLayoutTemplate', template : 'System/SystemLayoutTemplate',
regions : { regions : {
info : '#info', status : '#status',
logs : '#logs', logs : '#logs',
updates : '#updates', updates : '#updates',
backup : '#backup', backup : '#backup',
tasks : '#tasks' tasks : '#tasks'
}, },
ui : { ui : {
infoTab : '.x-info-tab', statusTab : '.x-status-tab',
logsTab : '.x-logs-tab', logsTab : '.x-logs-tab',
updatesTab : '.x-updates-tab', updatesTab : '.x-updates-tab',
backupTab : '.x-backup-tab', backupTab : '.x-backup-tab',
tasksTab : '.x-tasks-tab' tasksTab : '.x-tasks-tab'
}, },
events : { events : {
'click .x-info-tab' : '_showInfo', 'click .x-status-tab' : '_showStatus',
'click .x-logs-tab' : '_showLogs', 'click .x-logs-tab' : '_showLogs',
'click .x-updates-tab' : '_showUpdates', 'click .x-updates-tab' : '_showUpdates',
'click .x-backup-tab' : '_showBackup', 'click .x-backup-tab' : '_showBackup',
@ -58,7 +58,7 @@ module.exports = Marionette.Layout.extend({
this._showTasks(); this._showTasks();
break; break;
default: default:
this._showInfo(); this._showStatus();
} }
}, },
_navigate : function(route){ _navigate : function(route){
@ -67,13 +67,14 @@ module.exports = Marionette.Layout.extend({
replace : true replace : true
}); });
}, },
_showInfo : function(e){ _showStatus : function (e) {
if(e) { if (e) {
e.preventDefault(); e.preventDefault();
} }
this.info.show(new SystemInfoLayout());
this.ui.infoTab.tab('show'); this.status.show(new SystemInfoLayout());
this._navigate('system/info'); this.ui.statusTab.tab('show');
this._navigate('system/status');
}, },
_showLogs : function(e){ _showLogs : function(e){
if(e) { if(e) {

@ -1,9 +1,9 @@
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li><a href="#info" class="x-info-tab no-router">Info</a></li> <li><a href="#status" class="x-status-tab no-router">Status</a></li>
<li><a href="#logs" class="x-logs-tab no-router">Logs</a></li>
<li><a href="#updates" class="x-updates-tab no-router">Updates</a></li> <li><a href="#updates" class="x-updates-tab no-router">Updates</a></li>
<li><a href="#backup" class="x-backup-tab no-router">Backup</a></li>
<li><a href="#tasks" class="x-tasks-tab no-router">Tasks</a></li> <li><a href="#tasks" class="x-tasks-tab no-router">Tasks</a></li>
<li><a href="#backup" class="x-backup-tab no-router">Backup</a></li>
<li><a href="#logs" class="x-logs-tab no-router">Logs</a></li>
<li class="lifecycle-controls pull-right"> <li class="lifecycle-controls pull-right">
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown" data-container="body"> <button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown" data-container="body">
@ -23,9 +23,9 @@
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div class="tab-pane" id="info"></div> <div class="tab-pane" id="status"></div>
<div class="tab-pane" id="logs"></div>
<div class="tab-pane" id="updates"></div> <div class="tab-pane" id="updates"></div>
<div class="tab-pane" id="backup"></div>
<div class="tab-pane" id="tasks"></div> <div class="tab-pane" id="tasks"></div>
<div class="tab-pane" id="backup"></div>
<div class="tab-pane" id="logs"></div>
</div> </div>

@ -13,7 +13,7 @@ module.exports = Marionette.ItemView.extend({
} }
this.updating = true; this.updating = true;
var self = this; var self = this;
var promise = CommandController.Execute('installUpdate', {updatePackage : this.model.toJSON()}); var promise = CommandController.Execute('applicationUpdate');
promise.done(function(){ promise.done(function(){
window.setTimeout(function(){ window.setTimeout(function(){
self.updating = false; self.updating = false;

@ -5,14 +5,17 @@
- {{ShortDate releaseDate}} - {{ShortDate releaseDate}}
</span> </span>
<span class="status"> <span class="status">
{{#unless_eq branch compare="master"}}
<span class="label label-default">{{branch}}</span>
{{/unless_eq}}
{{#if installed}} {{#if installed}}
<span class="label label-success">Installed</span> <span class="label label-success">Installed</span>
{{else}} {{else}}
{{#if latest}} {{#if latest}}
{{#if installable}} {{#if installable}}
<span class="label label-default install-update x-install-update">Install</span> <span class="label label-info install-update x-install-update">Install Latest</span>
{{else}} {{else}}
<span class="label label-default label-disabled" title="Cannot install an older version">Install</span> <span class="label label-info label-disabled" title="Cannot install an older version">Install Latest</span>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}

Loading…
Cancel
Save