6

I am trying to prevent my application from being shutdown by windows. The application is running on windows 8 and written in XE6. I tried following code but it seems to be completely ignored. To test it I simply send "end task" to it through the task manager. What I need is a way to let my application finish what its doing when the application is closed by the user, by the task manager of by a windows shutdown. Normal closing is not a problem, this is handled by the FormCloseQuery event. But the other 2 methods I can't get to work. Until windows XP this was easy by catching the wm_endsession and the wm_queryendsession, starting from vista you need the use ShutDownBlockReasonCreate, which returns true but does not seems to work anyway.

procedure WMQueryEndSession(var Msg : TWMQueryEndSession); message WM_QUERYENDSESSION;
procedure WMEndSession(var Msg: TWMEndSession); message WM_ENDSESSION;

function ShutdownBlockReasonCreate(hWnd: HWND; Reason: LPCWSTR): Bool; stdcall; external user32;
function ShutdownBlockReasonDestroy(hWnd: HWND): Bool; stdcall; external user32;


procedure TForm1.WMEndSession(var Msg: TWMEndSession);
begin
  inherited;

  Msg.Result := lresult(False);
  ShutdownBlockReasonCreate(Handle, 'please wait while muting...');
  Sleep(45000); // do your work here
  ShutdownBlockReasonDestroy(Handle);
end;

procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  inherited;
  Msg.Result := lresult(False);
  ShutdownBlockReasonCreate(Handle, 'please wait while muting...');
  Sleep(45000); // do your work here
  ShutdownBlockReasonDestroy(Handle);
end;

Update

Changing the message result to true and removing the sleep changes nothing.

procedure TForm1.WMEndSession(var Msg: TWMEndSession);
begin
  inherited;
  Msg.Result := lresult(True);
  ShutdownBlockReasonDestroy(Application.MainForm.Handle);
  ShutdownBlockReasonCreate(Application.MainForm.Handle, 'please wait while muting...');
end;

procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  inherited;
  Msg.Result := lresult(True);
  ShutdownBlockReasonDestroy(Application.MainForm.Handle);
  ShutdownBlockReasonCreate(Application.MainForm.Handle, 'please wait while muting...');
end;
LU RD
  • 34,438
  • 5
  • 88
  • 296
