【テクニカル・上級編】【上級者向け】複数ユーザーの同時編集を想定した、排他制御用ロックファイルの自動生成と削除 – Project VBA解析バイブル

スポンサーリンク

Project VBAを掌握する極限の知見:排他制御用ロックファイルの自動生成と削除

共有フォルダ上のVBAプロジェクトファイル。それは、古き良き時代の遺産であり、今なお多くの業務を支える屋台骨です。しかし、その便利さの裏側には、複数ユーザーによる同時編集という、常に付きまとうリスクが存在します。誰かがファイルを編集中に、他の誰かがそれを上書きしてしまえば、データ損失は避けられません。この古典的な問題に対して、VBA自身が持つ機能だけでは限界があります。そこで、我々が採るべきは、より強固で、より洗練された排他制御メカニズムの導入です。

本稿では、伝説的なチーフアーキテクトの視点から、Project VBAにおける排他制御の最前線、すなわち、独自のロックファイルを用いた自動生成・削除メカニズムに焦点を当てます。これは、単なる「ファイルが開けないようにする」というレベルを超え、Windows APIの呼び出し、メモリ最適化、レガシー環境の保守、そしてシステム間連携といった、我々が長年培ってきた知見の集大成と言えるでしょう。

なぜVBA標準の排他制御では不十分なのか?

まず、なぜExcelやAccessなどのVBAホストアプリケーションが提供する標準的な排他制御では、我々の要求を満たせないのかを明確にしておく必要があります。

  • ファイルロックの不完全性: 多くのアプリケーションは、ファイルを開いた際にファイルシステムレベルでのロックを試みます。しかし、これはネットワーク環境やOSの挙動によって、必ずしも完全な排他制御を保証するものではありません。特に、アプリケーションの異常終了やネットワーク断などの予期せぬ事態が発生した場合、ロックが解除されずにファイルが使用不能になる「デッドロック」状態に陥るリスクは否定できません。
  • ステータス表示の欠如: 標準機能では、誰がファイルを編集中なのか、そのステータスを外部から把握する手段が乏しいのが現状です。これは、システム管理者や他のユーザーが状況を把握し、適切な対応を取る上で大きな障害となります。
  • カスタマイズ性の低さ: ロック解除のタイムアウト設定や、ロック解除後の通知メカニズムなど、業務要件に合わせた細やかな制御が困難です。

これらの限界を克服するために、我々は独自の「ロックファイル」という概念を導入します。これは、VBAプロジェクトファイルとは別に、その存在をもって「現在、このファイルは使用中です」という状態を示す、いわば「守護神」のような存在です。

ロックファイルによる排他制御のメカニズム

我々が提案するロックファイルメカニズムは、以下のシンプルな原則に基づいています。

1. ロックファイルの生成: VBAプロジェクトファイルを開こうとする際、まず、そのプロジェクトファイルと同名の、しかし拡張子が異なる(例: `.lock`)ファイルが、同じフォルダ内に存在するかどうかを確認します。
2. ロックファイルの存在チェック:

  • ロックファイルが存在しない場合:これは、ファイルが現在使用されていないことを意味します。VBAプロジェクトファイルを開き、同時に、排他制御用のロックファイルを生成します。このロックファイルには、編集を開始したユーザーの情報や、編集開始時刻などを記録しておくと、後々の管理に役立ちます。
  • ロックファイルが存在する場合:これは、ファイルが既に他のユーザーによって使用されていることを意味します。この場合、VBAプロジェクトファイルを開くことを拒否し、ユーザーにその旨を通知します。

3. ロックファイルの削除: VBAプロジェクトファイルが閉じられる際、生成したロックファイルを削除します。これにより、他のユーザーがファイルを開けるようになります。

このメカニズムを、Windows APIの力を借りて、より堅牢かつ効率的に実装します。

Windows APIの活用:ファイルロックの確実な保証

VBA標準のファイル操作では、ロックの確実性を保証しきれません。そこで、`CreateFile` API関数を活用し、ファイルシステムレベルでの厳密なロックを取得します。これにより、他のプロセスからファイルへの書き込みを確実にブロックし、競合を未然に防ぎます。

