PowerShell 技能连载 - 事件注册与处理

适用于 PowerShell 5.1 及以上版本

背景

在自动化运维场景中,很多操作并不是”调用-等待-返回”的同步模式。文件系统的变动、WMI 对象的状态变更、进程的启动与退出——这些事情发生的时间不可预测。如果用轮询(polling)的方式去反复检查,不仅浪费 CPU 资源,还会引入检测延迟。PowerShell 提供了一套事件订阅机制,允许你注册对特定事件的关注,当事件触发时自动执行预定义的处理逻辑,实现”被动响应”而非”主动轮询”的编程模型。

PowerShell 的事件系统围绕三个核心 cmdlet 展开:Register-EngineEvent 用于注册 PowerShell 引擎自身发出的事件(例如自定义的 .NET 事件);Register-ObjectEvent 用于订阅 .NET 对象的事件(例如 FileSystemWatcher 的文件变更事件);Register-WmiEvent 用于订阅 WMI 事件(例如进程创建)。配合 Get-EventRemove-EventUnregister-Event,构成了完整的事件生命周期管理能力。

本文将围绕 Register-EngineEventRegister-ObjectEvent,通过三个递进的场景,演示如何在实际工作中使用 PowerShell 的事件注册与处理机制。

基础:使用 Register-EngineEvent 构建自定义事件

Register-EngineEvent 是 PowerShell 事件体系中最简单直接的入口。它订阅由 PowerShell 引擎发出的 Eventing 事件,最常用的场景是通过 New-Event 手动触发自定义事件,再由注册的 Action 脚本块来响应。这种模式非常适合在模块或脚本内部实现松耦合的消息通信——事件的发送方不需要知道谁在监听,接收方也不需要知道谁在发送。

下面的示例模拟了一个批量任务处理流程:主循环在处理每批任务后发出一个自定义事件,事件携带批次的处理结果;独立的监听器捕获事件并记录日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 步骤 1:注册自定义引擎事件,监听 "BatchCompleted" 事件
$action = {
$eventData = $Event.SourceEventArgs
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] 批次事件: $($eventData.BatchName) | " +
"成功: $($eventData.SuccessCount) | 失败: $($eventData.FailCount) | " +
"耗时: $($eventData.Duration)ms"
Write-Output $logEntry
}

Register-EngineEvent -SourceIdentifier "BatchCompleted" -Action $action | Out-Null

# 步骤 2:定义批次数据并逐批处理
$batches = @(
@{ Name = "用户导入-第1批"; Count = 150 }
@{ Name = "用户导入-第2批"; Count = 230 }
@{ Name = "用户导入-第3批"; Count = 180 }
@{ Name = "用户导入-第4批"; Count = 310 }
@{ Name = "用户导入-第5批"; Count = 95 }
)

foreach ($batch in $batches) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()

# 模拟处理:随机决定成功和失败的数量
$failCount = Get-Random -Minimum 0 -Maximum ([math]::Min(5, $batch.Count))
$successCount = $batch.Count - $failCount

# 模拟处理耗时
Start-Sleep -Milliseconds (Get-Random -Minimum 50 -Maximum 200)
$sw.Stop()

# 通过 New-Event 触发自定义事件
$eventArgs = [PSCustomObject]@{
BatchName = $batch.Name
SuccessCount = $successCount
FailCount = $failCount
Duration = $sw.ElapsedMilliseconds
}

New-Event -SourceIdentifier "BatchCompleted" -Sender $null -EventArguments $eventArgs | Out-Null
}

# 步骤 3:清理事件注册
Unregister-Event -SourceIdentifier "BatchCompleted"

Write-Output ""
Write-Output "所有批次处理完毕"

执行结果示例:

1
2
3
4
5
6
7
[2025-12-10 10:23:15] 批次事件: 用户导入-第1批 | 成功: 149 | 失败: 1 | 耗时: 127ms
[2025-12-10 10:23:15] 批次事件: 用户导入-第2批 | 成功: 228 | 失败: 2 | 耗时: 153ms
[2025-12-10 10:23:15] 批次事件: 用户导入-第3批 | 成功: 178 | 失败: 2 | 耗时: 89ms
[2025-12-10 10:23:15] 批次事件: 用户导入-第4批 | 成功: 308 | 失败: 2 | 耗时: 142ms
[2025-12-10 10:23:15] 批次事件: 用户导入-第5批 | 成功: 92 | 失败: 3 | 耗时: 67ms

所有批次处理完毕

这段代码的核心思路是”生产者-消费者”解耦。主循环只负责处理业务逻辑并通过 New-Event 发出通知,不关心谁来处理、怎么处理。而 Register-EngineEvent 注册的 Action 脚本块相当于一个独立的消费者,它通过 $Event 自动变量访问事件数据($Event.SourceEventArgs 对应 New-Event-EventArguments 参数)。注意最后必须调用 Unregister-Event 来清理订阅,否则事件监听器会一直驻留在 PowerShell 会话中。

