2005. június 25., szombat

Correct handling of Windows shutdown in complex applications


Problem/Question/Abstract:

In complex applications it is necessary to correctly process all application finalization steps like OnClose and OnDestroy event handlers for all forms and Data Modules. However after the application has responded to WM_ENDSESSION message (and TApplication does this automatically) lots of API functions fail due to system shutwdown. How to ensure, that all OnDestroy handlers will work correctly?

Answer:

First let's take a look at the following code:

project XXX;
{... }
var
  DM: TMyDataModule;

begin
  DM := TMyDataModule.Create;
  {... }
  Application.Run;
  DM.Free;
end;

procedure TMyDataModule.DataModuleDestroy(Sender: TObject);
var
  I: Integer;
  J: integer;
begin
  for i := 0 to 5 do
  begin
    MessageBeep(MB_ICONQUESTION);
    if MessageBox(0, PChar('Datamodule destroying - ' + IntToStr(i)), nil,
      MB_SYSTEMMODAL) = 0 then
    begin
      j := GetLastError;
      MessageBeep(MB_ICONEXCLAMATION);
      MessageBox(0, PChar('MessageBox error - ' + IntToStr(j)), nil, MB_SYSTEMMODAL);
    end;
  end;
  MessageBeep(MB_ICONEXCLAMATION);
  MessageBox(0, 'Datamodule destroyed', nil, MB_SYSTEMMODAL);
end;

Our goal is to get 7 messageboxes.
If you reproduce this code in your application, you will get one message box window, that will immediately disappear. That is not what we want. What should we do?
The solution is to not tell windows that the application can be closed until OnDestroy is executed. But if the message is processed in window message dispatching loop, how can we get out of the loop without returning control to Windows?
Let's take a look at threads. Windows starts to send WM_ENDSESSION after all windows return 1 in responce to WM_QUERYENDSESSION. And the solution is simple: create a window in another thread and let it process WM_QUERYENDSESSION message in the way, that will shutdown our application correctly. The code in brief is:

if Msg.Msg = WM_QUERYENDSESSION then
begin
  Synchronize(CloseApp);
  WaitForSingleObject(StopWatcherEvent, INFINITE);
  ResetEvent(StopWatcherEvent);
  Msg.Result := 1;
end
else
  {  ... }

CloseApp function calls Application.MainForm.Close. The application is closed. StopWatcherEvent is set only in finalization clause, which is executed after all forms and datamodules are destroyed ;).

Here is the complete code of the watcher unit. It has been tested under Windows NT 4.0 SP6.

{====================================================}
{                                                    }
{   EldoS Visual Components                          }
{                                                    }
{   Copyright (c) 1998-2000, EldoS                   }
{                                                    }
{====================================================}

unit ElShutdownWatcher;

interface

implementation

uses
  Forms, Classes, Windows, Messages, SysUtils;

type
  TShutdownThread = class(TThread)
  private
    Wnd: HWND;
    procedure WndProc(var Msg: TMessage);
    procedure CloseApp;
  protected
    procedure Execute; override;
  end;

var
  StopWatcherEvent: THandle;

procedure TShutdownThread.CloseApp;
begin
  if (Application.MainForm <> nil) and (not Application.Terminated) then
    Application.MainForm.Close
  else
    PostMessage(Application.Handle, WM_QUIT, 0, 0);
end;

procedure TShutdownThread.WndProc(var Msg: TMessage);
begin
  if Msg.Msg = WM_QUERYENDSESSION then
  begin
    Synchronize(CloseApp);
    WaitForSingleObject(StopWatcherEvent, INFINITE);
    ResetEvent(StopWatcherEvent);
    Msg.Result := 1;
  end
  else
    DefWindowProc(Wnd, Msg.Msg, Msg.wParam, msg.lParam);
end;

procedure TShutdownThread.Execute;
var
  Msg: TMsg;
  i: LongBool;
begin
  StopWatcherEvent := CreateEvent(nil, true, false, nil);
  Wnd := AllocateHWND(WndProc);
  repeat
    i := GetMessage(Msg, 0, 0, 0);
    if i = TRUE then
    begin
      TranslateMessage(Msg);
      DispatchMessage(Msg);
      if WaitForSingleObject(StopWatcherEvent, 0) = WAIT_OBJECT_0 then
        break;
    end;
  until i <> TRUE;
  DeallocateHWND(Wnd);
  CloseHandle(StopWatcherEvent);
  StopWatcherEvent := 0;
end;

var
  Watcher: TShutdownThread;

initialization

  Watcher := TShutdownThread.Create(true);
  Watcher.FreeOnTerminate := true;
  Watcher.Resume;

finalization
  if StopWatcherEvent <> 0 then
    SetEvent(StopWatcherEvent);

end.

Nincsenek megjegyzések:

Megjegyzés küldése