共有リソースの静寂を設計せよ:Visioにおけるステンシル・ロック競合を回避する極限の運用設計
エンジニア諸君、あるいはシステムの安定性を司る管理官たちよ。
「動けばいい」という思考停止が、エンタープライズ環境においてどれほど致命的なコストを招くか、君たちは既に理解しているはずだ。特に、Visioを基盤とした図面自動生成システムを複数ユーザーで共有する環境において、ステンシル(.vssx)ファイルの「ファイルロック」問題は、単なるエラーではなく、システム全体の信頼性を揺るがす「設計上の欠陥」となり得る。
標準的な `Documents.Open` を愚直に呼び出す実装。それは、共有ドライブ上のリソースに対して無防備に「書き込み権限」を要求する行為に等しい。一人のユーザーがステンシルを編集モードで開いた瞬間、あるいはプロセスが異常終了してゾンビプロセスがファイルを掴んだ瞬間、他の全ユーザーの自動化プロセスは、無慈悲な「ファイル使用中」の例外と共に崩壊する。
本稿では、そのような脆弱な設計を脱却し、Windows APIを用いたプリフライト・チェックと、COMオブジェクトの厳格なライフサイクル管理によって、「競合を発生させない、読み取り専用ステンシル・アクセス」を実現するためのアーキテクチャを詳解する。
1. 脆弱な設計の正体:なぜ標準メソッドでは不十分なのか
Visioのオブジェクトモデルにおける `Stencil` オブジェクトは、単なる図面ファイルではない。それはマスターシェイプの集合体であり、アプリケーションのメモリ空間に深く食い込むリソースである。
多くの開発者が陥るミスは、以下のフローだ。
1. `Application.Documents.Open(path)` を実行する。
2. エラーが発生したら `On Error Resume Next` で握りつぶすか、リトライを繰り返す。
この設計は二重の意味で破綻している。
第一に、リトライ・ループはファイルロックの解消を早めない。 むしろ、不必要なI/O負荷を増大させ、ロックの保持時間を延ばす可能性がある。
第二に、Visioの `Open` メソッドは、内部的にファイルの排他的アクセスを試行する挙動を見せることがある。 読み取りたいだけなのに、書き込み権限を要求して自ら競合を引き起こしているのだ。
我々が目指すべきは、「ファイルが利用可能であることをOSレベルで確認してから、Visioに読み取り専用として渡す」という、二段構えの防御策である。
2. Windows APIによる「先行検知」の実装
VBAの標準関数(`Dir` や `FileLen`)では、ファイルが「他者に占有されているか」を正確に判定することはできない。そこで、`Kernel32.dll` の `CreateFile` APIを呼び出し、共有モード(Sharing Mode)を指定してファイルへのアクセスを試みる。
これにより、ファイルが書き込み権限を奪われていないか、あるいは読み取り専用で開ける状態にあるかを、Visioのオブジェクトモデルに触れる前に判別する。
' --- Module: modWinAPI ---
Option Explicit
' Windows API 宣言
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
''' <summary>
''' ファイルが他プロセスによって排他的にロックされていないかを確認する
''' </summary>
Public Function IsFileAccessible(ByVal filePath As String) As Boolean
Dim hFile As LongPtr
' 読み取り専用モードかつ、他者の読み書きを許可する設定でオープンを試みる
' これに失敗する場合、ファイルは誰かによって「排他的(Exclusive)」にロックされている
hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ Or FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0)
If hFile = INVALID_HANDLE_VALUE Then
IsFileAccessible = False
Else
CloseHandle hFile
IsFileAccessible = True
End If
End Function
3. 堅牢なステンシル・マネージャーの構築
APIによる事前チェックが完了したら、次はVisioへの安全なロードだ。ここで重要なのは、ステンシルを「ドキュメント」としてではなく、明示的に「ステンシル」として扱い、かつメモリ管理を徹底することである。
VBAにおけるCOMオブジェクトの解放(`Set obj = Nothing`)は、単なる作法ではない。参照カウントを適切に減らし、メモリ上のゾンビを排除するための、生存戦略である。
' --- Module: modStencilManager ---
Option Explicit
' 依存関係: Microsoft Visio Object Library
''' <summary>
''' 安全にステンシルをロードし、マスターシェイプへのアクセスを提供する
''' </summary>
Public Sub ExecuteSafeStencilOperation(ByVal stencilPath As String)
Dim vApp As Visio.Application
Dim vStencil As Visio.Stencil
Dim vMaster As Visio.Master
Dim i As Integer
On Error GoTo ErrorHandler
' 1. 事前チェック: Windows APIによるロック確認
If Not modWinAPI.IsFileAccessible(stencilPath) Then
Err.Raise vbObjectError + 513, "StencilManager", "ステンシルファイルが他のプロセスによって排他的にロックされています。"
End If
Set vApp = Visio.Application
' 2. ステンシルのロード
' Visioにおいて、ステンシルはDocumentsコレクションの一部として管理される
' 読み取り専用で開くための直接的な引数は限定的だが、
' 既存のドキュメントとしてではなく、新しいステンシルとして追加するアプローチをとる
Set vStencil = vApp.Documents.Add(stencilPath)
' ステンシルが正しく開かれたか確認
If vStencil Is Nothing Then
Err.Raise vbObjectError + 514, "StencilManager", "ステンシルのロードに失敗しました。"
End If
' 3. マスターシェイプの操作(例)
Debug.Print "--- Stencil: " & vStencil.Name & " ---"
For i = 1 To vStencil.Masters.Count
Set vMaster = vStencil.Masters.Item(i)
Debug.Print "Master Found: " & vMaster.Name
' ここでマスターのプロパティを参照するなどの処理を行う
Next i
CleanUp:
' 4. 厳格なライフサイクル管理(ここが極限の知見)
' オブジェクトを逆順に、明示的に解放する
On Error Resume Next
If Not vMaster Is Nothing Then Set vMaster = Nothing
If Not vStencil Is Nothing Then
' ステンシルを閉じる。引数にFalseを渡して保存を要求しない(読み取り専用運用のため)
vStencil.Close
Set vStencil = Nothing
End If
If Not vApp Is Nothing Then Set vApp = Nothing
Exit Sub
ErrorHandler:
MsgBox "Critical Error: " & Err.Description, vbCritical, "System Architecture Failure"
Resume CleanUp
End Sub
4. アーキテクトの視点:運用設計における「レガシー」への向き合い方
この実装を実戦投入する際、シニアエンジニアが考慮すべきは、コードの美しさではなく「例外が発生した後のシステムの状態」である。
ゾンビプロセスの撲滅
VBAの実行中にエラーが発生し、`CleanUp` を経ずにプロセスが停止した場合、Visioのインスタンスがメモリに残り、ファイルロックを保持し続ける。これは、社内システムにおける「朝一番のトラブル」の典型例だ。
これを防ぐには、以下の運用ルールを組み込むべきである。
- プロセス監視タスク: 定期的に `visio.exe` のプロセスをスキャンし、ユーザーがログインしていない時間帯に生存しているプロセスを強制終了するバッチを組む。
- COMの参照カウント: 外部アプリケーション(Excelや独自のVB.NETツール)からVisioを操作する場合、`Marshal.ReleaseComObject` 等を用いて、確実に参照カウントをゼロにする設計を徹底せよ。
パフォーマンスの重み
大量のマスターシェイプを持つステンシルをロードする場合、I/Oコストは無視できない。
もし、同一セッション内で何度も同じステンシルを参照する必要があるならば、「シングルトン・パターン」を採用し、一度ロードした `Stencil` オブジェクトをメモリ内にキャッシュ(保持)しておく設計に切り替えろ。ただし、その場合は「誰かが書き込みモードで開いた場合にキャッシュを無効化する」という整合性管理が必要になる。
結言
ステンシルの管理とは、単に図形を呼び出すことではない。それは、共有リソースという「動的な境界線」を、いかにして静的で予測可能なものに変えるかという、高度なシステム設計の問題である。
Windows APIを使いこなし、オブジェクトの寿命を支配する。その一歩先を行く設計こそが、プロフェッショナルとアマチュアを分かつ境界線である。
健闘を祈る。
