PowerShellでFileSystemWatcherを使ってファイル変更を検知する方法

当ページのリンクには広告が含まれています。
PowerShellでFileSystemWatcherを使ってファイルの作成・変更・削除を自動検知する方法を解説するアイキャッチ画像

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を使う基本手順

Step

監視対象と通知条件を設定する

まずウォッチャー本体を作成して、監視対象のフォルダや条件を設定します。

PowerShell – watcherの基本設定
$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で組み合わせられます。
Step

Created・Changed・Deleted・Renamedを登録する

ウォッチャーを作ったら、各イベントに処理(アクション)を登録します。
Register-ObjectEventを使うと、イベントが発生したときにスクリプトブロックが非同期で実行されます。

PowerShell – イベント登録とアクション定義
# 変更検知時の処理(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で各イベントに処理を登録します。
  • RenamedOldFullPathFullPathを使って変更前後のパスを取得できます。
  • EnableRaisingEvents = $true にするとウォッチャーがイベント発火を開始します。
  • while ($true) { Start-Sleep -Seconds 1 }の箇所は次のステップで少し改良します。
Step

監視を開始し、終了時に解除する

スクリプトを終了する時はUnregister-Eventでイベント購読を解除したうえで監視を停止してFileSystemWatcherを破棄します。

PowerShell
# 監視を開始
$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 "モニタリング終了"
}
サンプル(分割なしフルコード)
PowerShell
$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を削除。
PowerShell – 実行結果
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:変更ログをファイルに記録する

コンソールに出力するだけでなく、ログファイルに追記する例です。

PowerShell – ファイル変更ログ記録スクリプト
$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:作成されたファイルを自動コピーする

特定フォルダに新規ファイルが置かれたら別フォルダへ自動コピーする例です。
取り込みフォルダの監視などで使えます。

PowerShell – ファイル自動コピースクリプト
$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イベントが発火します。ハンドリングの例です。

PowerShell
Register-ObjectEvent `
    -InputObject $watcher `
    -EventName "Error" `
    -SourceIdentifier "Watcher_Error" `
    -Action {
        $exception = $Event.SourceEventArgs.GetException()
        Write-Warning "監視エラーが発生しました: $($exception.Message)"
    }

ファイルの書き込み完了を待ってから処理する

Createdイベントは監視フォルダー内にファイルやフォルダーが作成されたタイミングで発生します。ただし、イベントが発生した時点で、ファイルへの書き込みが完了しているとは限りません。

特に、次のようなケースでは作成直後のファイルがまだ別のプロセスによって使用されていることがあります。

  • サイズの大きいファイルをコピーしている
  • ネットワーク経由でファイルを保存している
  • アプリケーションがファイルを少しずつ書き込んでいる
  • 一時ファイルを作成してから名前を変更する方式で保存している

FileSystemWatcherのCreatedイベントはファイルが作成されたことを通知するものであり、後続の書き込み処理の完了までは保証しません

固定時間の待機だけでは不十分な場合がある

ローカルの次のように一定時間待ってからファイルを処理できます。

PowerShell
Start-Sleep -Milliseconds 500

ファイルサイズ、保存先の速度、ネットワークの状態によって書き込み時間は変わりますので、「500ミリ秒待てば必ず完了する」とは限りません。

実運用では決められた時間だけ待つのではなく、ファイルを開けるか定期的に確認する方法が扱いやすいです。

ファイルを開けるまでリトライする

次のWait-FileReady関数では対象ファイルを排他的に開けるか確認します。

さらに、ファイルサイズと最終更新日時が2回続けて変化しなかった場合に、処理可能と判断します。

PowerShell
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とイベント登録部分を差し替えてください。

PowerShell
$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-Null

RetryCountIntervalMillisecondsは扱うファイルのサイズや保存環境に応じて調整してください。

たとえば、数GBのファイルやネットワーク経由のコピーを扱う場合は、リトライ回数を増やす必要があります。

この方法でも、別のアプリケーションが確認後にファイルを再び更新する可能性までは完全に排除できません。重要な処理では、ファイルサイズ、チェックサム、完了ファイルの有無など、送信側と受信側で完了条件を決める方法も検討してください。


バッファあふれ・権限・常駐時の注意点

FileSystemWatcherを短時間の確認用として使う場合は、基本的なサンプルでも十分です。しかし長時間のフォルダー監視や業務処理へ使用する場合は、次の3点に注意が必要です。

  • 短時間に大量の変更が発生した場合のバッファあふれ
  • 監視フォルダーや出力先に対する実行アカウントの権限
  • PowerShellプロセスを終了させないための常駐方法

短時間に変更が集中するとイベントを取りこぼすことがある

FileSystemWatcherが受け取った変更通知は内部バッファへ一時的に保存されます。

短時間に大量のファイルが作成・変更されると、処理が追いつかず内部バッファがあふれる場合があります。バッファがあふれた場合は、一部の変更通知が失われる可能性があります。

バッファあふれを防ぐには、最初に監視範囲と通知条件を絞ります。

PowerShell
$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でも不要な通知をNotifyFilterIncludeSubdirectoriesで減らしてから、必要に応じて内部バッファを調整する方法が案内されています。内部バッファを大きくするとメモリ消費が増えるため、必要最小限にすることが推奨されています。

