【テクニカル・上級編】【実務中級者向け】FSOによるファイル更新時のデータ破損を防ぐ「アトミック書き換え(一時作成+瞬時置換)」パターン – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

VBScriptを掌握する極限の知見:FSOによるファイル更新時のデータ破損を防ぐ「アトミック書き換え」パターン

レガシーシステムの深部、あるいはWindowsタスクスケジューラによって深夜の静寂の中で稼働するVBScript。現代のモダンな開発言語から見れば前時代的遺物のように映るかもしれないが、インフラへの依存度を極限まで削ぎ落とし、OS標準機能だけで完結するこのスクリプト群は、今なおミッションクリティカルな現場の裏側を支え続けている。

その現場において、最も看過しやすく、かつ致命的な障害を引き起こすのが「ファイル書き込み中のデータ破損(ゼロバイト化・途中までの書き込み)」である。

`Scripting.FileSystemObject`(以下、FSO)の `OpenTextFile` でファイルを `ForWriting` モードで開いた瞬間、ターゲットファイルは切り詰められ(Truncated)、元のデータは消滅する。もしその瞬間に、電源断、OSの強制再起動、あるいはVBScriptのメモリ例外(Out of Memory)が発生したらどうなるか。ファイルは破損し、翌朝のバッチ処理は阿鼻叫喚の様相を呈することになる。

このリスクを根絶するため、OSのファイルシステムレベルの挙動を利用した「アトミック書き換え(Atomic Replace)パターン」をここに提示する。

—

1. なぜ「直接上書き」は悪なのか:ファイルシステムの裏側

通常のVBScriptによるファイル更新処理は、以下の手順を踏む。

‘ 【アンチパターン】直接上書きの構造
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set ts = fso.OpenTextFile(“C:\Data\master.dat”, 2, True) ‘ ← ここでファイルサイズが 0 になる
ts.WriteLine “New Critical Data…”
ts.Close

`OpenTextFile` に `2 (ForWriting)` を指定した瞬間、ファイルハンドルが排他制御で確保され、既存データは揮発する。この「データがない空白の時間(Vulnerability Window)」が存在する限り、システムは常にクラッシュの恐怖と隣り合わせだ。

プロフェッショナルなエンジニアであれば、書き込み処理は常に「不可分(アトミック)」であるべきだと知っている。処理が成功するか、あるいは一切実行されないかのどちらかでなければならない。

—

2. 解決策:アトミック書き換えパターンの全貌

アトミック書き換えの本質はシンプルだ。
1. 安全な別領域(同一ディレクトリ内)に一時ファイル(Temporary File)を生成する。
2. すべての書き込みとバッファフラッシュを一時ファイル上で完結させる。
3. OSのファイル移動(MoveFile)または置換の原子性を利用し、一瞬で本番ファイルと差し替える。

同一ボリューム内(同一NTFSパーティション内)におけるファイルのリネーム/移動は、ファイルシステムのMFT(Master File Table)のポインタ書き換えるだけであり、実データのコピーは発生しない。そのため、極めて高速かつアトミック(不可分)に実行される。

実装コード:堅牢性を極めたVBScriptモジュール

以下のコードは、エラーハンドリング、一意な一時ファイル名の生成、および確実なリソース解放を網羅した実用モジュールである。

Option Explicit

‘ 実行例
Call AtomicWriteTextFile(“C:\Data\config.json”, “{” & vbCrLf & ” ” & Chr(34) & “status” & Chr(34) & “: ” & Chr(34) & “ok” & Chr(34) & vbCrLf & “}”)

Sub AtomicWriteTextFile(ByVal targetFilePath, ByVal fileContent)
Dim fso, folderPath, fileName, tempFilePath
Dim textStream
Dim objShell, guid

Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ 1. ターゲットパスの検証と分解
folderPath = fso.GetParentFolderName(targetFilePath)
fileName = fso.GetFileName(targetFilePath)

If Not fso.FolderExists(folderPath) Then
Err.Raise 53, “AtomicWrite”, “指定されたフォルダが存在しません: ” & folderPath
End If

‘ 2. 同一フォルダ内に衝突しない一意な一時ファイル名を生成(GUIDを活用)
Set objShell = CreateObject(“Scriptlet.TypeLib”)
guid = objShell.Guid
Set objShell = Nothing

‘ GUIDから括弧とハイフンを除去してプレフィックスとする
guid = Replace(Replace(Replace(guid, “{“, “”), “}”, “”), “-“, “”)
tempFilePath = fso.BuildPath(folderPath, “~tmp_” & Left(guid, 12) & “_” & fileName)