进阶:使用 Register-ObjectEvent 监控文件系统变动

Register-ObjectEventRegister-EngineEvent 更强大,它可以订阅任意 .NET 对象的事件。最典型的应用场景是使用 System.IO.FileSystemWatcher 来监控目录的文件变动——当文件被创建、修改、重命名或删除时,自动触发处理逻辑。这在日志监控、配置热更新、文件同步等场景中非常实用。

下面的示例创建一个临时目录,用 FileSystemWatcher 监控其中的文件变动,然后模拟一系列文件操作来验证事件是否被正确捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 步骤 1:创建临时监控目录
$watchPath = Join-Path $env:TEMP "PSWatchDemo_$(Get-Random)"
New-Item -Path $watchPath -ItemType Directory | Out-Null

Write-Output "监控目录: $watchPath"
Write-Output ""

# 步骤 2:创建 FileSystemWatcher 并注册事件
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watchPath
$watcher.Filter = "*.*"
$watcher.EnableRaisingEvents = $true
$watcher.IncludeSubdirectories = $false

# 注册文件创建事件
$createdAction = {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$time = Get-Date -Format "HH:mm:ss.fff"
$script:fileEventLog += "[$time] 文件创建: $name"
}

# 注册文件修改事件
$changedAction = {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$time = Get-Date -Format "HH:mm:ss.fff"
$script:fileEventLog += "[$time] 文件修改: $name"
}

# 注册文件删除事件
$deletedAction = {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$time = Get-Date -Format "HH:mm:ss.fff"
$script:fileEventLog += "[$time] 文件删除: $name"
}

# 注册文件重命名事件
$renamedAction = {
$oldName = $Event.SourceEventArgs.OldName
$newName = $Event.SourceEventArgs.Name
$time = Get-Date -Format "HH:mm:ss.fff"
$script:fileEventLog += "[$time] 文件重命名: $oldName -> $newName"
}

# 初始化事件日志
$script:fileEventLog = @()

Register-ObjectEvent -InputObject $watcher -EventName "Created" -Action $createdAction -SourceIdentifier "FileCreated" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Changed" -Action $changedAction -SourceIdentifier "FileChanged" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Deleted" -Action $deletedAction -SourceIdentifier "FileDeleted" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Renamed" -Action $renamedAction -SourceIdentifier "FileRenamed" | Out-Null

# 步骤 3:模拟文件操作
Write-Output "开始模拟文件操作..."
Write-Output ""

# 创建文件
"Hello World" | Out-File -FilePath (Join-Path $watchPath "test.txt") -Encoding utf8
Start-Sleep -Milliseconds 300

# 修改文件
"Updated content at $(Get-Date)" | Out-File -FilePath (Join-Path $watchPath "test.txt") -Encoding utf8 -Append
Start-Sleep -Milliseconds 300

# 重命名文件
Rename-Item -Path (Join-Path $watchPath "test.txt") -NewName "renamed.txt"
Start-Sleep -Milliseconds 300

# 删除文件
Remove-Item -Path (Join-Path $watchPath "renamed.txt")
Start-Sleep -Milliseconds 300

# 创建多个新文件
foreach ($i in 1..3) {
"data-$i" | Out-File -FilePath (Join-Path $watchPath "file$i.dat") -Encoding utf8
Start-Sleep -Milliseconds 200
}

# 等待事件队列处理完毕
Start-Sleep -Milliseconds 500

# 步骤 4:输出捕获到的事件日志
Write-Output "=== 文件系统事件日志 ==="
foreach ($entry in $script:fileEventLog) {
Write-Output " $entry"
}

# 步骤 5:清理
Unregister-Event -SourceIdentifier "FileCreated"
Unregister-Event -SourceIdentifier "FileChanged"
Unregister-Event -SourceIdentifier "FileDeleted"
Unregister-Event -SourceIdentifier "FileRenamed"
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Remove-Item -Path $watchPath -Recurse -Force -ErrorAction SilentlyContinue

Write-Output ""
Write-Output "事件监控已停止,临时目录已清理"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
监控目录: /tmp/PSWatchDemo_18473

开始模拟文件操作...

=== 文件系统事件日志 ===
[10:45:12.341] 文件创建: test.txt
[10:45:12.648] 文件修改: test.txt
[10:45:12.955] 文件重命名: test.txt -> renamed.txt
[10:45:13.261] 文件删除: renamed.txt
[10:45:13.567] 文件创建: file1.dat
[10:45:13.774] 文件创建: file2.dat
[10:45:13.981] 文件创建: file3.dat