それでもバッファが不足する場合は少しずつ増やして検証します。

PowerShell
# 例:内部バッファを16KBに設定
$watcher.InternalBufferSize = 16384

最初から大きな値へ変更するのではなく16KB、32KBのように段階的に増やし、実際のイベント発生量で確認します。

また、イベントアクション内で時間のかかる処理を大量に実行すると、後続処理が滞る原因になります。画像変換、圧縮、外部APIへの送信などの重い処理を行う場合は、イベント内では対象パスをキューやログへ登録するだけにして、実際の処理を別のスクリプトやジョブへ分ける方法もあります。

Errorイベントで監視エラーを記録する

FileSystemWatcherが監視を継続できなくなった場合や、内部バッファがあふれた場合はErrorイベントが発生します。

次のように登録しておくと、原因となった例外メッセージを確認できます。

PowerShell
Register-ObjectEvent `
    -InputObject $watcher `
    -EventName "Error" `
    -SourceIdentifier "Watcher_Error" `
    -Action {
        $exception = $Event.SourceEventArgs.GetException()

        Write-Warning "監視エラーが発生しました: $($exception.Message)"
    } |
    Out-Null

ErrorEventArgsに含まれる例外はGetException()で取得できます。

スクリプト終了時には、ほかのイベントと同様に登録を解除します。

PowerShell
Unregister-Event `
    -SourceIdentifier "Watcher_Error" `
    -ErrorAction SilentlyContinue

バッファあふれが発生するとどのイベントを取りこぼしたかを通知だけから完全に特定することは困難です。

ファイルの取り込み漏れが許されない処理ではErrorイベントの発生時に監視フォルダーを再走査し、処理済み一覧と照合する仕組みも必要になります。

監視元と出力先のアクセス権限を確認する

FileSystemWatcherを実行するアカウントには監視対象フォルダーを参照する権限が必要です。さらに、ファイルのコピーやログ出力を行う場合は、コピー先やログ出力先への書き込み権限も必要になります。

最低限、次の権限を確認してください。

対象必要になる主な権限
監視フォルダーフォルダーの参照、一覧表示
処理対象ファイル読み取り
コピー先フォルダー作成、書き込み、上書き
ログ出力先ファイル作成、追記
削除・移動処理変更、削除

スクリプトの開始時にフォルダーが存在するか確認しておくと、設定ミスを早めに発見できます。

PowerShell
$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
}

コピー先への書き込み権限も事前に確認したい場合は、一時ファイルを作成して削除します。

PowerShell
$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プロセスを終了させないために次の待機処理を使用しています。

PowerShell
while ($true) {
    Start-Sleep -Seconds 1
}

この方法は動作確認には便利ですがPowerShellの画面を閉じたり、PCを再起動したりすると監視も終了します。

長時間運用する場合は、次のような実行方法を検討してください。

  • タスクスケジューラーでPC起動時にスクリプトを開始する
  • 実行に失敗した場合の再起動設定を行う
  • Windowsサービスとして実行する
  • 起動、終了、監視エラーをログへ記録する
  • 定期的にプロセスやログの更新状態を確認する

また、通常終了時やエラー終了時に後片付けが行われるように待機処理はtryfinallyで囲みます。

PowerShell
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 を閉じても監視を続けたい場合は?

タスクスケジューラ等でスクリプトをバックグラウンド実行するか、Windowsサービス化するのが現実的です。手軽に試すならStart-Process powershell -WindowStyle Hidden -ArgumentList "-File C:\scripts\watcher.ps1"で非表示ウィンドウで起動する方法もあります。

常駐・定期実行の方法を選びたい場合は、タスクスケジューラー・PowerShell・WSL cronの使い分けも確認しておくと安心です。

特定の拡張子だけ監視したい。

$watcher.Filter = "*.csv"のようにフィルターで絞れます。
複数の拡張子を対象にしたい場合はFilter = "*.*"にしてアクション内でif ($info.Name -match "\.csv$|\.txt$")のように絞り込むとよいです。

サブフォルダも含めて監視したい。

$watcher.IncludeSubdirectories = $trueにするだけです。ただし監視範囲が広がるのでイベント数が増え、バッファあふれのリスクも上がります。

管理者権限は必要?

監視するフォルダへの読み取り権限があれば基本的には動きます。
C:\Windows など保護されたフォルダを対象にする場合は管理者として実行してください

まとめ

  • PowerShellは.NETのFileSystemWatcherクラスをそのまま使えるよ。
  • Register-ObjectEventでイベント駆動の非同期処理が書けるよ。
  • EnableRaisingEvents = $trueを忘れると何も起きないよ。
  • イベントアクションへ外部の値を渡すときは-MessageData$Event.MessageDataを使えるよ。
  • 終了時はUnregister-EventDispose()で後片付けしておくのが安全だよ。

ポーリングより負荷が低く、リアルタイム性もそれなりに高いのがFileSystemWatcherの便利なところですね!
業務の自動化スクリプトにぜひ組み込んでみてください。

ログファイルの追記を確認したいだけなら、Get-Content -Waitを使う方法もあります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次