GuidoG
  • 11,359
  • 6
  • 44
  • 79
  • 1
    See [How to pause windows shut-down](http://stackoverflow.com/a/18347424/576719). – LU RD Aug 27 '14 at 20:31
  • 1
    You can't say that the functions are "a hoax". Check the return value of `ShutDownBlockReasonCreate`, and if it returns false use `GetLastError` to find out why it failed. You can't say "the API isn't working" when you don't bother to check return values to find out why it isn't. – Ken White Aug 27 '14 at 20:31
  • The function returns true if I call it from a button, I cant check the result in the WMQueryEndSession because the application shuts down before I can check its value. – GuidoG Aug 27 '14 at 20:45
  • Tried the code in the link above, does not work. Is this changed again in windows 8 perhaps ? Or could XE6 be the problem ? – GuidoG Aug 27 '14 at 20:46
  • 1
    > *" To test it I simply send "end task" to it through the task manager."* > This has no relevance with the code you're testing, your application window will not be sent WM_QUERYENDSESSION, WM_ENDSESSION messages with this test. – Sertac Akyuz Aug 27 '14 at 21:41
  • 2
    You can use `OutputDebugString` to see what the function result is, and view it in the Messages window of the IDE even after your app has ended if you run it from the IDE. *Always* check API call results; if you don't, you can't blame them for not functioning properly as you haven't *used* them properly. – Ken White Aug 27 '14 at 22:20

3 Answers3

12

According to the documentation to block shutdown you need to return FALSE in response to WM_QUERYENDSESSION.

What's more, you must not do work in this message handler. The work must happen elsewhere. If you don't respond to this message in a timely fashion the system won't wait for you.

  • Call ShutdownBlockReasonCreate before you start working.
  • Whilst working return FALSE from WM_QUERYENDSESSION. Don't work whilst handling this message. Return immediately.
  • When the work is done call ShutdownBlockReasonDestroy.

The handler for WM_QUERYENDSESSION can look like this:

procedure TMainForm.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  if Working then
    Msg.Result := 0
  else
    inherited;
end;

And then the code that performs the work needs to call ShutdownBlockReasonCreate before the work starts, ShutdownBlockReasonDestroy when the work ends, and make sure that the Working property used above evaluates to True during work.

If your work blocks the main thread then you are in trouble. The main thread must be responsive, otherwise the system won't wait for you. Putting the work in a thread is often the way forward. If your main window is not visible then you don't get a chance to block shutdown. The details are explained here: http://msdn.microsoft.com/en-us/library/ms700677.aspx

If you get as far as being sent WM_ENDSESSION then it's too late. The system is going down come what may.

To test it I simply send "end task" to it through the task manager.

That has nothing to do with shutdown blocking. The way you test shutdown blocking is to logoff. If the user insists on killing your process there is little that you can do about it. Sertac's answer covers this in detail.

Finally, ignoring the return values of API calls is also very poor form. Don't do that.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • In addition "If an application times out responding to WM_QUERYENDSESSION or WM_ENDSESSION, Windows will terminate it." So your "Sleep(45000); // do your work here" will cause this timeout. http://msdn.microsoft.com/en-us/library/ms700677(v=vs.85).aspx – Andreas Hausladen Aug 27 '14 at 20:30
  • I tried returning TRUE but it makes no difference. Also without the sleep command it still not works. REturning TRUE and ommiting the ShutDownBlockReasonDestroy does not work. I have tried just about every combination I can think of, tried different codes found using google, nothing seems to work. Do you have a working example maybe – GuidoG Aug 27 '14 at 20:31
  • I don't have sample code to hand, but I have used these APIs successfully. Clearly they aren't a hoax and I cannot understand why you think they are. Do you that ms add APIs that don't work? – David Heffernan Aug 27 '14 at 20:35
  • I edited the question to code more like you suggested. Still does not work, what can I be doing wrong. I said they where a hoax because they seem to be doing nothing, was meant sarcastic. – GuidoG Aug 27 '14 at 20:38
  • Maybe if I could have a look at your working example I can figure out what I am doing wrong here ? – GuidoG Aug 27 '14 at 20:40
  • Read the docs here: http://msdn.microsoft.com/en-us/library/ms700677(v=vs.85).aspx Is your top level window visible and responding to messages? Is your work in the main thread? – David Heffernan Aug 27 '14 at 20:48
  • It is a test application, only one form that is visible and currently no work done yet, so the form and the main thread can respond to messages. The code you see is all the code there is in this test application. – GuidoG Aug 27 '14 at 20:51
  • Where did I changed my question ? I do not understand, the question remains the same and I would appreciate very much if I could look at a working example on how to do this. – GuidoG Aug 27 '14 at 20:57
  • You changed false to true and removed the sleep calls. So my answer looks daft. – David Heffernan Aug 27 '14 at 21:14
  • I see, you mean the code I changed. I changed that to match the suggestion you gave, sorry. I tried your last suggestion (this was how it worked in windows xp) but it also does not work. I am at a loss here, could it be that windows 8 is doing things different again or maybe XE6 has some changes that cause this ? – GuidoG Aug 27 '14 at 21:30
  • The documentation says to return true to allow your application to be terminated. This is specifically what the OP does NOT want, so returning FALSE is appropriate to the need. Whether the need is itself appropriate is a separate question. – Deltics Aug 28 '14 at 03:25
  • + ... As far as I can tell the answer to that separate question is emphatically "NO", and the OP can only be helped if they describe what it is that the application is trying to *do*, rather than simply asking for a way to make it *not* do something without providing any context. :) – Deltics Aug 28 '14 at 03:42
  • @Deltics thank you. I mis read the docs late at night. Very stupid of me. – David Heffernan Aug 28 '14 at 05:12
  • Now, I think, your modification makes my answer look daft. I wouldn't post it if yours answered the question you know.. – Sertac Akyuz Aug 28 '14 at 08:32
  • @Sertac I made a real mess of it I know. I'm sorry. Your answer adds lots of good detail on *End Task* behaviour. So it's very valuable. Please keep it. The upvotes (one being mine) confirm that. My upvotes are ill deserved. Sorry. – David Heffernan Aug 28 '14 at 09:33
  • @David - My only objection was that your answer included the fault with the testing method after I posted it as an answer, it's really the only thing that matters here. The code included in the question works, I wrote it and tested it, it's linked with LU RD's comment. Upvotes are not your fault, and does not bother me. – Sertac Akyuz Aug 28 '14 at 10:13
  • @Sertac I understand. Perhaps I should just have referenced your answer. – David Heffernan Aug 28 '14 at 10:18
