-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Is your feature request related to a problem? Please describe.
I have .net core processes that runs a Kestrel server for hosting asp.net core apps on-premise.
One of this app is about spawning graphical "tiles" processes on the screen and show different info (video, text) in each tile (the screen is effectively covered by these tiles.
Porting this app on linux has its own challenges, so we're sticking with Windows for now.
Since these processes are graphical, we can't host this application in a Windows service.
When the windows PC and the apps are deployed, there will be no keyboard or mouse, but we might need to move the PC and need to shut it down properly. We configured windows so that it shut downs when the power button is pressed, but then, the application gets killed, which can corrupt the configuration files if they are being written.
In order to avoid that, I was looking for a solution that would block windows shutdown until the .net core process exits. It turns out there is a solution in the WIN32 API, but you need a window. So I hacked something around that.
Describe the solution you'd like
I'd like that when Windows shuts down, the runtime would be able to detect that automatically, and triggers a graceful shutdown.
I'm also OK with a solution like:
builder.HandleWindowsShutdown();
Additional context
Here is the solution I came up with, as a IHostedService
:
namespace Dev3I.Cameleon.Common
{
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
internal class GracefulShutdownService : IHostedService
{
private readonly IHostApplicationLifetime _appLifetime;
/// <summary>
/// The fake window handle, created by SystemEvents
/// </summary>
private IntPtr _windowHandle;
public GracefulShutdownService(
IHostApplicationLifetime appLifetime)
{
this._appLifetime = appLifetime;
}
public Task StartAsync(CancellationToken cancellationToken)
{
this._appLifetime.ApplicationStopped.Register(this.OnStopped);
// Tells windows that this application gets shutdown notifications with high priority (higher than normal process that have priority of 0x280)
SetProcessShutdownParameters(0x300, SHUTDOWN_NORETRY);
SystemEvents.InvokeOnEventsThread(new Action(() =>
{
//TODO : implement nullability annotations
// HACK; uses reflection because I couldn't get the window handle with GetActiveWindow...
var events = (SystemEvents)typeof(SystemEvents).GetField("s_systemEvents", BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null);
if (events != null)
{
var hwnd = (IntPtr?) typeof(SystemEvents).GetField("_windowHandle", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(events);
if (hwnd.HasValue)
{
this._windowHandle = hwnd.Value;
}
}
}));
SystemEvents.SessionEnding += this.OnSystemEventsOnSessionEnding;
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
SystemEvents.SessionEnding -= this.OnSystemEventsOnSessionEnding;
return Task.CompletedTask;
}
private void OnStopped()
{
if (this._windowHandle != IntPtr.Zero)
{
//Allows windows to continue shutting down
if (!ShutdownBlockReasonDestroy(this._windowHandle))
{
var error = Marshal.GetLastWin32Error();
//TODO: Log error
}
}
}
private void OnSystemEventsOnSessionEnding(object s, SessionEndingEventArgs e)
{
//Windows session is shutting down, exiting gracefully
if (this._windowHandle != IntPtr.Zero)
{
//Blocking windows shutdown until the application is properly stopped.
if (!ShutdownBlockReasonCreate(this._windowHandle, "Application is shutting down"))
{
var error = Marshal.GetLastWin32Error();
//TODO: Log error
}
else
{
e.Cancel = true;
}
}
this._appLifetime.StopApplication();
}
public const uint SHUTDOWN_NORETRY = 0x00000001;
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string reason);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetProcessShutdownParameters(uint dwLevel, uint dwFlags);
}
}