Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find the HWND that is preventing shutdown?

Somewhere in my application (along with 3rd party libraries of code) is a window procedure that is preventing Windows from:

  • logging off
  • shutting down
  • restarting

I found one spot in my code where I made the extraordinarily common mistake of calling DefWindowProc, but calling it incorrectly:

Before:

void Grobber.BroadcastListenerWindowProc(ref TMessage msg)
{
   DefWindowProc(_broadcastListenerHwnd, msg.msg, msg.wparam, msg.lparam);
}

After:

void Grobber.BroadcastListenerWindowProc(ref TMessage msg)
{
   //20170207: Forgetting to set the result can, for example, prevent Windows from restarting
   msg.Result = DefWindowProc(_broadcastListenerHwnd, msg.msg, msg.wparam, msg.lparam);
}

I fixed that bug, and my test program no longer halted the shutdown.

But a full application does

I'm now faced with having to tear a program down to nothing, until my computer finally reboots.

Somewhere deep inside my application is a Window procedure attached to an HWND that is returning zero to WM_QUERYENDSESSION. If only i knew the HWND, i could use the Spy++ to find the Window.

But how can i find that hwnd?

The Windows Application event log notes the process that halt a shutdown:

enter image description here

And there very well be a more detailed log in the more detailed Applications and Services Logs. But those are undocumented.

How can i find my problematic hwnd?

Attempts

I tried to use EnumThreadWindows to get all the windows of my "main" thread, with the idea of manually sending WM_QUERYENDSESSION to them all to see who returns false:

var
   wnds: TList<HWND>;

function DoFindWindow(Window: HWnd; Param: LPARAM): Bool; stdcall;
var
   wnds: TList<HWND>;
begin
   wnds := TList<HWND>(Param);
   wnds.Add(Window);
   Result := True;
end;

wnds := TList<HWND>.Create;
enumProc := @DoFindWindow;
EnumThreadWindows(GetCurrentThreadId, EnumProc, LPARAM(wnds));

Now i have a list of twelve hwnds. Poke them:

var
   window: HWND;
   res: LRESULT;

for window in wnds do
begin
    res := SendMessage(window, WM_QUERYENDSESSION, 0, 0);
    if res = 0 then
    begin
        ShowMessage('Window: '+IntToHex(window, 8)+' returned false to WM_QUERYENDSESSION');
    end;
end;

But nobody did return zero.

So that's one tube down the drain.

like image 376
Ian Boyd Avatar asked Feb 07 '17 16:02

Ian Boyd


2 Answers

EnumThreadWindows only enumerates the windows of one particular thread. It could be that the offending window was created in a thread. So I'd suggest that you use EnumWindows to enum all top level windows in your application for your test.

It's enough to initialize COM in a thread and you'll have a window you don't know about. That way a call to WaitForSingleObject in a thread could be your culprit: Debugging an application that would not behave with WM_QUERYENDSESSION

like image 95
Sebastian Z Avatar answered Nov 07 '22 07:11

Sebastian Z


This might sound a bit like overkill but here goes. I would solve this using code hooks for AllocateHWnd and DeallocateHWnd. We had to solve a different issue related to handles and it worked well for us.

Your replacement routines will just be copies of the versions in System.Classes. You will also need to copy all of the dependencies (PObjectInstance, TObjectInstance, CodeBytes, PInstanceBlock, TInstanceBlock, InstBlockList, InstFreeList, StdWndProc, CalcJmpOffset, MakeObjectInstance, FreeObjectInstance, CleanupInstFreeList, GetFreeInstBlockItemCount, ReleaseObjectInstanceBlocks, UtilWindowClass) from that unit. The only difference is that you log all allocated and deallocated handles in your replacement routines. It would help to include stack traces too.

That will give you a list of all of the handles that are allocated at the time of your shutdown along with their calling stack traces.

The basic structure is something like this. I can't post full code because it's mostly VCL code with the exception of the code hooks and logging.

const
{$IF Defined(CPUX86)}
  CodeBytes = 2;
{$ELSEIF Defined(CPUX64)}
  CodeBytes = 8;
{$ENDIF CPU}
  InstanceCount = (4096 - SizeOf(Pointer) * 2 - CodeBytes) div SizeOf(TObjectInstance) - 1;

type
  PInstanceBlock = ^TInstanceBlock;
  TInstanceBlock = packed record
    ...
  end;

var
  InstBlockList: PInstanceBlock;
  InstFreeList: PObjectInstance;

{ Standard window procedure }
function StdWndProc(Window: HWND; Message: UINT; WParam: WPARAM; LParam: WPARAM): LRESULT; stdcall;
...

function CalcJmpOffset(Src, Dest: Pointer): Longint;
...

function MakeObjectInstance(const AMethod: TWndMethod): Pointer;
...

procedure FreeObjectInstance(ObjectInstance: Pointer);
...

procedure CleanupInstFreeList(BlockStart, BlockEnd: PByte);
...

function GetFreeInstBlockItemCount(Item: PObjectInstance; Block: PInstanceBlock): Integer;
...

procedure ReleaseObjectInstanceBlocks;
...

var
  UtilWindowClass: TWndClass = (
    ... );

function AllocateHWnd(const AMethod: TWndMethod): HWND;
begin
  < Logging/Stack trace code here >
  ...
end;

procedure DeallocateHWnd(Wnd: HWND);
begin
  < Logging/Stack trace code here >
  ...
end;

It may also be necessary to hook and log SetWindowLong, SetWindowLongA and SetWindowLongW too.

like image 1
Graymatter Avatar answered Nov 07 '22 09:11

Graymatter