【極限の信頼性】OneDrive/SharePoint同期競合を制する:Windows APIを用いたファイルロック監視と堅牢な保存アルゴリズム
クラウドストレージ、特にOneDriveやSharePointが標準となった現代の設計環境において、AutoCAD VBAエンジニアが直面する最も「醜悪」な問題の一つが、保存時のファイルロック競合である。
設計者が作成した図面を自動保存、あるいはバッチ処理で出力しようとした瞬間、`Error 70: Permission Denied`(書き込み権限なし)が投げられる。これはAutoCADのバグではない。AutoCADがファイルへの書き込みを試みるコンマ数秒の隙を突き、OneDriveの同期エンジン(File System Filter Driver)がファイルのハッシュ値を計算し、アップロードのためにファイルハンドルを独占しているのだ。
一般的なエンジニアは、`On Error Resume Next`でエラーを握りつぶすか、単純な`Sleep`を数秒入れる程度の、脆弱なコードで済ませようとする。しかし、我々プロフェッショナルが構築すべきは、OSの低レイヤーに干渉し、ファイルの状態を正確に検知して、同期の完了を「待機」する、極めて堅牢なリトライアルゴリズムである。
本稿では、Windows APIを駆使してファイルロック状態を監視し、同期遅延を克服する実装の真髄を解説する。
1. 競合のメカニズム:なぜ単純なリトライでは足りないのか
OneDrive等の同期クライアントは、ファイルが更新されたことを検知すると、即座にファイルシステムへのコールバックを行い、読み取り専用または排他的なモードでファイルを開く。
問題は、「ファイルが存在すること」と「ファイルに書き込み可能であること」は全く別次元の概念であるという点だ。
VBAの標準的なエラーハンドリングでは、「ファイルが開けない」という結果しか得られない。しかし、我々が必要としているのは、「なぜ開けないのか?(他プロセスによるロックか、あるいは単純な権限不足か)」という、原因の特定である。これを実現するために、`kernel32.dll` の `CreateFile` 関数を直接呼び出し、ファイルハンドル取得の成否を判定する手法を用いる。
2. 実装:Windows APIによるファイルロック検知
まず、64bit環境(現代のAutoCADの標準)を前提とした、Windows APIの宣言から始める。ここで重要なのは、`LongPtr` を使用し、ポインタの整合性を保つことだ。
Option Explicit
' --- Windows API Declarations ---
If VBA7 Then
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 Declare PtrSafe Function GetLastError Lib "kernel32" () As Long
Else
Private Declare Function CreateFile Lib "kernel32" Alias "CreateFileA" ( _
ByVal lpFileName As String, _
ByVal dwDesiredAccess As Long, _
ByVal dwShareMode As Long, _
ByVal lpSecurityAttributes As Long, _
ByVal dwCreationDisposition As Long, _
ByVal dwFlagsAndAttributes As Long, _
ByVal hTemplateFile As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" ( _
ByVal hObject As Long) As Long
Private Declare Function GetLastError Lib "kernel32" () As Long
End If
' --- Constants ---
Private Const GENERIC_WRITE As Long = &H40000000
Private Const FILE_SHARE_READ As Long = &H1
Private Const FILE_SHARE_WRITE As Long = &H2
Private Const FILE_SHARE_DELETE As Long = &H4
Private Const OPEN_EXISTING As Long = 3
Private Const INVALID_HANDLE_VALUE As LongPtr = -1
' Windows Error Codes
Private Const ERROR_SHARING_VIOLATION As Long = 32
Private Const ERROR_LOCK_VIOLATION As Long = 33
ロック状態を判定するコアロジック
`IsFileLocked` 関数は、対象のファイルに対して「書き込み権限を持った排他的なアクセス」を試みる。もし同期エンジンがハンドルを握っていれば、`CreateFile` は `INVALID_HANDLE_VALUE` を返し、`GetLastError` は `ERROR_SHARING_VIOLATION` を示す。
''' <summary>
''' 指定されたファイルが他のプロセス(OneDrive等)によってロックされているか判定する
''' </summary>
''' <param name="strPath">ファイルのフルパス</param>
''' <returns>ロックされている場合はTrue</returns>
Private Function IsFileLocked(ByVal strPath As String) As Boolean
Dim hFile As LongPtr
' 書き込み権限(GENERIC_WRITE)を要求し、他のプロセスによる共有を一切許可しない設定で試行
' これにより、同期エンジンがハンドルを保持している場合に確実に検知できる
hFile = CreateFile(strPath, GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0)
If hFile = INVALID_HANDLE_VALUE Then
' エラーコードが共有違反(32)またはロック違反(33)であれば、ロックされていると判断
If GetLastError() = ERROR_SHARING_VIOLATION Or GetLastError() = ERROR_LOCK_VIOLATION Then
IsFileLocked = True
Exit Function
End If
Else
' 成功した場合は、即座にハンドルを閉じる
CloseHandle hFile
End If
IsFileLocked = False
End Function
3. 堅牢な保存アルゴリズム:Exponential Backoff(指数バックオフ)の導入
単に一定間隔でリトライするだけでは、同期が長引いた際に無駄なCPUリソースを消費し続ける。プロフェッショナルな実装では、リトライ間隔を徐々に広げる「指数バックオフ」的なアプローチ、あるいは一定の閾値に基づいた待機戦略を採用すべきだ。
また、`DoEvents` を適切に挟むことで、OS側のメッセージキューを処理させ、同期プロセスがハンドルを解放する隙を与える必要がある。
''' <summary>
''' クラウド同期の競合を回避しながら、安全に図面を保存する
''' </summary>
''' <param name="acadDoc">保存対象の図面オブジェクト</param>
''' <param name="strSavePath">保存先のフルパス</param>
Public Sub RobustSaveAs(ByRef acadDoc As Object, ByVal strSavePath As String)
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 10
Dim waitTime As Double
Dim currentWait As Double
' 1. 保存前のオブジェクト整合性チェック
' (ここでメモリリークや参照不備を排除しておく)
If acadDoc Is Nothing Then Err.Raise vbObjectError + 1, "RobustSave", "Document object is null."
' 2. リトライループ開始
For retryCount = 1 To MAX_RETRIES
' ファイルがロックされているか確認
If Not IsFileLocked(strSavePath) Then
' ロックされていなければ保存を実行
On Error Resume Next
acadDoc.SaveAs strSavePath
If Err.Number = 0 Then
Debug.Print "Success: Saved at retry " & retryCount
Exit Sub
Else
' 保存自体に失敗した場合(ディスクフル、パス不正等)は、即座にエラーを投げる
Err.Raise Err.Number, "RobustSave", "Save failed: " & Err.Description
End If
On Error GoTo 0
End If
' 3. バックオフ戦略:待機時間を指数関数的に増加させる (0.5s, 1s, 2s, 4s...)
currentWait = 0.5 * (2 ^ (retryCount - 1))
Debug.Print "Conflict detected. Retrying in " & currentWait & " seconds... (Attempt " & retryCount & ")"
' 待機処理
waitTime = Timer
Do While Timer < waitTime + currentWait
DoEvents ' OSに制御を戻し、同期プロセスの進行を促す
' Timerのラップアラウンド(深夜0時付近)への配慮が必要な場合は追加ロジックを挿入
Loop
Next retryCount
' 4. 最大リトライ回数を超えた場合
Err.Raise vbObjectError + 2, "RobustSave", "File is locked by another process (likely OneDrive/SharePoint) after maximum retries."
End Sub
4. アーキテクトの視点:オブジェクトのライフサイクルとメモリ管理
この高度な保存処理を実装する上で、忘れてはならない極めて重要な原則がある。それは、「AutoCADのCOMオブジェクトと、Windowsのファイルハンドルは、明確に異なるライフサイクルを持つ」ということだ。
VBA側で、`ThisDrawing` などのオブジェクトを不適切に保持し続けたり、保存処理中に他のプロセスがそのオブジェクトのメモリ領域を汚染したりすると、致命的なアクセス違反(Access Violation)を招く。
1. 明示的な解放: ループやバッチ処理の中で大量の図面を扱う場合、`Set doc = Nothing` を確実に行い、参照カウントを確実に減らすこと。
2. エラーの伝播: `RobustSave` のような低レイヤーに近い関数は、エラーを「解決」してはいけない。エラーを「検知」し、適切に「呼び出し元へ伝播(Raise)」させること。これにより、上位の管理システムが「ユーザーに介入を求めるべきか」「ログに記録してスキップすべきか」を判断できる。
結論
OneDrive/SharePoint環境におけるAutoCAD VBAの開発は、もはや単なる「コマンドの自動化」ではない。それは、非同期に動作するクラウド同期エンジンと、同期的なCADプロセスとの間で、いかにして整合性を保つかという「分散システムの調整(Orchestration)」の領域である。
Windows APIによるファイルハンドルの直接監視、そして指数バックオフを用いたリトライ戦略。これらを実装することで、あなたのシステムは「たまに失敗するツール」から、「プロフェッショナルの現場に耐えうる信頼の基盤」へと昇華する。
技術の真髄は、平時ではなく、予期せぬ競合が発生する「極限状態」においてこそ証明されるのだ。