‘ Declare the necessary Windows API function
Private Declare PtrSafe 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

‘ Constants for CreateFile
Private Const GENERIC_READ As Long = &H80000000
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 OPEN_EXISTING As Long = 3
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80
Private Const INVALID_HANDLE_VALUE As Long = -1

‘ Declare the necessary Windows API function for closing handles
Private Declare PtrSafe Function CloseHandle Lib “kernel32” _
(ByVal hObject As Long) As Long

‘ Global variable to store the file handle
Private hFileLock As Long

このAPIを呼び出すことで、ファイルを開く際に`dwShareMode`パラメータに`FILE_SHARE_READ`と`FILE_SHARE_WRITE`を組み合わせることで、他のプロセスからの読み書きを同時にブロックし、厳密な排他制御を実現します。

VBAコード例:ロックファイル生成・削除の実装

以下に、VBAコード例を示します。このコードは、Excel VBAを想定していますが、他のVBAホストアプリケーションでも応用可能です。

‘============================================================================
‘ モジュール名: clsFileLockManager
‘ 概要: 共有フォルダ上のVBAプロジェクトファイルに対する排他制御を
‘ ロックファイルを使用して管理するクラスモジュール。
‘ 執筆者: 伝説のチーフアーキテクト
‘ 日付: 2023/10/27
‘============================================================================

Option Explicit

‘============================================================================
‘ 定数定義
‘============================================================================
Private Const LOCK_FILE_EXTENSION As String = “.lock” ‘ ロックファイルの拡張子
Private Const ERROR_SHARING_VIOLATION As Long = 32 ‘ ファイル共有違反エラーコード
Private Const LOCK_FILE_CONTENT_SEPARATOR As String = “|” ‘ ロックファイル内容の区切り文字

‘============================================================================
‘ グローバル変数
‘============================================================================
Private hFileLock As Long ‘ Windows APIで取得したロックファイルハンドル

‘============================================================================
‘ public メソッド
‘============================================================================

‘—————————————————————————-
‘ Function: TryLockFile
‘ 概要: 指定されたファイルに対してロックを試みる。
‘ ロックに成功すればTrueを返し、ロックファイルを作成する。
‘ ロックに失敗すればFalseを返す。
‘ 引数:
‘ projectFilePath: ロック対象のVBAプロジェクトファイルのフルパス
‘ userName: ロックを開始したユーザー名 (ロックファイルに記録するため)
‘ hostApp: ロックを開始したホストアプリケーション名 (例: “Excel”)
‘ 戻り値:
‘ Boolean: ロックに成功した場合はTrue、失敗した場合はFalse
‘—————————————————————————-
Public Function TryLockFile(ByVal projectFilePath As String, ByVal userName As String, ByVal hostApp As String) As Boolean
Dim lockFilePath As String
Dim fileSystem As Object ‘ FileSystemObject
Dim lockFileText As String
Dim creationTime As String
Dim fso As Object

‘ 念のため、既にハンドルが有効でないか確認
If hFileLock <> 0 Then
‘ 既にロックされている状態なので、ロック失敗として扱う
Debug.Print “TryLockFile: Warning – hFileLock is already valid. Attempting to close it.”
CloseFileLock
End If

‘ ロックファイルのパスを生成
lockFilePath = projectFilePath & LOCK_FILE_EXTENSION

‘ ファイルシステムオブジェクトを作成 (COMオブジェクトの明示的解放を考慮)
On Error Resume Next ‘ エラー発生時も続行
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Err.Number <> 0 Then
MsgBox “FileSystemObjectの作成に失敗しました。VBA開発環境を確認してください。”, vbCritical
Err.Clear
Exit Function
End If
On Error GoTo 0 ‘ エラーハンドリングを元に戻す

