如何找到阻止关机的 HWND?

How to find the HWND that is preventing shutdown?

我的应用程序中的某处(以及第 3 方代码库)是一个 window 过程,它阻止 Windows 来自:

我在我的代码中发现一个地方我犯了一个非常常见的错误调用 DefWindowProc,但调用不正确:

之前:

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

之后:

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

我修复了那个错误,我的 test program 不再停止关机。

但完整的应用程序可以

我现在面临着不得不将程序拆成零的问题,直到我的计算机最终重新启动。

在我的应用程序深处的某个地方有一个 Window 过程附加到一个 HWND,它是 return 零到 WM_QUERYENDSESSION。如果我知道 HWND,我就可以使用 Spy++ 找到 Window.

但是我怎样才能找到 hwnd

Windows应用程序事件日志记录了停止关闭的进程

在更详细的 应用程序和服务日志 中很可能会有更详细的日志。但这些都没有记录。

如何找到我的问题 hwnd

尝试次数

我尝试使用 EnumThreadWindows 获取我的 "main" 线程中的所有 windows,我的想法是手动将 WM_QUERYENDSESSION 发送给他们,看看谁 returns :

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

现在我有一个包含 12 个 hwnd 的列表。戳他们:

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;

但是没有人return归零。

所以这是下水道的一根管子。

这听起来有点矫枉过正,但可以。我会使用 AllocateHWndDeallocateHWnd 的代码挂钩来解决这个问题。我们必须解决一个与手柄相关的不同问题,它对我们来说效果很好。

您的替换例程将只是 System.Classes 中版本的副本。您还需要从该单元复制所有依赖项 (PObjectInstance, TObjectInstance, CodeBytes, PInstanceBlock, TInstanceBlock, InstBlockList, InstFreeList, StdWndProc, CalcJmpOffset, MakeObjectInstance, FreeObjectInstance, CleanupInstFreeList, GetFreeInstBlockItemCount, ReleaseObjectInstanceBlocks, UtilWindowClass)。唯一的区别是您在替换例程中记录了所有分配和释放的句柄。包含堆栈跟踪也会有所帮助。

这将为您提供关闭时分配的所有句柄及其调用堆栈跟踪的列表。

基本结构是这样的。我无法 post 完整代码,因为它主要是 VCL 代码,但代码挂钩和日志记录除外。

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;

可能还需要挂钩和记录 SetWindowLong, SetWindowLongASetWindowLongW

EnumThreadWindows 仅枚举一个特定线程的 windows。可能是有问题的 window 是在线程中创建的。因此,我建议您使用 EnumWindows 枚举应用程序中的所有顶级 windows 以进行测试。

在一个线程中初始化 COM 就足够了,你将有一个你不知道的 window。这样在线程中调用 WaitForSingleObject 可能是你的罪魁祸首: Debugging an application that would not behave with WM_QUERYENDSESSION