ステンシル破損という静かなる崩壊:Visio VBAにおける堅牢な自己修復アーキテクチャの構築
エンタープライズ環境におけるVisioの自動化は、単なる「図形の描画」ではない。それは、複雑なシステム構成図、ネットワークトポロジ、あるいは高度なプロセスフローを定義する「設計図の生成エンジン」である。
そのエンジンの心臓部を担うのが、ステンシル(.vssx)ファイルだ。
しかし、現場のエンジニアが直面する現実は、美しいコードとは程遠い。ネットワークストレージへの書き込み遅延、不適切なプロセス終了、あるいはOSのファイルシステムレベルでの不整合。これらは、昨日まで完璧に動作していたステンシルを、突如として「読み込み不能なゴミ」へと変貌させる。
一般的な開発者は `On Error Resume Next` でエラーを握りつぶし、あるいは単純な `If Dir(path) <> “”` で存在確認を行うに留まる。だが、それはプロフェッショナルの仕事ではない。ファイルが存在しても、その中身が壊れていれば、COMオブジェクトのロードプロセスは不可解な挙動を示し、最悪の場合、Visioプロセス自体をゾンビ化させる。
本稿では、ステンシルの整合性を検証し、破損時にバックアップから自動復旧させる、極限まで堅牢性を高めたアーキテクチャを解説する。
1. 破損のメカニズムと防御の設計思想
ステンシルファイル(特にOOXML形式の .vssx)は、内部的にはXMLの集合体である。ファイルが完全に書き込まれる前にプロセスが遮断されると、XMLの構造が破壊され、Visioの `Documents.Open` メソッドは、エラーコードすら返さずに低レベルな例外をスローすることがある。
我々が構築すべきは、以下の3層からなる防御アーキテクチャである。
1. L1: Existence & Lock Check(ファイルは存在するか? 他のプロセスが掴んでいないか?)
2. L2: Integrity Validation(ファイルはVisioが認識可能な構造を保っているか?)
3. L3: Automated Recovery(破損時、即座に世代管理されたバックアップへ切り替え可能か?)
2. 実装:堅牢なステンシル・マネージャー
以下に示すのは、Windows APIを用いてファイルロック状態を検知し、破損時に自動復旧を行う高度な制御ロジックを組み込んだモジュールである。
Option Explicit
' --- Windows API Declarations ---
' ファイルのオープン状態を確認し、ロックされているかを判定するために使用
Private Declare PtrSafe Function CreateFile Lib "kernel32" Alias "CreateFileA" ( _
ByVal lpFileName As String, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As LongPtr, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As LongPtr) As LongPtr
Private Declare PtrSafe Function CloseHandle Lib "kernel32" (ByVal hObject As LongPtr) As Long
Private Const GENERIC_READ As Long = &H80000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const FILE_SHARE_WRITE As Long = &H2
Private Const OPEN_EXISTING As Long = 3
Private Const INVALID_HANDLE_VALUE As LongPtr = -1
' --- Constants ---
Private Const ERROR_CORRUPT_STENCIL As Integer = 1001 ' カスタムエラーコード
''' <summary>
''' ステンシルの整合性を検証し、必要に応じて復旧を行う
''' </summary>
''' <param name="targetPath">対象のステンシルパス</param>
Public Sub SecureLoadStencil(ByVal targetPath As String)
Dim backupPath As String
Dim doc As Visio.Document
Dim isValid As Boolean
backupPath = targetPath & ".bak"
On Error GoTo ErrorHandler
' 1. ファイルの存在とロック状態のチェック
If Not FileExists(targetPath) Then
Err.Raise vbObjectError + Error_FileNotFound, "SecureLoadStencil", "Target file not found."
End If
If IsFileLocked(targetPath) Then
Err.Raise vbObjectError + Error_FileLocked, "SecureLoadStencil", "File is currently locked by another process."
End If
' 2. 整合性チェック (実際に開けるか試行)
isValid = TestStencilIntegrity(targetPath)
If Not isValid Then
Debug.Print "Critical: Stencil corruption detected. Initiating recovery..."
' 3. 復旧プロセス
If FileExists(backupPath) Then
Call PerformRecovery(targetPath, backupPath)
' 復旧後の再ロード試行
isValid = TestStencilIntegrity(targetPath)
Else
Err.Raise vbObjectError + ERROR_CORRUPT_STENCIL, "SecureLoadStencil", "Stencil corrupted and no backup available."
End If
End If
' 4. 最終的なロード
Set doc = Visio.Application.Documents.Open(targetPath)
Debug.Print "Stencil loaded successfully: " & doc.Name
' オブジェクトのライフサイクル管理:
' ここでdocを戻り値として返す設計にするのが理想的だが、
' 今回はデモのためロード完了までを記述
Exit Sub
ErrorHandler:
' 致命的なエラーログの記録(システム管理者向け)
LogError targetPath, Err.Description
MsgBox "Critical System Error: " & Err.Description, vbCritical, "Stencil Manager"
End Sub
''' <summary>
''' ステンシルをバックグラウンドで読み込み、構造が壊れていないか検証する
''' </summary>
Private Function TestStencilIntegrity(ByVal path As String) As Boolean
Dim testDoc As Visio.Document
On Error Resume Next
' 非表示状態で試行することで、ユーザーへの干渉を最小限にする
Set testDoc = Visio.Application.Documents.Open(path)
If Err.Number = 0 And Not testDoc Is Nothing Then
testDoc.Close
TestStencilIntegrity = True
Else
TestStencilIntegrity = False
End If
On Error GoTo 0
' メモリ解放の徹底
Set testDoc = Nothing
End Function
''' <summary>
''' ファイルロックを判定する (Windows API)
''' </summary>
Private Function IsFileLocked(ByVal filePath As String) As Boolean
Dim hFile As LongPtr
hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0)
If hFile = INVALID_HANDLE_VALUE Then
IsFileLocked = True
Else
CloseHandle hFile
IsFileLocked = False
End If
End Function
''' <summary>
''' バックアップからオリジナルへの上書き復旧
''' </summary>
Private Sub PerformRecovery(ByVal target As String, ByVal backup As String)
Dim fso As Object
Set fso = CreateObject("Scripting.FileSystemObject")
' 既存の破損ファイルを削除(ロックされている場合はここで失敗するため、前段のチェックが重要)
If fso.FileExists(target) Then fso.DeleteFile target, True
fso.CopyFile backup, target, True
Set fso = Nothing
Debug.Print "Recovery completed: " & target
End Sub
Private Function FileExists(ByVal path As String) As Boolean
FileExists = (Dir(path) <> "")
End Function
Private Sub LogError(ByVal context As String, ByVal msg As String)
' 実際の実装では、イベントログやDBへ書き出す
Debug.Print "[ERROR][" & Now & "][" & context & "] " & msg
End Sub
' カスタムエラー定数
Private Const Error_FileNotFound As Long = 513
Private Const Error_FileLocked As Long = 514
3. 極限の知見:メモリ管理とオブジェクトのライフサイクル
シニアエンジニアがこのコードを見て、次に疑うのは「メモリリークとゾンビプロセス」だ。
COMオブジェクトの明示的解放
VBAにおいて `Set obj = Nothing` は、単なるポインタのクリアではない。それは、参照カウント(Reference Counting)をデクリメントする操作である。特にVisioのような重厚なCOMアプリケーションでは、`Documents.Open` で生成されたオブジェクトが適切に `Close` されず、かつ `Nothing` にされない場合、Visioのプロセス内に「目に見えないドキュメント」が残り続ける。これが蓄積すると、メモリ消費の増大だけでなく、ファイルロックが解除されない原因となる。
私の設計では、`TestStencilIntegrity` 内で必ず `testDoc.Close` と `Set testDoc = Nothing` をセットで行っている。これにより、整合性チェックという「検証作業」が、メインの「業務プロセス」に副作用を与えないことを保証している。
Windows APIによる低レイヤー制御の必要性
なぜ `Dir()` ではなく `CreateFile` API を使うのか?
`Dir()` はファイルが存在するかどうかしか教えない。しかし、エンタープライズ環境では「ファイルは存在するが、ネットワーク越しに別のセッションが書き込み権限を保持している」という状態が頻発する。この状態で `Documents.Open` を強行すると、Visioは不安定な状態に陥る。APIを用いて `GENERIC_READ` でのアクセスを試みることで、ファイルシステムレベルでの真の可用性を判定している。
4. 運用への提言:バックアップ戦略
このコードを機能させるには、運用ルールとしての「バックアップ自動生成」が不可欠である。
単なる手動コピーではない。ステンシルを保存(`doc.Save`)する際に、以下のロジックを組み込んだラッパー関数を用意すべきだ。
1. シャドウコピー方式: 保存の直前に、現在のファイルを `.bak` としてコピーする。
2. 世代管理: `.bak` を一つにするのではなく、タイムスタンプ付き(`stencil_20231027.vssx`)で保持し、破損時のロールバック地点を複数確保する。
結論
Visio VBAにおける「堅牢性」とは、エラーが起きないことを祈ることではない。「エラーが起きることを前提とし、システムが自律的にそのダメージを最小化するように設計すること」である。
APIによる低レイヤーの検証、厳格なオブジェクトのライフサイクル管理、そして自動復旧フロー。これらを統合して初めて、レガシーな環境においても信頼に足る「プロフェッショナルな自動化システム」へと昇華するのだ。
