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

适用于 PowerShell 5.1 及以上版本

在文件服务器和开发环境中,监控文件变化是常见的需求:自动编译源代码变更、检测配置文件被篡改、审计共享文件夹的访问记录。.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
41
42
43
44
45
46
47
48
49
# 创建文件系统监控器
$watcher = [System.IO.FileSystemWatcher]::new()
$watcher.Path = "C:\Projects\MyApp"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor
[System.IO.NotifyFilters]::LastWrite -bor
[System.IO.NotifyFilters]::Size

# 注册事件处理
$action = {
$changeType = $Event.SourceEventArgs.ChangeType
$fullPath = $Event.SourceEventArgs.FullPath
$name = $Event.SourceEventArgs.Name
$timestamp = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss.fff")

$logEntry = "$timestamp | $changeType | $fullPath"
$logEntry | Out-File "C:\Logs\FileChanges.log" -Append

# 根据变更类型输出不同颜色
switch ($changeType) {
"Created" { Write-Host $logEntry -ForegroundColor Green }
"Changed" { Write-Host $logEntry -ForegroundColor Yellow }
"Deleted" { Write-Host $logEntry -ForegroundColor Red }
"Renamed" { Write-Host "$logEntry (from $($Event.SourceEventArgs.OldName))" -ForegroundColor Cyan }
}
}

# 注册各类事件
Register-ObjectEvent -InputObject $watcher -EventName "Created" -Action $action -SourceIdentifier "File.Created" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Changed" -Action $action -SourceIdentifier "File.Changed" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Deleted" -Action $action -SourceIdentifier "File.Deleted" | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Renamed" -Action $action -SourceIdentifier "File.Renamed" | Out-Null

# 启用引发事件
$watcher.EnableRaisingEvents = $true
Write-Host "文件监控已启动:$($watcher.Path)" -ForegroundColor Green
Write-Host "监控子目录:$($watcher.IncludeSubdirectories)" -ForegroundColor Cyan
Write-Host "按 Ctrl+C 停止监控"

# 查看已注册的事件
Get-EventSubscriber | Where-Object { $_.SourceIdentifier -like "File.*" } |
Select-Object SourceIdentifier, Action |
Format-Table -AutoSize

# 停止监控(在需要时运行)
# $watcher.EnableRaisingEvents = $false
# Get-EventSubscriber -SourceIdentifier "File.*" | Unregister-Event
# $watcher.Dispose()

执行结果示例:

1
2
3
4
5
6
7
8
9
文件监控已启动:C:\Projects\MyApp
监控子目录:True
按 Ctrl+C 停止监控
SourceIdentifier Action
---------------- ------
File.Created System.Management.Automation.PSEventJob
File.Changed System.Management.Automation.PSEventJob
File.Deleted System.Management.Automation.PSEventJob
File.Renamed System.Management.Automation.PSEventJob