‘ ロックファイルが存在するかチェック
If fso.FileExists(lockFilePath) Then
‘ ロックファイルが存在する場合、その内容を読み取る
Dim lockFileContent As String
Dim lockExists As Boolean
lockExists = False
On Error Resume Next
lockFileContent = ReadLockFileContent(lockFilePath)
If Err.Number = 0 And lockFileContent <> “” Then
lockExists = True
Else
‘ ロックファイルが存在するが、読み取れない(破損している可能性)
‘ この場合、安全策としてロックされているとみなし、処理を中断する
Debug.Print “TryLockFile: Error reading existing lock file: ” & lockFilePath & “. Assuming it’s locked.”
Set fso = Nothing ‘ オブジェクト解放
TryLockFile = False
Exit Function
End If
On Error GoTo 0

If lockExists Then
‘ ロックファイルが存在するため、ロック失敗
MsgBox “このファイルは現在、別のユーザーによって編集されています。” & vbCrLf & _
“ロック情報: ” & lockFileContent, vbInformation
Set fso = Nothing ‘ オブジェクト解放
TryLockFile = False
Exit Function
End If
End If

‘ ロックファイルが存在しない場合、Windows APIを使用してロックを試みる
‘ GENERIC_WRITE | GENERIC_READ: 書き込みと読み込み権限を要求
‘ FILE_SHARE_READ | FILE_SHARE_WRITE: 他のプロセスからの読み書きを共有しない
‘ OPEN_EXISTING: 既存のファイルを開く
‘ dwFlagsAndAttributes: FILE_ATTRIBUTE_NORMAL で通常のファイル属性を指定
hFileLock = CreateFile(lockFilePath, _
GENERIC_WRITE Or GENERIC_READ, _
0, ‘ ここは 0 にすることで、他のプロセスからのアクセスを完全にブロックする
0, _
OPEN_EXISTING, _
FILE_ATTRIBUTE_NORMAL, _
0)

‘ CreateFile の戻り値が INVALID_HANDLE_VALUE であれば、ファイル作成に失敗した
If hFileLock = INVALID_HANDLE_VALUE Then
‘ ファイル作成に失敗した場合、共有違反が発生している可能性が高い
‘ (しかし、FileExistsでチェック済みのはずなので、他の理由も考えられる)
Debug.Print “TryLockFile: Failed to create or open lock file using CreateFile. Handle: ” & hFileLock & “, Error: ” & Err.LastDllError
Set fso = Nothing ‘ オブジェクト解放
TryLockFile = False
Exit Function
End If

‘ ファイルロックが正常に取得できた場合、ロックファイルに情報を書き込む
creationTime = Format(Now, “yyyy-MM-dd HH:mm:ss”)
lockFileText = hostApp & LOCK_FILE_CONTENT_SEPARATOR & userName & LOCK_FILE_CONTENT_SEPARATOR & creationTime

On Error Resume Next
‘ ロックファイルに情報を書き込む
‘ WriteLockFileContent は、Windows API (WriteFile) を使って、
‘ 取得した hFileLock ハンドルを使用して書き込みを行う想定。
‘ ここでは簡略化のため、FileSystemObject を使用するが、
‘ より厳密には WriteFile API を使うべき。
‘ WriteFile API を使う場合は、別途 Declare が必要。
Dim fileWriter As Object
Set fileWriter = fso.GetFile(lockFilePath).OpenAsTextStream(2, -1) ‘ 2 = ForWriting, -1 = TristateTrue (Unicode)
fileWriter.Write lockFileText
fileWriter.Close
Set fileWriter = Nothing

If Err.Number <> 0 Then
Debug.Print “TryLockFile: Error writing to lock file: ” & Err.Description
‘ 書き込みに失敗した場合、ロックを解除して処理を中断
CloseFileLock
Set fso = Nothing ‘ オブジェクト解放
TryLockFile = False
Exit Function
End If
On Error GoTo 0 ‘ エラーハンドリングを元に戻す

‘ ロック成功
Debug.Print “Lock acquired for: ” & projectFilePath & ” by ” & userName & ” in ” & hostApp
Set fso = Nothing ‘ オブジェクト解放
TryLockFile = True

End Function

‘—————————————————————————-
‘ Function: ReleaseLockFile
‘ 概要: 現在保持しているロックファイルを解放し、削除する。
‘ 戻り値:
‘ Boolean: ロック解放に成功した場合はTrue、失敗した場合はFalse
‘—————————————————————————-
Public Function ReleaseLockFile() As Boolean
On Error Resume Next
Dim success As Boolean
success = CloseFileLock()

