1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-12-16 11:37:58 +02:00

Added single instance policy

This commit is contained in:
kayone 2013-11-25 18:46:12 -08:00
parent 266d1a43d9
commit c219be8c8d
14 changed files with 254 additions and 51 deletions

View File

@ -60,6 +60,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="ContainerFixture.cs" />
<Compile Include="NzbDroneProcessServiceFixture.cs" />
<Compile Include="RouterTest.cs" />
<Compile Include="MonitoringProviderTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

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

View File

@ -15,6 +15,7 @@ public interface IProcessProvider
{
ProcessInfo GetCurrentProcess();
ProcessInfo GetProcessById(int id);
List<ProcessInfo> FindProcessByName(string name);
void OpenDefaultBrowser(string url);
void WaitForExit(Process process);
void SetPriority(int processId, ProcessPriorityClass priority);
@ -74,6 +75,11 @@ public ProcessInfo GetProcessById(int id)
return processInfo;
}
public List<ProcessInfo> 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 @@ private static ProcessInfo ConvertToProcessInfo(Process process)
process.Refresh();
ProcessInfo processInfo = null;
try
{
if (process.Id <= 0 || process.HasExited) return null;
if (process.Id <= 0) return null;
return new ProcessInfo
processInfo = new ProcessInfo();
processInfo.Id = process.Id;
processInfo.Name = process.ProcessName;
processInfo.StartPath = GetExeFileName(process);
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)

View File

@ -32,8 +32,10 @@ public static void Main(string[] args)
Thread.Sleep(1000);
}
}
catch (TerminateApplicationException)
catch (TerminateApplicationException e)
{
Logger.Info("Application has been terminated. Reason " + e.Reason);
return;
}
catch (Exception e)
{

View File

@ -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 @@ public class NzbDroneServiceFactory : ServiceBase, INzbDroneServiceFactory
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 @@ public void Start()
_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();

View File

@ -7,9 +7,11 @@
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 @@ public static IContainer Start(StartupArguments args, IUserAlert 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();
}
}
}

View File

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

View File

@ -116,6 +116,8 @@
</Compile>
<Compile Include="AccessControl\FirewallAdapter.cs" />
<Compile Include="AccessControl\UrlAclAdapter.cs" />
<Compile Include="BrowserService.cs" />
<Compile Include="NzbDroneProcessService.cs" />
<Compile Include="IUserAlert.cs" />
<Compile Include="Owin\NlogTextWriter.cs" />
<Compile Include="Owin\OwinServiceProvider.cs" />

View File

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

View File

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

View File

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

View File

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

View File

@ -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 @@ public interface ISystemTrayApp
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 @@ private void LaunchBrowser(object sender, EventArgs e)
{
try
{
_processProvider.OpenDefaultBrowser(_hostController.AppUrl);
_browserService.LaunchWebUI();
}
catch (Exception)
{

View File

@ -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 @@ public static void Main(string[] args)
LogTargets.Register(startupArgs, false, true);
var container = Bootstrap.Start(startupArgs, new MessageBoxUserAlert());
container.Register<ISystemTrayApp, SystemTrayApp>();
container.Resolve<ISystemTrayApp>().Start();
var bootstrap = new Bootstrap(startupArgs, new MessageBoxUserAlert());
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)
{
@ -34,5 +40,7 @@ public static void Main(string[] args)
MessageBox.Show(text: message, buttons: MessageBoxButtons.OK, icon: MessageBoxIcon.Error, caption: "Epic Fail!");
}
}
}
}