如何找到阻止关机的 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归零。
所以这是下水道的一根管子。
这听起来有点矫枉过正,但可以。我会使用 AllocateHWnd
和 DeallocateHWnd
的代码挂钩来解决这个问题。我们必须解决一个与手柄相关的不同问题,它对我们来说效果很好。
您的替换例程将只是 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, SetWindowLongA
和 SetWindowLongW
。
EnumThreadWindows
仅枚举一个特定线程的 windows。可能是有问题的 window 是在线程中创建的。因此,我建议您使用 EnumWindows
枚举应用程序中的所有顶级 windows 以进行测试。
在一个线程中初始化 COM 就足够了,你将有一个你不知道的 window。这样在线程中调用 WaitForSingleObject 可能是你的罪魁祸首:
Debugging an application that would not behave with WM_QUERYENDSESSION
我的应用程序中的某处(以及第 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归零。
所以这是下水道的一根管子。
这听起来有点矫枉过正,但可以。我会使用 AllocateHWnd
和 DeallocateHWnd
的代码挂钩来解决这个问题。我们必须解决一个与手柄相关的不同问题,它对我们来说效果很好。
您的替换例程将只是 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, SetWindowLongA
和 SetWindowLongW
。
EnumThreadWindows
仅枚举一个特定线程的 windows。可能是有问题的 window 是在线程中创建的。因此,我建议您使用 EnumWindows
枚举应用程序中的所有顶级 windows 以进行测试。
在一个线程中初始化 COM 就足够了,你将有一个你不知道的 window。这样在线程中调用 WaitForSingleObject 可能是你的罪魁祸首: Debugging an application that would not behave with WM_QUERYENDSESSION