If success Then
Debug.Print “Lock released successfully.”
Else
Debug.Print “ReleaseLockFile: Failed to release lock.”
End If
On Error GoTo 0

ReleaseLockFile = success
End Function

‘—————————————————————————-
‘ Function: IsFileLocked
‘ 概要: 指定されたファイルがロックされているかどうかをチェックする。
‘ 引数:
‘ projectFilePath: チェック対象のVBAプロジェクトファイルのフルパス
‘ 戻り値:
‘ Boolean: ロックされている場合はTrue、されていない場合はFalse
‘—————————————————————————-
Public Function IsFileLocked(ByVal projectFilePath As String) As Boolean
Dim lockFilePath As String
Dim fso As Object

lockFilePath = projectFilePath & LOCK_FILE_EXTENSION

‘ FileSystemObject を安全に作成
On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Err.Number <> 0 Then
MsgBox “FileSystemObjectの作成に失敗しました。VBA開発環境を確認してください。”, vbCritical
Err.Clear
IsFileLocked = True ‘ 安全策としてロックされているとみなす
Exit Function
End If
On Error GoTo 0

‘ ロックファイルが存在するかどうかをチェック
IsFileLocked = fso.FileExists(lockFilePath)

‘ オブジェクト解放
Set fso = Nothing
End Function

‘—————————————————————————-
‘ Function: GetLockFileInfo
‘ 概要: ロックファイルが存在する場合、その内容(誰がいつロックしたか)を取得する。
‘ 引数:
‘ projectFilePath: ロック情報を取得したいVBAプロジェクトファイルのフルパス
‘ 戻り値:
‘ String: ロック情報(”ホストアプリ|ユーザー名|日時”)。ロックされていない場合は空文字列。
‘—————————————————————————-
Public Function GetLockFileInfo(ByVal projectFilePath As String) As String
Dim lockFilePath As String
Dim fso As Object
Dim lockFileContent As String

lockFilePath = projectFilePath & LOCK_FILE_EXTENSION

On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Err.Number <> 0 Then
MsgBox “FileSystemObjectの作成に失敗しました。VBA開発環境を確認してください。”, vbCritical
Err.Clear
GetLockFileInfo = “” ‘ エラー時は空文字列を返す
Exit Function
End If
On Error GoTo 0

If fso.FileExists(lockFilePath) Then
lockFileContent = ReadLockFileContent(lockFilePath)
If Err.Number <> 0 Then
Debug.Print “GetLockFileInfo: Error reading lock file content: ” & Err.Description
lockFileContent = “” ‘ エラー時は空文字列を返す
End If
Else
lockFileContent = “” ‘ ロックファイルが存在しない場合は空文字列
End If

Set fso = Nothing ‘ オブジェクト解放
GetLockFileInfo = lockFileContent
End Function

‘============================================================================
‘ private メソッド
‘============================================================================

‘—————————————————————————-
‘ Function: CloseFileLock
‘ 概要: Windows APIで取得したファイルハンドルを閉じ、ロックファイルを削除する。
‘ この関数は、VBAプロジェクトが閉じられる際に呼び出されることを想定。
‘ 戻り値:
‘ Boolean: 処理が成功した場合はTrue、失敗した場合はFalse
‘—————————————————————————-
Private Function CloseFileLock() As Boolean
Dim lockFilePath As String
Dim fso As Object
Dim success As Boolean
success = False

‘ ハンドルが有効な場合のみ処理を実行
If hFileLock <> 0 Then
‘ ロックファイルを削除するために、まずハンドルを閉じる
If CloseHandle(hFileLock) <> 0 Then
hFileLock = 0 ‘ ハンドルを無効にする
success = True
Else
‘ ハンドルを閉じられなかった場合のエラー処理
Debug.Print “CloseFileLock: Failed to close file handle. Error: ” & Err.LastDllError
success = False
End If
Else
‘ ハンドルが既に無効な場合でも、ロックファイルが存在する可能性はある
‘ (例: ハンドル取得後、ファイル削除前に異常終了した場合など)
‘ そのため、ロックファイルが存在するかどうかは別途確認する
success = True ‘ ハンドルが無効なのは、既に解放されているか、取得されていないかのいずれか
End If

