Added single instance policy

pull/6/head
kayone 11 years ago
parent 266d1a43d9
commit c219be8c8d

@ -60,6 +60,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ContainerFixture.cs" /> <Compile Include="ContainerFixture.cs" />
<Compile Include="NzbDroneProcessServiceFixture.cs" />
<Compile Include="RouterTest.cs" /> <Compile Include="RouterTest.cs" />
<Compile Include="MonitoringProviderTest.cs" /> <Compile Include="MonitoringProviderTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.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<SingleInstancePolicy>
{
private const int CURRENT_PROCESS_ID = 5;
[SetUp]
public void Setup()
{
Mocker.GetMock<IProcessProvider>().Setup(c => c.GetCurrentProcess())
.Returns(new ProcessInfo() { Id = CURRENT_PROCESS_ID });
}
[Test]
public void should_continue_if_only_instance()
{
Mocker.GetMock<IProcessProvider>()
.Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME))
.Returns(new List<ProcessInfo>());
Mocker.GetMock<IProcessProvider>().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME))
.Returns(new List<ProcessInfo>
{
new ProcessInfo{Id = CURRENT_PROCESS_ID}
});
Subject.EnforceSingleInstance();
Mocker.GetMock<IBrowserService>().Verify(c => c.LaunchWebUI(), Times.Never());
}
[Test]
public void should_enforce_if_another_console_is_running()
{
Mocker.GetMock<IProcessProvider>()
.Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME))
.Returns(new List<ProcessInfo>
{
new ProcessInfo{Id = 10}
});
Mocker.GetMock<IProcessProvider>().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME))
.Returns(new List<ProcessInfo>
{
new ProcessInfo{Id = CURRENT_PROCESS_ID}
});
Assert.Throws<TerminateApplicationException>(() => Subject.EnforceSingleInstance());
Mocker.GetMock<IBrowserService>().Verify(c => c.LaunchWebUI(), Times.Once());
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_return_false_if_another_gui_is_running()
{
Mocker.GetMock<IProcessProvider>()
.Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_CONSOLE_PROCESS_NAME))
.Returns(new List<ProcessInfo>
{
new ProcessInfo{Id = CURRENT_PROCESS_ID}
});
Mocker.GetMock<IProcessProvider>().Setup(c => c.FindProcessByName(ProcessProvider.NZB_DRONE_PROCESS_NAME))
.Returns(new List<ProcessInfo>
{
new ProcessInfo{Id = 10}
});
Assert.Throws<TerminateApplicationException>(() => Subject.EnforceSingleInstance());
Mocker.GetMock<IBrowserService>().Verify(c => c.LaunchWebUI(), Times.Once());
ExceptionVerification.ExpectedWarns(1);
}
}
}

@ -15,6 +15,7 @@ namespace NzbDrone.Common.Processes
{ {
ProcessInfo GetCurrentProcess(); ProcessInfo GetCurrentProcess();
ProcessInfo GetProcessById(int id); ProcessInfo GetProcessById(int id);
List<ProcessInfo> FindProcessByName(string name);
void OpenDefaultBrowser(string url); void OpenDefaultBrowser(string url);
void WaitForExit(Process process); void WaitForExit(Process process);
void SetPriority(int processId, ProcessPriorityClass priority); void SetPriority(int processId, ProcessPriorityClass priority);
@ -74,6 +75,11 @@ namespace NzbDrone.Common.Processes
return processInfo; return processInfo;
} }
public List<ProcessInfo> FindProcessByName(string name)
{
return Process.GetProcessesByName(name).Select(ConvertToProcessInfo).Where(c => c != null).ToList();
}
public void OpenDefaultBrowser(string url) public void OpenDefaultBrowser(string url)
{ {
Logger.Info("Opening URL [{0}]", url); Logger.Info("Opening URL [{0}]", url);
@ -213,23 +219,29 @@ namespace NzbDrone.Common.Processes
process.Refresh(); process.Refresh();
ProcessInfo processInfo = null;
try 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, processInfo = null;
StartPath = GetExeFileName(process), }
Name = process.ProcessName
};
} }
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) private static string GetExeFileName(Process process)