6

Your code seems to be completely ignored because you're not testing it. You're sending "end task" to it through the task manager, the code you posted is only effective when the system is shutting down.

What is different with Windows 8 seems to be how task manager behaves. Before Windows 8, an end task from task manager will first try closing the app gracefully (sends a WM_CLOSE), this you're handling with OnCloseQuery. But when the application denies closing, the task manager will offer ending the process forcefully. This, you cannot handle. Same if you choose "end process" from task manager.

The Windows 8 task manager does not offer an additional dialog for forcefully closing the application, but immediately proceeds doing so when the application denies closing.

Sertac Akyuz
  • 54,131
  • 4
  • 102
  • 169
1

Here are some solution for some different cases tested in Delphi 11.1 Alexandria:

The Form OnCloseQuery gets called when app is being closed by user or by system shutdown, to know which one of these two events is triggered, call GetSystemMetrics and pass SM_SHUTTINGDOWN as an argument.

System metric SM_SHUTTINGDOWN is set when there is a pending system shutdown, clear otherwise.

This is all you need if you only want say suppress exit confirmation message to the user if system is shutting down:

procedure TForm1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  if GetSystemMetrics (SM_SHUTTINGDOWN) > 0 then
    Exit;  // app is being closed by a system shutdown

   // app being closed by user, ask user to confirm exit
  if not ConfirmAppExit then
    CanClose := False;
end;

Please note if the system is shutting down your Form OnCloseQuery will be called but your Form OnClose will not be called, any code you put in OnClose will not be executed. So, don't put code there if you want it to be executed on system shutdown, Instead, use the WM_EndSession handler described below.

If you want more than that and be able to block the shutdown, First write a handler for the message WM_QueryEndSession:

procedure WMQueryEndSession(var Msg: TWMQueryEndSession); message WM_QueryEndSession;

Inside this handler do not do anything except returning message result, this message is sent to your app to check if it agrees to shutdown the system or not, it does not mean the system shutdown is taking place right now, because all apps must agree to this message first, but if just one app denies this message (return False) then shutdown will not happen (When shutdown is really taking place you will receive WM_EndSession message).

In the WM_QueryEndSession handler check if there are any critical running tasks which must be completed in one go or if interrupted will cause data loss, If there is a critical task running return False to deny system shutdown, like:

procedure TForm1.WMQueryEndSession(var Msg: TWMQueryEndSession);
begin
  if CriticalTaskRunning then
    Msg.Result := 0              // Deny system shutdown
  else
    inherited;                       // Agree to system shutdown, Same as Msg.Result := 1
end;

Do not return False if your task is not critical and can be interrupted, and do not interrupt your task at this point, just return True and keep your task running because some other app may deny the shutdown and you just interrupted your task for nothing!, Interrupt your task only when you receive WM_EndSession message which means all applications agreed to the shutdown and the system is really shutting down.

By returning False, shutdown is now denied, using ShutdownBlockReasonCreat at this time is redundant, but you can use that to explain to the user (in shutdown screen) why your app is blocking shutdown. If you use this be sure to call ShutdownBlockReasonDestroy after your task is finished.

When receiving WM_EndSession you know now the system is really shutting down. If you need to do cleanup, abort any running tasks, save changes close DB/files ...etc, then you can use ShutdownBlockReasonCreate to block shutdown until you finish cleaning up, then unblock shutdown once finished, like:

procedure WMEndSession (var Msg: TWMEndSession);
begin
  if CleanUpRequired then
    begin
      ShutdownBlockReasonCreate (Handle, 'My app is preparing to close, just a sec...');
      try
        DoCleanUp;
      finally
        ShutdownBlockReasonDestroy (Handle);
      end;
    end;
end;

Some other shutdown block methods suggest you create a shutdown block every time you start a task and destroy the shutdown block once your task is finished even if the system is not shutting down! my approach here is only create a shutdown block when it is necessary and only when the system is really shutting down.

I Hope it's useful for someone!

Delphine
  • 49
  • 6