You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 08:43:35