事件监控已停止,临时目录已清理

这段代码有几个值得注意的设计要点。第一,FileSystemWatcher 必须设置 EnableRaisingEvents = $true 才能触发事件,否则它只是一个静默的对象。第二,Action 脚本块中的 $Event.SourceEventArgs 对应 .NET 事件参数(FileSystemEventArgs),其中包含 Name(文件名)、ChangeType(变动类型)等属性。对于重命名事件,参数类型是 RenamedEventArgs,额外提供 OldName 属性。第三,Action 脚本块在 PowerShell 的事件队列线程中执行,不在主线程中,因此需要使用 $script: 作用域修饰符来让主线程能够读取 Action 中记录的数据。第四,文件操作之间加入了 Start-Sleep 延迟,因为事件处理是异步的,需要给事件队列足够的处理时间。

实战:构建事件驱动的配置热更新系统

在实际运维中,应用程序的配置文件更新后通常需要重启服务才能生效。通过 PowerShell 的事件机制,我们可以构建一个”配置热更新”系统:监控配置文件的变动,当检测到变更时自动验证新配置的合法性,验证通过后触发重载操作,全程无需人工干预。

下面的示例演示了如何将 FileSystemWatcherRegister-EngineEvent 结合,构建一个多层事件处理管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# 步骤 1:创建模拟配置文件和目录
$configDir = Join-Path $env:TEMP "PSConfigHotReload_$(Get-Random)"
New-Item -Path $configDir -ItemType Directory | Out-Null

$configFile = Join-Path $configDir "appsettings.json"
$initialConfig = @{
AppName = "OrderService"
Version = "1.0.0"
Debug = $false
MaxRetries = 3
Timeout = 30
LogPath = "/var/log/orderservice"
} | ConvertTo-Json

$initialConfig | Out-File -FilePath $configFile -Encoding utf8

Write-Output "配置文件: $configFile"
Write-Output ""

# 步骤 2:定义配置验证和重载逻辑
$script:configState = @{
CurrentConfig = $null
ReloadHistory = @()
IsHealthy = $true
}

function Test-ConfigValid {
param([string]$JsonString)

try {
$config = $JsonString | ConvertFrom-Json
$errors = @()

if (-not $config.AppName) {
$errors += "缺少 AppName"
}
if ($config.Timeout -and $config.Timeout -lt 5) {
$errors += "Timeout 不能小于 5 秒"
}
if ($config.MaxRetries -and $config.MaxRetries -gt 10) {
$errors += "MaxRetries 不能大于 10"
}

if ($errors.Count -gt 0) {
return @{ Valid = $false; Errors = $errors; Config = $config }
}

return @{ Valid = $true; Errors = @(); Config = $config }
}
catch {
return @{ Valid = $false; Errors = @("JSON 解析失败: $($_.Exception.Message)"); Config = $null }
}
}

# 步骤 3:注册配置变更事件处理管道
# 第一层:FileSystemWatcher 检测文件修改
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $configDir
$watcher.Filter = "appsettings.json"
$watcher.EnableRaisingEvents = $true

$fileChangedAction = {
$filePath = $Event.SourceEventArgs.FullPath
$time = Get-Date -Format "HH:mm:ss"

Start-Sleep -Milliseconds 200

try {
$content = Get-Content -Path $filePath -Raw -ErrorAction Stop

# 将文件内容作为事件参数传递给下一层
$payload = [PSCustomObject]@{
FilePath = $filePath
Content = $content
DetectTime = $time
}

New-Event -SourceIdentifier "ConfigFileChanged" -EventArguments $payload | Out-Null
}
catch {
$script:configState.ReloadHistory += "[$time] 读取配置文件失败: $($_.Exception.Message)"
}
}

# 第二层:验证配置并执行重载
$configChangedAction = {
$payload = $Event.SourceEventArgs
$time = Get-Date -Format "HH:mm:ss"

$validation = Test-ConfigValid -JsonString $payload.Content

if ($validation.Valid) {
$script:configState.CurrentConfig = $validation.Config
$script:configState.IsHealthy = $true
$script:configState.ReloadHistory += "[$time] 配置重载成功: $($validation.Config.AppName) v$($validation.Config.Version) (Timeout=$($validation.Config.Timeout)s)"
}
else {
$script:configState.IsHealthy = $false
$errorList = $validation.Errors -join ", "
$script:configState.ReloadHistory += "[$time] 配置验证失败: $errorList (保持旧配置)"
}
}

Register-ObjectEvent -InputObject $watcher -EventName "Changed" -Action $fileChangedAction -SourceIdentifier "WatchConfig" | Out-Null
Register-EngineEvent -SourceIdentifier "ConfigFileChanged" -Action $configChangedAction -SourceIdentifier "HandleConfigChange" | Out-Null