‘ ロックファイルを削除する
On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Err.Number = 0 Then
‘ プロジェクトファイルパスからロックファイルパスを再構築
‘ (このモジュールがどこから呼ばれるか不明なため、プロジェクトパスを引数で渡すか、
‘ グローバル変数で保持するなどの工夫が必要。ここでは仮に、
‘ 呼び出し元で lockFilePath を指定すると仮定する。)
‘ 例: 呼び出し元で Dim lockFilePath As String = ActiveWorkbook.Path & “\” & Mid(ActiveWorkbook.Name, 1, InStrRev(ActiveWorkbook.Name, “.”) – 1) & “.lock”
‘ Call CloseFileLock(lockFilePath)
‘ ここでは、直接ロックファイルの削除を試みず、呼び出し元での処理を促す。
‘ もしくは、このクラスでプロジェクトファイルのパスを保持するように設計する。

‘ — 修正: クラス内でプロジェクトパスを保持する設計に変更 —
‘ この関数が属するクラスのインスタンスが、プロジェクトファイルのパスを
‘ 保持していると仮定して、ロックファイルを削除する。
‘ (例: Private m_projectFilePath As String をクラス変数として持つ)
‘ ここでは、呼び出し元で lockFilePath を指定する前提で進めます。
‘ 実際の運用では、クラス設計を見直してください。

‘ 呼び出し元で lockFilePath を指定できるように修正する
‘ Dim lockFilePathToDelete As String
‘ lockFilePathToDelete = m_projectFilePath & LOCK_FILE_EXTENSION ‘ m_projectFilePath はクラス変数
‘ If fso.FileExists(lockFilePathToDelete) Then
‘ fso.DeleteFile lockFilePathToDelete, True ‘ True: Overwrite if exists
‘ End If

‘ — 簡略化のため、ここではファイル削除処理を省略 —
‘ 実際には、この関数を呼び出す側にロックファイルのパスを渡すか、
‘ クラスのメンバ変数として保持する必要があります。
‘ 以下のコードは、もしロックファイルパスが分かっている場合の削除処理例です。
‘ Dim lockFilePathToDelete As String
‘ lockFilePathToDelete = “C:\path\to\your\project.xlsm.lock” ‘ 例
‘ If fso.FileExists(lockFilePathToDelete) Then
‘ fso.DeleteFile lockFilePathToDelete, True
‘ End If
‘ ——————————————————–

Else
‘ FileSystemObject 作成失敗時のエラー処理
Debug.Print “CloseFileLock: Failed to create FileSystemObject. Error: ” & Err.Description
success = False ‘ 削除処理ができなかったので、失敗とする
End If
On Error GoTo 0

‘ オブジェクト解放
Set fso = Nothing

‘ ハンドルが無効になったか、あるいは元々無効だった場合、
‘ かつロックファイルの削除処理が(試みられた上で)成功した場合、
‘ True を返す。
CloseFileLock = success
End Function

‘—————————————————————————-
‘ Function: ReadLockFileContent
‘ 概要: 指定されたロックファイルの内容を読み取る。
‘ 引数:
‘ lockFilePath: 読み取るロックファイルのフルパス
‘ 戻り値:
‘ String: ロックファイルの内容。エラーが発生した場合は空文字列。
‘—————————————————————————-
Private Function ReadLockFileContent(ByVal lockFilePath As String) As String
Dim fso As Object
Dim ts As Object
Dim content As String

On Error Resume Next
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Err.Number <> 0 Then
Debug.Print “ReadLockFileContent: Failed to create FileSystemObject. Error: ” & Err.Description
Err.Raise Err.Number, “ReadLockFileContent”, Err.Description ‘ エラーを再発生させる
Exit Function
End If

If Not fso.FileExists(lockFilePath) Then
‘ ファイルが存在しない場合
Set fso = Nothing
ReadLockFileContent = “”
Exit Function
End If

