「ファイル作成」を検知しただけで満足していないか?――書き込みロックを回避する、極限のファイル監視設計論
業務自動化の現場において、多くのエンジニアが陥る「初歩的かつ致命的な罠」がある。
それは、「ファイルが作成された瞬間に、そのファイルを操作しようとする」ことだ。
「ファイルが生成された」というイベントをトリガーに、後続の処理(ファイル移動、DBへのインポート、解析など)を開始する。一見、論理的に正しい。しかし、実務環境では、その瞬間に必ずと言っていいほど `Permission denied` や `File in use` といった実行時エラーが牙をむく。
なぜか? 答えは単純だ。「ファイルが作成されたこと」と「ファイルの書き込みが完了したこと」は、全く別次元の事象だからだ。
本稿では、WMI(Windows Management Instrumentation)によるイベント駆動型の検知と、FileSystemObjectによる状態検証を組み合わせた、プロフェッショナルが実戦で用いる「堅牢なファイル監視ロジック」を伝授する。
1. なぜ、あなたの監視スクリプトは失敗するのか
ファイルが作成されるプロセスを、OSのレイヤーで分解してみよう。
1. ファイル・ハンドル生成: OSがファイルの実体(エントリー)を作成する。
2. データ・ストリーム書き込み: アプリケーションがデータを書き込み続ける。(←ここが問題だ)
3. ファイル・クローズ: アプリケーションが書き込みを終え、ハンドルを解放する。
一般的な `FileSystemObject` のループや、単純な WMI の `__InstanceCreationEvent` だけでは、ステップ1の瞬間を検知してしまう。この時、ファイルはまだ「書き込み中」であり、他のプロセスによって排他的ロックがかかっている。
この状態でファイルを開こうとすれば、スクリプトはクラッシュするか、不完全な(中身が空、あるいは途切れた)データを処理することになる。これは「自動化」ではなく、単なる「エラーの自動生成器」だ。
2. 解決策:二段構えのアーキテクチャ
堅牢な監視を実現するためには、以下の二段構えの設計が必要不可欠である。
第1層:WMIによる「イベント駆動型」の検知
ポーリング(一定間隔でフォルダを巡回してファイルを探す手法)は、CPUリソースを無駄に消費し、検知の遅延も発生する。プロフェッショナルは、OSのイベント通知を利用する WMI (Windows Management Instrumentation) を使い、ファイルが「現れた」瞬間を低負荷でキャッチする。
第2層:FileSystemObjectによる「安定性検証」
ファイルが検知された後、即座に処理へ移るのではなく、「ファイルサイズの変化が止まったか」および「排他的に開けるか」を確認する待機ロジックを挟み込む。これが「書き込み完了」を担保する防波堤となる。
3. 実装:プロダクション・グレードの監視スクリプト
以下に、そのまま実務に投入できるレベルの VBScript コードを示す。単なるコピペで終わらせず、コード内の「設計意図」を読み解いてほしい。
' ==============================================================================
' Script Name: RobustFileWatcher.vbs
' Description: WMIとFSOを組み合わせ、ファイル書き込み完了を確実に検知する
' Author: Chief Architect
' ==============================================================================
Option Explicit
' --- 設定領域 ---
Const WATCH_DIR = "C:\\Work\\TargetFolder\\" ' 監視対象フォルダ (WMI用に\\を使用)
Const CHECK_INTERVAL = 1000 ' サイズ確認の間隔 (ms)
Const MAX_RETRIES = 30 ' 最大待機回数
Const LOG_FILE = "C:\\Work\\monitor_log.txt"
' --- オブジェクト初期化 ---
Dim objFSO, objWMI, objEventSink, objWatcher
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
' 監視対象のフルパス(FSO用)
Dim strWatchPath
strWatchPath = Replace(WATCH_DIR, "\\", "\")
WScript.Echo "監視を開始しました: " & strWatchPath
' ------------------------------------------------------------------------------
' メインプロセス:WMIイベント待機
' ------------------------------------------------------------------------------
Sub StartMonitoring()
Dim strQuery, objEvents, objFile
' WMI Query: 指定ディレクトリ内でのファイル作成イベントを監視
' 注意: WMIのパス形式は '\\' を使用する必要がある
strQuery = "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
"WHERE TargetInstance ISA 'CIM_DataFile' " & _
"AND TargetInstance.Drive = '" & Left(strWatchPath, 2) & "' " & _
"AND TargetInstance.Path = '" & Replace(strWatchPath, "\", "\\") & "' "
' イベントの取得
Set objEvents = objWMI.ExecNotificationQuery(strQuery)
Do
' イベントが発生するまでここでブロック(待機)
Set objFile = objEvents.NextEvent
Dim strFilePath
strFilePath = objFile.TargetInstance.Name ' フルパスを取得
WScript.Echo "【検知】ファイル作成を確認: " & strFilePath
' ファイルの安定性を検証
If WaitForFileReady(strFilePath) Then
WScript.Echo "【成功】書き込み完了を確認。後続処理を開始します。"
ProcessFile strFilePath
Else
WScript.Echo "【警告】ファイルがロックされたまま、またはタイムアウトしました。"
End If
Loop
End Sub
' ------------------------------------------------------------------------------
' 関数:ファイルの安定性を検証する (Size Stability Check)
' ------------------------------------------------------------------------------
Function WaitForFileReady(strFilePath)
Dim objFile, lngLastSize, lngCurrentSize, i, bIsReady
bIsReady = False
lngLastSize = -1
For i = 1 To MAX_RETRIES
On Error Resume Next
Set objFile = objFSO.GetFile(strFilePath)
If Err.Number = 0 Then
lngCurrentSize = objFile.Size
' 1. サイズに変化がないか
' 2. かつ、ファイルを開けるか(排他ロックの確認)
If lngCurrentSize = lngLastSize And lngCurrentSize > 0 Then
' 最終確認:実際に開けるかテスト
If TestFileAccess(strFilePath) Then
bIsReady = True
Exit For
End If
End If
lngLastSize = lngCurrentSize
Err.Clear
Else
' ファイルがまだ見えない、あるいはアクセス不可の場合
Err.Clear
End If
On Error GoTo 0
WScript.Sleep CHECK_INTERVAL
Next
WaitForFileReady = bIsReady
End Function
' ------------------------------------------------------------------------------
' 関数:ファイルへのアクセス権をテストする
' ------------------------------------------------------------------------------
Function TestFileAccess(strFilePath)
Dim objStream
On Error Resume Next
' 読み取りモードで開けるか試行
Set objStream = objFSO.OpenTextFile(strFilePath, 1)
If Err.Number = 0 Then
objStream.Close
TestFileAccess = True
Else
TestFileAccess = False
End If
On Error GoTo 0
End Function
' ------------------------------------------------------------------------------
' 関数:後続の業務ロジック(ここに実際の処理を書く)
' ------------------------------------------------------------------------------
Sub ProcessFile(strFilePath)
' 例: ファイルを別の場所に移動する、あるいはDBに登録するなど
' ここではログへの記録のみ
Dim objLog
Set objLog = objFSO.OpenTextFile(LOG_FILE, 8, True)
objLog.WriteLine Now & " - Processed: " & strFilePath
objLog.Close
End Sub
' 実行開始
StartMonitoring
4. アーキテクトの視点:なぜこのコードは「強い」のか
このコードが、単なる「動くコード」ではなく「プロダクション・コード」である理由を解説する。
① 「サイズ変化の停止」と「アクセス権」の二重チェック
多くのエンジニアは、ファイルサイズの比較だけで満足する。しかし、ファイルサイズが止まった直後でも、OSがファイルハンドルを閉じるまでの僅かなタイムラグ(ミリ秒単位)が存在する。
私の設計では、`TestFileAccess` 関数を導入し、「実際に開けること」を最終的な合格条件としている。これが、エラー率を限りなくゼロに近づける鍵だ。
② WMIによるリソース効率の最適化
`Do…Loop` 内で `FSO.FileExists` を回し続けるポーリング方式は、監視対象のファイル数が増えると劇的にパフォーマンスを悪化させる。WMIの `ExecNotificationQuery` を使うことで、OSが「ファイルができたぞ」と教えてくれるまで、スクリプトは極めて低いCPU負荷で待機できる。
③ エラーハンドリングの局所化
`On Error Resume Next` をスクリプトの冒頭に書くようなことは、プロとして絶対に避けるべきだ。エラーハンドリングは、`WaitForFileReady` 内のように、「エラーが想定される箇所」にのみ局所的に適用する。これにより、予期せぬ致命的なバグ(構文ミスやメモリ不足など)を隠蔽することなく、制御可能なエラー(ファイルロック)だけをハンドリングできる。
5. 運用に向けたアドバイス
このロジックを実務に組み込む際は、以下の点に留意せよ。
- タイムアウト設計: `MAX_RETRIES` は業務要件に合わせて調整すること。巨大なファイル(数GB単位)を扱う場合、書き込み完了まで数分かかるケースを想定しなければならない。
- 監視対象のパス形式: WMIのクエリにおいて、パスのバックスラッシュ(`\`)はエスケープが必要になる。ここを間違えると、スクリプトは沈黙し続ける。
- ログの重要性: 自動化ツールは「動いていること」よりも「なぜ止まったか」が重要だ。必ずエラーログを出力する仕組みを持たせておくこと。
結論
「ファイルが作成された」という事象は、プロセスの始まりに過ぎない。
真に堅牢なシステムを構築したいのであれば、その裏側にあるOSの挙動、ファイルシステムのライフサイクル、そしてリソースの競合を理解しなければならない。
この知見を武器に、単なる「スクリプト屋」から、信頼される「自動化エンジニア」へと昇華してほしい。