# 初始加载配置
$initContent = Get-Content -Path $configFile -Raw
$initValidation = Test-ConfigValid -JsonString $initContent
$script:configState.CurrentConfig = $initValidation.Config
Write-Output "初始配置加载完成: $($initValidation.Config.AppName) v$($initValidation.Config.Version)"
Write-Output ""

# 步骤 4:模拟多次配置变更
Write-Output "=== 开始模拟配置变更 ==="
Write-Output ""

# 变更 1:正常更新版本号和超时时间
Write-Output "--- 变更 1: 更新版本号和超时 ---"
$updated1 = @{
AppName = "OrderService"
Version = "1.1.0"
Debug = $false
MaxRetries = 3
Timeout = 45
LogPath = "/var/log/orderservice"
} | ConvertTo-Json

$updated1 | Out-File -FilePath $configFile -Encoding utf8
Start-Sleep -Milliseconds 800

# 变更 2:设置无效的超时值(应该被拒绝)
Write-Output "--- 变更 2: 设置无效超时 (Timeout=2) ---"
$invalidConfig = @{
AppName = "OrderService"
Version = "1.2.0"
Debug = $true
MaxRetries = 3
Timeout = 2
LogPath = "/var/log/orderservice"
} | ConvertTo-Json

$invalidConfig | Out-File -FilePath $configFile -Encoding utf8
Start-Sleep -Milliseconds 800

# 变更 3:修正为有效配置
Write-Output "--- 变更 3: 修正配置 ---"
$updated3 = @{
AppName = "OrderService"
Version = "1.2.0"
Debug = $true
MaxRetries = 5
Timeout = 60
LogPath = "/var/log/orderservice/v2"
} | ConvertTo-Json

$updated3 | Out-File -FilePath $configFile -Encoding utf8
Start-Sleep -Milliseconds 800

# 步骤 5:输出重载历史
Write-Output ""
Write-Output "=== 配置重载历史 ==="
foreach ($entry in $script:configState.ReloadHistory) {
Write-Output " $entry"
}

Write-Output ""
Write-Output "=== 当前配置状态 ==="
Write-Output " 应用: $($script:configState.CurrentConfig.AppName)"
Write-Output " 版本: $($script:configState.CurrentConfig.Version)"
Write-Output " 调试: $($script:configState.CurrentConfig.Debug)"
Write-Output " 超时: $($script:configState.CurrentConfig.Timeout)s"
Write-Output " 重试: $($script:configState.CurrentConfig.MaxRetries)"
Write-Output " 健康: $($script:configState.IsHealthy)"

# 步骤 6:清理
Unregister-Event -SourceIdentifier "WatchConfig"
Unregister-Event -SourceIdentifier "HandleConfigChange"
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Remove-Item -Path $configDir -Recurse -Force -ErrorAction SilentlyContinue

Write-Output ""
Write-Output "配置热更新系统已停止"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
配置文件: /tmp/PSConfigHotReload_32891

初始配置加载完成: OrderService v1.0.0

=== 开始模拟配置变更 ===

--- 变更 1: 更新版本号和超时 ---
--- 变更 2: 设置无效超时 (Timeout=2) ---
--- 变更 3: 修正配置 ---

=== 配置重载历史 ===
[10:58:23] 配置重载成功: OrderService v1.1.0 (Timeout=45s)
[10:58:24] 配置验证失败: Timeout 不能小于 5 秒 (保持旧配置)
[10:58:25] 配置重载成功: OrderService v1.2.0 (Timeout=60s)

=== 当前配置状态 ===
应用: OrderService
版本: 1.2.0
调试: True
超时: 60s
重试: 5
健康: True

配置热更新系统已停止

这段代码展示了一个两层事件管道的设计。第一层是 FileSystemWatcherChanged 事件,它在文件被修改时触发,读取文件内容并通过 New-Event 将数据传递到第二层。第二层是 Register-EngineEvent 注册的 ConfigFileChanged 处理器,它负责验证配置的合法性并执行重载逻辑。这种分层设计的好处是每层只关注单一职责——文件监控层不关心配置验证,验证层不关心文件是怎么被修改的。变更 2 的故意设置了无效的 Timeout=2,验证函数正确地拒绝了这次变更,保持了上一次的有效配置不变。