@ -32,8 +32,10 @@ namespace NzbDrone.Console
Thread.Sleep(1000); Thread.Sleep(1000);
} }
} }
catch (TerminateApplicationException) catch (TerminateApplicationException e)
{ {
Logger.Info("Application has been terminated. Reason " + e.Reason);
return;
} }
catch (Exception e) catch (Exception e)
{ {

@ -1,8 +1,6 @@
using System; using System.ServiceProcess;
using System.ServiceProcess;
using NLog; using NLog;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes;
using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration;
using NzbDrone.Host.Owin; using NzbDrone.Host.Owin;
@ -20,20 +18,20 @@ namespace NzbDrone.Host
private readonly IConfigFileProvider _configFileProvider; private readonly IConfigFileProvider _configFileProvider;
private readonly IRuntimeInfo _runtimeInfo; private readonly IRuntimeInfo _runtimeInfo;
private readonly IHostController _hostController; private readonly IHostController _hostController;
private readonly IProcessProvider _processProvider;
private readonly PriorityMonitor _priorityMonitor; private readonly PriorityMonitor _priorityMonitor;
private readonly IStartupArguments _startupArguments; private readonly IStartupArguments _startupArguments;
private readonly IBrowserService _browserService;
private readonly Logger _logger; private readonly Logger _logger;
public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController, IRuntimeInfo runtimeInfo, public NzbDroneServiceFactory(IConfigFileProvider configFileProvider, IHostController hostController,
IProcessProvider processProvider, PriorityMonitor priorityMonitor, IStartupArguments startupArguments, Logger logger) IRuntimeInfo runtimeInfo, PriorityMonitor priorityMonitor, IStartupArguments startupArguments, IBrowserService browserService, Logger logger)
{ {
_configFileProvider = configFileProvider; _configFileProvider = configFileProvider;
_hostController = hostController; _hostController = hostController;
_runtimeInfo = runtimeInfo; _runtimeInfo = runtimeInfo;
_processProvider = processProvider;
_priorityMonitor = priorityMonitor; _priorityMonitor = priorityMonitor;
_startupArguments = startupArguments; _startupArguments = startupArguments;
_browserService = browserService;
_logger = logger; _logger = logger;
} }
@ -50,15 +48,7 @@ namespace NzbDrone.Host
_runtimeInfo.IsUserInteractive && _runtimeInfo.IsUserInteractive &&
_configFileProvider.LaunchBrowser) _configFileProvider.LaunchBrowser)
{ {
try _browserService.LaunchWebUI();
{
_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);
}
} }
_priorityMonitor.Start(); _priorityMonitor.Start();

@ -7,9 +7,11 @@ using NzbDrone.Core.Datastore;
namespace NzbDrone.Host 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(); var logger = NzbDroneLogger.GetLogger();
@ -21,15 +23,25 @@ namespace NzbDrone.Host
if (!PlatformValidation.IsValidate(userAlert)) 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<Router>().Route();
return container;
} }
public void Start()
{
DbFactory.RegisterDatabase(Container);
Container.Resolve<Router>().Route();
}
public void EnsureSingleInstance()
{
Container.Resolve<ISingleInstancePolicy>().EnforceSingleInstance();
}
} }
} }

@ -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);
}
}
}
}

@ -116,6 +116,8 @@
</Compile> </Compile>
<Compile Include="AccessControl\FirewallAdapter.cs" /> <Compile Include="AccessControl\FirewallAdapter.cs" />
<Compile Include="AccessControl\UrlAclAdapter.cs" /> <Compile Include="AccessControl\UrlAclAdapter.cs" />
<Compile Include="BrowserService.cs" />
<Compile Include="NzbDroneProcessService.cs" />
<Compile Include="IUserAlert.cs" /> <Compile Include="IUserAlert.cs" />
<Compile Include="Owin\NlogTextWriter.cs" /> <Compile Include="Owin\NlogTextWriter.cs" />
<Compile Include="Owin\OwinServiceProvider.cs" /> <Compile Include="Owin\OwinServiceProvider.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();
}
}
}

