PowerShellでフォルダー内の変更を監視するにはSystem.IO.FileSystemWatcherで監視対象を設定し、Register-ObjectEventでCreated・Changed・Deleted・Renamedイベントに処理を登録します。一定間隔で確認するポーリングとは異なり、ファイルシステムの変更通知を受けて処理を実行できます。
この記事ではその基本的な使い方から、実運用の参考になるスクリプト例まで解説します。
PowerShellの基本操作や実践コマンドをまとめて確認したい場合はこちらも参考にしてください。
FileSystemWatcherとは
System.IO.FileSystemWatcherは.NET Framework / .NETのクラスで、OSレベルのファイルシステム変更通知を受け取る仕組みです。
PowerShellは.NETクラスをそのまま利用できるので、追加インストール不要でスクリプト内から使えますよ。
FileSystemWatcher Class (System.IO) | Microsoft Learndocs
監視できるイベントは次の4種類です。
| イベント名 | 内容 |
|---|---|
| Created | ファイル・フォルダが作成された |
| Changed | ファイルの中身、属性、サイズ、最終書き込み時刻などが変更された ※Changedは環境や保存方法によって複数回発火しやすいようです。 |
| Deleted | ファイル・フォルダが削除された |
| Renamed | ファイル・フォルダ名が変更された |
PowerShellでFileSystemWatcherを使う基本手順
監視対象と通知条件を設定する
まずウォッチャー本体を作成して、監視対象のフォルダや条件を設定します。
$watcher = New-Object System.IO.FileSystemWatcher
# 監視するフォルダのパス
$watcher.Path = "C:\Logs"
# 監視対象のファイル名フィルター(例: *.log で log ファイルに限定)
$watcher.Filter = "*.*"
# サブフォルダも監視するか
$watcher.IncludeSubdirectories = $false
# 何の変化を検知するか(NotifyFilters)
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite `
-bor [System.IO.NotifyFilters]::FileName設定項目の解説です。
- Path
監視したいフォルダのパス。ファイル単体の指定はできないのでフォルダを指定します。 - Filter
監視対象のファイル名フィルター。*.logのように拡張子を絞り込むことも可能です。 - IncludeSubdirectories
$trueにするとサブフォルダ内の変更も検知します。デフォルトは$falseでサブフォルダは検知されません。 - NotifyFilter
LastWrite(最終書き込み時刻)、FileName(ファイル名)、DirectoryName(フォルダ名)、Sizeなど複数を-borで組み合わせられます。
Created・Changed・Deleted・Renamedを登録する
ウォッチャーを作ったら、各イベントに処理(アクション)を登録します。Register-ObjectEventを使うと、イベントが発生したときにスクリプトブロックが非同期で実行されます。
# 変更検知時の処理(Created / Changed / Deleted 共通)
$action = {
$info = $Event.SourceEventArgs
$path = $info.FullPath
$type = $info.ChangeType
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] $type : $path"
}
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Created" `
-Action $action `
-SourceIdentifier "Watcher_Created"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Changed" `
-Action $action `
-SourceIdentifier "Watcher_Changed"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Deleted" `
-Action $action `
-SourceIdentifier "Watcher_Deleted"
# リネームは専用のイベント引数を使う
$renameAction = {
$info = $Event.SourceEventArgs
$oldPath = $info.OldFullPath
$newPath = $info.FullPath
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] Renamed : $oldPath -> $newPath"
}
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Renamed" `
-Action $renameAction `
-SourceIdentifier "Watcher_Renamed"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Error" `
-SourceIdentifier "Watcher_Error" `
-Action {
$exception = $Event.SourceEventArgs.GetException()
Write-Warning "監視エラーが発生しました: $($exception.Message)"
}Register-ObjectEventで各イベントに処理を登録します。RenamedはOldFullPathとFullPathを使って変更前後のパスを取得できます。EnableRaisingEvents = $trueにするとウォッチャーがイベント発火を開始します。while ($true) { Start-Sleep -Seconds 1 }の箇所は次のステップで少し改良します。
監視を開始し、終了時に解除する
スクリプトを終了する時はUnregister-Eventでイベント購読を解除したうえで監視を停止してFileSystemWatcherを破棄します。
# 監視を開始
$watcher.EnableRaisingEvents = $true
Write-Host "モニタリング開始:$($watcher.Path)"
Write-Host "Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。"
try {
# スクリプトを終了させないように待機
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
# イベントの登録を解除
Unregister-Event -SourceIdentifier "Watcher_Created" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Changed" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Deleted" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Renamed" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Error" -ErrorAction SilentlyContinue
# 監視を停止してからオブジェクトを破棄
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "モニタリング終了"
}サンプル(分割なしフルコード)
$watcher = New-Object System.IO.FileSystemWatcher
# 監視するフォルダのパス
$watcher.Path = "C:\Logs"
# 監視対象のファイル名フィルター(例: *.log で log ファイルに限定)
$watcher.Filter = "*.*"
# サブフォルダも監視するか
$watcher.IncludeSubdirectories = $false
# 何の変化を検知するか(NotifyFilters)
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite `
-bor [System.IO.NotifyFilters]::FileName
# 変更検知時の処理(Created / Changed / Deleted 共通)
$action = {
$info = $Event.SourceEventArgs
$path = $info.FullPath
$type = $info.ChangeType
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] $type : $path"
}
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Created" `
-Action $action `
-SourceIdentifier "Watcher_Created"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Changed" `
-Action $action `
-SourceIdentifier "Watcher_Changed"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Deleted" `
-Action $action `
-SourceIdentifier "Watcher_Deleted"
# リネームは専用のイベント引数を使う
$renameAction = {
$info = $Event.SourceEventArgs
$oldPath = $info.OldFullPath
$newPath = $info.FullPath
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Write-Host "[$time] Renamed : $oldPath -> $newPath"
}
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Renamed" `
-Action $renameAction `
-SourceIdentifier "Watcher_Renamed"
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Error" `
-SourceIdentifier "Watcher_Error" `
-Action {
$exception = $Event.SourceEventArgs.GetException()
Write-Warning "監視エラーが発生しました: $($exception.Message)"
}
# 監視を開始
$watcher.EnableRaisingEvents = $true
Write-Host "モニタリング開始:$($watcher.Path)"
Write-Host "Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。"
try {
# スクリプトを終了させないように待機
while ($true) { Start-Sleep -Seconds 1 }
}
finally {
# イベントの登録を解除
Unregister-Event -SourceIdentifier "Watcher_Created" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Changed" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Deleted" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Renamed" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "Watcher_Error" -ErrorAction SilentlyContinue
# 監視を停止してからオブジェクトを破棄
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "モニタリング終了"
}FileSystemWatcherの実行結果
上記のサンプルのPowerShellを実行してからC:\Logsディレクトリで下記を行った時の出力です。
- エクスプローラー上で「右クリック > 新規作成 > テキストドキュメント」を選択。
→新規 テキスト ドキュメント.txtが出来上がってリネームで新規 テキスト ドキュメントが選択されている状態。 - そのままtestと入力して
test.txtでファイル名を確定。 test.txtを開いて「Test Write」と入力して保存。- 保存後
test.txtを削除。
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
1 Watcher_Created NotStarted False …
2 Watcher_Changed NotStarted False …
3 Watcher_Deleted NotStarted False …
4 Watcher_Renamed NotStarted False …
5 Watcher_Error NotStarted False …
モニタリング開始:C:\Logs
Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。
[実際の日時] Created : C:\Logs\新規 テキスト ドキュメント.txt
[実際の日時] Renamed : C:\Logs\新規 テキスト ドキュメント.txt -> C:\Logs\test.txt
[実際の日時] Changed : C:\Logs\test.txt
[実際の日時] Deleted : C:\Logs\test.txt
モニタリング終了ps1ファイルとして保存して実行できない場合は、PowerShellの実行ポリシーも確認してみてください。

実用例1:変更ログをファイルに記録する
コンソールに出力するだけでなく、ログファイルに追記する例です。
$watchFolder = "C:\Logs"
$logFile = "C:\Temp\watch_log.txt"
# 監視対象フォルダーが存在するか確認
if (-not (Test-Path -LiteralPath $watchFolder -PathType Container)) {
throw "監視対象フォルダーが存在しません: $watchFolder"
}
# ログの出力先フォルダーがなければ作成
$logDirectory = Split-Path -Path $logFile -Parent
if (-not (Test-Path -LiteralPath $logDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $logDirectory -Force -ErrorAction Stop | Out-Null
}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watchFolder
$watcher.Filter = "*.log"
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::FileName
$action = {
$info = $Event.SourceEventArgs
$targetLogFile = [string]$Event.MessageData
$path = $info.FullPath
$type = $info.ChangeType
$time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$entry = "[$time] $type : $path"
try {
Add-Content -LiteralPath $targetLogFile -Value $entry -ErrorAction Stop
Write-Host $entry
}
catch {
Write-Warning "ログの書き込みに失敗しました: $($_.Exception.Message)"
}
}
Register-ObjectEvent -InputObject $watcher -EventName "Created" -SourceIdentifier "WL_Created" -MessageData $logFile -Action $action | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Changed" -SourceIdentifier "WL_Changed" -MessageData $logFile -Action $action | Out-Null
Register-ObjectEvent -InputObject $watcher -EventName "Deleted" -SourceIdentifier "WL_Deleted" -MessageData $logFile -Action $action | Out-Null
$watcher.EnableRaisingEvents = $true
Write-Host "監視中。ログ出力先: $logFile"
Write-Host "Ctrl+Cで停止します。"
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
Unregister-Event -SourceIdentifier "WL_Created" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "WL_Changed" -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier "WL_Deleted" -ErrorAction SilentlyContinue
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "監視を終了しました。"
}イベントアクションへ外部の値を渡す場合はRegister-ObjectEventの-MessageDataで値を渡し、アクション内の$Event.MessageDataから参照できます。この例ではログファイルのパスを各イベント登録へ渡しています。
実用例2:作成されたファイルを自動コピーする
特定フォルダに新規ファイルが置かれたら別フォルダへ自動コピーする例です。
取り込みフォルダの監視などで使えます。
$srcFolder = "C:\Drop"
$dstFolder = "C:\Processed"
# サンプルをそのまま実行できるようにフォルダーを作成
foreach ($folder in @($srcFolder, $dstFolder)) {
if (-not (Test-Path -LiteralPath $folder -PathType Container)) {
New-Item -ItemType Directory -Path $folder -Force -ErrorAction Stop | Out-Null
}
}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $srcFolder
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName
$action = {
$info = $Event.SourceEventArgs
$srcPath = $info.FullPath
$name = $info.Name
$destination = [string]$Event.MessageData
$dstPath = Join-Path -Path $destination -ChildPath $name
# Created発生直後は書き込み中の場合があるため少し待つ
Start-Sleep -Milliseconds 500
try {
Copy-Item -LiteralPath $srcPath -Destination $dstPath -Force -ErrorAction Stop
Write-Host "コピー完了: $name"
}
catch {
Write-Warning "コピー失敗: $name - $($_.Exception.Message)"
}
}
Register-ObjectEvent -InputObject $watcher -EventName "Created" -SourceIdentifier "AC_Created" -MessageData $dstFolder -Action $action | Out-Null
$watcher.EnableRaisingEvents = $true
Write-Host "自動コピー監視中: $srcFolder -> $dstFolder"
Write-Host "Ctrl+Cで停止します。"
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
Unregister-Event -SourceIdentifier "AC_Created" -ErrorAction SilentlyContinue
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "自動コピー監視を終了しました。"
}ファイル作成直後はまだ書き込み中のことがあります。500ミリ秒など短時間待ってから処理すると失敗を減らせますが、ファイルサイズによっては待ち時間の調整が必要です。
Changedイベントが複数回発火する場合の対策
Changedイベントはアプリケーションの保存方法によって1回の操作でも複数回発生することがあります。処理を1回にまとめたい場合は、同じファイルパスに対するイベントが短時間に連続したとき、一定時間内の重複を無視するデバウンス処理を追加します。
⚠️ 多数の変更が短時間に発生すると、内部バッファがあふれてイベントを取りこぼすことがあります。
FileSystemWatcherには内部バッファがあり、イベントが大量発生するとあふれてErrorイベントが発火します。ハンドリングの例です。PowerShellRegister-ObjectEvent ` -InputObject $watcher ` -EventName "Error" ` -SourceIdentifier "Watcher_Error" ` -Action { $exception = $Event.SourceEventArgs.GetException() Write-Warning "監視エラーが発生しました: $($exception.Message)" }
ファイルの書き込み完了を待ってから処理する
Createdイベントは監視フォルダー内にファイルやフォルダーが作成されたタイミングで発生します。ただし、イベントが発生した時点で、ファイルへの書き込みが完了しているとは限りません。
特に、次のようなケースでは作成直後のファイルがまだ別のプロセスによって使用されていることがあります。
- サイズの大きいファイルをコピーしている
- ネットワーク経由でファイルを保存している
- アプリケーションがファイルを少しずつ書き込んでいる
- 一時ファイルを作成してから名前を変更する方式で保存している
FileSystemWatcherのCreatedイベントはファイルが作成されたことを通知するものであり、後続の書き込み処理の完了までは保証しません。
固定時間の待機だけでは不十分な場合がある
ローカルの次のように一定時間待ってからファイルを処理できます。
Start-Sleep -Milliseconds 500ファイルサイズ、保存先の速度、ネットワークの状態によって書き込み時間は変わりますので、「500ミリ秒待てば必ず完了する」とは限りません。
実運用では決められた時間だけ待つのではなく、ファイルを開けるか定期的に確認する方法が扱いやすいです。
ファイルを開けるまでリトライする
次のWait-FileReady関数では対象ファイルを排他的に開けるか確認します。
さらに、ファイルサイズと最終更新日時が2回続けて変化しなかった場合に、処理可能と判断します。
function Wait-FileReady {
param(
[Parameter(Mandatory)]
[string]$LiteralPath,
[int]$RetryCount = 20,
[int]$IntervalMilliseconds = 500
)
$lastSignature = $null
for ($attempt = 1; $attempt -le $RetryCount; $attempt++) {
# フォルダーの場合は処理しない
if ([System.IO.Directory]::Exists($LiteralPath)) {
return $false
}
# ファイルがまだ存在しない場合は次の確認まで待つ
if (-not [System.IO.File]::Exists($LiteralPath)) {
if ($attempt -lt $RetryCount) {
Start-Sleep -Milliseconds $IntervalMilliseconds
}
continue
}
$stream = $null
$signature = $null
try {
# 他のプロセスが使用していないか確認
$stream = [System.IO.File]::Open(
$LiteralPath,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::None
)
$lastWriteTime = [System.IO.File]::GetLastWriteTimeUtc(
$LiteralPath
).Ticks
# ファイルサイズと最終更新日時を組み合わせて比較
$signature = "{0}:{1}" -f $stream.Length, $lastWriteTime
}
catch [System.UnauthorizedAccessException] {
throw "ファイルを読み取る権限がありません: $LiteralPath"
}
catch [System.IO.IOException] {
# 書き込み中や他プロセスによる使用中の場合
$lastSignature = $null
}
finally {
if ($null -ne $stream) {
$stream.Dispose()
}
}
if ($null -ne $signature) {
# 2回続けて状態が変わっていなければ処理可能と判断
if ($signature -eq $lastSignature) {
return $true
}
$lastSignature = $signature
}
if ($attempt -lt $RetryCount) {
Start-Sleep -Milliseconds $IntervalMilliseconds
}
}
return $false
}この例では500ミリ秒間隔で最大20回確認します。
1回目にファイルを開けた場合でもすぐには処理可能と判断しません。次の確認でもファイルサイズと最終更新日時が変わっていなければ、書き込みが落ち着いたと判断します。
自動コピー処理へ組み込む
先ほどの自動コピー例では、固定のStart-Sleepを削除し、次のように変更できます。
次のコードは、先ほどの自動コピー例にある$dstFolderと$watcherをそのまま使用します。コード全体を置き換えるのではなく、$actionとイベント登録部分を差し替えてください。
$settings = @{
Destination = $dstFolder
RetryCount = 20
IntervalMilliseconds = 500
}
$action = {
function Wait-FileReady {
param(
[Parameter(Mandatory)]
[string]$LiteralPath,
[int]$RetryCount = 20,
[int]$IntervalMilliseconds = 500
)
$lastSignature = $null
for ($attempt = 1; $attempt -le $RetryCount; $attempt++) {
if ([System.IO.Directory]::Exists($LiteralPath)) {
return $false
}
if (-not [System.IO.File]::Exists($LiteralPath)) {
if ($attempt -lt $RetryCount) {
Start-Sleep -Milliseconds $IntervalMilliseconds
}
continue
}
$stream = $null
$signature = $null
try {
$stream = [System.IO.File]::Open(
$LiteralPath,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::None
)
$lastWriteTime = [System.IO.File]::GetLastWriteTimeUtc(
$LiteralPath
).Ticks
$signature = "{0}:{1}" -f $stream.Length, $lastWriteTime
}
catch [System.UnauthorizedAccessException] {
throw "ファイルを読み取る権限がありません: $LiteralPath"
}
catch [System.IO.IOException] {
$lastSignature = $null
}
finally {
if ($null -ne $stream) {
$stream.Dispose()
}
}
if ($null -ne $signature) {
if ($signature -eq $lastSignature) {
return $true
}
$lastSignature = $signature
}
if ($attempt -lt $RetryCount) {
Start-Sleep -Milliseconds $IntervalMilliseconds
}
}
return $false
}
$info = $Event.SourceEventArgs
$settings = $Event.MessageData
$srcPath = $info.FullPath
$name = $info.Name
# フォルダー作成イベントは対象外
if ([System.IO.Directory]::Exists($srcPath)) {
return
}
try {
$ready = Wait-FileReady `
-LiteralPath $srcPath `
-RetryCount $settings.RetryCount `
-IntervalMilliseconds $settings.IntervalMilliseconds
if (-not $ready) {
Write-Warning "ファイルの書き込み完了を確認できませんでした: $srcPath"
return
}
$dstPath = Join-Path `
-Path $settings.Destination `
-ChildPath $name
Copy-Item `
-LiteralPath $srcPath `
-Destination $dstPath `
-Force `
-ErrorAction Stop
Write-Host "コピー完了: $name"
}
catch {
Write-Warning "コピー失敗: $name - $($_.Exception.Message)"
}
}
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Created" `
-SourceIdentifier "AC_Created" `
-MessageData $settings `
-Action $action |
Out-NullRetryCountとIntervalMillisecondsは扱うファイルのサイズや保存環境に応じて調整してください。
たとえば、数GBのファイルやネットワーク経由のコピーを扱う場合は、リトライ回数を増やす必要があります。
この方法でも、別のアプリケーションが確認後にファイルを再び更新する可能性までは完全に排除できません。重要な処理では、ファイルサイズ、チェックサム、完了ファイルの有無など、送信側と受信側で完了条件を決める方法も検討してください。
バッファあふれ・権限・常駐時の注意点
FileSystemWatcherを短時間の確認用として使う場合は、基本的なサンプルでも十分です。しかし長時間のフォルダー監視や業務処理へ使用する場合は、次の3点に注意が必要です。
- 短時間に大量の変更が発生した場合のバッファあふれ
- 監視フォルダーや出力先に対する実行アカウントの権限
- PowerShellプロセスを終了させないための常駐方法
短時間に変更が集中するとイベントを取りこぼすことがある
FileSystemWatcherが受け取った変更通知は内部バッファへ一時的に保存されます。
短時間に大量のファイルが作成・変更されると、処理が追いつかず内部バッファがあふれる場合があります。バッファがあふれた場合は、一部の変更通知が失われる可能性があります。
バッファあふれを防ぐには、最初に監視範囲と通知条件を絞ります。
$watcher.Filter = "*.log"
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName `
-bor [System.IO.NotifyFilters]::LastWriteたとえば、.logファイルだけを対象にする場合は、Filterを*.logにします。
サブフォルダーを監視する必要がなければIncludeSubdirectoriesは$falseにします。NotifyFilterにも実際に必要な変更だけを指定してください。
Microsoft Learnでも不要な通知をNotifyFilterやIncludeSubdirectoriesで減らしてから、必要に応じて内部バッファを調整する方法が案内されています。内部バッファを大きくするとメモリ消費が増えるため、必要最小限にすることが推奨されています。
それでもバッファが不足する場合は少しずつ増やして検証します。
# 例:内部バッファを16KBに設定
$watcher.InternalBufferSize = 16384最初から大きな値へ変更するのではなく16KB、32KBのように段階的に増やし、実際のイベント発生量で確認します。
また、イベントアクション内で時間のかかる処理を大量に実行すると、後続処理が滞る原因になります。画像変換、圧縮、外部APIへの送信などの重い処理を行う場合は、イベント内では対象パスをキューやログへ登録するだけにして、実際の処理を別のスクリプトやジョブへ分ける方法もあります。
Errorイベントで監視エラーを記録する
FileSystemWatcherが監視を継続できなくなった場合や、内部バッファがあふれた場合はErrorイベントが発生します。
次のように登録しておくと、原因となった例外メッセージを確認できます。
Register-ObjectEvent `
-InputObject $watcher `
-EventName "Error" `
-SourceIdentifier "Watcher_Error" `
-Action {
$exception = $Event.SourceEventArgs.GetException()
Write-Warning "監視エラーが発生しました: $($exception.Message)"
} |
Out-NullErrorEventArgsに含まれる例外はGetException()で取得できます。
スクリプト終了時には、ほかのイベントと同様に登録を解除します。
Unregister-Event `
-SourceIdentifier "Watcher_Error" `
-ErrorAction SilentlyContinueバッファあふれが発生するとどのイベントを取りこぼしたかを通知だけから完全に特定することは困難です。
ファイルの取り込み漏れが許されない処理ではErrorイベントの発生時に監視フォルダーを再走査し、処理済み一覧と照合する仕組みも必要になります。
監視元と出力先のアクセス権限を確認する
FileSystemWatcherを実行するアカウントには監視対象フォルダーを参照する権限が必要です。さらに、ファイルのコピーやログ出力を行う場合は、コピー先やログ出力先への書き込み権限も必要になります。
最低限、次の権限を確認してください。
| 対象 | 必要になる主な権限 |
|---|---|
| 監視フォルダー | フォルダーの参照、一覧表示 |
| 処理対象ファイル | 読み取り |
| コピー先フォルダー | 作成、書き込み、上書き |
| ログ出力先 | ファイル作成、追記 |
| 削除・移動処理 | 変更、削除 |
スクリプトの開始時にフォルダーが存在するか確認しておくと、設定ミスを早めに発見できます。
$watchFolder = "C:\Drop"
$dstFolder = "C:\Processed"
if (-not (Test-Path -LiteralPath $watchFolder -PathType Container)) {
throw "監視対象フォルダーが存在しません: $watchFolder"
}
if (-not (Test-Path -LiteralPath $dstFolder -PathType Container)) {
New-Item `
-ItemType Directory `
-Path $dstFolder `
-Force `
-ErrorAction Stop |
Out-Null
}コピー先への書き込み権限も事前に確認したい場合は、一時ファイルを作成して削除します。
$testFile = Join-Path `
-Path $dstFolder `
-ChildPath ".filesystemwatcher_write_test_$PID.tmp"
try {
Set-Content `
-LiteralPath $testFile `
-Value "write test" `
-ErrorAction Stop
Remove-Item `
-LiteralPath $testFile `
-Force `
-ErrorAction Stop
}
catch {
throw "コピー先へ書き込めません: $($_.Exception.Message)"
}タスクスケジューラーやWindowsサービスから実行する場合は、普段デスクトップへログインしているユーザーではなく、タスクやサービスに設定したアカウントの権限が使用されます。Windowsサービスの実行アカウントは、ローカルおよびネットワークリソースへアクセスする際のセキュリティコンテキストになります。
ネットワーク共有を監視する場合は次のようなUNCパスを使用します。
$watchFolder = "\\server01\share\Logs"
FileSystemWatcherはローカルフォルダーだけでなくネットワークドライブやリモートコンピューター上のフォルダーも監視できます。
ただし、タスクスケジューラーや管理者権限で実行したプロセスでは、普段使用しているドライブ文字が表示されない場合があります。そのためZ:\Logsのような割り当て済みドライブより\\server\share\LogsのようなUNCパスを使用する方が環境差を減らせます。
ネットワーク共有では共有アクセス許可とNTFSアクセス許可の両方を確認してください。
while ($true)だけではPC再起動後に復旧しない
記事内のサンプルでは、PowerShellプロセスを終了させないために次の待機処理を使用しています。
while ($true) {
Start-Sleep -Seconds 1
}この方法は動作確認には便利ですがPowerShellの画面を閉じたり、PCを再起動したりすると監視も終了します。
長時間運用する場合は、次のような実行方法を検討してください。
- タスクスケジューラーでPC起動時にスクリプトを開始する
- 実行に失敗した場合の再起動設定を行う
- Windowsサービスとして実行する
- 起動、終了、監視エラーをログへ記録する
- 定期的にプロセスやログの更新状態を確認する
また、通常終了時やエラー終了時に後片付けが行われるように待機処理はtryとfinallyで囲みます。
try {
while ($true) {
Start-Sleep -Seconds 1
}
}
finally {
Unregister-Event `
-SourceIdentifier "Watcher_Created" `
-ErrorAction SilentlyContinue
Unregister-Event `
-SourceIdentifier "Watcher_Changed" `
-ErrorAction SilentlyContinue
Unregister-Event `
-SourceIdentifier "Watcher_Deleted" `
-ErrorAction SilentlyContinue
Unregister-Event `
-SourceIdentifier "Watcher_Renamed" `
-ErrorAction SilentlyContinue
Unregister-Event `
-SourceIdentifier "Watcher_Error" `
-ErrorAction SilentlyContinue
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
Write-Host "モニタリング終了"
}常駐処理では正常に動いているときのログだけでなく、開始時刻、終了時刻、例外内容も記録しておくと、監視が停止した原因を調査しやすくなります。
FileSystemWatcherは変更を通知する仕組みであり、後続処理の成功や取り込み漏れがないことまで保証する仕組みではありません。重要な処理では、エラー記録、再試行、処理済み管理、定期的なフォルダー再走査を組み合わせてください。
よくある質問
まとめ
- PowerShellは.NETの
FileSystemWatcherクラスをそのまま使えるよ。 Register-ObjectEventでイベント駆動の非同期処理が書けるよ。EnableRaisingEvents = $trueを忘れると何も起きないよ。- イベントアクションへ外部の値を渡すときは
-MessageDataと$Event.MessageDataを使えるよ。 - 終了時は
Unregister-EventとDispose()で後片付けしておくのが安全だよ。
ポーリングより負荷が低く、リアルタイム性もそれなりに高いのがFileSystemWatcherの便利なところですね!
業務の自動化スクリプトにぜひ組み込んでみてください。
ログファイルの追記を確認したいだけなら、Get-Content -Waitを使う方法もあります。









