From 24ba4d54f0fab806991f496f7022479b87c83947 Mon Sep 17 00:00:00 2001 From: Jeremy Perrin Date: Tue, 24 Oct 2017 10:00:09 -0500 Subject: [PATCH 1/2] Added blocking ConsoleServiceHost I copied over ConsoleRunHost from topshelf and modified it to work with DotNetCore.WindowsService library. The console service host blocks until the user hits the CancelKey(Ctrl+Break or Ctrl+C). The ExampleService.cs is updated to reflect the library update. The update allows for debugging of services which are push based rather than pull based. --- Solution/DotNetCore.WindowsService.sln | 10 +- .../ExampleService.cs | 33 +++-- .../ExampleServiceTimer.cs | 9 +- .../Program.cs | 6 +- .../ConsoleServiceHost.cs | 135 ++++++++++++++++++ .../ExitCode.cs | 20 +++ .../ServiceRunner.cs | 49 ++++--- 7 files changed, 215 insertions(+), 47 deletions(-) create mode 100644 Source/PeterKottas.DotNetCore.WindowsService/ConsoleServiceHost.cs create mode 100644 Source/PeterKottas.DotNetCore.WindowsService/ExitCode.cs diff --git a/Solution/DotNetCore.WindowsService.sln b/Solution/DotNetCore.WindowsService.sln index 8b52ad9..0f8774d 100644 --- a/Solution/DotNetCore.WindowsService.sln +++ b/Solution/DotNetCore.WindowsService.sln @@ -1,13 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.14 +VisualStudioVersion = 15.0.27004.2005 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution items", "{8D5372CB-67BA-415A-B9B0-6C3771A6907E}" - ProjectSection(SolutionItems) = preProject - ..\global.json = ..\global.json - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PeterKottas.DotNetCore.WindowsService", "..\Source\PeterKottas.DotNetCore.WindowsService\PeterKottas.DotNetCore.WindowsService.csproj", "{19F85232-0FED-439E-90BF-BDCD6567F2B3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PeterKottas.DotNetCore.WindowsService.Example", "..\Source\PeterKottas.DotNetCore.WindowsService.Example\PeterKottas.DotNetCore.WindowsService.Example.csproj", "{63C67004-73D7-4D49-8A3F-3CBC6554BB46}" @@ -36,4 +31,7 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6DD17605-730C-4A8D-8875-05FD5096DAEF} + EndGlobalSection EndGlobal diff --git a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs index eed4685..8d24185 100644 --- a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs +++ b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs @@ -1,25 +1,25 @@ -using PeterKottas.DotNetCore.WindowsService.Interfaces; +using Microsoft.Extensions.PlatformAbstractions; +using PeterKottas.DotNetCore.WindowsService.Interfaces; using System; using System.IO; -using System.Diagnostics; -using Microsoft.Extensions.PlatformAbstractions; -using System.ServiceProcess; -using System.Threading.Tasks; +using System.Timers; namespace PeterKottas.DotNetCore.WindowsService.Example { - public class ExampleService : IMicroService + public class ExampleService : IMicroService { - private IMicroServiceController controller; + private IMicroServiceController _controller; + + private Timer _timer = new Timer(1000); public ExampleService() { - controller = null; + _controller = null; } public ExampleService(IMicroServiceController controller) { - this.controller = controller; + _controller = controller; } private string fileName = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "log.txt"); @@ -28,14 +28,19 @@ public void Start() Console.WriteLine("I started"); Console.WriteLine(fileName); File.AppendAllText(fileName, "Started\n"); - if (controller != null) - { - controller.Stop(); - } + + _timer.Elapsed += _timer_Elapsed; + _timer.Start(); } - public void Stop() + private void _timer_Elapsed(object sender, ElapsedEventArgs e) + { + File.AppendAllText(fileName, string.Format("Polling at {0}\n", DateTime.Now.ToString("o"))); + } + + public void Stop() { + _timer.Stop(); File.AppendAllText(fileName, "Stopped\n"); Console.WriteLine("I stopped"); } diff --git a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleServiceTimer.cs b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleServiceTimer.cs index 1a7f8ff..0bc96ed 100644 --- a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleServiceTimer.cs +++ b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleServiceTimer.cs @@ -1,13 +1,12 @@ -using System; -using System.IO; -using System.Diagnostics; +using Microsoft.Extensions.PlatformAbstractions; using PeterKottas.DotNetCore.WindowsService.Base; using PeterKottas.DotNetCore.WindowsService.Interfaces; -using Microsoft.Extensions.PlatformAbstractions; +using System; +using System.IO; namespace PeterKottas.DotNetCore.WindowsService.Example { - public class ExampleServiceTimer : MicroService, IMicroService + public class ExampleServiceTimer : MicroService, IMicroService { private IMicroServiceController controller; diff --git a/Source/PeterKottas.DotNetCore.WindowsService.Example/Program.cs b/Source/PeterKottas.DotNetCore.WindowsService.Example/Program.cs index 488bdfe..c195d15 100644 --- a/Source/PeterKottas.DotNetCore.WindowsService.Example/Program.cs +++ b/Source/PeterKottas.DotNetCore.WindowsService.Example/Program.cs @@ -1,14 +1,10 @@ using Microsoft.Extensions.PlatformAbstractions; -using PeterKottas.DotNetCore.WindowsService; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace PeterKottas.DotNetCore.WindowsService.Example { - public class Program + public class Program { public static void Main(string[] args) { diff --git a/Source/PeterKottas.DotNetCore.WindowsService/ConsoleServiceHost.cs b/Source/PeterKottas.DotNetCore.WindowsService/ConsoleServiceHost.cs new file mode 100644 index 0000000..12478da --- /dev/null +++ b/Source/PeterKottas.DotNetCore.WindowsService/ConsoleServiceHost.cs @@ -0,0 +1,135 @@ +using PeterKottas.DotNetCore.WindowsService.Interfaces; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace PeterKottas.DotNetCore.WindowsService +{ + /// + /// Copy of Topshelf ConsoleRunHost + /// https://github.com/Topshelf/Topshelf/blob/develop/src/Topshelf/Hosts/ConsoleRunHost.cs + /// + class ConsoleServiceHost + where SERVICE : IMicroService + { + private InnerService _consoleService = null; + private HostConfiguration _innerConfig = null; + private ExitCode _exitCode = 0; + private ManualResetEvent _exit = null; + private volatile bool _hasCancelled = false; + + public ConsoleServiceHost(InnerService consoleService, HostConfiguration innerConfig) + { + _consoleService = consoleService + ?? throw new ArgumentNullException(nameof(consoleService)); + + _innerConfig = innerConfig + ?? throw new ArgumentNullException(nameof(innerConfig)); + } + + internal ExitCode Run() + { + AppDomain.CurrentDomain.UnhandledException += CatchUnhandledException; + + bool started = false; + try + { + Console.WriteLine("Starting up as a console service host"); + + _exit = new ManualResetEvent(false); + _exitCode = ExitCode.Ok; + + Console.Title = _consoleService.ServiceName; + Console.CancelKeyPress += HandleCancelKeyPress; + + _consoleService.Start(_innerConfig.ExtraArguments.ToArray(), () => Console.WriteLine("Stopping console service host")); + started = true; + + Console.WriteLine("The {0} service is now running, press Control+C to exit.", _consoleService.ServiceName); + + _exit.WaitOne(); + } + catch (Exception ex) + { + Console.WriteLine("An exception occurred", ex); + + return ExitCode.AbnormalExit; + } + finally + { + if (started) + StopService(); + + _exit.Close(); + (_exit as IDisposable).Dispose(); + } + + return _exitCode; + } + + internal void StopService() + { + try + { + if (_hasCancelled) + return; + + Console.WriteLine("Stopping the {0} service", _consoleService.ServiceName); + + Task stopTask = Task.Run(() => _consoleService.Stop()); + if (!stopTask.Wait(TimeSpan.FromMilliseconds(150))) + throw new Exception("The service failed to stop (returned false)."); + + _exitCode = ExitCode.Ok; + } + catch (Exception ex) + { + Console.WriteLine("The service did not shut down gracefully: {0}", ex.ToString()); + _exitCode = ExitCode.AbnormalExit; + } + finally + { + Console.WriteLine("The {0} service has stopped.", _consoleService.ServiceName); + _exitCode = ExitCode.Ok; + } + } + + private void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + if (e.SpecialKey == ConsoleSpecialKey.ControlBreak) + { + Console.WriteLine("Control+Break detected, terminating service (not cleanly, use Control+C to exit cleanly)"); + return; + } + + e.Cancel = true; + + if (_hasCancelled) + return; + + Console.WriteLine("Control+C detected, attempting to stop service."); + Task stopTask = Task.Run(() => _consoleService.Stop()); + if (stopTask.Wait(TimeSpan.FromMilliseconds(150))) + { + _hasCancelled = true; + _exit.Set(); + } + else + { + _hasCancelled = false; + Console.WriteLine("The service is not in a state where it can be stopped."); + } + } + + private void CatchUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Console.WriteLine("The service threw an unhandled exception: {0}", e.ToString()); + + if (!e.IsTerminating) + return; + + _exitCode = ExitCode.UnhandledServiceException; + _exit.Set(); + } + } +} diff --git a/Source/PeterKottas.DotNetCore.WindowsService/ExitCode.cs b/Source/PeterKottas.DotNetCore.WindowsService/ExitCode.cs new file mode 100644 index 0000000..a43f9f7 --- /dev/null +++ b/Source/PeterKottas.DotNetCore.WindowsService/ExitCode.cs @@ -0,0 +1,20 @@ +namespace PeterKottas.DotNetCore.WindowsService +{ + /// + /// Copy of: https://github.com/Topshelf/Topshelf/blob/develop/src/Topshelf/TopshelfExitCode.cs + /// + enum ExitCode + { + Ok = 0, + AbnormalExit = 1, + SudoRequired = 2, + ServiceAlreadyInstalled = 3, + ServiceNotInstalled = 4, + StartServiceFailed = 5, + StopServiceFailed = 6, + ServiceAlreadyRunning = 7, + UnhandledServiceException = 8, + ServiceNotRunning = 9, + SendCommandFailed = 10, + } +} diff --git a/Source/PeterKottas.DotNetCore.WindowsService/ServiceRunner.cs b/Source/PeterKottas.DotNetCore.WindowsService/ServiceRunner.cs index 5f5c885..0e5a2da 100644 --- a/Source/PeterKottas.DotNetCore.WindowsService/ServiceRunner.cs +++ b/Source/PeterKottas.DotNetCore.WindowsService/ServiceRunner.cs @@ -128,22 +128,38 @@ public static int Run(Action> runAction) try { runAction(hostConfiguration); - if (innerConfig.Action == ActionEnum.Run || innerConfig.Action == ActionEnum.RunInteractive) - { - var controller = new MicroServiceController( - () => - { - var task = Task.Factory.StartNew(() => - { - UsingServiceController(innerConfig, (sc, cfg) => StopService(cfg, sc)); - }); - //task.Wait(); - } - ); - innerConfig.Service = innerConfig.ServiceFactory(innerConfig.ExtraArguments, controller); - } - ConfigureService(innerConfig); - return 0; + if (innerConfig.Action == ActionEnum.Run) + innerConfig.Service = innerConfig.ServiceFactory(innerConfig.ExtraArguments, + new MicroServiceController(() => + { + var task = Task.Factory.StartNew(() => + { + UsingServiceController(innerConfig, (sc, cfg) => StopService(cfg, sc)); + }); + } + )); + else if (innerConfig.Action == ActionEnum.RunInteractive) + { + var consoleService = new InnerService(innerConfig.Name, () => Start(innerConfig), () => Stop(innerConfig)); + var consoleHost = new ConsoleServiceHost(consoleService, innerConfig); + + innerConfig.Service = innerConfig.ServiceFactory(innerConfig.ExtraArguments, + new MicroServiceController(() => + { + var task = Task.Factory.StartNew(() => + { + consoleHost.StopService(); + }); + } + )); + + // Return the console host run result, so we get some idea what failed if result is not OK + return (int)consoleHost.Run(); + } + + ConfigureService(innerConfig); + + return 0; } catch (Exception e) { @@ -304,7 +320,6 @@ private static void ConfigureService(HostConfiguration config) serviceHost.Run(); break; case ActionEnum.RunInteractive: - Start(config); break; case ActionEnum.Stop: UsingServiceController(config, (sc, cfg) => StopService(cfg, sc)); From f0cde8f7381b7bd7b649d3dc1bf67c938c485956 Mon Sep 17 00:00:00 2001 From: Jeremy Perrin Date: Thu, 26 Oct 2017 09:41:47 -0500 Subject: [PATCH 2/2] Code review updates --- .editorconfig | 5 +++++ Solution/DotNetCore.WindowsService.sln | 5 +++++ .../ExampleService.cs | 20 +++++++++++-------- 3 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1252530 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.cs] +indent_style = space +indent_size = 4 diff --git a/Solution/DotNetCore.WindowsService.sln b/Solution/DotNetCore.WindowsService.sln index 0f8774d..f77f865 100644 --- a/Solution/DotNetCore.WindowsService.sln +++ b/Solution/DotNetCore.WindowsService.sln @@ -9,6 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PeterKottas.DotNetCore.Wind EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PeterKottas.DotNetCore.WindowsService.MinimalTemplate", "..\Source\Templates\PeterKottas.DotNetCore.WindowsService.MinimalTemplate\PeterKottas.DotNetCore.WindowsService.MinimalTemplate.csproj", "{D5FE4BD5-85F0-42D8-89D7-C8AF272278F3}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7DFFD5B6-AE3F-478B-9893-5010AEA29096}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs index 8d24185..7f1c2e0 100644 --- a/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs +++ b/Source/PeterKottas.DotNetCore.WindowsService.Example/ExampleService.cs @@ -6,20 +6,20 @@ namespace PeterKottas.DotNetCore.WindowsService.Example { - public class ExampleService : IMicroService + public class ExampleService : IMicroService { - private IMicroServiceController _controller; + private IMicroServiceController controller; - private Timer _timer = new Timer(1000); + private Timer timer = new Timer(1000); public ExampleService() { - _controller = null; + controller = null; } public ExampleService(IMicroServiceController controller) { - _controller = controller; + this.controller = controller; } private string fileName = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "log.txt"); @@ -29,8 +29,12 @@ public void Start() Console.WriteLine(fileName); File.AppendAllText(fileName, "Started\n"); - _timer.Elapsed += _timer_Elapsed; - _timer.Start(); + /** + * A timer is a simple example. But this could easily + * be a port or messaging queue client + */ + timer.Elapsed += _timer_Elapsed; + timer.Start(); } private void _timer_Elapsed(object sender, ElapsedEventArgs e) @@ -40,7 +44,7 @@ private void _timer_Elapsed(object sender, ElapsedEventArgs e) public void Stop() { - _timer.Stop(); + timer.Stop(); File.AppendAllText(fileName, "Stopped\n"); Console.WriteLine("I stopped"); }