配置文件变更监控

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-ConfigFileMonitor {
param(
[Parameter(Mandatory)]
[string]$ConfigPath,

[string]$LogPath = "C:\Logs\ConfigChanges.log"
)

$configDir = Split-Path $ConfigPath -Parent
$configFile = Split-Path $ConfigPath -Leaf

$watcher = [System.IO.FileSystemWatcher]::new($configDir, $configFile)
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor
[System.IO.NotifyFilters]::Size

$action = {
$filePath = $Event.SourceEventArgs.FullPath
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

# 读取变更后的文件内容
Start-Sleep -Milliseconds 100 # 等待文件写入完成

try {
$content = Get-Content $filePath -Raw -ErrorAction Stop
$json = $content | ConvertFrom-Json -ErrorAction Stop

# 验证配置结构
$required = @("appName", "version", "database")
$missing = $required | Where-Object {
-not ($json.PSObject.Properties.Name -contains $_)
}

if ($missing) {
$msg = "$timestamp | INVALID | $filePath | 缺少字段:$($missing -join ', ')"
Write-Host $msg -ForegroundColor Red
} else {
$msg = "$timestamp | VALID | $filePath | version=$($json.version)"
Write-Host $msg -ForegroundColor Green
}

$msg | Out-File $using:LogPath -Append

} catch {
$msg = "$timestamp | PARSE_ERROR | $filePath | $($_.Exception.Message)"
Write-Host $msg -ForegroundColor Red
$msg | Out-File $using:LogPath -Append
}
}

Register-ObjectEvent -InputObject $watcher -EventName "Changed" `
-Action $action -SourceIdentifier "Config.Watch" | Out-Null

$watcher.EnableRaisingEvents = $true
Write-Host "配置文件监控已启动:$ConfigPath" -ForegroundColor Green

return $watcher
}

# 启动监控
# $configWatcher = Start-ConfigFileMonitor -ConfigPath "C:\MyApp\appsettings.json"

执行结果示例:

1
2
3
配置文件监控已启动:C:\MyApp\appsettings.json
2025-09-09 14:30:15 | VALID | C:\MyApp\appsettings.json | version=2.5.0
2025-09-09 14:35:22 | INVALID | C:\MyApp\appsettings.json | 缺少字段:database

实用监控工具集

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
# 综合文件监控仪表板
function Start-FileMonitorDashboard {
param(
[string[]]$Paths = @("C:\Projects", "C:\Config"),
[int]$DurationMinutes = 30
)

$logFile = "C:\Logs\FileMonitor_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$stats = [System.Collections.Concurrent.ConcurrentDictionary[string, int]]::new()
$stats["Created"] = 0
$stats["Changed"] = 0
$stats["Deleted"] = 0
$stats["Renamed"] = 0

$watchers = @()
$endTime = (Get-Date).AddMinutes($DurationMinutes)

foreach ($path in $Paths) {
if (-not (Test-Path $path)) {
Write-Host "路径不存在:$path" -ForegroundColor Red
continue
}

$watcher = [System.IO.FileSystemWatcher]::new()
$watcher.Path = $path
$watcher.IncludeSubdirectories = $true
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor
[System.IO.NotifyFilters]::LastWrite

$eventAction = {
$changeType = $Event.SourceEventArgs.ChangeType.ToString()
$fullPath = $Event.SourceEventArgs.FullPath
$timestamp = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")

# 更新统计
$stats = $using:stats
$stats.AddOrUpdate($changeType, 1, { param($k, $v) $v + 1 })

# 过滤临时文件
if ($fullPath -notmatch '\.(tmp|bak|~)$') {
"$timestamp|$changeType|$fullPath" | Out-File $using:logFile -Append
}
}

"Created", "Changed", "Deleted", "Renamed" | ForEach-Object {
Register-ObjectEvent -InputObject $watcher -EventName $_ `
-Action $eventAction -SourceIdentifier "Monitor.$($_).$($path.GetHashCode())" | Out-Null
}

$watcher.EnableRaisingEvents = $true
$watchers += $watcher
Write-Host "已启动监控:$path" -ForegroundColor Green
}

Write-Host "`n监控运行中,时长 $DurationMinutes 分钟..." -ForegroundColor Cyan
Write-Host "日志文件:$logFile`n"

# 定期显示统计
while ((Get-Date) -lt $endTime) {
Start-Sleep -Seconds 30
Write-Host "--- $(Get-Date -Format 'HH:mm:ss') 统计 ---" -ForegroundColor Cyan
foreach ($key in @("Created", "Changed", "Deleted", "Renamed")) {
Write-Host " $key : $($stats[$key])"
}
}

# 清理
foreach ($w in $watchers) {
$w.EnableRaisingEvents = $false
$w.Dispose()
}
Get-EventSubscriber -SourceIdentifier "Monitor.*" | Unregister-Event

Write-Host "`n监控已停止。最终统计:" -ForegroundColor Yellow
foreach ($key in @("Created", "Changed", "Deleted", "Renamed")) {
Write-Host " $key : $($stats[$key])"
}
}

# 启动 5 分钟监控演示
# Start-FileMonitorDashboard -Paths @("C:\Projects") -DurationMinutes 5

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
已启动监控:C:\Projects
已启动监控:C:\Config

监控运行中,时长 30 分钟...
日志文件:C:\Logs\FileMonitor_20250909_143000.log