注意事项

  1. Action 脚本块在后台线程中执行,不在主线程中。这意味着 Action 内部不能直接使用主线程的变量(除非使用 $script:$global: 作用域修饰符),也不能直接将输出写入管道。如果需要将 Action 的结果传回主线程,推荐的做法是在 Action 中写入 $script: 作用域的变量或使用 New-Event 传递到下一级事件处理器。

  2. FileSystemWatcher 可能对同一文件变更触发多次 Changed 事件。许多编辑器(如 VS Code)在保存文件时采用”写入临时文件-重命名替换”的策略,这会导致 ChangedRenamed 事件同时触发。即使使用简单的 Out-File,某些文件系统也可能产生多次事件。建议在 Action 中加入防抖逻辑(例如记录上次处理时间,两次处理间隔小于阈值则跳过)来避免重复处理。

  3. 事件队列是进程级的,不会跨 PowerShell 会话传播Register-EngineEventRegister-ObjectEvent 注册的处理器只对当前 PowerShell 会话(runsapce)有效。如果你在一个 PowerShell 进程中注册事件,然后在另一个 PowerShell 窗口中触发 New-Event,后者不会产生任何效果。跨进程的事件通信需要借助其他机制(如命名管道、消息队列或文件系统信号)。

  4. 大量注册的事件处理器会影响 PowerShell 性能。每个 Register-*Event 调用都会在事件队列中创建一个订阅,当订阅数量过多或事件触发频率过高时,PowerShell 的后台事件处理线程可能成为瓶颈。如果需要监控大量对象或高频率事件,建议限制同时活跃的订阅数量,或使用 .NET 的 TaskCancellationToken 来实现更高效的事件处理。

  5. Unregister-Event 必须在正确的时机调用。如果在 Action 脚本块正在执行时调用 Unregister-Event,可能导致 Action 被中断。推荐的做法是在脚本或模块的清理阶段(如 finally 块或模块的 OnRemove 事件中)统一注销所有事件。可以使用 Get-EventSubscriber 列出当前所有活跃的事件订阅,然后批量注销。

  6. $Event 自动变量只在 Action 脚本块内有效$Event 是 PowerShell 在调用 Action 时自动注入的,包含 SourceEventArgs(事件参数)、Sender(事件发送者)、TimeGenerated(事件生成时间)等属性。试图在 Action 外部访问 $Event 会得到 $null。同样,$EventSubscriber 自动变量提供了当前订阅的信息,可以用来在 Action 中动态注销自身。

PowerShell 技能连载 - 事件驱动自动化

适用于 PowerShell 5.1 及以上版本(Windows)

传统的自动化脚本是”拉模型”——定时轮询检查状态再执行操作。事件驱动是”推模型”——当特定事件发生时自动触发处理逻辑。Windows 和 PowerShell 提供了丰富的事件机制:WMI 事件、.NET 对象事件、文件系统变更事件、Windows 事件日志事件。掌握事件驱动编程,可以构建响应迅速、资源高效的自动化系统。

本文将讲解 PowerShell 中的事件驱动模式及其在运维自动化中的应用。

WMI 事件订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 监控进程创建事件
$query = "SELECT * FROM __InstanceCreationEvent WITHIN 5 WHERE TargetInstance ISA 'Win32_Process'"

Register-WmiEvent -Query $query -SourceIdentifier "ProcessCreated" -Action {
$proc = $Event.SourceEventArgs.NewEvent.TargetInstance
$name = $proc.Name
$pid = $proc.ProcessId
$time = Get-Date -Format 'HH:mm:ss'

Write-Host "[$time] 新进程:$name (PID: $pid)" -ForegroundColor Green

if ($name -in @("cmd.exe", "powershell.exe", "pwsh.exe")) {
Write-Host " 警告:检测到命令行进程启动" -ForegroundColor Yellow
}
}

Write-Host "已订阅进程创建事件" -ForegroundColor Cyan
Write-Host "打开新的命令行窗口测试..." -ForegroundColor DarkGray

# 监控服务状态变更
$serviceQuery = "SELECT * FROM __InstanceModificationEvent WITHIN 10 WHERE TargetInstance ISA 'Win32_Service' AND TargetInstance.State <> PreviousInstance.State"

Register-WmiEvent -Query $serviceQuery -SourceIdentifier "ServiceStateChanged" -Action {
$svc = $Event.SourceEventArgs.NewEvent.TargetInstance
$prev = $Event.SourceEventArgs.NewEvent.PreviousInstance
$name = $svc.Name
$newState = $svc.State
$oldState = $prev.State

$color = if ($newState -eq 'Running') { 'Green' } else { 'Red' }
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] 服务变更:$name $oldState => $newState" -ForegroundColor $color
}

Write-Host "已订阅服务状态变更事件" -ForegroundColor Cyan

# 清理事件订阅
# Get-EventSubscriber | Unregister-Event
# Get-Job | Where-Object { $_.Name -match 'ProcessCreated|ServiceStateChanged' } | Remove-Job

执行结果示例:

