适用于 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
|
执行结果示例:
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 }
|
执行结果示例:
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])" } }
|
执行结果示例:
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
Created : 5 Changed : 12 Deleted : 2 Renamed : 1
监控已停止。最终统计: Created : 23 Changed : 67 Deleted : 8 Renamed : 3
|
注意事项
- 缓冲区溢出:高频文件变更可能超出 FileSystemWatcher 内部缓冲区(默认 8KB),增大
InternalBufferSize 属性可缓解
- 重复事件:许多编辑器保存文件时会触发多个 Changed 事件,需要用去重逻辑(如时间窗口内相同路径合并)
- 网络驱动器:FileSystemWatcher 对网络路径(UNC)的监控不稳定,建议在文件所在服务器本地运行
- 资源释放:监控结束或脚本退出前务必调用
Dispose() 释放系统资源
- 权限要求:监控进程需要对目标目录有读取权限,某些系统目录可能需要管理员权限
- 事件队列:如果事件处理脚本执行过慢,事件会堆积在队列中,避免在事件处理中做耗时操作