diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js index da381420c..a9e56d857 100644 --- a/frontend/src/Settings/General/GeneralSettings.js +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -101,6 +101,7 @@ class GeneralSettings extends Component { isWindowsService, isDocker, mode, + packageUpdateMechanism, onInputChange, onConfirmResetApiKey, ...otherProps @@ -163,6 +164,7 @@ class GeneralSettings extends Component { advancedSettings={advancedSettings} settings={settings} isWindows={isWindows} + packageUpdateMechanism={packageUpdateMechanism} isDocker={isDocker} onInputChange={onInputChange} /> @@ -208,6 +210,7 @@ GeneralSettings.propTypes = { isWindowsService: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired, mode: PropTypes.string.isRequired, + packageUpdateMechanism: PropTypes.string.isRequired, onInputChange: PropTypes.func.isRequired, onConfirmResetApiKey: PropTypes.func.isRequired, onConfirmRestart: PropTypes.func.isRequired diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js index 9f2ec7da1..dacae1a39 100644 --- a/frontend/src/Settings/General/GeneralSettingsConnector.js +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -28,6 +28,7 @@ function createMapStateToProps() { isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service', isDocker: systemStatus.isDocker, mode: systemStatus.mode, + packageUpdateMechanism: systemStatus.packageUpdateMechanism, ...sectionSettings }; } diff --git a/frontend/src/Settings/General/UpdateSettings.js b/frontend/src/Settings/General/UpdateSettings.js index 590eb9647..cec91302d 100644 --- a/frontend/src/Settings/General/UpdateSettings.js +++ b/frontend/src/Settings/General/UpdateSettings.js @@ -1,22 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; +import titleCase from 'Utilities/String/titleCase'; import { inputTypes, sizes } from 'Helpers/Props'; import FieldSet from 'Components/FieldSet'; import FormGroup from 'Components/Form/FormGroup'; import FormLabel from 'Components/Form/FormLabel'; import FormInputGroup from 'Components/Form/FormInputGroup'; -const branchValues = [ - 'develop', - 'nightly' -]; - function UpdateSettings(props) { const { advancedSettings, settings, isWindows, isDocker, + packageUpdateMechanism, onInputChange } = props; @@ -31,10 +28,20 @@ function UpdateSettings(props) { return null; } - const updateOptions = [ - { key: 'builtIn', value: 'Built-In' }, - { key: 'script', value: 'Script' } - ]; + const usingExternalUpdateMechanism = packageUpdateMechanism !== 'builtIn'; + + const updateOptions = []; + + if (usingExternalUpdateMechanism) { + updateOptions.push({ + key: packageUpdateMechanism, + value: titleCase(packageUpdateMechanism) + }); + } else { + updateOptions.push({ key: 'builtIn', value: 'Built-In' }); + } + + updateOptions.push({ key: 'script', value: 'Script' }); if (isDocker) { return ( @@ -53,13 +60,13 @@ function UpdateSettings(props) { Branch @@ -127,6 +134,7 @@ UpdateSettings.propTypes = { settings: PropTypes.object.isRequired, isWindows: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired, + packageUpdateMechanism: PropTypes.string.isRequired, onInputChange: PropTypes.func.isRequired }; diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 7751068a9..d04e8b99e 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -15,6 +15,8 @@ class About extends Component { render() { const { version, + packageVersion, + packageAuthor, isNetCore, isMono, isDocker, @@ -36,6 +38,14 @@ class About extends Component { data={version} /> + { + packageVersion && + + } + { isMono && @@ -50,24 +57,29 @@ class Updates extends Component { { hasUpdateToInstall && -
+
{ - !isDocker && - - Install Latest - - } - - { - isDocker && -
- An update is available. Please update your Docker image and re-create the container. -
+ (updateMechanism === 'builtIn' || updateMechanism === 'script') && !isDocker ? + + Install Latest + : + + + + +
+ {externalUpdaterMessages[updateMechanism] || externalUpdaterMessages.external} +
+
} { @@ -188,6 +200,7 @@ Updates.propTypes = { items: PropTypes.array.isRequired, isInstallingUpdate: PropTypes.bool.isRequired, isDocker: PropTypes.bool.isRequired, + updateMechanism: PropTypes.string, shortDateFormat: PropTypes.string.isRequired, onInstallLatestPress: PropTypes.func.isRequired }; diff --git a/src/Lidarr.Api.V1/System/SystemModule.cs b/src/Lidarr.Api.V1/System/SystemModule.cs index e157063b1..7b24f3e66 100644 --- a/src/Lidarr.Api.V1/System/SystemModule.cs +++ b/src/Lidarr.Api.V1/System/SystemModule.cs @@ -18,6 +18,7 @@ namespace Lidarr.Api.V1.System private readonly IConfigFileProvider _configFileProvider; private readonly IMainDatabase _database; private readonly ILifecycleService _lifecycleService; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; public SystemModule(IAppFolderInfo appFolderInfo, IRuntimeInfo runtimeInfo, @@ -26,7 +27,8 @@ namespace Lidarr.Api.V1.System IRouteCacheProvider routeCacheProvider, IConfigFileProvider configFileProvider, IMainDatabase database, - ILifecycleService lifecycleService) + ILifecycleService lifecycleService, + IDeploymentInfoProvider deploymentInfoProvider) : base("system") { _appFolderInfo = appFolderInfo; @@ -37,6 +39,7 @@ namespace Lidarr.Api.V1.System _configFileProvider = configFileProvider; _database = database; _lifecycleService = lifecycleService; + _deploymentInfoProvider = deploymentInfoProvider; Get("/status", x => GetStatus()); Get("/routes", x => GetRoutes()); Post("/shutdown", x => Shutdown()); @@ -71,7 +74,10 @@ namespace Lidarr.Api.V1.System UrlBase = _configFileProvider.UrlBase, RuntimeVersion = _platformInfo.Version, RuntimeName = PlatformInfo.Platform, - StartTime = _runtimeInfo.StartTime + StartTime = _runtimeInfo.StartTime, + PackageVersion = _deploymentInfoProvider.PackageVersion, + PackageAuthor = _deploymentInfoProvider.PackageAuthor, + PackageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism }; } diff --git a/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs b/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs new file mode 100644 index 000000000..560699b4b --- /dev/null +++ b/src/NzbDrone.Core/Configuration/DeploymentInfoProvider.cs @@ -0,0 +1,105 @@ +using System; +using System.IO; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Update; + +namespace NzbDrone.Core.Configuration +{ + public interface IDeploymentInfoProvider + { + string PackageVersion { get; } + string PackageAuthor { get; } + string PackageBranch { get; } + UpdateMechanism PackageUpdateMechanism { get; } + + string ReleaseVersion { get; } + string ReleaseBranch { get; } + + bool IsExternalUpdateMechanism { get; } + UpdateMechanism DefaultUpdateMechanism { get; } + string DefaultBranch { get; } + } + + public class DeploymentInfoProvider : IDeploymentInfoProvider + { + public DeploymentInfoProvider(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) + { + var bin = appFolderInfo.StartUpFolder; + var packageInfoPath = Path.Combine(bin, "..", "package_info"); + var releaseInfoPath = Path.Combine(bin, "release_info"); + + PackageUpdateMechanism = UpdateMechanism.BuiltIn; + DefaultBranch = "aphrodite"; + + if (Path.GetFileName(bin) == "bin" && diskProvider.FileExists(packageInfoPath)) + { + var data = diskProvider.ReadAllText(packageInfoPath); + + PackageVersion = ReadValue(data, "PackageVersion"); + PackageAuthor = ReadValue(data, "PackageAuthor"); + PackageUpdateMechanism = ReadEnumValue(data, "UpdateMethod", UpdateMechanism.BuiltIn); + PackageBranch = ReadValue(data, "Branch"); + + ReleaseVersion = ReadValue(data, "ReleaseVersion"); + + if (PackageBranch.IsNotNullOrWhiteSpace()) + { + DefaultBranch = PackageBranch; + } + } + + if (diskProvider.FileExists(releaseInfoPath)) + { + var data = diskProvider.ReadAllText(releaseInfoPath); + + ReleaseVersion = ReadValue(data, "ReleaseVersion", ReleaseVersion); + ReleaseBranch = ReadValue(data, "Branch"); + + if (ReleaseBranch.IsNotNullOrWhiteSpace()) + { + DefaultBranch = ReleaseBranch; + } + } + + DefaultUpdateMechanism = PackageUpdateMechanism; + } + + private static string ReadValue(string fileData, string key, string defaultValue = null) + { + var match = Regex.Match(fileData, "^" + key + "=(.*)$", RegexOptions.Multiline); + if (match.Success) + { + return match.Groups[1].Value.Trim(); + } + + return defaultValue; + } + + private static T ReadEnumValue(string fileData, string key, T defaultValue) + where T : struct + { + var value = ReadValue(fileData, key); + if (value != null && Enum.TryParse(value, true, out var result)) + { + return result; + } + + return defaultValue; + } + + public string PackageVersion { get; private set; } + public string PackageAuthor { get; private set; } + public string PackageBranch { get; private set; } + public UpdateMechanism PackageUpdateMechanism { get; private set; } + + public string ReleaseVersion { get; private set; } + public string ReleaseBranch { get; set; } + + public bool IsExternalUpdateMechanism => PackageUpdateMechanism >= UpdateMechanism.External; + public UpdateMechanism DefaultUpdateMechanism { get; private set; } + public string DefaultBranch { get; private set; } + } +} diff --git a/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs b/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs new file mode 100644 index 000000000..79e91bce1 --- /dev/null +++ b/src/NzbDrone.Core/Update/ConfigureUpdateMechanism.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Update +{ + public interface IUpdaterConfigProvider + { + } + + public class UpdaterConfigProvider : IUpdaterConfigProvider, IHandle + { + private readonly Logger _logger; + private readonly IConfigFileProvider _configFileProvider; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; + + public UpdaterConfigProvider(IDeploymentInfoProvider deploymentInfoProvider, IConfigFileProvider configFileProvider, Logger logger) + { + _deploymentInfoProvider = deploymentInfoProvider; + _configFileProvider = configFileProvider; + _logger = logger; + } + + public void Handle(ApplicationStartedEvent message) + { + var updateMechanism = _configFileProvider.UpdateMechanism; + var packageUpdateMechanism = _deploymentInfoProvider.PackageUpdateMechanism; + + var externalMechanisms = Enum.GetValues(typeof(UpdateMechanism)) + .Cast() + .Where(v => v >= UpdateMechanism.External) + .ToArray(); + + foreach (var externalMechanism in externalMechanisms) + { + if ((packageUpdateMechanism != externalMechanism && updateMechanism == externalMechanism) || + (packageUpdateMechanism == externalMechanism && updateMechanism == UpdateMechanism.BuiltIn)) + { + _logger.Info("Update mechanism {0} not supported in the current configuration, changing to {1}.", updateMechanism, packageUpdateMechanism); + ChangeUpdateMechanism(packageUpdateMechanism); + break; + } + } + + if (_deploymentInfoProvider.IsExternalUpdateMechanism) + { + var currentBranch = _configFileProvider.Branch; + var packageBranch = _deploymentInfoProvider.PackageBranch; + if (packageBranch.IsNotNullOrWhiteSpace() && packageBranch != currentBranch) + { + _logger.Info("External updater uses branch {0} instead of the currently selected {1}, changing to {0}.", packageBranch, currentBranch); + ChangeBranch(packageBranch); + } + } + } + + private void ChangeUpdateMechanism(UpdateMechanism updateMechanism) + { + var config = new Dictionary + { + [nameof(_configFileProvider.UpdateMechanism)] = updateMechanism + }; + _configFileProvider.SaveConfigDictionary(config); + } + + private void ChangeBranch(string branch) + { + var config = new Dictionary + { + [nameof(_configFileProvider.Branch)] = branch + }; + _configFileProvider.SaveConfigDictionary(config); + } + } +} diff --git a/src/NzbDrone.Core/Update/InstallUpdateService.cs b/src/NzbDrone.Core/Update/InstallUpdateService.cs index 1746422fa..fbbf29346 100644 --- a/src/NzbDrone.Core/Update/InstallUpdateService.cs +++ b/src/NzbDrone.Core/Update/InstallUpdateService.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Update private readonly IProcessProvider _processProvider; private readonly IVerifyUpdates _updateVerifier; private readonly IStartupContext _startupContext; + private readonly IDeploymentInfoProvider _deploymentInfoProvider; private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; private readonly IBackupService _backupService; @@ -43,6 +44,7 @@ namespace NzbDrone.Core.Update IProcessProvider processProvider, IVerifyUpdates updateVerifier, IStartupContext startupContext, + IDeploymentInfoProvider deploymentInfoProvider, IConfigFileProvider configFileProvider, IRuntimeInfo runtimeInfo, IBackupService backupService, @@ -63,6 +65,7 @@ namespace NzbDrone.Core.Update _processProvider = processProvider; _updateVerifier = updateVerifier; _startupContext = startupContext; + _deploymentInfoProvider = deploymentInfoProvider; _configFileProvider = configFileProvider; _runtimeInfo = runtimeInfo; _backupService = backupService; @@ -237,6 +240,18 @@ namespace NzbDrone.Core.Update return; } + // Safety net, ConfigureUpdateMechanism should take care of invalid settings + if (_configFileProvider.UpdateMechanism == UpdateMechanism.BuiltIn && _deploymentInfoProvider.IsExternalUpdateMechanism) + { + _logger.ProgressDebug("Built-In updater disabled, please use {0} to install", _deploymentInfoProvider.PackageUpdateMechanism); + return; + } + else if (_configFileProvider.UpdateMechanism != UpdateMechanism.Script && _deploymentInfoProvider.IsExternalUpdateMechanism) + { + _logger.ProgressDebug("Update available, please use {0} to install", _deploymentInfoProvider.PackageUpdateMechanism); + return; + } + try { InstallUpdate(latestAvailable); diff --git a/src/NzbDrone.Core/Update/UpdateMechanism.cs b/src/NzbDrone.Core/Update/UpdateMechanism.cs index 8b647a1e7..f87dca79c 100644 --- a/src/NzbDrone.Core/Update/UpdateMechanism.cs +++ b/src/NzbDrone.Core/Update/UpdateMechanism.cs @@ -1,8 +1,11 @@ -namespace NzbDrone.Core.Update +namespace NzbDrone.Core.Update { public enum UpdateMechanism { BuiltIn = 0, - Script = 1 + Script = 1, + External = 10, + Apt = 11, + Docker = 12 } }