1
2
3
4
5
6
7
已订阅进程创建事件
打开新的命令行窗口测试...
[08:30:15] 新进程:cmd.exe (PID: 12345)
警告:检测到命令行进程启动
[08:30:45] 新进程:notepad.exe (PID: 12346)
已订阅服务状态变更事件
[08:31:10] 服务变更:Spooler Running => Stopped

文件到达自动处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 监控文件到达并自动处理
function Start-FileArrivalProcessor {
param(
[string]$WatchPath = "C:\DropZone",
[string]$ProcessedPath = "C:\DropZone\Processed",
[string]$ErrorPath = "C:\DropZone\Errors"
)

foreach ($path in @($WatchPath, $ProcessedPath, $ErrorPath)) {
New-Item $path -ItemType Directory -Force | Out-Null
}

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $WatchPath
$watcher.Filter = "*.csv"
$watcher.EnableRaisingEvents = $true

$action = {
$filePath = $Event.SourceEventArgs.FullPath
$fileName = $Event.SourceEventArgs.Name

Start-Sleep -Seconds 2

Write-Host "[$(Get-Date -Format 'HH:mm:ss')] 检测到新文件:$fileName" -ForegroundColor Cyan

try {
$data = Import-Csv $filePath -Encoding UTF8
$rows = $data.Count
Write-Host " 数据行数:$rows" -ForegroundColor Green

$destPath = Join-Path $ProcessedPath $fileName
Move-Item $filePath $destPath -Force
Write-Host " 已移至处理目录" -ForegroundColor Green
} catch {
$errorDest = Join-Path $ErrorPath $fileName
Move-Item $filePath $errorDest -Force
Write-Host " 处理失败:$($_.Exception.Message)" -ForegroundColor Red
}
}

Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier "FileArrival"

Write-Host "文件到达处理器已启动:$WatchPath" -ForegroundColor Green
}

Start-FileArrivalProcessor

执行结果示例:

1
2
3
4
文件到达处理器已启动:C:\DropZone
[08:35:10] 检测到新文件:orders-20250828.csv
数据行数:150
已移至处理目录

定时事件与调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 使用 Timer 对象创建定时任务
function Start-PeriodicCheck {
param(
[scriptblock]$CheckScript,
[int]$IntervalSeconds = 60,
[string]$Name = "PeriodicCheck"
)

$timer = New-Object System.Timers.Timer
$timer.Interval = $IntervalSeconds * 1000
$timer.AutoReset = $true

$action = {
try {
& $CheckScript
} catch {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] 检查异常:$($_.Exception.Message)" -ForegroundColor Red
}
}

Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $action -SourceIdentifier $Name
$timer.Start()

Write-Host "定时检查已启动:$Name(间隔 ${IntervalSeconds}s)" -ForegroundColor Green
return $timer
}

# 每 60 秒检查磁盘空间
$diskTimer = Start-PeriodicCheck -Name "DiskSpaceCheck" -IntervalSeconds 60 -CheckScript {
$disk = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"
$freeGB = [math]::Round($disk.FreeSpace / 1GB, 2)
$usedPct = [math]::Round(($disk.Size - $disk.FreeSpace) / $disk.Size * 100, 1)

if ($usedPct -gt 90) {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] 磁盘告警!C盘使用率 $usedPct%,剩余 $freeGB GB" -ForegroundColor Red
} else {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] C盘正常:$usedPct%(剩余 $freeGB GB)" -ForegroundColor Green
}
}

# 每 30 秒检查关键服务
$svcTimer = Start-PeriodicCheck -Name "ServiceCheck" -IntervalSeconds 30 -CheckScript {
$services = @("W3SVC", "MSSQLSERVER", "MyApp")
foreach ($svcName in $services) {
$svc = Get-Service $svcName -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -ne 'Running') {
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] $svcName 异常:$($svc.Status)" -ForegroundColor Red
}
}
}

# 停止定时器
# $diskTimer.Stop()
# $svcTimer.Stop()
# Get-EventSubscriber | Unregister-Event

执行结果示例:

1
2
3
4
定时检查已启动:DiskSpaceCheck(间隔 60s)
定时检查已启动:ServiceCheck(间隔 30s)
[08:31:00] C盘正常:72.4%(剩余 55.2 GB)
[08:31:30] [08:32:00] C盘正常:72.4%(剩余 55.2 GB)

