-
Notifications
You must be signed in to change notification settings - Fork 14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
now listening to Unix Signals to exit gracefully #7
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
| ||
using Mono.Unix; | ||
using Mono.Unix.Native; | ||
|
||
namespace Topshelf.Linux | ||
{ | ||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Threading; | ||
#if !NET35 | ||
using System.Threading.Tasks; | ||
#endif | ||
using Logging; | ||
using Microsoft.Win32; | ||
using Runtime; | ||
|
||
public class LinuxConsoleRunHost : | ||
Host, | ||
HostControl | ||
{ | ||
readonly LogWriter _log = HostLogger.Get<LinuxConsoleRunHost>(); | ||
readonly HostEnvironment _environment; | ||
readonly ServiceHandle _serviceHandle; | ||
readonly HostSettings _settings; | ||
int _deadThread; | ||
|
||
TopshelfExitCode _exitCode; | ||
volatile bool _hasCancelled; | ||
|
||
public LinuxConsoleRunHost(HostSettings settings, HostEnvironment environment, ServiceHandle serviceHandle) | ||
{ | ||
if (settings == null) | ||
throw new ArgumentNullException("settings"); | ||
if (environment == null) | ||
throw new ArgumentNullException("environment"); | ||
|
||
_settings = settings; | ||
_environment = environment; | ||
_serviceHandle = serviceHandle; | ||
|
||
if (settings.CanSessionChanged) | ||
{ | ||
SystemEvents.SessionSwitch += OnSessionChanged; | ||
} | ||
} | ||
|
||
void OnSessionChanged(object sender, SessionSwitchEventArgs e) | ||
{ | ||
var arguments = new ConsoleSessionChangedArguments(e.Reason); | ||
|
||
_serviceHandle.SessionChanged(this, arguments); | ||
} | ||
|
||
|
||
public TopshelfExitCode Run() | ||
{ | ||
Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory); | ||
|
||
AppDomain.CurrentDomain.UnhandledException += CatchUnhandledException; | ||
|
||
if (_environment.IsServiceInstalled(_settings.ServiceName)) | ||
{ | ||
if (!_environment.IsServiceStopped(_settings.ServiceName)) | ||
{ | ||
_log.ErrorFormat("The {0} service is running and must be stopped before running via the console", | ||
_settings.ServiceName); | ||
|
||
return TopshelfExitCode.ServiceAlreadyRunning; | ||
} | ||
} | ||
|
||
bool started = false; | ||
try | ||
{ | ||
_log.Debug("Starting up as a console application"); | ||
_exitCode = TopshelfExitCode.Ok; | ||
|
||
Console.Title = _settings.DisplayName; | ||
Console.CancelKeyPress += HandleCancelKeyPress; | ||
|
||
if (!_serviceHandle.Start(this)) | ||
throw new TopshelfException("The service failed to start (return false)."); | ||
|
||
started = true; | ||
|
||
_log.InfoFormat("The {0} service is now running, press Control+C to exit.", _settings.ServiceName); | ||
|
||
var terminationSignals = GetUnixTerminationSignals(); | ||
UnixSignal.WaitAny(terminationSignals); | ||
} | ||
catch (Exception ex) | ||
{ | ||
_log.Error("An exception occurred", ex); | ||
|
||
return TopshelfExitCode.AbnormalExit; | ||
} | ||
finally | ||
{ | ||
if (started) | ||
StopService(); | ||
HostLogger.Shutdown(); | ||
} | ||
return _exitCode; | ||
} | ||
|
||
|
||
void HostControl.RequestAdditionalTime(TimeSpan timeRemaining) | ||
{ | ||
|
||
} | ||
|
||
|
||
void HostControl.Stop() | ||
{ | ||
_log.Info("Service Stop requested, exiting."); | ||
} | ||
|
||
|
||
void HostControl.Restart() | ||
{ | ||
_log.Info("Service Restart requested, but we don't support that here, so we are exiting."); | ||
} | ||
|
||
|
||
void CatchUnhandledException(object sender, UnhandledExceptionEventArgs e) | ||
{ | ||
_log.Fatal("The service threw an unhandled exception", (Exception)e.ExceptionObject); | ||
|
||
HostLogger.Shutdown(); | ||
|
||
if (e.IsTerminating) | ||
{ | ||
_exitCode = TopshelfExitCode.UnhandledServiceException; | ||
|
||
#if !NET35 | ||
// it isn't likely that a TPL thread should land here, but if it does let's no block it | ||
if (Task.CurrentId.HasValue) | ||
{ | ||
return; | ||
} | ||
#endif | ||
|
||
// this is evil, but perhaps a good thing to let us clean up properly. | ||
int deadThreadId = Interlocked.Increment(ref _deadThread); | ||
Thread.CurrentThread.IsBackground = true; | ||
Thread.CurrentThread.Name = "Unhandled Exception " + deadThreadId.ToString(); | ||
while (true) | ||
Thread.Sleep(TimeSpan.FromHours(1)); | ||
} | ||
} | ||
|
||
|
||
void StopService() | ||
{ | ||
try | ||
{ | ||
if (_hasCancelled == false) | ||
{ | ||
_log.InfoFormat("Stopping the {0} service", _settings.ServiceName); | ||
|
||
if (!_serviceHandle.Stop(this)) | ||
throw new TopshelfException("The service failed to stop (returned false)."); | ||
} | ||
} | ||
catch (Exception ex) | ||
{ | ||
_log.Error("The service did not shut down gracefully", ex); | ||
} | ||
finally | ||
{ | ||
_serviceHandle.Dispose(); | ||
|
||
_log.InfoFormat("The {0} service has stopped.", _settings.ServiceName); | ||
} | ||
} | ||
|
||
|
||
void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs consoleCancelEventArgs) | ||
{ | ||
if (consoleCancelEventArgs.SpecialKey == ConsoleSpecialKey.ControlBreak) | ||
{ | ||
_log.Error("Control+Break detected, terminating service (not cleanly, use Control+C to exit cleanly)"); | ||
return; | ||
} | ||
|
||
consoleCancelEventArgs.Cancel = true; | ||
|
||
if (_hasCancelled) | ||
return; | ||
|
||
_log.Info("Control+C detected, attempting to stop service."); | ||
if (_serviceHandle.Stop(this)) | ||
{ | ||
_hasCancelled = true; | ||
} | ||
else | ||
{ | ||
_hasCancelled = false; | ||
_log.Error("The service is not in a state where it can be stopped."); | ||
} | ||
} | ||
|
||
private static UnixSignal[] GetUnixTerminationSignals() | ||
{ | ||
return new[] | ||
{ | ||
new UnixSignal(Signum.SIGINT), | ||
new UnixSignal(Signum.SIGTERM), | ||
new UnixSignal(Signum.SIGQUIT), | ||
new UnixSignal(Signum.SIGHUP) | ||
}; | ||
} | ||
|
||
|
||
|
||
class ConsoleSessionChangedArguments : | ||
SessionChangedArguments | ||
{ | ||
readonly SessionChangeReasonCode _reasonCode; | ||
readonly int _sessionId; | ||
|
||
public ConsoleSessionChangedArguments(SessionSwitchReason reason) | ||
{ | ||
_reasonCode = (SessionChangeReasonCode)Enum.ToObject(typeof(SessionChangeReasonCode), (int)reason); | ||
_sessionId = Process.GetCurrentProcess().SessionId; | ||
} | ||
|
||
public SessionChangeReasonCode ReasonCode | ||
{ | ||
get { return _reasonCode; } | ||
} | ||
|
||
public int SessionId | ||
{ | ||
get { return _sessionId; } | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using Topshelf.Builders; | ||
|
||
namespace Topshelf.Linux | ||
{ | ||
using System; | ||
using Hosts; | ||
using Logging; | ||
using Runtime; | ||
|
||
|
||
public class LinuxRunBuilder : RunBuilder | ||
{ | ||
static readonly LogWriter _log = HostLogger.Get<LinuxRunBuilder>(); | ||
|
||
public LinuxRunBuilder(HostEnvironment environment, HostSettings settings) : base(environment, settings) | ||
{ | ||
} | ||
|
||
public override Host Build(ServiceBuilder serviceBuilder) | ||
{ | ||
ServiceHandle serviceHandle = serviceBuilder.Build(Settings); | ||
return CreateHost(serviceHandle); | ||
} | ||
|
||
Host CreateHost(ServiceHandle serviceHandle) | ||
{ | ||
if (Environment.IsRunningAsAService) | ||
{ | ||
|
||
_log.Debug("Running as a service, creating service host."); | ||
return Environment.CreateServiceHost(Settings, serviceHandle); | ||
} | ||
|
||
_log.Debug("Running as a console application, creating the console host."); | ||
return new LinuxConsoleRunHost(Settings, Environment, serviceHandle); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
|
||
<!-- | ||
// Copyright 2013 - Pablo Ruiz Garcia <pablo.ruiz at gmail.com> | ||
// | ||
|
@@ -15,5 +14,6 @@ | |
// specific language governing permissions and limitations under the License. | ||
--> | ||
<packages> | ||
<package id="Topshelf" version="3.2.0" targetFramework="net40" /> | ||
<package id="Mono.Posix" version="4.0.0.0" targetFramework="net40" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should avoid dependency on Mono.Posix.. as it is not binary compatible between builds available on MS.NET, and the binary included with mono linux' packages. This makes pretty hard to build/deploy the same binaries capable of working on both platforms (MS.NET & Mono/Linux). May you add to MonoHelper.cs all P/Invoke code required for Unix Signals management? Regards There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there is a conditional check using MonoHelper.RunningOnLinux I don't think there will ever be an issue of working on both Windows and Mono/Linux since on Windows the code is never JIT'd. It should be lazy and only loaded once executed. That will only happen on Linux and there its ok to call the Unix Signals. Do you agree with this statement? That is how I understand things should work but I might be wrong. FYI..I did test my application on both Windows and Ubuntu and both ran without issue. Looking at trying to bring in the functionality required for Unix signals might be more complicated and brittle. I would need to dig deeper but at first glance a decent amount of code will need to be ported over into MonoHelper. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I probably missexplained the issue. It does not have to do with JITing or not. The actual binaries of Mono.Posix.dll which came bundled with mono is not the same as the one distributed in order to be used on MS.NET (ie. there are different conditional compilation flags in use between both Mono.Posix.dll builds). On mono/linux Mono.Posix.dll comes bundled (and it is available from GAC). On windows you should bundle Mono.Posix.dll within your own application binaries (like for example, by referencing the available NuGet packages from your solution, as you did). However, when running an actual application which relies on Mono.Posix.dll and which includes a local Mono.Posix.dll binary, it conflicts with the on available at Mono's GAC.. Something which has happened before to a few users of TopShelf.Linux (we used to reference Mono.Posix at some point). In order to avoid such conflicts, the actual Mono.Posix code required was "transplanted" in MonoHelper.cs as to avoid referencing Mono.Posix.dll |
||
<package id="Topshelf" version="3.2.0" targetFramework="net40" /> | ||
</packages> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I see, this will break those cases when the service is running under "mono-service" (as those are not intended to run under ConsoleRunHost.
AFAIK, you should modify LinuxHostEnvironment::CreateServiceHost(..) in order to instantiate a this new LinuxConsoleRunHost, instead of forcing a new builder to override the host everytime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I maybe wrong.. at least from inspecting https://github.com/Topshelf/Topshelf/blob/0eec82de0140192d54daddc18f3e1f7fbeb4ba00/src/Topshelf/Configuration/Builders/RunBuilder.cs