mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-18 16:29:41 +02:00
Added single instance policy
This commit is contained in:
parent
266d1a43d9
commit
c219be8c8d
@ -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" />
|
||||
|
91
src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs
Normal file
91
src/NzbDrone.App.Test/NzbDroneProcessServiceFixture.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
40
src/NzbDrone.Host/BrowserService.cs
Normal file
40
src/NzbDrone.Host/BrowserService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
47
src/NzbDrone.Host/NzbDroneProcessService.cs
Normal file
47
src/NzbDrone.Host/NzbDroneProcessService.cs
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
{
|
||||
public interface IHostController
|
||||
{
|
||||
string AppUrl { get; }
|
||||
void StartServer();
|
||||
void StopServer();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -4,5 +4,11 @@ namespace NzbDrone.Host
|
||||
{
|
||||
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.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)
|
||||
{
|
||||
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user