diff --git a/src/Jackett.Server/Helper.cs b/src/Jackett.Server/Helper.cs index 4485a3b81..b0e4dbb80 100644 --- a/src/Jackett.Server/Helper.cs +++ b/src/Jackett.Server/Helper.cs @@ -6,6 +6,7 @@ using Jackett.Common.Models.Config; using Jackett.Common.Services; using Jackett.Common.Services.Interfaces; using Jackett.Common.Utils.Clients; +using Microsoft.AspNetCore.Hosting; using NLog; using NLog.Config; using NLog.Targets; @@ -19,8 +20,10 @@ namespace Jackett.Server public static class Helper { public static IContainer ApplicationContainer { get; set; } - + public static IApplicationLifetime applicationLifetime; private static bool _automapperInitialised = false; + public static ConsoleOptions ConsoleOptions { get; set; } + public static void Initialize() { @@ -32,13 +35,13 @@ namespace Jackett.Server _automapperInitialised = true; } - ProcessRuntimeSettings(); + ProcessSettings(); //Load the indexers ServerService.Initalize(); } - private static void ProcessRuntimeSettings() + private static void ProcessSettings() { RuntimeSettings runtimeSettings = ServerConfiguration.RuntimeSettings; @@ -74,11 +77,83 @@ namespace Jackett.Server Logger.Info("Jackett Data will be stored in: " + runtimeSettings.CustomDataFolder); } - // Use Proxy if (runtimeSettings.ProxyConnection != null) { Logger.Info("Proxy enabled. " + runtimeSettings.ProxyConnection); } + + if (ConsoleOptions.Install || ConsoleOptions.Uninstall || ConsoleOptions.StartService || ConsoleOptions.StopService || ConsoleOptions.ReserveUrls) + { + bool isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; + + if (!isWindows) + { + Logger.Error($"ReserveUrls and service arguments only apply to Windows, please remove them from your start arguments"); + Environment.Exit(1); + } + } + + + /* ====== Actions ===== */ + + // Install service + if (ConsoleOptions.Install) + { + Logger.Info("Initiating Jackett service install"); + ServiceConfigService.Install(); + Environment.Exit(1); + } + + // Uninstall service + if (ConsoleOptions.Uninstall) + { + Logger.Info("Initiating Jackett service uninstall"); + ServiceConfigService.Uninstall(); + Environment.Exit(1); + } + + // Start Service + if (ConsoleOptions.StartService) + { + if (!ServiceConfigService.ServiceRunning()) + { + Logger.Info("Initiating Jackett service start"); + ServiceConfigService.Start(); + } + Environment.Exit(1); + } + + // Stop Service + if (ConsoleOptions.StopService) + { + if (ServiceConfigService.ServiceRunning()) + { + Logger.Info("Initiating Jackett service stop"); + ServiceConfigService.Stop(); + } + Environment.Exit(1); + } + + // Reserve urls + if (ConsoleOptions.ReserveUrls) + { + Logger.Info("Initiating ReserveUrls"); + ServerService.ReserveUrls(doInstall: true); + Environment.Exit(1); + } + } + + public static void RestartWebHost() + { + Logger.Info("Restart of the web application host (not process) initiated"); + Program.isWebHostRestart = true; + applicationLifetime.StopApplication(); + } + + public static void StopWebHost() + { + Logger.Info("Jackett is being stopped"); + applicationLifetime.StopApplication(); } public static IConfigurationService ConfigService @@ -97,6 +172,14 @@ namespace Jackett.Server } } + public static IServiceConfigService ServiceConfigService + { + get + { + return ApplicationContainer.Resolve(); + } + } + public static ServerConfig ServerConfiguration { get diff --git a/src/Jackett.Server/Jackett.Server.csproj b/src/Jackett.Server/Jackett.Server.csproj index 1ce71fa70..623f93167 100644 --- a/src/Jackett.Server/Jackett.Server.csproj +++ b/src/Jackett.Server/Jackett.Server.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Jackett.Server/Program.cs b/src/Jackett.Server/Program.cs index 3dabf3f6b..0d7885fcb 100644 --- a/src/Jackett.Server/Program.cs +++ b/src/Jackett.Server/Program.cs @@ -21,15 +21,17 @@ namespace Jackett.Server public static class Program { public static IConfiguration Configuration { get; set; } - private static RuntimeSettings Settings { get; set; } + public static bool isWebHostRestart = false; public static void Main(string[] args) { AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; - var parser = new Parser(); - var optionsResult = parser.ParseArguments(args); + var commandLineParser = new Parser(); + var optionsResult = commandLineParser.ParseArguments(args); + var runtimeDictionary = new Dictionary(); + ConsoleOptions consoleOptions = new ConsoleOptions(); optionsResult.WithNotParsed(errors => { @@ -41,8 +43,6 @@ namespace Jackett.Server return; }); - var runtimeDictionary = new Dictionary(); - ConsoleOptions consoleOptions = new ConsoleOptions(); optionsResult.WithParsed(options => { if (string.IsNullOrEmpty(options.Client)) @@ -53,9 +53,11 @@ namespace Jackett.Server Settings = options.ToRunTimeSettings(); consoleOptions = options; - runtimeDictionary = GetValues(Settings); + runtimeDictionary = GetValues(Settings); }); + Helper.ConsoleOptions = consoleOptions; + var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(runtimeDictionary); @@ -111,7 +113,12 @@ namespace Jackett.Server tempContainer.Dispose(); tempContainer = null; - CreateWebHostBuilder(args, url).Build().Run(); + //TODO Handle scenario where user changes the port in the UI and we need to restart the WebHost with new port + do + { + isWebHostRestart = false; + CreateWebHostBuilder(args, url).Build().Run(); + } while (isWebHostRestart); } public static Dictionary GetValues(object obj) @@ -145,8 +152,8 @@ namespace Jackett.Server public static IWebHostBuilder CreateWebHostBuilder(string[] args, string[] urls) => WebHost.CreateDefaultBuilder(args) .UseConfiguration(Configuration) - .UseUrls(urls) - .PreferHostingUrls(true) + .UseUrls(urls) + .PreferHostingUrls(true) .UseStartup(); } } diff --git a/src/Jackett.Server/Services/ServiceConfigService.cs b/src/Jackett.Server/Services/ServiceConfigService.cs index 961c2c917..5fb71a0c8 100644 --- a/src/Jackett.Server/Services/ServiceConfigService.cs +++ b/src/Jackett.Server/Services/ServiceConfigService.cs @@ -1,28 +1,27 @@ using NLog; using System; -using System.Collections.Specialized; -using System.Configuration.Install; using System.Diagnostics; using System.IO; using System.Linq; using System.ServiceProcess; using Jackett.Common.Services.Interfaces; -namespace Jackett.Services +namespace Jackett.Server.Services { - public class ServiceConfigService : IServiceConfigService { private const string NAME = "Jackett"; - private const string DESCRIPTION = "Additional indexers for Sonarr"; + private const string DESCRIPTION = "API Support for your favorite torrent trackers"; private const string SERVICEEXE = "JackettService.exe"; private IConfigurationService configService; + private IProcessService processService; private Logger logger; - public ServiceConfigService(IConfigurationService c, Logger l) + public ServiceConfigService(IConfigurationService c, IProcessService p, Logger l) { configService = c; + processService = p; logger = l; } @@ -65,30 +64,17 @@ namespace Jackett.Services } else { - var installer = new ServiceProcessInstaller - { - Account = ServiceAccount.LocalSystem - }; - - var serviceInstaller = new ServiceInstaller(); - var exePath = Path.Combine(configService.ApplicationFolder(), SERVICEEXE); if (!File.Exists(exePath) && Debugger.IsAttached) { exePath = Path.Combine(configService.ApplicationFolder(), "..\\..\\..\\Jackett.Service\\bin\\Debug", SERVICEEXE); } - string[] cmdline = { @"/assemblypath=" + exePath}; + string arg = $"create {NAME} start= auto binpath= \"{exePath}\" DisplayName= {NAME}"; - var context = new InstallContext("jackettservice_install.log", cmdline); - serviceInstaller.Context = context; - serviceInstaller.DisplayName = NAME; - serviceInstaller.ServiceName = NAME; - serviceInstaller.Description = DESCRIPTION; - serviceInstaller.StartType = ServiceStartMode.Automatic; - serviceInstaller.Parent = installer; + processService.StartProcessAndLog("sc.exe", arg, true); - serviceInstaller.Install(new ListDictionary()); + processService.StartProcessAndLog("sc.exe", $"description {NAME} \"{DESCRIPTION}\"", true); } } @@ -96,11 +82,7 @@ namespace Jackett.Services { RemoveService(); - var serviceInstaller = new ServiceInstaller(); - var context = new InstallContext("jackettservice_uninstall.log", null); - serviceInstaller.Context = context; - serviceInstaller.ServiceName = NAME; - serviceInstaller.Uninstall(null); + processService.StartProcessAndLog("sc.exe", $"delete {NAME}", true); logger.Info("The service was uninstalled."); } @@ -120,12 +102,18 @@ namespace Jackett.Services service.Refresh(); if (service.Status == ServiceControllerStatus.Stopped) + { logger.Info("Service stopped."); + } else + { logger.Error("Failed to stop the service"); + } } else + { logger.Warn("The service was already stopped"); + } } } } diff --git a/src/Jackett.Server/Startup.cs b/src/Jackett.Server/Startup.cs index cc9c67bf8..940e760be 100644 --- a/src/Jackett.Server/Startup.cs +++ b/src/Jackett.Server/Startup.cs @@ -81,6 +81,7 @@ namespace Jackett.Server builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); IContainer container = builder.Build(); Helper.ApplicationContainer = container; @@ -94,7 +95,7 @@ namespace Jackett.Server public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) { applicationLifetime.ApplicationStopping.Register(OnShutdown); - + Helper.applicationLifetime = applicationLifetime; app.UseResponseCompression(); app.UseDeveloperExceptionPage(); diff --git a/src/Jackett.Service.Windows/Jackett.Service.Windows.csproj b/src/Jackett.Service.Windows/Jackett.Service.Windows.csproj new file mode 100644 index 000000000..e35755411 --- /dev/null +++ b/src/Jackett.Service.Windows/Jackett.Service.Windows.csproj @@ -0,0 +1,13 @@ + + + Exe + JackettService + net461 + + + + + + + + \ No newline at end of file diff --git a/src/Jackett.Service.Windows/Program.cs b/src/Jackett.Service.Windows/Program.cs new file mode 100644 index 000000000..bba75de67 --- /dev/null +++ b/src/Jackett.Service.Windows/Program.cs @@ -0,0 +1,17 @@ +using System.ServiceProcess; + +namespace Jackett.Service.Windows +{ + internal static class Program + { + private static void Main() + { + ServiceBase[] ServicesToRun; + ServicesToRun = new ServiceBase[] + { + new Service() + }; + ServiceBase.Run(ServicesToRun); + } + } +} diff --git a/src/Jackett.Service.Windows/Service.cs b/src/Jackett.Service.Windows/Service.cs new file mode 100644 index 000000000..b5b17639a --- /dev/null +++ b/src/Jackett.Service.Windows/Service.cs @@ -0,0 +1,29 @@ +using Jackett.Server; +using System.ServiceProcess; +using System.Threading; +using System.Threading.Tasks; + +namespace Jackett.Service.Windows +{ + public class Service : ServiceBase + { + private CancellationTokenSource tokenSource = new CancellationTokenSource(); + + protected override void OnStart(string[] args) + { + CancellationToken token = tokenSource.Token; + + Task.Run(async () => + { + //Registering callback that would cancel downloading + token.Register(() => Helper.StopWebHost()); + Jackett.Server.Program.Main(new string[0]); + }, token); + } + + protected override void OnStop() + { + tokenSource.Cancel(true); + } + } +} diff --git a/src/Jackett.sln b/src/Jackett.sln index 77cff8447..91ecb849d 100644 --- a/src/Jackett.sln +++ b/src/Jackett.sln @@ -39,7 +39,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".NET Core", ".NET Core", "{ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{AA50F785-12B8-4669-8D4F-EAFB49258E60}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jackett.Server", "Jackett.Server\Jackett.Server.csproj", "{84182782-EDBC-4342-ADA6-72B7694D0862}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Server", "Jackett.Server\Jackett.Server.csproj", "{84182782-EDBC-4342-ADA6-72B7694D0862}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jackett.Service.Windows", "Jackett.Service.Windows\Jackett.Service.Windows.csproj", "{45EA874E-4FF6-4D35-AE62-668EDA5E910D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -87,6 +89,10 @@ Global {84182782-EDBC-4342-ADA6-72B7694D0862}.Debug|Any CPU.Build.0 = Debug|Any CPU {84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.ActiveCfg = Release|Any CPU {84182782-EDBC-4342-ADA6-72B7694D0862}.Release|Any CPU.Build.0 = Release|Any CPU + {45EA874E-4FF6-4D35-AE62-668EDA5E910D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45EA874E-4FF6-4D35-AE62-668EDA5E910D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45EA874E-4FF6-4D35-AE62-668EDA5E910D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45EA874E-4FF6-4D35-AE62-668EDA5E910D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -105,6 +111,7 @@ Global {FF8B9A1B-AE7E-4F14-9C37-DA861D034738} = {AA50F785-12B8-4669-8D4F-EAFB49258E60} {6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9} = {AA50F785-12B8-4669-8D4F-EAFB49258E60} {84182782-EDBC-4342-ADA6-72B7694D0862} = {6A06EC9B-AF21-4DE8-9B50-BC7E3C2C78B9} + {45EA874E-4FF6-4D35-AE62-668EDA5E910D} = {FF8B9A1B-AE7E-4F14-9C37-DA861D034738} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {54BC4102-8B85-49C1-BA12-257D941D1B97}