0

I'm using the following c # code to temporarily block the shutdown of a WinForm application without success, what I observe is that the System doesn't shutdown at all, probably because the work I have to do when receiving the shutdown notification is being made on the UI thread. Windows does not terminate the application if the application is unresponsive after 30 secs as documented. See the attached image.enter image description here

public Form1()
{
    InitializeComponent();

    // Define the priority of the application (0x3FF = The higher priority)
    SetProcessShutdownParameters(0x3FF, SHUTDOWN_NORETRY);
}

[DllImport("user32.dll")]
public extern static bool ShutdownBlockReasonCreate(IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string pwszReason);

[DllImport("user32.dll")]
public extern static bool ShutdownBlockReasonDestroy(IntPtr hWnd);

[DllImport("kernel32.dll")]
static extern bool SetProcessShutdownParameters(uint dwLevel, uint dwFlags);

private static int WM_QUERYENDSESSION = 0x11;
private static int WM_ENDSESSION = 0x16;
public const uint SHUTDOWN_NORETRY = 0x00000001;

private ManualResetEvent rEvent = new ManualResetEvent(false);
private bool blocked = false;
protected override void WndProc(ref System.Windows.Forms.Message m)
{
    if (m.Msg == WM_QUERYENDSESSION)
    {
        if (!blocked)
        {
            blocked = true;
            ShutdownBlockReasonCreate(this.Handle, "Closing in progress");

            this.BeginInvoke((Action)(() =>
            {
                // My clean-up work on UI thread
                Thread.Sleep(600000);

                // Allow Windows to shutdown
                ShutdownBlockReasonDestroy(this.Handle);
            }));

            m.Result = IntPtr.Zero;
        }
        else
        {
            m.Result = (IntPtr)1;
        }
    }

    if (m.Msg == WM_ENDSESSION)
    {
        if (blocked)
        {
            ShutdownBlockReasonDestroy(this.Handle);
            m.Result = (IntPtr)1;
        }
    }

    // If this is WM_QUERYENDSESSION, the closing event should be  
    // raised in the base WndProc.  
    base.WndProc(ref m);

} //WndProc
Francesco
  • 1
  • 1
  • When you get the first `WM_QUERYENDSESSION` message , set a flag, indicating that `ShutdownBlockReasonCreate()` has been called. Set `m.Result = IntPtr.Zero;`. After that, If the flag is already `true`, just set `m.Result = (IntPtr)1;`. When the message is `WM_ENDSESSION` and the flag is set to `true`, call `ShutdownBlockReasonDestroy()`. Set `m.Result = (IntPtr)1;`. Otherwise, `base.WndProc(ref m);`. Remove `this.BeginInvoke(...)`, `Thread.Sleep(20000);` and the second call to `ShutdownBlockReasonCreate()`. Don't use `return;`. – Jimi Oct 30 '19 at 17:30
  • Thank for your answer, the Thread.Sleep(20000) simulates the work I have to do when closing and this work must be performed on the UI thread. My problem is that I might find myself stuck on the execution of this work without ever responding to the WM_ENDSESSION, I need the this.BeginInvoke(...) to perform the work after answering the WM_ENDSESSION. – Francesco Oct 31 '19 at 08:38
  • If you need to perform some changes related to UI elements (while the System is shutting down? Maybe you mean you need to pick up something from UI controls), you can `BeginInvoke()` UI components, since BeginInvoke can also be used from the same Thread (the UI Thread, here). You just need to be aware that you have asynchronous responses. Note that this procedure won't have much time to complete. A User can override the block and also the System won't wait forever (it may actually wait 30 seconds). If you think it'll take less than this, no problem (consider the User's actions, though). – Jimi Oct 31 '19 at 10:09
  • Anyway (in case it's not clear), don't call `BeginInvoke` from the `WndProc` override. Call your clean-up/shut-down proc when you get the `WM_QUERYENDSESSION` message (not `WM_ENDSESSION`). From this procedure, if you need to, you can `BeginInvoke()` UI components. Since you know in advance that you might need to do something when the System shuts down, try to setup your code so that it doesn't have a reason to interact with the UI in the first place. – Jimi Oct 31 '19 at 10:38
  • As you have mentioned, I have read that the application has 30 seconds to respond to WM_ENDSESSION, and then Windows terminates the application if the application is unresponsive. [3]. During my test I have never encountered this situation, the ShutDown just wait my application to complete the operations and, if I find myself stuck on the execution of the work, the System doesn't shutdown at all. – Francesco Oct 31 '19 at 12:09
  • Did you make the (required) modifications I suggested? Can you post an image of the screen that the System presents to the user when an app asks to delay the shut-down proc? – Jimi Nov 01 '19 at 09:52
  • Hi Jimi, I have updated my code with your suggestions, I have attached the image requested as well. Thanks. – Francesco Nov 04 '19 at 09:07
  • Yes, that is the default behavior when a User initiate a System Restart in Windows 7. Since it's an User request, the User is prompted and asked to take action (since forcing a system ShutDown/Restart may cause loss of data or compromise/destroy a storage unit). If the Restart is caused by a system component (a component that acts as part of the System), this component may specify to force the shutdown in a definite time-out (30 seconds is common, in other occasions the time-out is 10 minutes, the User is prompted once then reminded 30 seconds before the action is completed). – Jimi Nov 04 '19 at 09:29
  • For more informations, see the Remarks section of [WM_ENDSESSION](https://learn.microsoft.com/en-us/windows/win32/shutdown/wm-endsession). -- Do yourself a favor: don't use `Thread.Sleep()` while you're also testing your app for responsiveness. Plus, if you want the Restart/ShutDown proc to resume, you have to close the app: the System is waiting for it to finish its job! I'll post a sample, using a Timer, maybe. – Jimi Nov 04 '19 at 09:53

1 Answers1

0

Note: don't test this functionality from the Visual Studio IDE:
Build the executable and run the Application from it.

Toggle private bool AllowEndSession true/false to disable/enable the System restart block.

It simulates a busy application that still needs to complete its work when the WM_QUERYENDSESSION message is received. Of course your App needs to be in a condition to respond to this message: the UI thread must be responsive (i.e., the App is doing work on a thread other than the UI thread).

Also, you should evaluate lParam, since it could be ENDSESSION_CRITICAL (the System itself may be forced to shut-down - power shortage and UPC running on fumes, as a possible edge case. A critical System Service failure as a more generic cause).

If the Application is not busy, but it requires to perform clean-up operations or other tasks that may take more time, it should return FALSE (IntPtr.Zero) when WM_QUERYENDSESSION is received and initiate the procedure that is the reason of the delay request when it receives WM_ENDSESSION

When an application returns TRUE for this message, it receives the WM_ENDSESSION message, regardless of how the other applications respond to the WM_QUERYENDSESSION message. Each application should return TRUE or FALSE immediately upon receiving this message, and defer any cleanup operations until it receives the WM_ENDSESSION message.

As a note, the request of a block should be used only when User data can be compromised for specific reasons or some hardware is completing an operation (as a CD/DVD write). Other procedures/tasks performed by the application must be completed all in due time.


When AllowEndSession = false; and the Application receives a WM_QUERYENDSESSION message, a System.Windows.Forms.Timer is started, with a time-out of 10 seconds (simulating a busy but responsive application the will take that amount of time to terminate a critical job).

The System will present to the User the classic restart block screen, informing that an App has requested to delay the restart process, showing the block reason string the App provided (The Cleaning Up/Doing stuff... Wait a sec passed to ShutdownBlockReasonCreate, here).

When the timer elapses (ticks), the job is terminated and the App will close its Main Form, call Application.Exit or whatever and also call ShutdownBlockReasonDestroy().

At this point, the Shut-Down/Restart procedure will resume, the System screen will update its status, close the remaining application, if any, still running and proceed to shut down.

private bool AllowEndSession = false;
private bool ShutbownBlockReasonCreated = false;
private System.Windows.Forms.Timer shutDownTimer = null;

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_QUERYENDSESSION:
            if (!AllowEndSession) {
                bool result = ShutdownBlockReasonCreate(this.Handle, "Cleaning Up/Doing stuff... Wait a sec");

                shutDownTimer = new System.Windows.Forms.Timer();
                shutDownTimer.Tick += (s, evt) => {
                    ShutbownBlockReasonCreated = false;
                    ShutdownBlockReasonDestroy(this.Handle);
                    shutDownTimer.Enabled = false;
                    shutDownTimer.Dispose();
                    this.Close();
                };
                shutDownTimer.Interval = 10000;
                shutDownTimer.Enabled = true;

                ShutbownBlockReasonCreated = true;
                m.Result = IntPtr.Zero;
            }
            else {
                m.Result = (IntPtr)1;
            }
            break;
        case WM_ENDSESSION:
            if (ShutbownBlockReasonCreated) {
                ShutdownBlockReasonDestroy(this.Handle);
            }
            m.Result = (IntPtr)1;
            break;
        default:
            base.WndProc(ref m);
            break;
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61