diff --git a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj index 2b3db8178..274499d34 100644 --- a/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj +++ b/src/NzbDrone.App.Test/NzbDrone.Host.Test.csproj @@ -60,6 +60,7 @@ + diff --git a/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs new file mode 100644 index 000000000..e204713fb --- /dev/null +++ b/src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.Model; +using NzbDrone.Common.Processes; +using NzbDrone.Host; +using NzbDrone.Test.Common; + +namespace NzbDrone.App.Test +{ + [TestFixture] + public class NzbDroneProcessServiceFixture : TestBase + { + private const int CURRENT_PROCESS_ID = 5; + + [SetUp] + public void Setup() + { + Mocker.GetMock().Setup(c => c.GetCurrentProcess()) + .Returns(new ProcessInfo() { Id = CURRENT_PROCESS_ID }); + } + + [Test] + public void should_continue_if_only_instance() + { + Mocker.GetMock() + .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Returns(new List()); + + Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo{Id = CURRENT_PROCESS_ID} + }); + + + Subject.EnforceSingleInstance(); + + + Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Never()); + + } + + [Test] + public void should_enforce_if_another_console_is_running() + { + Mocker.GetMock() + .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo{Id = 10} + }); + + Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo{Id = CURRENT_PROCESS_ID} + }); + + + + Assert.Throws(() => Subject.EnforceSingleInstance()); + Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); + ExceptionVerification.ExpectedWarns(1); + } + + [Test] + public void should_return_false_if_another_gui_is_running() + { + Mocker.GetMock() + .Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo{Id = CURRENT_PROCESS_ID} + + }); + + Mocker.GetMock().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME)) + .Returns(new List + { + new ProcessInfo{Id = 10} + }); + + + + Assert.Throws(() => Subject.EnforceSingleInstance()); + Mocker.GetMock().Verify(c => c.LaunchWebUI(), Times.Once()); + ExceptionVerification.ExpectedWarns(1); + } + } +} diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 75bde50f8..30e120828 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Common.Processes { ProcessInfo GetCurrentProcess(); ProcessInfo GetProcessById(int id); + List FindProcessByName(string name); void OpenDefaultBrowser(string url); void WaitForExit(Process process); void SetPriority(int processId, ProcessPriorityClass priority); @@ -74,6 +75,11 @@ namespace NzbDrone.Common.Processes return processInfo; } + public List FindProcessByName(string name) + { + return Process.GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList(); + } + public void OpenDefaultBrowser(string url) { Logger.Info("Opening URL [{0}]", url); @@ -213,23 +219,29 @@ namespace NzbDrone.Common.Processes process.Refresh(); + ProcessInfo processInfo = null; + try { - if (process.Id <= 0 || process.HasExited) return null; + if (process.Id <= 0) return null; + + processInfo = new ProcessInfo(); + processInfo.Id = process.Id; + processInfo.Name = process.ProcessName; + processInfo.StartPath = GetExeFileName(process); - return new ProcessInfo + if (process.HasExited) { - Id = process.Id, - StartPath = GetExeFileName(process), - Name = process.ProcessName - }; + processInfo = null; + } } - catch (Win32Exception) + catch (Win32Exception e) { - Logger.Warn("Coudn't get process info for " + process.ProcessName); + Logger.WarnException("Couldn't get process info for " + process.ProcessName, e); } - return null; + return processInfo; + } private static string GetExeFileName(Process process) diff --git a/src/NzbDrone.Console/ConsoleApp.cs b/src/NzbDrone.Console/ConsoleApp.cs index 21f80bd82..27f5068fe 100644 --- a/src/NzbDrone.Console/ConsoleApp.cs +++ b/src/NzbDrone.Console/ConsoleApp.cs @@ -32,8 +32,10 @@ namespace NzbDrone.Console Thread.Sleep(1000); } } - catch (TerminateApplicationException) + catch (TerminateApplicationException e) { + Logger.Info("Application has been terminated. Reason " + e.Reason); + return; } catch (Exception e) { diff --git a/src/NzbDrone.Host/ApplicationServer.cs b/src/NzbDrone.Host/ApplicationServer.cs index a0410c348..3eed9da4c 100644 --- a/src/NzbDrone.Host/ApplicationServer.cs +++ b/src/NzbDrone.Host/ApplicationServer.cs @@ -1,8 +1,6 @@ -using System; -using System.ServiceProcess; +using System.ServiceProcess; using NLog; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Processes; using NzbDrone.Core.Configuration; using NzbDrone.Host.Owin; @@ -20,20 +18,20 @@ namespace NzbDrone.Host private readonly IConfigFileProvider _configFileProvider; private readonly IRuntimeInfo _runtimeInfo; private readonly IHostController _hostController; - private readonly IProcessProvider _processProvider; private readonly PriorityMonitor _priorityMonitor; private readonly IStartupArguments _startupArguments; + private readonly IBrowserService _browserService; private readonly Logger _logger; - public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController, IRuntimeInfo runtimeInfo, - IProcessProvider processProvider, PriorityMonitor priorityMonitor, IStartupArguments startupArguments, Logger logger) + public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController, + IRuntimeInfo runtimeInfo, PriorityMonitor priorityMonitor, IStartupArguments startupArguments, IBrowserService browserService, Logger logger) { _configFileProvider = configFileProvider; _hostController = hostController; _runtimeInfo = runtimeInfo; - _processProvider = processProvider; _priorityMonitor = priorityMonitor; _startupArguments = startupArguments; + _browserService = browserService; _logger = logger; } @@ -50,15 +48,7 @@ namespace NzbDrone.Host _runtimeInfo.IsUserInteractive && _configFileProvider.LaunchBrowser) { - try - { - _logger.Info("Starting default browser. {0}", _hostController.AppUrl); - _processProvider.OpenDefaultBrowser(_hostController.AppUrl); - } - catch (Exception e) - { - _logger.ErrorException("Failed to open URL in default browser.", e); - } + _browserService.LaunchWebUI(); } _priorityMonitor.Start(); diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index fb1d23eec..d5b315156 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -7,9 +7,11 @@ using NzbDrone.Core.Datastore; namespace NzbDrone.Host { - public static class Bootstrap + public class Bootstrap { - public static IContainer Start(StartupArguments args, IUserAlert userAlert) + public IContainer Container { get; private set; } + + public Bootstrap(StartupArguments args, IUserAlert userAlert) { var logger = NzbDroneLogger.GetLogger(); @@ -21,15 +23,25 @@ namespace NzbDrone.Host if (!PlatformValidation.IsValidate(userAlert)) { - throw new TerminateApplicationException(); + throw new TerminateApplicationException("Missing system requirements"); } - var container = MainAppContainerBuilder.BuildContainer(args); + Container = MainAppContainerBuilder.BuildContainer(args); - DbFactory.RegisterDatabase(container); - container.Resolve().Route(); - return container; } + + public void Start() + { + DbFactory.RegisterDatabase(Container); + Container.Resolve().Route(); + } + + + public void EnsureSingleInstance() + { + Container.Resolve().EnforceSingleInstance(); + } + } } \ No newline at end of file diff --git a/src/NzbDrone.Host/BrowserService.cs b/src/NzbDrone.Host/BrowserService.cs new file mode 100644 index 000000000..7ae6e13bb --- /dev/null +++ b/src/NzbDrone.Host/BrowserService.cs @@ -0,0 +1,40 @@ +using System; +using NLog; +using NzbDrone.Common.Processes; +using NzbDrone.Core.Configuration; + +namespace NzbDrone.Host +{ + public interface IBrowserService + { + void LaunchWebUI(); + } + + public class BrowserService : IBrowserService + { + private readonly IProcessProvider _processProvider; + private readonly IConfigFileProvider _configFileProvider; + private readonly Logger _logger; + + public BrowserService(IProcessProvider processProvider, IConfigFileProvider configFileProvider, Logger logger) + { + _processProvider = processProvider; + _configFileProvider = configFileProvider; + _logger = logger; + } + + public void LaunchWebUI() + { + var url = string.Format("http://localhost:{0}", _configFileProvider.Port); + try + { + _logger.Info("Starting default browser. {0}", url); + _processProvider.OpenDefaultBrowser(url); + } + catch (Exception e) + { + _logger.ErrorException("Couldn't open defult browser to " + url, e); + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 4cc61b5ce..7a0f05c83 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -116,6 +116,8 @@ + + diff --git a/src/NzbDrone.Host/NzbDroneProcessService.cs b/src/NzbDrone.Host/NzbDroneProcessService.cs new file mode 100644 index 000000000..3ee601f84 --- /dev/null +++ b/src/NzbDrone.Host/NzbDroneProcessService.cs @@ -0,0 +1,47 @@ +using System.Linq; +using NLog; +using NzbDrone.Common.Processes; + +namespace NzbDrone.Host +{ + public interface ISingleInstancePolicy + { + void EnforceSingleInstance(); + } + + public class SingleInstancePolicy : ISingleInstancePolicy + { + private readonly IProcessProvider _processProvider; + private readonly IBrowserService _browserService; + private readonly Logger _logger; + + public SingleInstancePolicy(IProcessProvider processProvider, IBrowserService browserService, Logger logger) + { + _processProvider = processProvider; + _browserService = browserService; + _logger = logger; + } + + public void EnforceSingleInstance() + { + if (IsAlreadyRunning()) + { + _logger.Warn("Another instance of NzbDrone is already running."); + _browserService.LaunchWebUI(); + throw new TerminateApplicationException("Another instance is already running"); + } + } + + private bool IsAlreadyRunning() + { + var currentId = _processProvider.GetCurrentProcess().Id; + var consoleIds = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME).Select(c => c.Id); + var guiIds = _processProvider.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME).Select(c => c.Id); + + var otherProcesses = consoleIds.Union(guiIds).Except(new[] { currentId }); + + return otherProcesses.Any(); + } + + } +} \ No newline at end of file diff --git a/src/NzbDrone.Host/Owin/IHostController.cs b/src/NzbDrone.Host/Owin/IHostController.cs index 026bff89e..130b48d4b 100644 --- a/src/NzbDrone.Host/Owin/IHostController.cs +++ b/src/NzbDrone.Host/Owin/IHostController.cs @@ -2,7 +2,6 @@ { public interface IHostController { - string AppUrl { get; } void StartServer(); void StopServer(); } diff --git a/src/NzbDrone.Host/Owin/OwinHostController.cs b/src/NzbDrone.Host/Owin/OwinHostController.cs index d750ee646..1a5d4ae9b 100644 --- a/src/NzbDrone.Host/Owin/OwinHostController.cs +++ b/src/NzbDrone.Host/Owin/OwinHostController.cs @@ -107,11 +107,6 @@ namespace NzbDrone.Host.Owin } } - public string AppUrl - { - get { return string.Format("http://localhost:{0}", _configFileProvider.Port); } - } - public void StopServer() { if (_host == null) return; diff --git a/src/NzbDrone.Host/TerminateApplicationException.cs b/src/NzbDrone.Host/TerminateApplicationException.cs index 04464bea3..ad703ca18 100644 --- a/src/NzbDrone.Host/TerminateApplicationException.cs +++ b/src/NzbDrone.Host/TerminateApplicationException.cs @@ -4,5 +4,11 @@ namespace NzbDrone.Host { public class TerminateApplicationException : ApplicationException { + public TerminateApplicationException(string reason) + { + Reason = reason; + } + + public string Reason { get; private set; } } } \ No newline at end of file diff --git a/src/NzbDrone/SysTray/SysTrayApp.cs b/src/NzbDrone/SysTray/SysTrayApp.cs index 3068bcd5c..47c5342c1 100644 --- a/src/NzbDrone/SysTray/SysTrayApp.cs +++ b/src/NzbDrone/SysTray/SysTrayApp.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Windows.Forms; using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Processes; +using NzbDrone.Host; using NzbDrone.Host.Owin; namespace NzbDrone.SysTray @@ -14,16 +14,14 @@ namespace NzbDrone.SysTray public class SystemTrayApp : Form, ISystemTrayApp { - private readonly IProcessProvider _processProvider; - private readonly IHostController _hostController; + private readonly IBrowserService _browserService; private readonly NotifyIcon _trayIcon = new NotifyIcon(); private readonly ContextMenu _trayMenu = new ContextMenu(); - public SystemTrayApp(IProcessProvider processProvider, IHostController hostController) + public SystemTrayApp(IBrowserService browserService) { - _processProvider = processProvider; - _hostController = hostController; + _browserService = browserService; } @@ -84,7 +82,7 @@ namespace NzbDrone.SysTray { try { - _processProvider.OpenDefaultBrowser(_hostController.AppUrl); + _browserService.LaunchWebUI(); } catch (Exception) { diff --git a/src/NzbDrone/WindowsApp.cs b/src/NzbDrone/WindowsApp.cs index 5b20de23b..8d070e502 100644 --- a/src/NzbDrone/WindowsApp.cs +++ b/src/NzbDrone/WindowsApp.cs @@ -10,7 +10,7 @@ namespace NzbDrone { public static class WindowsApp { - private static readonly Logger Logger = NzbDroneLogger.GetLogger(); + private static readonly Logger Logger = NzbDroneLogger.GetLogger(); public static void Main(string[] args) { @@ -20,12 +20,18 @@ namespace NzbDrone LogTargets.Register(startupArgs, false, true); - var container = Bootstrap.Start(startupArgs, new MessageBoxUserAlert()); - container.Register(); - container.Resolve().Start(); + var bootstrap = new Bootstrap(startupArgs, new MessageBoxUserAlert()); + + bootstrap.EnsureSingleInstance(); + + bootstrap.Start(); + bootstrap.Container.Register(); + bootstrap.Container.Resolve().Start(); + } - catch (TerminateApplicationException) + catch (TerminateApplicationException e) { + Logger.Info("Application has been terminated. Reason " + e.Reason); } catch (Exception e) { @@ -34,5 +40,7 @@ namespace NzbDrone MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!"); } } + + } }