【排他ロック検知】OpenAsTextStreamメソッドの例外処理を利用した「他プロセス使用中」ファイルの判定ロジック
レガシーシステムの裏側、あるいは日常的な業務自動化の現場において、VBScript(Visual Basic Scripting Edition)は今なおインフラストラクチャの隙間を埋める不可欠な接着剤として稼働し続けている。
しかし、Excelや基幹系アプリケーションが排他制御下で握りしめているファイルを、無策のままスクリプトが直撃した瞬間、容赦のない「実行時エラー」と共にプロセスは非業の死を遂げる。
「ファイルが別のプロセスによって使用されています」
この古典的かつ破壊的なエラーを防ぐためだけに、無駄なリトライループや、不安定なWMI監視を実装していないだろうか?
今回は、FSO(FileSystemObject)の `OpenAsTextStream` メソッドが持つ挙動と、VBScriptの例外処理メカニズム(`On Error Resume Next`)の裏側を極限までハックし、「他プロセス使用中(排他ロック)」のファイルをミリ秒単位のオーバーヘッドで正確に検知・判定する技術を詳解する。
—
1. なぜ「存在確認」や「属性チェック」では不十分なのか?
多くの初学者が陥るアンチパターンは、ファイルに対して何らかのアクションを起こす前に、存在確認や属性チェックを行うことだ。
‘ 【アンチパターン】これでは競合(Race Condition)を防げない
Set fso = CreateObject(“Scripting.FileSystemObject”)
If fso.FileExists(targetPath) Then
‘ この瞬間に、別プロセスがファイルを開く可能性がある(Time-of-check to time-of-use)
Set file = fso.OpenTextFile(targetPath, 1)
End If
マルチタスクOSであるWindows環境において、ファイルが存在すること(Time-of-check)と、それを安全に読み書きできること(Time-of-use)の間には、必ず時間的な乖離(TOCTOU脆弱性)が存在する。
ファイルが「存在」していても、それが他者によって「排他ロック」されているかどうかは、実際にOSのファイルシステムに対してオープン要求を投げ、カーネルから拒絶されるか否かを確認する以外に知る術はない。
ここで、余計なWin32 API(CreateFile等)をVBからDllImportなしで呼び出す苦行を行う必要はない。FSOの内部実装が発する例外を、正確にトラップすれば十分なのである。
—
2. `OpenAsTextStream` のライフサイクルと例外挙動の真髄
`FileSystemObject` の `File` オブジェクトが持つ `OpenAsTextStream` メソッド(あるいは `OpenTextFile`)は、ファイルをストリームとして開く際に、OSのファイル共有モード(Share Mode)の制限を直接受ける。
通常、他プロセス(Excelなど)が書き込み権限を保持したままファイルを開いている場合、FSO経由で読み取り専用(`ForReading` / 定数: `1`)としてオープンを試みると、OSのI/Oマネージャーは即座にアクセス拒否(Access Denied)を返上し、VBScriptランタイムへエラーコード `0x800A004C`(「ファイルは見つからないか、または開くことができません」/ Error 75: Path/File access error)を送出する。
この「OSレベルの排他制御による拒絶を、VBScriptのエラーとして捕捉する」というアプローチこそが、最も確実かつ環境依存性の低い排他ロック検知メカニズムである。
—
3. 実装コード:極限まで最適化された排他判定関数
以下のコードは、実務の現場でそのまま流用可能な、堅牢性とパフォーマンスを極限まで高めた排他判定ロジックの実装例である。
Option Explicit
Const ForReading = 1
Const TristateUseDefault = -2
‘ ==============================================================================
‘ 関数名: IsFileLocked
‘ 概要: 指定されたファイルが他プロセスによって排他ロックされているかを判定する
‘ 引数: strPath (String) – 判定対象のファイルパス
‘ 戻り値: Boolean – ロックされている場合は True、それ以外は False
‘ ==============================================================================
Function IsFileLocked(ByVal strPath)
Dim fso, targetFile, textStream
Dim lngErrorNumber
IsFileLocked = False ‘ 初期値は「ロックされていない(安全)」
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ ファイルが存在しない場合は、そもそもロックされていない(または対象外)
If Not fso.FileExists(strPath) Then
Set fso = Nothing
Exit Function
End If
‘ エラーハンドリングの有効化(トラップモードへ移行)
On Error Resume Next
Err.Clear
‘ 対象ファイルオブジェクトの取得
Set targetFile = fso.GetFile(strPath)
If Err.Number <> 0 Then
‘ ファイルアクセス権限そのものがない等の異常系
IsFileLocked = True
On Error GoTo 0
Set fso = Nothing
Exit Function
End If
‘ 【核心】OpenAsTextStreamによる排他オープン試行
‘ 第1引数: ForReading (1)
‘ 第2引数: TristateUseDefault (-2) [システムのデフォルトエンコーディング]
Set textStream = targetFile.OpenAsTextStream(ForReading, TristateUseDefault)
lngErrorNumber = Err.Number
‘ エラーハンドリングを即座に通常モードに戻す(スコープ汚染の防止)
On Error GoTo 0
‘ エラーが発生した場合の解析
‘ 75 (0x800A004C): パスまたはファイル名が正しくありません / アクセスできません
‘ 70 (0x800A0046): 書き込み権限がありません (Permission denied)
If lngErrorNumber <> 0 Then
‘ エラー番号に応じた詳細な切り分けが必要な場合はここでハンドリングする
‘ 他プロセスによる排他ロック時は主に Err.Number = 75 が発生する
IsFileLocked = True
Else
‘ 正常に開けた場合は、直ちにストリームを閉じてリソースを解放する
textStream.Close
End If
‘ 明示的なオブジェクトの解放(メモリリークおよびハンドルリークの根絶)
Set textStream = Nothing
Set targetFile = Nothing
Set fso = Nothing
End Function
‘ — 実行テスト用スニペット —
Dim targetFilePath
targetFilePath = “C:\Automation\SharedData.xlsx”
WScript.Echo “=== 排他ロック検知テスト開始 ===”
If IsFileLocked(targetFilePath) Then
WScript.Echo “[WARNING] ファイルは現在、別プロセス(Excel等)で使用されています。”
‘ ここで処理をスキップする、あるいはリトライキューに入れる等のロジックを展開
Else
WScript.Echo “[SUCCESS] ファイルは解放されています。処理を継続します。”
‘ 本処理の実行
End If
—
4. シニアエンジニアが押さえるべき実装上のアーキテクチャ知見
このコード片は極めてシンプルに見えるが、レガシー環境やエンタープライズの現場で耐えうるよう、いくつかの高度な設計思想が組み込まれている。
① スコープ汚染の排除と `On Error GoTo 0` の厳格な管理
VBScriptの最大の悪習は、`On Error Resume Next` を記述したままスクリプト全体を汚染し、後続の致命的なバグ(変数のタイポや予期せぬゼロ除算)を闇に葬り去ることだ。
本実装では、例外が発生しうる極小のスコープ(`OpenAsTextStream` の実行行)の直前でトラップを有効化し、判定直後に必ず `On Error GoTo 0` でデフォルトの例外挙動へ復帰させている。
② COMオブジェクトのライフサイクルとハンドルの即時解放
VBScriptのガベージコレクタ(Reference Counting方式)は、スクリプトブロックが終了するまでメモリやファイルハンドルを保持し続ける傾向がある。
特に `OpenAsTextStream` によって開かれたファイルハンドルは、OSのリソースを圧迫する。
判定が成功(`Err.Number = 0`)した瞬間に、即座に `.Close` メソッドを呼び出し、さらに `Set textStream = Nothing` によって参照カウントをデクリメントしている点に注目してほしい。
これにより、ファイルハンドルのリークによる「ファイルが消せない・移動できない」という二次災害を完全に防いでいる。
③ ネットワーク共有フォルダ(UNCパス)における挙動の罠
このロジックをローカル環境だけでなく、NASやWindowsファイルサーバー(UNCパス: `\\server\share\file.xlsx`)上のファイルに対して適用する場合、SMBプロトコルのキャッシュやレイテンシによって、排他ロックの検知にわずかなタイムラグが生じることがある。
高頻度でポーリング(監視)を行う設計にする場合は、単発の判定に頼るのではなく、以下のような短時間のバックオフ(リトライ)ロジックを外側にラップすることが望ましい。
Function IsFileLockedWithRetry(ByVal strPath, ByVal maxRetries, ByVal intervalMs)
Dim i
For i = 1 to maxRetries
If Not IsFileLocked(strPath) Then
IsFileLockedWithRetry = False
Exit Function
End If
‘ WScript.Sleep はミリ秒単位ではなくミリ秒指定が可能だがWSH環境依存に注意
WScript.Sleep intervalMs
Next
IsFileLockedWithRetry = True ‘ リトライ回数を超えてもロックされていた
End Function
—
5. 結びにかえて
VBScriptは「古い言語」として片付けられがちだが、OSのプリミティブなAPIやファイルシステムの挙動と直接対話できるという意味において、極めて純度の高いスクリプト環境である。
`OpenAsTextStream` の例外を逆手に取ったこの排他ロック検知ロジックは、エラーを単に「避ける」のではなく、エラーを「情報として利用する」というシニアエンジニアリングの極みである。
現場のシステム安定稼働のため、ぜひこの知見をあなたの自動化スクリプトのアーキテクチャに組み込んでほしい。