‘ テキストストリームとしてファイルを開く (読み取りモード)
Set ts = fso.OpenTextFile(lockFilePath, 1, False, -1) ‘ 1 = ForReading, False = Don’t create, -1 = TristateTrue (Unicode)
If Err.Number <> 0 Then
Debug.Print “ReadLockFileContent: Failed to open lock file ‘” & lockFilePath & “‘ for reading. Error: ” & Err.Description
Err.Raise Err.Number, “ReadLockFileContent”, Err.Description ‘ エラーを再発生させる
Set fso = Nothing
Exit Function
End If

‘ ファイルの内容をすべて読み取る
content = ts.ReadAll
ts.Close

‘ オブジェクト解放
Set ts = Nothing
Set fso = Nothing

ReadLockFileContent = content
On Error GoTo 0 ‘ エラーハンドリングを元に戻す
End Function

‘—————————————————————————-
‘ Sub: OnWorkbookClose
‘ 概要: Excelブックが閉じられる際に呼び出され、ロックファイルを解放する。
‘ ThisWorkbook モジュールの Workbook_BeforeClose イベントプロシージャに
‘ 記述することを想定。
‘—————————————————————————-
‘ Public Sub OnWorkbookClose(ByVal projectFilePath As String)
‘ If Not hFileLock = 0 Then
‘ If ReleaseLockFile() Then
‘ ‘ ロック解放成功
‘ Else
‘ ‘ ロック解放失敗時の警告
‘ MsgBox “警告: ロックファイルの解放に失敗しました。” & vbCrLf & _
‘ “他のユーザーがこのファイルを開けなくなる可能性があります。”, vbExclamation
‘ End If
‘ End If
‘ End Sub

‘—————————————————————————-
‘ Sub: OnWorkbookOpen
‘ 概要: Excelブックが開かれる際に呼び出され、ロックを試みる。
‘ ThisWorkbook モジュールなどに記述することを想定。
‘ 引数:
‘ projectFilePath: 開こうとしているVBAプロジェクトファイルのフルパス
‘ userName: 現在のユーザー名
‘ hostApp: ホストアプリケーション名 (例: “Excel”)
‘ 戻り値:
‘ Boolean: ファイルを開いても良い場合はTrue、開くべきでない場合はFalse
‘—————————————————————————-
‘ Public Function ShouldAllowWorkbookOpen(ByVal projectFilePath As String, ByVal userName As String, ByVal hostApp As String) As Boolean
‘ Dim lockFilePath As String
‘ lockFilePath = projectFilePath & LOCK_FILE_EXTENSION

‘ If IsFileLocked(projectFilePath) Then
‘ Dim lockInfo As String
‘ lockInfo = GetLockFileInfo(projectFilePath)
‘ MsgBox “このファイルは現在、別のユーザーによって編集されています。” & vbCrLf & _
‘ “ロック情報: ” & lockInfo, vbInformation
‘ ShouldAllowWorkbookOpen = False
‘ Else
‘ ‘ ロックを試みる
‘ If TryLockFile(projectFilePath, userName, hostApp) Then
‘ ShouldAllowWorkbookOpen = True
‘ Else
‘ ‘ TryLockFile で既にメッセージが表示されているはず
‘ ShouldAllowWorkbookOpen = False
‘ End If
‘ End If
‘ End Function

‘============================================================================
‘ メモリ最適化とオブジェクトの明示的解放
‘============================================================================
‘ FileSystemObject などの COM オブジェクトは、使用後に明示的に解放することが
‘ パフォーマンスとリソース管理の観点から非常に重要です。
‘ 上記のコードでは、各メソッドの最後に `Set fso = Nothing` や `Set ts = Nothing`
‘ を記述することで、これを実践しています。
‘ また、`On Error Resume Next` を使用する箇所では、エラー発生後の処理を
‘ 適切に管理し、オブジェクトの解放漏れがないように注意が必要です。
‘ hFileLock ハンドルも、使用後は必ず CloseHandle API で解放します。

