win32gui.EnumWindows传入无效窗口句柄致脚本崩溃,求原因分析
问题描述
我在VPS上持续运行一段自研Python脚本已数月,该脚本用于检测特定窗口是否长时间停留在指定标题,若超过90秒则关闭窗口。
脚本代码如下:
import time import datetime import win32gui import win32process max_downtime = 90 pid_downtimes = {} cur_pids = [] prev_pids = [] def winEnumHandler(hwnd, ctx): class_name = win32gui.GetClassName(hwnd) window_text = win32gui.GetWindowText(hwnd) if class_name != "ConsoleWindowClass": return if not window_text.startswith("Plutonium"): return if "Server restarter" in window_text: return if "Dedicated Server" in window_text: return tid, pid = win32process.GetWindowThreadProcessId(hwnd) if pid not in pid_downtimes: pid_downtimes[pid] = 0 pid_downtimes[pid] += 1 if pid_downtimes[pid] >= max_downtime: print("Server restarted at:", datetime.datetime.now()) win32gui.SendMessage(hwnd, 16) # close window return cur_pids.append(pid) while True: win32gui.EnumWindows(winEnumHandler, None) old_pids = [pid for pid in prev_pids if pid not in cur_pids] for pid in old_pids: if pid in pid_downtimes: del pid_downtimes[pid] prev_pids = cur_pids cur_pids = [] time.sleep(1)
近日脚本首次崩溃,报错信息如下:
Traceback (most recent call last): line 43, in <module> win32gui.EnumWindows(winEnumHandler, None) line 12, in winEnumHandler class_name = win32gui.GetClassName(hwnd) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ pywintypes.error: (1400, 'GetClassName', 'Invalid window handle.')
此前脚本运行一直正常,疑惑为何win32gui.EnumWindows会向winEnumHandler传入无效窗口句柄,恳请各位分析原因。
问题分析与解决方案
原因分析
- Windows的
EnumWindows是异步枚举过程:枚举开始后,系统会遍历所有顶层窗口,但在遍历期间,部分窗口可能被关闭或销毁,导致原本有效的hwnd变成无效句柄。 - 脚本运行数月才触发问题,是因为这种窗口销毁恰好发生在枚举过程中的概率较低,属于偶发的竞态条件问题。
修复方案
在调用win32gui.GetClassName等窗口操作前,先通过win32gui.IsWindow验证句柄是否有效,过滤已销毁的窗口。修改后的代码如下:
import time import datetime import win32gui import win32process max_downtime = 90 pid_downtimes = {} cur_pids = [] prev_pids = [] def winEnumHandler(hwnd, ctx): # 先验证窗口句柄是否有效 if not win32gui.IsWindow(hwnd): return class_name = win32gui.GetClassName(hwnd) window_text = win32gui.GetWindowText(hwnd) if class_name != "ConsoleWindowClass": return if not window_text.startswith("Plutonium"): return if "Server restarter" in window_text: return if "Dedicated Server" in window_text: return tid, pid = win32process.GetWindowThreadProcessId(hwnd) if pid not in pid_downtimes: pid_downtimes[pid] = 0 pid_downtimes[pid] += 1 if pid_downtimes[pid] >= max_downtime: print("Server restarted at:", datetime.datetime.now()) # 关闭前再次验证窗口是否存在,避免无效句柄报错 if win32gui.IsWindow(hwnd): win32gui.SendMessage(hwnd, 16) # close window return cur_pids.append(pid) while True: win32gui.EnumWindows(winEnumHandler, None) old_pids = [pid for pid in prev_pids if pid not in cur_pids] for pid in old_pids: if pid in pid_downtimes: del pid_downtimes[pid] prev_pids = cur_pids cur_pids = [] time.sleep(1)
额外说明
IsWindow函数会检查句柄是否对应一个现存窗口,能有效避免因窗口销毁导致的无效句柄操作。- 在调用
SendMessage关闭窗口前再次验证,是因为从获取句柄到执行关闭操作的间隙,窗口也可能被其他进程销毁。
内容的提问来源于stack exchange,提问作者Justin
相关产品推荐
相关产品推荐