Windows 事件日志触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 监控 Windows 事件日志
function Watch-EventLog {
param(
[string]$LogName = "Application",
[int]$EventId,
[string]$Source,
[string]$Level = "Error"
)

$query = "*[System/Level=$(switch ($Level) { 'Critical' { 1 } 'Error' { 2 } 'Warning' { 3 } 'Info' { 4 } })]"

if ($EventId) {
$query = "*[System/EventID=$EventId]"
}

Register-WmiEvent -Query @"
SELECT * FROM __InstanceCreationEvent WITHIN 5
WHERE TargetInstance ISA 'Win32_NTLogEvent'
AND TargetInstance.LogFile = '$LogName'
"@ -SourceIdentifier "EventLog_$LogName" -Action {
$evt = $Event.SourceEventArgs.NewEvent.TargetInstance
$time = $evt.TimeGenerated
$source = $evt.SourceName
$msg = $evt.Message

if ($msg.Length -gt 100) { $msg = $msg.Substring(0, 100) + "..." }

Write-Host "[$time] 事件:$source" -ForegroundColor Yellow
Write-Host " $msg" -ForegroundColor DarkGray
}

Write-Host "已订阅事件日志:$LogName" -ForegroundColor Green
}

# 监控应用程序错误
Watch-EventLog -LogName "Application" -Level "Error"

# 查看当前所有事件订阅
Write-Host "`n当前事件订阅:" -ForegroundColor Cyan
Get-EventSubscriber | Select-Object SourceIdentifier, SubscriptionId |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
已订阅事件日志:Application

当前事件订阅:
SourceIdentifier SubscriptionId
----------------- --------------
ProcessCreated 1
ServiceStateChanged 2
FileArrival 3
DiskSpaceCheck 4
ServiceCheck 5
EventLog_Application 6

注意事项

  1. 事件队列:PowerShell 事件存储在内存队列中,长时间运行可能积压大量事件,定期用 Get-Event 处理
  2. 资源释放:事件订阅和 FileSystemWatcher 使用完毕后必须清理,否则会持续消耗资源
  3. WITHIN 间隔:WMI 事件的 WITHIN 参数控制轮询间隔,间隔越短 CPU 使用越高
  4. 事件丢失:高频事件可能被丢弃(缓冲区溢出),关键场景需要补充轮询机制
  5. 会话限制:事件订阅只在当前 PowerShell 会话中有效,需要持久化请使用计划任务或 Windows 服务
  6. 权限要求:某些 WMI 事件订阅需要管理员权限

PowerShell 技能连载 - 文件系统监控进阶

适用于 PowerShell 5.1 及以上版本(Windows),FileSystemWatcher 需要 .NET Framework/Core

文件系统监控是自动化运维的重要能力——监控配置文件变更触发服务重载、监控上传目录自动处理新文件、监控日志目录异常增长告警。.NET 的 FileSystemWatcher 类提供了操作系统级的文件变更通知机制,比轮询扫描高效得多。结合 PowerShell 的事件处理能力,可以构建响应迅速的文件系统自动化工作流。

本文将讲解 FileSystemWatcher 的高级用法、事件处理模式,以及实用的自动化场景。

基础文件监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 创建 FileSystemWatcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Config"
$watcher.Filter = "*.json"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true

