Skip to content

[Hosting][Windows][Kestrel] Graceful shutdown when windows is powering off #18718

@jeremyVignelles

Description

@jeremyVignelles

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);
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-hostingIncludes Hostingarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions