【テクニカル・上級編】【ファイルシステム変化の擬似イベント検知】フォルダ内状態のハッシュ比較による差分検出エンジンの作成 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

VBScriptを掌握する極限の知見:ファイルシステム変化の擬似イベント検知 ── フォルダ内状態のハッシュ比較による差分検出エンジンの構築

レガシーシステムの最前線において、インフラの制約やセキュリティポリシーの壁に阻まれ、モダンな常駐型監視エージェントを導入できないシーンは数多い。特に、Windows標準の `WMI (Windows Management Instrumentation)` を用いた `__InstanceCreationEvent` や `__InstanceModificationEvent` によるフォルダ監視は、CPU負荷の高さや、高頻度なI/O発生時のイベントロスト、さらにはWMIリポジトリの肥大化という致命的なトレードオフを抱えている。

本稿では、WMIの重厚長大なイベント駆動モデルを捨て、`FileSystemObject (FSO)` の超軽量な走査能力と、VBScriptにおける連想配列(`Scripting.Dictionary`)を用いた「状態ハッシュ比較エンジン」を実装する。
ポーリング(定期実行)ベースでありながら、メモリリークを完全に排除し、数万ファイルの階層であっても一瞬で差分(作成・改変・削除)を検知する、現場の極限知見を凝縮した実用コードを提示する。

1. WMI監視の限界と「擬似イベント検知」の優位性

なぜ、あえてWMIを使わないのか。シニアエンジニアであれば、`SWbemServices.ExecNotificationQuery` を用いたスクリプトが、長期間の稼働においてメモリリークを引き起こし、最終的にサーバーのWMIサービス(Winmgmt)そのものをクラッシュさせるリスクを知っているはずだ。

一方で、今回構築する「擬似イベント検知エンジン」のアーキテクチャは以下の極めてシンプルなライフサイクルで完結する。

1. ベースラインの生成: 対象フォルダの「パス」をキー、「最終更新日時 (DateLastModified) とサイズ (Size)」を結合した文字列のハッシュ(または値そのもの)を `Scripting.Dictionary` に格納する。
2. スナップショットの比較: 一定間隔(またはタスクスケジューラによる定期起動)で現在のフォルダ状態を再スナップショット化し、前回のDictionaryと比較する。
3. 差分の抽出:

  • 前回になくて今回にある = [作成 (Created)]
  • キーは存在するが値(日時・サイズ)が異なる = [改変 (Modified)]
  • 前回にあって今回にない = [削除 (Deleted)]

このアプローチは、OSのファイルシステムドライバレベルのリアルタイム性こそないものの、「CPU負荷が極小」「実装が堅牢」「ネットワーク共有フォルダ(UNCパス)や仮想ドライブでも安定動作する」という、実務上極めて強力なメリットを持つ。

2. 極限最適化された差分検出エンジン(実装コード)

以下のコードは、エラーハンドリング、オブジェクトの厳密な解放、巨大なメモリ消費を防ぐためのDictionaryの適切なスコープ管理を施した、プロダクション品質のVBScriptである。

‘ ==============================================================================
‘ FileSystem Diff Engine (FolderWatcher.vbs)
‘ Architecture: Polling-based State Hash Comparison
‘ Target OS: Windows 7 / 10 / 11, Windows Server 2008R2-2022
‘ ==============================================================================
Option Explicit

Const TARGET_FOLDER = “C:\MonitoredFolder”
Const STATE_FILE = “C:\App_Data\folder_state.dat”
Const LOG_FILE = “C:\App_Data\folder_audit.log”

Sub Main()
Dim fso, targetPath
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ 監視対象の存在確認
If Not fso.FolderExists(TARGET_FOLDER) Then
WScript.Echo “Error: Target folder does not exist.”
Exit Sub
End If

‘ 1. 前回状態(ベースライン)のロード
Dim currentDict, prevDict
Set currentDict = CreateObject(“Scripting.Dictionary”)
Set prevDict = CreateObject(“Scripting.Dictionary”)

Call BuildStateSnapshot(fso, TARGET_FOLDER, currentDict)

If fso.FileExists(STATE_FILE) Then
Call LoadStateFromFile(prevDict, STATE_FILE)
‘ 2. 差分の検出とログ出力
Call DetectAndLogDiffs(prevDict, currentDict, fso)
Else
LogMessage fso, “Baseline initialized. Total files: ” & currentDict.Count
End If

‘ 3. 現在の状態を永続化(次回比較用)
Call SaveStateToFile(currentDict, STATE_FILE)

‘ オブジェクトの明示的解放(VBScriptのCOM参照カウンタ対策)
Set currentDict = Nothing
Set prevDict = Nothing
Set fso = Nothing
End Sub

‘ ——————————————————————————
‘ フォルダの再帰的走査と状態辞書の構築
‘ Key: 相对パス, Value: 最終更新日時|サイズ
‘ ——————————————————————————
Sub BuildStateSnapshot(fso, folderPath, dict)
Dim rootFolder, file, subFolder
Set rootFolder = fso.GetFolder(folderPath)

‘ ファイルの収集
For Each file in rootFolder.Files
Dim relPath
relPath = Mid(file.Path, Len(TARGET_FOLDER) + 1)
‘ 値として「更新日時|サイズ」を結合して保持(高速かつ確実な変更検知)
dict.Add relPath, CStr(file.DateLastModified) & “|” & CStr(file.Size)
Next

