取消使用 ExecNotificationQuery 创建的 WMI 事件通知
Cancel WMI event notification created with ExecNotificationQuery
我可以在脚本中创建非永久性 WMI 事件查询,例如这个,它记录了接下来 5 个新 Notepad.exe 进程的 PID:
Set WMI = GetObject("winmgmts:\.\ROOT\cimv2")
wql = "SELECT * FROM __InstanceCreationEvent WITHIN 2 " & _
"WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'Notepad.exe'"
Set EventSource = WMI.ExecNotificationQuery(wql)
For i = 1 To 5
With EventSource.NextEvent(-1)
Wscript.Echo .TargetInstance.ProcessId
End With
Next
但我缺少一种明确取消 EventSource
的方法。否则,事件通知将在 WMI 内无限期地继续 运行,即使侦听生成的事件的脚本因任何原因终止 (*)。当脚本多次 运行 时,这将导致开销增加。
MSDN documentation of IWbemServices::ExecNotificationQuery
说:
The IWbemServices::ExecNotificationQuery method executes a query to receive events. The call returns immediately, and the user can poll the returned enumerator for events as they arrive. Releasing the returned enumerator cancels the query.
如何释放返回的枚举器?
EventSource
对象似乎不可枚举。尝试在其上使用 For Each
失败并显示 "VBScript 运行 时间错误:对象不支持此 属性 或方法",所以我可以不要在 For Each
循环的末尾使用隐式 Release
。
(*) 这是根据文档说明 “释放返回的枚举器取消查询。”,这意味着 not 释放枚举器会导致查询持续存在 - 但它也可以明确确认:
为此查询设置通知 "SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE TargetInstance ISA 'Win32_Directory' And TargetInstance.Name = 'C:\Test'"
并使用 Sysinternals Process Monitor 观察来自 WMI 服务 (WmiPrvSE.exe
) 的文件系统访问。 WMI 服务将开始每 2 秒轮询一次名为“C:\Test”的文件夹,并在设置监视的脚本结束后继续这样做。
重新启动 WMI 服务可以摆脱轮询,但显然这不是解决这种情况的好办法。
您可以使用 ExecNotificationQueryAsync
instead? That way you can pass it a SWbemSink
object that you can later call Cancel()
method 来取消接收器,它还应该删除与该接收器关联的任何事件消费者。
使用 ExecNotificationQuery()
方法的问题是它只允许您访问 SWbemEventSource
对象,该对象允许调用枚举器中的下一个事件。似乎不可能在事件消费者注册后使用该方法删除它。
运行这个:
Set FSO = CreateObject("Scripting.FileSystemObject")
Set WMI = GetObject("winmgmts:\.\ROOT\cimv2")
Set Sink = WScript.CreateObject("WbemScripting.SWbemSink", "Sink_")
wql = "SELECT * FROM __InstanceCreationEvent WITHIN 2 " & _
"WHERE TargetInstance ISA 'Win32_Directory' And TargetInstance.Name = 'C:\Test'"
WScript.Echo "Waiting for events..."
WMI.ExecNotificationQueryAsync Sink, wql
For i = 1 To 10
WScript.Echo i
Wscript.Sleep 1000
If i = 5 Then
FSO.CreateFolder "C:\Test"
WScript.Echo "Test folder created."
End If
Next
Sink.Cancel
WScript.Echo "Sink canceled."
FSO.DeleteFolder "C:\Test"
WScript.Echo "Test folder deleted."
Sub Sink_OnObjectReady(eventObject, asyncContext)
Set folder = eventObject.TargetInstance
WScript.Echo "__InstanceCreationEvent: " & folder.Name
End Sub
输出类似这样的东西:
Waiting for events...
1
2
3
4
5
Test folder created.
6
7
__InstanceCreationEvent: C:\Test
8
9
10
Sink canceled.
Test folder deleted.
之后,Process Monitor 将确认 WMI 已停止对文件夹创建的后台轮询。
警告:调用 Sink.Cancel
是绝对必要的。如果脚本意外终止,WMI 将 在后台继续轮询,重新启动 WMI 服务是摆脱轮询循环的唯一方法。
我可以在脚本中创建非永久性 WMI 事件查询,例如这个,它记录了接下来 5 个新 Notepad.exe 进程的 PID:
Set WMI = GetObject("winmgmts:\.\ROOT\cimv2")
wql = "SELECT * FROM __InstanceCreationEvent WITHIN 2 " & _
"WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.Name = 'Notepad.exe'"
Set EventSource = WMI.ExecNotificationQuery(wql)
For i = 1 To 5
With EventSource.NextEvent(-1)
Wscript.Echo .TargetInstance.ProcessId
End With
Next
但我缺少一种明确取消 EventSource
的方法。否则,事件通知将在 WMI 内无限期地继续 运行,即使侦听生成的事件的脚本因任何原因终止 (*)。当脚本多次 运行 时,这将导致开销增加。
MSDN documentation of IWbemServices::ExecNotificationQuery
说:
The IWbemServices::ExecNotificationQuery method executes a query to receive events. The call returns immediately, and the user can poll the returned enumerator for events as they arrive. Releasing the returned enumerator cancels the query.
如何释放返回的枚举器?
EventSource
对象似乎不可枚举。尝试在其上使用 For Each
失败并显示 "VBScript 运行 时间错误:对象不支持此 属性 或方法",所以我可以不要在 For Each
循环的末尾使用隐式 Release
。
(*) 这是根据文档说明 “释放返回的枚举器取消查询。”,这意味着 not 释放枚举器会导致查询持续存在 - 但它也可以明确确认:
为此查询设置通知 "SELECT * FROM __InstanceCreationEvent WITHIN 2 WHERE TargetInstance ISA 'Win32_Directory' And TargetInstance.Name = 'C:\Test'"
并使用 Sysinternals Process Monitor 观察来自 WMI 服务 (WmiPrvSE.exe
) 的文件系统访问。 WMI 服务将开始每 2 秒轮询一次名为“C:\Test”的文件夹,并在设置监视的脚本结束后继续这样做。
重新启动 WMI 服务可以摆脱轮询,但显然这不是解决这种情况的好办法。
您可以使用 ExecNotificationQueryAsync
instead? That way you can pass it a SWbemSink
object that you can later call Cancel()
method 来取消接收器,它还应该删除与该接收器关联的任何事件消费者。
使用 ExecNotificationQuery()
方法的问题是它只允许您访问 SWbemEventSource
对象,该对象允许调用枚举器中的下一个事件。似乎不可能在事件消费者注册后使用该方法删除它。
运行这个:
Set FSO = CreateObject("Scripting.FileSystemObject")
Set WMI = GetObject("winmgmts:\.\ROOT\cimv2")
Set Sink = WScript.CreateObject("WbemScripting.SWbemSink", "Sink_")
wql = "SELECT * FROM __InstanceCreationEvent WITHIN 2 " & _
"WHERE TargetInstance ISA 'Win32_Directory' And TargetInstance.Name = 'C:\Test'"
WScript.Echo "Waiting for events..."
WMI.ExecNotificationQueryAsync Sink, wql
For i = 1 To 10
WScript.Echo i
Wscript.Sleep 1000
If i = 5 Then
FSO.CreateFolder "C:\Test"
WScript.Echo "Test folder created."
End If
Next
Sink.Cancel
WScript.Echo "Sink canceled."
FSO.DeleteFolder "C:\Test"
WScript.Echo "Test folder deleted."
Sub Sink_OnObjectReady(eventObject, asyncContext)
Set folder = eventObject.TargetInstance
WScript.Echo "__InstanceCreationEvent: " & folder.Name
End Sub
输出类似这样的东西:
Waiting for events... 1 2 3 4 5 Test folder created. 6 7 __InstanceCreationEvent: C:\Test 8 9 10 Sink canceled. Test folder deleted.
之后,Process Monitor 将确认 WMI 已停止对文件夹创建的后台轮询。
警告:调用 Sink.Cancel
是绝对必要的。如果脚本意外终止,WMI 将 在后台继续轮询,重新启动 WMI 服务是摆脱轮询循环的唯一方法。