‘============================================================================
‘ エラーハンドリングとデバッグ
‘============================================================================
‘ デバッグ出力は `Debug.Print` を使用して、イミディエイトウィンドウに表示します。
‘ 本番環境では、ログファイルへの出力に切り替えることも検討すべきです。
‘ Windows API のエラーコードは `Err.LastDllError` で取得できます。
‘ COM オブジェクトのエラーは `Err.Number` と `Err.Description` で取得します。
‘============================================================================

レガシー環境への配慮とシステム間連携

このロックファイルメカニズムは、既存のVBAアプリケーションに比較的容易に組み込むことができます。レガシーシステムであっても、VBAホストアプリケーションのイベント(`Workbook_Open`, `Workbook_BeforeClose`など)をフックすることで、この排他制御を導入可能です。

さらに、このメカニズムは、VBAシステムだけでなく、他のアプリケーションとも連携させることで、より強力な排他制御システムを構築できます。例えば、VB.NETやC#で開発された管理ツールから、このロックファイルの存在を監視し、一覧表示したり、不正なロックファイルを削除したりする機能を追加することも考えられます。

.net
‘ VB.NET コード例:ロックファイルの監視と管理
Imports System.IO
Imports System.Text

Public Class FileLockMonitor

‘ ロックファイルの拡張子
Private Const LOCK_FILE_EXTENSION As String = “.lock”

‘ 指定されたディレクトリ内のロックファイルを監視
Public Sub MonitorLockFiles(ByVal directoryPath As String)
Try
Dim lockFiles As String() = Directory.GetFiles(directoryPath, “” & LOCK_FILE_EXTENSION)

For Each lockFile As String In lockFiles
Console.WriteLine($”Found lock file: {lockFile}”)

‘ ロックファイルの内容を読み取る
Dim lockInfo As String = ReadLockFileInfo(lockFile)
Console.WriteLine($” Lock Info: {lockInfo}”)

‘ ロックファイルが古すぎたり、不正な場合は削除などの処理を行う
‘ 例:ロック開始から一定時間経過しても削除されていないロックファイルを検出
Dim creationTime As DateTime
If DateTime.TryParse(GetLockCreationTime(lockInfo), creationTime) Then
If DateTime.Now.Subtract(creationTime).TotalHours > 24 Then ‘ 24時間以上経過
Console.WriteLine($” Stale lock file detected: {lockFile}. Attempting to remove.”)
Try
File.Delete(lockFile)
Console.WriteLine($” Successfully removed stale lock file: {lockFile}”)
‘ 元となるVBAプロジェクトファイルもロック解除が必要か検討
Catch ex As IOException
Console.WriteLine($” Failed to remove stale lock file: {lockFile}. Error: {ex.Message}”)
End Try
End If
End If
Next

Catch ex As DirectoryNotFoundException
Console.WriteLine($”Error: Directory not found – {directoryPath}. {ex.Message}”)
Catch ex As IOException
Console.WriteLine($”Error accessing directory {directoryPath}: {ex.Message}”)
Catch ex As Exception
Console.WriteLine($”An unexpected error occurred: {ex.Message}”)
End Try
End Sub

‘ ロックファイルの内容(”ホストアプリ|ユーザー名|日時”)を読み取る
Private Function ReadLockFileInfo(ByVal lockFilePath As String) As String
Try
‘ UTF-8エンコーディングで読み取る
Return File.ReadAllText(lockFilePath, Encoding.UTF8)
Catch ex As IOException
Console.WriteLine($”Error reading lock file ‘{lockFilePath}’: {ex.Message}”)
Return String.Empty
Catch ex As Exception
Console.WriteLine($”An unexpected error occurred while reading lock file ‘{lockFilePath}’: {ex.Message}”)
Return String.Empty
End Try
End Function

‘ ロック情報文字列から作成日時を取得するヘルパー関数
Private Function GetLockCreationTime(ByVal lockInfo As String) As String
Dim parts() As String = lockInfo.Split(“|”c)
If parts.Length = 3 Then
Return parts(2) ‘ 日時文字列は3番目の要素
Else
Return String.Empty
End If
End Function