--- 14:30:30 统计 ---
Created : 5
Changed : 12
Deleted : 2
Renamed : 1

监控已停止。最终统计:
Created : 23
Changed : 67
Deleted : 8
Renamed : 3

注意事项

  1. 缓冲区溢出:高频文件变更可能超出 FileSystemWatcher 内部缓冲区(默认 8KB),增大 InternalBufferSize 属性可缓解
  2. 重复事件:许多编辑器保存文件时会触发多个 Changed 事件,需要用去重逻辑(如时间窗口内相同路径合并)
  3. 网络驱动器:FileSystemWatcher 对网络路径(UNC)的监控不稳定,建议在文件所在服务器本地运行
  4. 资源释放:监控结束或脚本退出前务必调用 Dispose() 释放系统资源
  5. 权限要求:监控进程需要对目标目录有读取权限,某些系统目录可能需要管理员权限
  6. 事件队列:如果事件处理脚本执行过慢,事件会堆积在队列中,避免在事件处理中做耗时操作

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 处理或设置队列大小限制

PowerShell 技能连载 - 文件系统变更监控

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

背景

在日常工作和技术运维中,我们经常需要知道某个目录下发生了什么变化:配置文件是否被篡改、日志目录是否有新文件生成、用户文档是否被意外删除。传统的做法是写一个轮询脚本,每隔几秒扫描一次目录,但这种方式既低效又容易遗漏变化。

.NET 提供的 System.IO.FileSystemWatcher 类可以监听文件系统的变更事件,当目标目录中出现文件创建、修改、删除或重命名时,它会立即触发通知。结合 PowerShell 的事件注册机制,我们可以构建出高效、实时的文件系统监控方案——从安全审计到自动构建,再到实时备份,都可以基于这套机制实现。

FileSystemWatcher 基础

FileSystemWatcher 是 .NET 内置的文件系统监控组件。使用时需要指定要监控的目录路径,并配置需要监听的事件类型。下面是最基本的创建和启用方式:

1
2
3
4
5
6
7
8
9
10
11
# 创建 FileSystemWatcher 实例
$watcher = [System.IO.FileSystemWatcher]::new()

# 设置监控路径
$watcher.Path = "C:\Logs"

# 启用变更事件触发
$watcher.EnableRaisingEvents = $true

# 查看当前配置
$watcher | Format-List Path, Filter, EnableRaisingEvents
1
2
3
Path                 : C:\Logs
Filter : *.*
EnableRaisingEvents : True

创建 FileSystemWatcher 后,Path 属性指定了要监控的目录,EnableRaisingEvents 设为 $true 后才会真正开始触发事件。默认情况下 Filter*.*,表示监控所有文件。注意,此时虽然已经启用了事件触发,但我们还没有注册任何事件处理器,所以即使目录发生变化也不会有任何响应。

监控文件创建、修改、删除与重命名

FileSystemWatcher 提供了四类核心事件:CreatedChangedDeletedRenamed。我们可以使用 PowerShell 的 Register-ObjectEvent 来订阅这些事件,并通过 -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
$watcher = [System.IO.FileSystemWatcher]::new("C:\Logs")
$watcher.EnableRaisingEvents = $true

# 注册创建事件
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "FileCreated" -Action {
$name = $Event.SourceEventArgs.Name
$fullPath = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件创建: $name ($fullPath)"
}

# 注册修改事件
Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "FileChanged" -Action {
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件修改: $name ($changeType)"
}

# 注册删除事件
Register-ObjectEvent -InputObject $watcher -EventName Deleted -SourceIdentifier "FileDeleted" -Action {
$name = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件删除: $name"
}

# 注册重命名事件
Register-ObjectEvent -InputObject $watcher -EventName Renamed -SourceIdentifier "FileRenamed" -Action {
$oldName = $Event.SourceEventArgs.OldName
$newName = $Event.SourceEventArgs.Name
$time = $Event.TimeGenerated.ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] 文件重命名: $oldName -> $newName"
}

Write-Host "已注册全部事件,正在监控 C:\Logs ..."

注册完成后,每当 C:\Logs 目录下发生对应的文件操作,PowerShell 就会在后台执行相应的 Action 脚本块。此时如果我们在另一个终端中操作文件,监控端就会输出对应的日志信息。