On Error Resume Next

‘ 3. 一時ファイルへの書き込み
‘ ForWriting (2), Create (True), Unicode (TristateTrue = -1 または ASCII = 0)
Set textStream = fso.OpenTextFile(tempFilePath, 2, True, 0)
If Err.Number <> 0 Then
Call CleanUpAndRaise(fso, tempFilePath, textStream, “一時ファイルの作成に失敗しました: ” & Err.Description)
End If

textStream.Write fileContent
If Err.Number <> 0 Then
Call CleanUpAndRaise(fso, tempFilePath, textStream, “一時ファイルへの書き込みに失敗しました: ” & Err.Description)
End If

‘ バッファを確実にフラッシュしてクローズ
textStream.Close
Set textStream = Nothing

‘ 4. アトミック置換(MoveFileによる上書き)
‘ 同一ボリューム内であれば、Moveメソッドはアトミックに既存ファイルを置き換える
fso.MoveFile tempFilePath, targetFilePath
If Err.Number <> 0 Then
Call CleanUpAndRaise(fso, tempFilePath, textStream, “ファイル置換に失敗しました: ” & Err.Description)
End If

On Error GoTo 0
Set fso = Nothing
WScript.Echo “正常終了: アトミック書き換えが完了しました。”
Exit Sub

ErrorHandler:
‘ フォールバック:万が一異常が発生した場合のクリーンアップ
On Error Resume Next
If Not textStream Is Nothing Then textStream.Close
If fso.FileExists(tempFilePath) Then fso.DeleteFile tempFilePath, True
Set fso = Nothing
Err.Raise Err.Number, “AtomicWrite”, “致命的エラー: ” & Err.Description
End Sub

Sub CleanUpAndRaise(fso, tempFilePath, textStream, errorMessage)
Dim errNum, errDesc
errNum = Err.Number
errDesc = errorMessage

On Error Resume Next
If Not textStream Is Nothing Then textStream.Close
If fso.FileExists(tempFilePath) Then fso.DeleteFile tempFilePath, True

Err.Raise errNum, “AtomicWrite”, errDesc
End Sub

—

3. シニアエンジニアが押さえるべきアーキテクチャの急所

上記のコードが、単なる「ファイルを別名で作ってリネームするスクリプト」と一線を画す理由は、以下の3点に集約される。

① 同一ボリューム(同一パーティション)の絶対遵守

FSOの `MoveFile` メソッドは、移動元と移動先が異なるボリューム(例: `C:` から `D:`)にある場合、「データの物理コピー + 元データの削除」という非アトミックな挙動にフォールバックする。この瞬間、コピー中に電源が落ちればデータは完全に失われる。
そのため、一時ファイルは必ず「ターゲットファイルと同一のフォルダ(=同一ボリューム)」に生成しなければならない。コード内の `fso.GetParentFolderName(targetFilePath)` によるパス抽出は、この物理法則を担保するための必須要件である。

② リソースの確実な解放(Memory Leak防衛)

VBScriptのCOMコンポーネント(特にFSOやTextStream)は、ガベージコレクションのタイミングが曖昧であり、スクリプトが肥大化・常駐化した際にメモリリークやファイルハンドルの開放漏れを引き起こす。
例外発生時(`Err.Number <> 0`)においても、確実に `TextStream.Close` を呼び出し、ファイルロックを即時解除する防衛的コード(`CleanUpAndRaise` サブルーチン)を組み込んでいる。

③ 競合を防ぐエントロピーの確保

マルチプロセス環境やタスクの多重起動において、一時ファイルの命名規則が単なる `temp.dat` や `pid` 依存である場合、ファイルアクセスの競合(Collision)を引き起こす。
ここでは `Scriptlet.TypeLib` から生成される `GUID` を利用することで、宇宙空間で重複する確率すらない一意性を担保し、並行稼働するバッチ群からの安全性を確保している。

—

4. レガシーの限界を超えた先へ

VBScriptは枯れた技術であり、モダンなCI/CDパイプラインやコンテナ環境の主役から外れている。しかし、何十年と稼働し続けるWindows Serverの基盤や、社内ローカルPCの自動化タスクにおいて、この「OS標準機能だけで動く」という強みは代替不可能な価値を持つ。

ファイル破損という、エンジニアにとって最も悪夢的なインシデントを、モダンな理論(アトミック操作)をレガシーな構文で包み込むことによって完全に封じ込める。これこそが、技術を真に掌握したアーキテクトの仕事である。

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