—
VBScriptの限界を超えろ。複数プロセス競合を捩じ伏せる「堅牢なログ出力機構」の設計指針
Windows Script Host (WSH) という、枯れ果てた、しかし今なお現場の最前線で動き続ける技術において、我々エンジニアが直面する最も泥臭く、かつ致命的な問題がある。
それが、「複数プロセスの同時実行によるファイルアクセス競合」だ。
自動化ツールが普及し、タスクスケジューラや他のバッチから並列でスクリプトが叩かれるようになると、標準的な `FileSystemObject (FSO)` による `OpenTextFile` は、いとも容易く「書き込み禁止(エラー 70: Permission denied)」で崩壊する。
「エラーが出たら数秒待ってリトライすればいい」——そんな甘い考えは捨てろ。それは設計ではなく、ただの「祈り」だ。本稿では、プロフェッショナルが実務で採用すべき、軽量かつ堅牢なファイルベースの排他ロック制御を伝授する。
—
1. なぜあなたのログ出力は「落ちる」のか
VBScriptの `Scripting.FileSystemObject` は、OSレベルの高度な共有モード制御を隠蔽している。
通常、書き込みモード(ForAppending)でファイルを開くと、FSOは対象ファイルを排他的にロックしようとする。このとき、別のプロセスが既にファイルを開いていれば、即座に実行時エラーが発生してスクリプトは停止する。
これを回避するために `On Error Resume Next` で囲むだけの処理は、ログの欠損を招くだけでなく、デバッグを困難にする最悪のアンチパターンだ。
我々が実装すべきは、「ロックの取得」と「解放」を明示的に管理するセマフォ(信号機)的なアプローチである。
—
2. アーキテクチャ:フラグファイルによる排他制御
最も軽量で、かつVBScript環境において信頼性が高いのが、ログファイルと同名の `.lck`(ロックファイル) を一時的に作成する手法だ。
実装のロジック
1. ロックファイルの作成試行: `CreateTextFile` を使い、ロックファイルを「上書き禁止」で作成する。
2. 成功時: 書き込み権限を得たと判断し、本来のログファイルへ追記。完了後にロックファイルを削除する。
3. 失敗時: 他のプロセスが書き込み中であると判断。ランダムな待機時間(ジッター)を挟んでリトライする。
4. タイムアウト: 一定時間経過してもロックが取れない場合は、致命的なエラーとして処理を中断、または代替手段(イベントログ等)へ切り替える。
—
3. プロダクション・コード:`SafeLogger` クラス
以下のコードは、単なる関数の集まりではない。オブジェクトのライフサイクルと例外処理を考慮した、そのまま業務に投入できるクラスモジュール形式の設計例だ。
‘
‘ Class: SafeLogger
‘ 概要: 複数プロセスからの同時アクセスを考慮した堅牢なログ出力クラス
‘
Class SafeLogger
Private fso
Private logFilePath
Private lockFilePath
Private maxRetries
Private retryInterval ‘ ミリ秒
‘ 初期化
Private Sub Class_Initialize()
Set fso = CreateObject(“Scripting.FileSystemObject”)
maxRetries = 10 ‘ 最大リトライ回数
retryInterval = 500 ‘ 基本待機時間(ms)
End Sub
‘ プロパティ設定
Public Property Let TargetFile(path)
logFilePath = fso.GetAbsolutePathName(path)
lockFilePath = logFilePath & “.lck”
End Property
‘ メインメソッド: ログ書き込み
Public Sub WriteLog(message)
Dim i, lockFile, logStream
Dim success: success = False
Dim timeStamp: timeStamp = Now()
‘ 1. ロック取得のリトライループ
For i = 1 To maxRetries
On Error Resume Next
‘ ロックファイルを新規作成(第2引数 False で上書き禁止)
Set lockFile = fso.CreateTextFile(lockFilePath, False)
If Err.Number = 0 Then
‘ ロック取得成功
success = True
On Error GoTo 0
Exit For
Else
‘ ロック取得失敗(他プロセスが使用中)
Err.Clear
‘ 待機時間に揺らぎ(ジッター)を与えて衝突を分散させる
WScript.Sleep(retryInterval + Int(Rnd() 100))
End If
On Error GoTo 0
Next
If Not success Then
‘ 最終的にロックが取れなかった場合
‘ 実務ではここでイベントログに書き込むか、代替ファイルに出力する
Exit Sub
End If
‘ 2. 実際のログ書き込み処理
On Error Resume Next
Set logStream = fso.OpenTextFile(logFilePath, 8, True) ‘ 8 = ForAppending
If Err.Number = 0 Then
logStream.WriteLine “[” & timeStamp & “] ” & message
logStream.Close
End If
On Error GoTo 0
‘ 3. ロック解放(必ず実行)
ReleaseLock()
End Sub
Private Sub ReleaseLock()
On Error Resume Next
If fso.FileExists(lockFilePath) Then
fso.DeleteFile lockFilePath, True
End If
On Error GoTo 0
End Sub
Private Sub Class_Terminate()
Set fso = Nothing
End Sub
End Class
‘ — 使用例 —
Dim logger
Set logger = New SafeLogger
logger.TargetFile = “.\Application.log”
‘ 複数回呼び出しても、内部で適切に排他制御される
logger.WriteLog “プロセス開始: PID=” & CreateObject(“WScript.Shell”).Exec(“cmd /c echo”).ProcessID
logger.WriteLog “重要なトランザクションを実行中…”
logger.WriteLog “プロセス正常終了”
Set logger = Nothing
—
4. この設計が「強い」理由
① `CreateTextFile(path, False)` の原子性
FSOの `CreateTextFile` の第2引数を `False` に設定すると、ファイルが既に存在する場合にエラーを返してくれる。これはOSレベルでのファイル存在チェックと作成をアトミック(不可分)に行うため、VBScriptで実装できる最も確実なセマフォとなる。
② ジッター(揺らぎ)の導入
リトライ間隔を固定(例:500ms)にすると、複数のプロセスが全く同じタイミングでリトライを繰り返し、再び衝突する「ライブロック」状態に陥るリスクがある。`Int(Rnd() 100)` を加えることで、この同期現象を回避している。
③ 責務の分離
ログを書き込む側は「競合」を意識する必要はない。`logger.WriteLog` を呼ぶだけで、裏側の複雑な排他制御が完結する。これが、保守性の高いコードの絶対条件だ。
—
5. 実務上の注意点:オーファン・ロック(孤立したロック)
この方式には一つだけ弱点がある。ログ書き込み中にスクリプトが「強制終了(タスクキル等)」された場合、`.lck` ファイルが残ってしまい、以降のプロセスが永久に書き込めなくなる問題だ。
これを防ぐための「プロのひと工夫」を添えておこう。
スクリプトの開始時に、「ロックファイルの最終更新日時」を確認するロジックを追加せよ。もし更新日時が5分以上前であれば、それはクラッシュしたプロセスの遺物だと判断し、強制的に削除して良い。
‘ ロックファイルが古い場合に強制削除するガードロジック(例)
If fso.FileExists(lockFilePath) Then
Set f = fso.GetFile(lockFilePath)
If DateDiff(“n”, f.DateLastModified, Now()) > 5 Then
f.Delete True
End If
End If
終わりに
VBScriptは、正しく扱えば極めて強力な武器になる。しかし、その手軽さゆえに「壊れやすいツール」を量産してしまうエンジニアも多い。
今回紹介した排他制御は、ファイル操作の基本でありながら、分散処理の基礎に通じる重要な概念だ。このコードをあなたのツールボックスに加え、並列実行にも動じない、真に「現場で信頼されるシステム」を構築してほしい。