@ -2,7 +2,6 @@
{ {
public interface IHostController public interface IHostController
{ {
string AppUrl { get; }
void StartServer(); void StartServer();
void StopServer(); void StopServer();
} }

@ -107,11 +107,6 @@ namespace NzbDrone.Host.Owin
} }
} }
public string AppUrl
{
get { return string.Format("http://localhost:{0}", _configFileProvider.Port); }
}
public void StopServer() public void StopServer()
{ {
if (_host == null) return; if (_host == null) return;

@ -4,5 +4,11 @@ namespace NzbDrone.Host
{ {
public class TerminateApplicationException : ApplicationException public class TerminateApplicationException : ApplicationException
{ {
public TerminateApplicationException(string reason)
{
Reason = reason;
}
public string Reason { get; private set; }
} }
} }

@ -2,7 +2,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Windows.Forms; using System.Windows.Forms;
using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Processes; using NzbDrone.Host;
using NzbDrone.Host.Owin; using NzbDrone.Host.Owin;
namespace NzbDrone.SysTray namespace NzbDrone.SysTray
@ -14,16 +14,14 @@ namespace NzbDrone.SysTray
public class SystemTrayApp : Form, ISystemTrayApp public class SystemTrayApp : Form, ISystemTrayApp
{ {
private readonly IProcessProvider _processProvider; private readonly IBrowserService _browserService;
private readonly IHostController _hostController;
private readonly NotifyIcon _trayIcon = new NotifyIcon(); private readonly NotifyIcon _trayIcon = new NotifyIcon();
private readonly ContextMenu _trayMenu = new ContextMenu(); private readonly ContextMenu _trayMenu = new ContextMenu();
public SystemTrayApp(IProcessProvider processProvider, IHostController hostController) public SystemTrayApp(IBrowserService browserService)
{ {
_processProvider = processProvider; _browserService = browserService;
_hostController = hostController;
} }
@ -84,7 +82,7 @@ namespace NzbDrone.SysTray
{ {
try try
{ {
_processProvider.OpenDefaultBrowser(_hostController.AppUrl); _browserService.LaunchWebUI();
} }
catch (Exception) catch (Exception)
{ {

@ -10,7 +10,7 @@ namespace NzbDrone
{ {
public static class WindowsApp public static class WindowsApp
{ {
private static readonly Logger Logger = NzbDroneLogger.GetLogger(); private static readonly Logger Logger = NzbDroneLogger.GetLogger();
public static void Main(string[] args) public static void Main(string[] args)
{ {
@ -20,12 +20,18 @@ namespace NzbDrone
LogTargets.Register(startupArgs, false, true); LogTargets.Register(startupArgs, false, true);
var container = Bootstrap.Start(startupArgs, new MessageBoxUserAlert()); var bootstrap = new Bootstrap(startupArgs, new MessageBoxUserAlert());
container.Register<ISystemTrayApp, SystemTrayApp>();
container.Resolve<ISystemTrayApp>().Start(); bootstrap.EnsureSingleInstance();
bootstrap.Start();
bootstrap.Container.Register<ISystemTrayApp, SystemTrayApp>();
bootstrap.Container.Resolve<ISystemTrayApp>().Start();
} }
catch (TerminateApplicationException) catch (TerminateApplicationException e)
{ {
Logger.Info("Application has been terminated. Reason " + e.Reason);
} }
catch (Exception e) catch (Exception e)
{ {
@ -34,5 +40,7 @@ namespace NzbDrone
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!"); MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!");
} }
} }
} }
} }

Loading…
Cancel
Save