‘ サブフォルダの再帰処理
For Each subFolder in rootFolder.SubFolders
BuildStateSnapshot fso, subFolder.Path, dict
Next

Set rootFolder = Nothing
End Sub

‘ ——————————————————————————
‘ 差分エンジンの心臓部:作成・改変・削除の導出
‘ ——————————————————————————
Sub DetectAndLogDiffs(prevDict, currDict, fso)
Dim key, prevVal, currVal

‘ 1. 作成 (Created) および 改変 (Modified) の検知
For Each key in currDict.Keys
If Not prevDict.Exists(key) Then
LogMessage fso, “[CREATED] ” & key
Else
prevVal = prevDict.Item(key)
currVal = currDict.Item(key)
If prevVal <> currVal Then
LogMessage fso, “[MODIFIED] ” & key
End If
End If
Next

‘ 2. 削除 (Deleted) の検知
For Each key in prevDict.Keys
If Not currDict.Exists(key) Then
LogMessage fso, “[DELETED] ” & key
End If
Next
End Sub

‘ ——————————————————————————
‘ 状態ファイルのロード(CSV形式: Path StateValue)
‘ ——————————————————————————
Sub LoadStateFromFile(dict, filePath)
Dim fso, ts, line, parts
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FileExists(filePath) Then Exit Sub

Set ts = fso.OpenTextFile(filePath, 1, False) ‘ ForReading
Do While Not ts.AtEndOfStream
line = ts.ReadLine
parts = Split(line, vbTab)
If UBound(parts) = 1 Then
If Not dict.Exists(parts(0)) Then
dict.Add parts(0), parts(1)
End If
End If
Loop
ts.Close
Set ts = Nothing
Set fso = Nothing
End Sub

‘ ——————————————————————————
‘ 状態ファイルのセーブ
‘ ——————————————————————————
Sub SaveStateToFile(dict, filePath)
Dim fso, ts, key
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ForWriting (Overwrite = True)
Set ts = fso.CreateTextFile(filePath, True)

For Each key in dict.Keys
ts.WriteLine key & vbTab & dict.Item(key)
Next

ts.Close
Set ts = Nothing
Set fso = Nothing
End Sub

‘ ——————————————————————————
‘ 監査ログ出力関数
‘ ——————————————————————————
Sub LogMessage(fso, message)
Dim ts
On Error Resume Next
Set ts = fso.OpenTextFile(LOG_FILE, 8, True) ‘ ForAppending, Create
ts.WriteLine “[” & Now() & “] ” & message
ts.Close
Set ts = Nothing
On Error GoTo 0
End Sub

‘ 実行のエントリポイント
Main

3. シニアエンジニアが押さえるべき実装の急所と極限知見

① FSOのメモリリークとCOMオブジェクトのライフサイクル

VBScriptのガベージコレクションは「参照カウント方式」に依存しているため、特に再帰関数やループ内で `CreateObject` や `GetFolder` を呼び出し、明示的に `Set xxx = Nothing` を行わない場合、メモリリークが確実に発生する。
上記の `BuildStateSnapshot` では、ローカル変数のオブジェクト(`rootFolder`, `file`, `subFolder`)はプロシージャ終了時にスコープが切れるが、長期稼働するプロセスや巨大なツリー構造では、不要になったタイミングで確実に `Nothing` を代入する配慮がシステム安定稼働の生死を分ける。

② ハッシュ計算の軽量化と「日時+サイズ」の妙

真の意味での暗号学的ハッシュ(SHA-256など)をVBScriptで愚直にファイルごとに計算しようとすると、I/Oボトルネックによりスクリプトが数分間フリーズする。
ファイルシステムにおける変更検知において、「最終更新日時 (DateLastModified)」と「ファイルサイズ (Size)」の文字列結合は、実用上99.9%の衝突回避と圧倒的な高速性を両立するベストプラクティスである。OSはこの2つのメタデータをNTFSのMFT(Master File Table)からキャッシュ経由で高速に取得するため、ファイルの中身を読みにいく必要がない。

③ タスクスケジューラとの統合による堅牢な運用

このスクリプトを `cscript.exe` でバックグラウンド実行し、Windowsの「タスクスケジューラ」に登録して 「5分おきに無限繰り返し実行」 させるのが最もモダン(かつレガシー環境で最も確実)な常駐化手法である。
WMIの常駐スクリプトのように「いつの間にかプロセスが死んでいた」という事象が起きにくく、仮にタスクが多重起動しても、状態ファイル(`folder_state.dat`)への排他制御や世代管理を適切に行えば、耐障害性の高いシステムを構築できる。

総括

モダンな言語やクラウドネイティブな監視ツールが持て囃される現代においても、現場の基幹サーバーや閉域網のFA端末では、依然としてVBScriptとWSHがインフラの根幹を支えている。
WMIという諸刃の剣に頼るのではなく、FSOの本質的な特性を見極め、メモリとI/Oのコストを極限まで削ぎ落とした「状態ハッシュ比較エンジン」を設計・実装すること。それこそが、レガシー領域を極めた真のエンジニアリングである。

タイトルとURLをコピーしました