1
2
3
4
[2025-04-24 10:15:32] 文件创建: newfile.txt (C:\Logs\newfile.txt)
[2025-04-24 10:15:45] 文件修改: newfile.txt (Changed)
[2025-04-24 10:16:01] 文件重命名: newfile.txt -> report.txt
[2025-04-24 10:16:20] 文件删除: report.txt

需要注意,Changed 事件可能被多次触发——例如用记事本保存文件时,文件的内容和属性可能分别触发一次 Changed 事件。这在后续处理中需要做去重处理。

过滤器与监控范围设置

在实际场景中,我们往往不需要监控目录下的所有文件。FileSystemWatcher 提供了 FilterIncludeSubdirectories 两个属性来精确控制监控范围。

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
$watcher = [System.IO.FileSystemWatcher]::new("C:\Project\src")

# 只监控 .log 文件
$watcher.Filter = "*.log"

# 同时监控子目录
$watcher.IncludeSubdirectories = $true

# 设置缓冲区大小(默认 8KB,频繁变更时需要加大)
$watcher.InternalBufferSize = 65536 # 64KB

$watcher.EnableRaisingEvents = $true

# 注册事件,只关注 Created 和 Changed
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "LogCreated" -Action {
$path = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("HH:mm:ss.fff")
Write-Host "[$time] 新日志: $path"
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "LogChanged" -Action {
$path = $Event.SourceEventArgs.FullPath
$time = $Event.TimeGenerated.ToString("HH:mm:ss.fff")
Write-Host "[$time] 日志更新: $path"
}

Write-Host "正在监控 C:\Project\src 及子目录中的 *.log 文件..."
1
2
3
[10:20:15.123] 新日志: C:\Project\src\app\debug.log
[10:20:15.456] 日志更新: C:\Project\src\app\debug.log
[10:21:30.789] 新日志: C:\Project\src\api\access.log

Filter 属性只支持简单的通配符模式(如 *.log*.txt),不支持正则表达式或多个扩展名的组合过滤。如果需要同时监控 .log.txt,可以创建多个 FileSystemWatcher 实例,或者在 Action 中用条件判断自行过滤。InternalBufferSize 的设置很重要——当短时间内文件变更过于频繁时,默认的 8KB 缓冲区可能不够用,导致事件丢失。微软建议将其设置为 4KB 的整数倍,最大可到 64KB。

事件注册处理与生命周期管理

PowerShell 的事件订阅不会自动清理,如果不主动注销,它们会一直占用资源。良好的做法是在脚本中记录所有订阅标识,并在不再需要时统一注销。此外,事件处理器中产生的输出不会直接显示在控制台,而是存储在后台作业中,需要用 Receive-Job 来查看。

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
# 定义所有订阅标识
$subscriptionIds = @(
"FSW_Created",
"FSW_Changed",
"FSW_Deleted",
"FSW_Renamed"
)

$watcher = [System.IO.FileSystemWatcher]::new("C:\Config")
$watcher.EnableRaisingEvents = $true

# 注册事件并收集作业信息
$jobs = @()
$events = @("Created", "Changed", "Deleted", "Renamed")
$ids = $subscriptionIds

for ($i = 0; $i -lt $events.Count; $i++) {
$job = Register-ObjectEvent -InputObject $watcher `
-EventName $events[$i] `
-SourceIdentifier $ids[$i] `
-Action {
$info = @{
Time = $Event.TimeGenerated
Type = $Event.SourceEventArgs.ChangeType
Path = $Event.SourceEventArgs.FullPath
Name = $Event.SourceEventArgs.Name
}
# 将事件信息写入流,方便后续检索
[PSCustomObject]$info
}
$jobs += $job
}

Write-Host "已注册 $($events.Count) 个事件订阅"

# 查看当前所有事件订阅
Get-EventSubscriber | Format-Table SourceIdentifier, SubscriptionId, AutoUnregister

# 查看后台作业中收集到的事件
Start-Sleep -Seconds 5 # 等待一些事件发生
foreach ($job in $jobs) {
$results = Receive-Job -Job $job -ErrorAction SilentlyContinue
if ($results) {
$results | Format-Table Time, Type, Name -AutoSize
}
}
1
2
3
4
5
6
7
8
9
10
11
12
SourceIdentifier SubscriptionId AutoUnregister
----------------- -------------- --------------
FSW_Created 1 False
FSW_Changed 2 False
FSW_Deleted 3 False
FSW_Renamed 4 False

Time Type Name
---- ---- ----
2025/4/24 10:30:15 Created appsettings.json
2025/4/24 10:30:22 Changed appsettings.json
2025/4/24 10:31:05 Renamed old_config.json

使用完毕后,务必清理订阅和作业,否则会造成内存泄漏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 注销所有事件订阅
foreach ($id in $subscriptionIds) {
Unregister-Event -SourceIdentifier $id -ErrorAction SilentlyContinue
}

# 移除后台作业
foreach ($job in $jobs) {
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}

# 释放 FileSystemWatcher
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()

Write-Host "已清理所有事件订阅和资源"
1
已清理所有事件订阅和资源

实战:自动备份监控目录

将以上知识点整合起来,我们可以构建一个实用的自动备份脚本。当监控目录中的文件发生变更时,自动将最新版本复制到备份目录中,实现近实时的增量备份。

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
# 自动备份脚本配置
$SourcePath = "C:\ImportantDocs"
$BackupPath = "D:\Backup\ImportantDocs"

# 确保备份目录存在
if (-not (Test-Path $BackupPath)) {
New-Item -Path $BackupPath -ItemType Directory -Force | Out-Null
Write-Host "已创建备份目录: $BackupPath"
}

# 创建 FileSystemWatcher
$watcher = [System.IO.FileSystemWatcher]::new($SourcePath)
$watcher.IncludeSubdirectories = $true
$watcher.InternalBufferSize = 65536
$watcher.EnableRaisingEvents = $true

# 用于去重的哈希表(避免短时间内重复备份同一文件)
$recentBackup = @{}

# 注册创建和修改事件
Register-ObjectEvent -InputObject $watcher -EventName Created -SourceIdentifier "Backup_Created" -Action {
$sourceFile = $Event.SourceEventArgs.FullPath
$now = Get-Date

# 去重:5秒内同一文件不重复备份
if ($recentBackup.ContainsKey($sourceFile) -and
($now - $recentBackup[$sourceFile]).TotalSeconds -lt 5) {
return
}
$recentBackup[$sourceFile] = $now

# 计算备份目标路径
$relativePath = $sourceFile.Substring($SourcePath.Length).TrimStart("\")
$targetFile = Join-Path $BackupPath $relativePath

# 确保目标目录存在
$targetDir = Split-Path $targetFile -Parent
if (-not (Test-Path $targetDir)) {
New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
}

# 执行备份
try {
Copy-Item -Path $sourceFile -Destination $targetFile -Force
$timeStr = $now.ToString("HH:mm:ss")
Write-Host "[$timeStr] 备份成功: $relativePath"
}
catch {
Write-Host "[ERROR] 备份失败: $relativePath - $($_.Exception.Message)"
}
}

Register-ObjectEvent -InputObject $watcher -EventName Changed -SourceIdentifier "Backup_Changed" -Action {
$sourceFile = $Event.SourceEventArgs.FullPath
$now = Get-Date

# 去重:5秒内同一文件不重复备份
if ($recentBackup.ContainsKey($sourceFile) -and
($now - $recentBackup[$sourceFile]).TotalSeconds -lt 5) {
return
}
$recentBackup[$sourceFile] = $now

$relativePath = $sourceFile.Substring($SourcePath.Length).TrimStart("\")
$targetFile = Join-Path $BackupPath $relativePath

$targetDir = Split-Path $targetFile -Parent
if (-not (Test-Path $targetDir)) {
New-Item -Path $targetDir -ItemType Directory -Force | Out-Null
}

try {
# 修改时等待文件写入完成
Start-Sleep -Milliseconds 500
Copy-Item -Path $sourceFile -Destination $targetFile -Force
$timeStr = $now.ToString("HH:mm:ss")
Write-Host "[$timeStr] 增量备份: $relativePath"
}
catch {
Write-Host "[ERROR] 备份失败: $relativePath - $($_.Exception.Message)"
}
}

Write-Host "自动备份已启动: $SourcePath -> $BackupPath"
Write-Host "按 Ctrl+C 停止监控..."

# 保持脚本运行
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
# 清理
Unregister-Event -SourceIdentifier "Backup_Created" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Backup_Changed" -ErrorAction SilentlyContinue
Get-Job | Where-Object { $_.Name -match "^Backup_" } | Remove-Job -Force
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "监控已停止,资源已释放"
}
1
2
3
4
5
6
7
已创建备份目录: D:\Backup\ImportantDocs
自动备份已启动: C:\ImportantDocs -> D:\Backup\ImportantDocs
按 Ctrl+C 停止监控...
[10:45:12] 备份成功: report.docx
[10:45:38] 增量备份: report.docx
[10:46:05] 备份成功: subfolder\notes.txt
监控已停止,资源已释放

这个脚本包含了几个实用的设计要点:用哈希表进行去重以避免 Changed 事件的重复触发;在备份修改文件时加入了短暂延迟,确保文件写入完成;在 finally 块中确保资源始终被正确释放。

查看与管理事件订阅

在调试和运维过程中,我们需要随时了解当前有哪些活跃的事件订阅。PowerShell 提供了几个相关命令来管理这些订阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 列出当前所有事件订阅
Get-EventSubscriber | Format-Table SourceIdentifier, SubscriptionId, EventName -AutoSize

# 查看后台作业状态
Get-Job | Where-Object { $_.JobStateInfo.State -eq "Running" } |
Format-Table Id, Name, State -AutoSize

# 查看某个作业的输出
$job = Get-Job | Where-Object { $_.Name -eq "Backup_Created" } | Select-Object -First 1
if ($job) {
Receive-Job -Job $job -Keep
}

# 紧急清理:注销所有事件订阅
Get-EventSubscriber | Unregister-Event
Get-Job | Remove-Job -Force
Write-Host "已清理全部订阅和作业"
1
2
3
4
5
6
7
8
9
10
11
SourceIdentifier  SubscriptionId EventName
----------------- -------------- ---------
Backup_Created 1 Created
Backup_Changed 2 Changed

Id Name State
--- ---- -----
3 Backup_Created Running
4 Backup_Changed Running

已清理全部订阅和作业

在调试阶段,善用 Get-EventSubscriberGet-Job 可以快速定位问题。如果发现某个订阅不再触发事件,首先检查对应的 Job 是否还在运行状态。

注意事项

  1. 缓冲区溢出导致事件丢失:当短时间内发生大量文件变更时(如批量编译、解压缩),FileSystemWatcher 的内部缓冲区可能溢出。解决方法是增大 InternalBufferSize(最大 64KB),并在 Action 中做轻量处理以尽快释放缓冲区。

  2. Changed 事件重复触发:许多编辑器保存文件时会同时修改文件内容和属性(大小、写入时间等),导致一次保存触发多次 Changed 事件。建议在 Action 中加入时间窗口去重逻辑(如上面备份脚本中的 5 秒内去重)。

  3. 网络路径的可靠性FileSystemWatcher 监控网络共享路径时,网络中断会导致监控失效且不会自动恢复。生产环境中应在循环中检测连接状态并重建 Watcher。

  4. 文件锁定问题:文件刚创建或正在写入时,立即读取可能遇到文件锁定。在 Action 中加入短暂延迟(如 Start-Sleep -Milliseconds 500)可以缓解这个问题。

  5. 资源释放FileSystemWatcher 实现了 IDisposable 接口,使用完毕后必须调用 Dispose() 释放资源,同时用 Unregister-Event 注销事件订阅、用 Remove-Job 清理后台作业,否则会造成内存泄漏。

  6. 跨平台限制FileSystemWatcher 依赖 Windows 的文件系统通知机制,在 Linux/macOS 上的 .NET 虽然也有实现,但行为和可靠性可能有差异。如果需要跨平台方案,可以考虑轮询结合哈希校验的方式。