Windows下诊断阻止睡眠的 Bug

TL;DR: 在 Windows 上诊断导致“失眠”的程序非常简单。

我的雇主对 Windows 机器设置了一项策略,即每个计算机在一段时间不活动后自动锁定。我的一位同事注意到,如果他们在后台运行我们产品的最新版本,则此自动锁定不会生效。我被要求调查此事。

这个 Bug 的优先级相当低,因为乍一看,这种现象似乎不会造成太大的麻烦。我对所有这些在 Windows 上是如何运作的一无所知,但我有一种预感,如果锁屏被阻止,那么计算机进入睡眠状态也会受到同样的待遇。这是一个更大的问题,因为它可能会对笔记本电脑的电池续航时间产生巨大影响。

断点

重现过程非常简单(启动程序,然后等待 X 分钟,并注意锁屏未出现),而且我还有被调查软件的源代码,所以我的第一直觉是在允许你与 Windows 的此功能交互的函数上设置断点。第一个搜索结果是一个名为 SetThreadExecutionState 的函数:

允许应用程序通知系统它正在使用中,从而防止系统在应用程序运行时进入睡眠状态或关闭显示器。

我在这个函数上设置了一个断点,但根本没有命中。

好的,下一个搜索结果将我指向 PowerCreateRequestPowerSetRequest。这个 API 的基本思想是,你创建一个电源请求对象,并根据你的特定用例,增加/减少给定类型的请求的引用计数。

在这些函数上设置断点显示了以下事件序列:

  1. 当应用程序启动并显示 UI 时,调用了 PowerCreateRequest
  2. 不久之后,使用 PowerRequestDisplayRequired 调用了 PowerSetRequest
  3. 在程序的常规操作期间,没有进一步调用这些函数
  4. 退出时,调用了 PowerClearRequest

因此,总而言之,有人请求 不要 关闭显示器,并一直坚持这个请求直到程序退出。电源请求创建的调用堆栈如下(高度简化):

KERNEL32!PowerCreateRequest
libcef![…]
[…]
Product_exe!Application::MainMessageLoop
Product_exe!WinMain
[…]
ntdll!RtlUserThreadStart

libcef.dll 模块的存在大大缩小了范围,因为它意味着某种 Web 内容是罪魁祸首(该产品使用了 CEF)。在这一点上,这个 Bug 看起来很容易解决:最近,一个新的 onboarding 功能被合并到代码库中,它显示了一个小的浮动“软件中的新功能”类型的对话框,包括一个用于演示的小视频片段。

啊,也许同事在输入 Bug 时忽略了这个细节。我应该关闭这个对话框,问题肯定会消失 – 我想。令我惊讶的是,即使我关闭了这个对话框,问题也_没有_消失。

有时,你没看到的才是你(仍然)得到的

在这一点上,我的 PTSD 开始发作:也许这是那些超级罕见的 CEF Bug 之一,实际上除了你之外没有人遇到过,需要很长时间才能调试,然后在你升级到未来版本时随机消失(经历过,做过)。幸运的是,我有一个幸运的预感:如果代码——由于某种原因——实际上并没有关闭这个对话框,而是简单地隐藏了它呢?

由于我不会在这里详细说明的原因,在这种特定情况下,使用 Spy++ 测试这个理论更容易(即使我完全可以访问源代码):

  1. 我使用 Find Window 工具找到了有问题的窗口
  2. 我“关闭”了目标窗口
  3. 在 Spy++ 中,我点击了 Refresh 按钮

Spy++ 没有显示 Invalid window,而是仍然存在,证实了我的理论。我通知了引入此 onboarding 功能的团队,他们很快修复了这个 Bug。

其他工具

即使对于这个相对简单的案例,断点也绰绰有余,但在某些情况下,电源请求的数量可能会使这种方法变得不可行。幸运的是,还有一些替代方案。

Windows 自带的 powercfg 可以让你大致了解哪些程序有活动的电源请求(但这只是一个快照):

powercfg /requests
---
DISPLAY:
[PROCESS] \Device\HarddiskVolume2\Product.exe

如果你安装了 WDK,则有一个名为 pwrtest 的有用实用程序。使用“requests scenario”,你可以实时监控电源请求的生命周期(但仍然没有调用堆栈):

pwrtest.exe /requests
---
15:52:56 Create: \Device\HarddiskVolume2\Product.exe
Type:Application ProcessID:34712 SessionID:14
Allow: System Display AwayMode PerfBoost ExecutionRequired FullScreenVideo
Count: System:0 Display:0 AwayMode:0 PerfBoost:0
ExecutionRequired:0 FullScreenVideo:0
-------------------------------------------------------------------------------
15:52:56 Change: \Device\HarddiskVolume2\Product.exe
Count: System:0 *Display:1 AwayMode:0 PerfBoost:0
ExecutionRequired:0 FullScreenVideo:0
-------------------------------------------------------------------------------
15:53:24 Change: \Device\HarddiskVolume2\Product.exe
Count: System:0 Display:0 AwayMode:0 PerfBoost:0
ExecutionRequired:0 FullScreenVideo:0
-------------------------------------------------------------------------------
15:53:24 Close: \Device\HarddiskVolume2\Product.exe
-------------------------------------------------------------------------------

最后但同样重要的是,你可以使用 ETW 和 Microsoft-Windows-Kernel-Power provider 来获取你需要的所有数据,包括调用堆栈:

xperf -on PROC_THREAD+LOADER -start user -on Microsoft-Windows-Kernel-Power:::'stack'
---
@rem Repro the problem here
xperf -d k.etl
xperf -stop user
xperf -merge k.etl C:\user.etl merged.etl

Call stacks!