Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace Microsoft.Extensions.Hosting.Internal
{
Expand All @@ -20,7 +21,7 @@ private partial void RegisterShutdownHandlers()
Action<PosixSignalContext> handler = HandlePosixSignal;
_sigIntRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, handler);
_sigQuitRegistration = PosixSignalRegistration.Create(PosixSignal.SIGQUIT, handler);
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, handler);
_sigTermRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, OperatingSystem.IsWindows() ? HandleWindowsShutdown : handler);
}
}

Expand All @@ -32,6 +33,24 @@ private void HandlePosixSignal(PosixSignalContext context)
ApplicationLifetime.StopApplication();
}

private void HandleWindowsShutdown(PosixSignalContext context)
{
// for SIGTERM on Windows we must block this thread until the application is finished
// otherwise the process will be killed immediately on return from this handler

// don't allow Dispose to unregister handlers, since Windows has a lock that prevents the unregistration while this handler is running
// just leak these, since the process is exiting
_sigIntRegistration = null;
_sigQuitRegistration = null;
_sigTermRegistration = null;

ApplicationLifetime.StopApplication();

// We could wait for a signal here, like Dispose as is done in non-netcoreapp case, but those inevitably could have user
// code that runs after them in the user's Main. Instead we just block this thread completely and let the main routine exit.
Thread.Sleep(HostOptions.ShutdownTimeout);
}

private partial void UnregisterShutdownHandlers()
{
_sigIntRegistration?.Dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -18,14 +21,22 @@ public class ConsoleLifetimeExitTests
/// and the rest of "main" gets executed.
/// </summary>
[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[PlatformSpecific(TestPlatforms.AnyUnix)]
[InlineData(SIGTERM)]
[InlineData(SIGINT)]
[InlineData(SIGQUIT)]
public async Task EnsureSignalContinuesMainMethod(int signal)
{
using var remoteHandle = RemoteExecutor.Invoke(async () =>
// simulate signals on Windows by using a pipe to communicate with the remote process
using var messagePipe = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);

using var remoteHandle = RemoteExecutor.Invoke(async (pipeHandleAsString) =>
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// kick off a thread to simulate the signal on Windows
_ = Task.Run(() => SimulatePosixSignalWindows(pipeHandleAsString));
}

await Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
Expand All @@ -40,7 +51,7 @@ await Host.CreateDefaultBuilder()

Console.WriteLine("Run has completed");
return 123;
}, new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 });
}, messagePipe.GetClientHandleAsString(), new RemoteInvokeOptions() { Start = false, ExpectedExitCode = 123 });

remoteHandle.Process.StartInfo.RedirectStandardOutput = true;
remoteHandle.Process.Start();
Expand All @@ -53,7 +64,16 @@ await Host.CreateDefaultBuilder()
}

// send the signal to the process
kill(remoteHandle.Process.Id, signal);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// on Windows, we use the pipe to signal the process
messagePipe.WriteByte((byte)signal);
}
else
{
// on Unix, we send the signal directly
kill(remoteHandle.Process.Id, signal);
}

remoteHandle.Process.WaitForExit();

Expand All @@ -69,6 +89,69 @@ await Host.CreateDefaultBuilder()
[DllImport("libc", SetLastError = true)]
private static extern int kill(int pid, int sig);


private const int CTRL_C_EVENT = 0;
private const int CTRL_BREAK_EVENT = 1;
private const int CTRL_CLOSE_EVENT = 2;
private const int CTRL_LOGOFF_EVENT = 5;
private const int CTRL_SHUTDOWN_EVENT = 6;

private static unsafe void SimulatePosixSignalWindows(string pipeHandleAsString)
{
try
{
using var readPipe = new AnonymousPipeClientStream(PipeDirection.In, pipeHandleAsString);

int signal = (int)readPipe.ReadByte();

int ctrlType = (int)signal switch
{
SIGINT => CTRL_C_EVENT,
SIGQUIT => CTRL_BREAK_EVENT,
SIGTERM => CTRL_SHUTDOWN_EVENT,
_ => throw new ArgumentOutOfRangeException(nameof(signal), "Unsupported signal")
};

#if NETFRAMEWORK
if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT)
{
var handlerMethod = Type.GetType("System.Console, mscorlib")?.GetMethod("BreakEvent", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(handlerMethod);
handlerMethod.Invoke(null, [ctrlType]);
}
else // CTRL_SHUTDOWN_EVENT
{
var handlerField = typeof(AppDomain).GetField("_processExit", BindingFlags.NonPublic | BindingFlags.Instance);
Assert.NotNull(handlerField);
EventHandler handler = (EventHandler)handlerField.GetValue(AppDomain.CurrentDomain);
Assert.NotNull(handler);
handler.Invoke(AppDomain.CurrentDomain, null);
}
#else
// get the System.Runtime.InteropServices.PosixSignalRegistration.HandlerRoutine private method
var handlerMethod = typeof(PosixSignalRegistration).GetMethod("HandlerRoutine", BindingFlags.NonPublic | BindingFlags.Static);
Assert.NotNull(handlerMethod);

var handlerPtr = handlerMethod.MethodHandle.GetFunctionPointer();
delegate* unmanaged<int, int> handler = (delegate* unmanaged<int, int>)handlerPtr;

handler(ctrlType);

if (signal == SIGTERM)
{
// on Windows the OS will kill the process immediately after this
Environment.FailFast("Simulating shutdown");
}
#endif
}
catch (Exception ex)
{
// Exceptions on this thread will not be observed, nor will they cause the process to exit.
// Use failfast to ensure the process will exit without running any handlers.
Environment.FailFast(ex.ToString());
}
}

private class EnsureSignalContinuesMainMethodWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AutoGenerateBindingRedirects Condition="$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETFramework'">true</AutoGenerateBindingRedirects>
<EventSourceSupport Condition="'$(TestNativeAot)' == 'true'">true</EventSourceSupport>
</PropertyGroup>
Expand Down
Loading