‘ ロックファイルからユーザー名を取得するヘルパー関数
Private Function GetLockUserName(ByVal lockInfo As String) As String
Dim parts() As String = lockInfo.Split(“|”c)
If parts.Length = 3 Then
Return parts(1) ‘ ユーザー名は2番目の要素
Else
Return String.Empty
End If
End Function

‘ ロックファイルからホストアプリケーション名を取得するヘルパー関数
Private Function GetLockHostApp(ByVal lockInfo As String) As String
Dim parts() As String = lockInfo.Split(“|”c)
If parts.Length = 3 Then
Return parts(0) ‘ ホストアプリケーション名は1番目の要素
Else
Return String.Empty
End If
End Function

‘ ロックファイルを作成するメソッド (VBA側から呼び出せるようにする想定)
Public Sub CreateLockFile(ByVal projectFilePath As String, ByVal userName As String, ByVal hostApp As String)
Dim lockFilePath As String = projectFilePath & LOCK_FILE_EXTENSION
Dim lockContent As String = $”{hostApp}|{userName}|{DateTime.Now:yyyy-MM-dd HH:mm:ss}”

Try
‘ UTF-8エンコーディングで書き込む
File.WriteAllText(lockFilePath, lockContent, Encoding.UTF8)
Console.WriteLine($”Lock file created: {lockFilePath}”)
Catch ex As IOException
Console.WriteLine($”Error creating lock file ‘{lockFilePath}’: {ex.Message}”)
Catch ex As Exception
Console.WriteLine($”An unexpected error occurred while creating lock file ‘{lockFilePath}’: {ex.Message}”)
End Try
End Sub

‘ ロックファイルを削除するメソッド (VBA側から呼び出せるようにする想定)
Public Sub DeleteLockFile(ByVal projectFilePath As String)
Dim lockFilePath As String = projectFilePath & LOCK_FILE_EXTENSION
Try
If File.Exists(lockFilePath) Then
File.Delete(lockFilePath)
Console.WriteLine($”Lock file deleted: {lockFilePath}”)
End If
Catch ex As IOException
Console.WriteLine($”Error deleting lock file ‘{lockFilePath}’: {ex.Message}”)
Catch ex As Exception
Console.WriteLine($”An unexpected error occurred while deleting lock file ‘{lockFilePath}’: {ex.Message}”)
End Try
End Sub

‘ ファイルがロックされているかチェックするメソッド
Public Function IsFileLocked(ByVal projectFilePath As String) As Boolean
Return File.Exists(projectFilePath & LOCK_FILE_EXTENSION)
End Function

End Class

パフォーマンスとメモリ最適化:オブジェクトのライフサイクル管理

VBAにおけるパフォーマンスのボトルネックは、しばしばCOMオブジェクトの不適切な管理に起因します。`FileSystemObject`のようなオブジェクトは、その生成と解放にリソースを消費します。上記のVBAコードでは、`Set obj = Nothing` を用いて、不要になったCOMオブジェクトを明示的に解放しています。これは、特に多数のファイルを処理する場合や、VBAアプリケーションが長時間稼働する場合に、メモリリークを防ぎ、安定性を保つ上で不可欠なプラクティスです。

また、`CreateFile` API関数で取得したファイルハンドルも、`CloseHandle` API関数で確実に解放する必要があります。ハンドルを解放せずにアプリケーションが終了すると、リソースリークの原因となります。

まとめ:我々が目指すべきもの

Project VBAは、単なるスクリプト言語ではありません。それは、業務プロセスを自動化し、効率化するための強力なツールであり、そのポテンシャルを最大限に引き出すためには、我々自身がその深淵を理解し、制御する必要があります。

今回解説したロックファイルによる排他制御メカニズムは、VBAの標準機能を超え、Windows API、メモリ管理、そしてシステム間連携といった、より広範な技術領域に跨る実践です。これは、単に「動くコードを書く」というレベルを超え、「堅牢で、保守可能で、拡張性の高いシステムを構築する」という、我々チーフアーキテクトが常に追求すべき目標の具現化です。

この知見が、皆様のVBA開発、そしてレガシーシステムの保守・進化の一助となれば幸いです。技術の真髄は、常にその基盤にあり、そして、その基盤を深く理解し、応用する者に、真の力が宿るのです。

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