# 注册事件处理器
$action = {
$path = $Event.SourceEventArgs.FullPath
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

Write-Host "[$timestamp] $changeType : $name" -ForegroundColor Yellow

switch ($changeType) {
'Changed' { Write-Host " 文件已修改:$path" -ForegroundColor Cyan }
'Created' { Write-Host " 新文件:$path" -ForegroundColor Green }
'Deleted' { Write-Host " 文件已删除:$path" -ForegroundColor Red }
'Renamed' {
$oldName = $Event.SourceEventArgs.OldName
Write-Host " 重命名:$oldName => $name" -ForegroundColor Yellow
}
}
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -SourceIdentifier "FileChanged"
Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -SourceIdentifier "FileCreated"
Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $action -SourceIdentifier "FileDeleted"
Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -SourceIdentifier "FileRenamed"

Write-Host "文件监控已启动:C:\Config\*.json" -ForegroundColor Green
Write-Host "按 Enter 停止监控..." -ForegroundColor DarkGray
Read-Host

# 清理
Get-EventSubscriber | Unregister-Event
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()

执行结果示例:

1
2
3
4
5
文件监控已启动:C:\Config\*.json
[2025-06-19 08:30:15] Changed : appsettings.json
文件已修改:C:\Config\appsettings.json
[2025-06-19 08:30:45] Created : new-config.json
新文件:C:\Config\new-config.json

配置文件变更自动重载

当应用配置文件变更时,自动触发服务重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function Start-ConfigAutoReload {
<#
.SYNOPSIS
监控配置文件变更,自动重载服务
#>
param(
[Parameter(Mandatory)]
[string]$WatchPath,

[string]$Filter = "*.json",

[Parameter(Mandatory)]
[string]$ServiceName,

[int]$DebounceSeconds = 5
)

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $WatchPath
$watcher.Filter = $Filter
$watcher.EnableRaisingEvents = $true

$lastReload = [datetime]::MinValue

$action = {
$now = Get-Date
if (($now - $lastReload).TotalSeconds -lt $DebounceSeconds) {
return
}
$script:lastReload = $now

$fileName = $Event.SourceEventArgs.Name
Write-Host "[$(Get-Date -Format 'HH:mm:ss')] 检测到配置变更:$fileName" -ForegroundColor Yellow

# 验证 JSON 有效性
try {
$content = Get-Content $Event.SourceEventArgs.FullPath -Raw | ConvertFrom-Json
Write-Host " JSON 验证通过" -ForegroundColor Green
} catch {
Write-Host " JSON 验证失败,跳过重载:$($_.Exception.Message)" -ForegroundColor Red
return
}

# 重启服务
Write-Host " 正在重启服务:$ServiceName..." -ForegroundColor Cyan
Restart-Service -Name $ServiceName -Force
$svc = Get-Service $ServiceName
Write-Host " 服务状态:$($svc.Status)" -ForegroundColor $(if ($svc.Status -eq 'Running') { 'Green' } else { 'Red' })
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action `
-SourceIdentifier "ConfigAutoReload_$ServiceName"

Write-Host "配置自动重载已启动" -ForegroundColor Green
Write-Host " 监控路径:$WatchPath\$Filter"
Write-Host " 目标服务:$ServiceName"
}

# 启动监控
Start-ConfigAutoReload -WatchPath "C:\MyApp\config" `
-ServiceName "MyApp" -DebounceSeconds 5

执行结果示例:

1
2
3
4
5
6
7
配置自动重载已启动
监控路径:C:\MyApp\config\*.json
目标服务:MyApp
[08:30:15] 检测到配置变更:appsettings.json
JSON 验证通过
正在重启服务:MyApp...
服务状态:Running

上传目录自动处理

监控上传目录,自动处理新到达的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
function Start-UploadProcessor {
<#
.SYNOPSIS
监控上传目录,自动处理新文件
#>
param(
[string]$UploadPath = "C:\Uploads",
[string]$ProcessedPath = "C:\Uploads\processed",
[string]$ErrorPath = "C:\Uploads\errors"
)

foreach ($path in @($UploadPath, $ProcessedPath, $ErrorPath)) {
New-Item -Path $path -ItemType Directory -Force | Out-Null
}

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $UploadPath
$watcher.Filter = "*.csv"
$watcher.EnableRaisingEvents = $true

$action = {
$filePath = $Event.SourceEventArgs.FullPath
$fileName = $Event.SourceEventArgs.Name

# 等待文件完全写入(避免读取未完成的文件)
Start-Sleep -Seconds 2

# 检查文件是否仍在被写入
try {
$stream = [System.IO.File]::Open($filePath, 'Open', 'Read', 'Read')
$stream.Close()
} catch {
Write-Host "文件仍被锁定,跳过:$fileName" -ForegroundColor DarkGray
return
}

Write-Host "处理文件:$fileName" -ForegroundColor Cyan

try {
# 读取并处理 CSV
$data = Import-Csv $filePath
$rowCount = $data.Count

# 模拟数据处理
foreach ($row in $data) {
# 处理逻辑...
}

# 移动到已处理目录
$destPath = Join-Path $ProcessedPath $fileName
Move-Item $filePath $destPath -Force
Write-Host " 已处理 $rowCount 行,移至:$destPath" -ForegroundColor Green

} catch {
$errorPath = Join-Path $ErrorPath $fileName
Move-Item $filePath $errorPath -Force
Write-Host " 处理失败,移至:$errorPath" -ForegroundColor Red
Write-Host " 错误:$($_.Exception.Message)" -ForegroundColor Red
}
}

Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action `
-SourceIdentifier "UploadProcessor"

Write-Host "上传处理器已启动:$UploadPath" -ForegroundColor Green
}

Start-UploadProcessor

执行结果示例:

1
2
3
上传处理器已启动:C:\Uploads
处理文件:customers-20250619.csv
已处理 150 行,移至:C:\Uploads\processed\customers-20250619.csv

注意事项

  1. 文件锁检测:新创建的文件可能仍在被写入,处理前应等待并检测文件锁
  2. 防抖处理:同一文件的修改可能在短时间内触发多次事件,使用时间戳去重
  3. 缓冲区大小:大量快速变更时,FileSystemWatcher 的内部缓冲区可能溢出。通过 InternalBufferSize 属性增大缓冲区
  4. 网络驱动器:FileSystemWatcher 对网络路径(UNC 路径)的监控不太可靠,建议在文件服务器本地运行
  5. 资源释放:使用完毕后务必调用 Dispose() 释放 FileSystemWatcher 和取消事件订阅
  6. 事件队列:PowerShell 的事件队列可能积压,定期使用 Get-Event 处理或设置队列大小限制