【上級者/チーフアーキテクトが説く】共有フォルダの暗部:マルチユーザー環境におけるProject VBAファイル排他制御の極限実装
Microsoft Project(MSP)を企業内の複数名で運用する際、多くのシステム管理者が直面するのが「共有フォルダ上の`.mpp`ファイルを巡る排他制御の欠如」という悪夢である。
MS Projectの標準機能や単純なVBAの `Workbooks.Open` のような発想では、ファイルを開く際にロックの競合が発生し、ランタイムエラー(実行時エラー)や最悪のデータ破損を引き起こす。Excelであれば「読み取り専用で開く」ダイアログが親切にポップアップするが、Project VBAの背後にあるCOMオブジェクト群は、そんな融通を利かせてはくれない。
今回は、数々のレガシーシステムと闘ってきたシニアエンジニアに向けて、Windows APIのファイル共有モード制御と、Projectオブジェクトモデルのライフサイクル管理を完全に同期させた、プロダクションレベルの排他制御アーキテクトコードを提示する。
—
1. なぜVBA標準のファイルオープンでは太刀打ちできないのか
Project VBAにおいて、単にファイルを読み書きしようと `FileOpenEx` メソッドを実行した場合、すでに他者が排他ロックをかけているファイルを検知する前に、VBAランタイムがトラップ不可能な致命的エラーを投げるか、勝手に「読み取り専用」でサイレントオープンしてしまい、後続のベースライン設定やタスク更新処理でデータ不整合を起こす。
真のプロフェッショナルであれば、ファイルを開くという重い処理を行う前に、OSレベルでファイルの占有状態を安全にプローブ(探査)しなければならない。
これには、Windows APIの `CreateFileW` 関数を用い、「他のプロセスに書き込み権を与えずにオープンできるか」をテストする手法が最も確実かつ低オーバーヘッドである。
—
2. 実装アーキテクチャ:APIによるファイルロックプローブとCOM制御
以下のコードは、共有フォルダ上の `.mpp` ファイルに対し、強制的な破壊を伴わずに安全な排他チェックを行い、安全な領域のみでProjectインスタンスを操作する実践的モジュールである。
Option Explicit
‘ ==============================================================================
‘ モジュール名: MdlFileLockController
‘ 概要: Windows APIを活用したMS Projectファイルの排他制御および安全なオープン処理
‘ ==============================================================================
‘ Win32 API declarations for file sharing check
If VBA7 Then
Private Declare PtrSafe Function CreateFileW Lib “kernel32” ( _
ByVal lpFileName As LongPtr, _
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 LongPtr
Private Declare PtrSafe Function GetLastError Lib “kernel32” () As Long
Else
Private Declare Function CreateFileW Lib “kernel32” ( _
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 for CreateFileW
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 LongPtr = -1
‘ System Error Codes
Private Const ERROR_SHARING_VIOLATION As Long = 32
Private Const ERROR_LOCK_VIOLATION As Long = 33
Public Enum ProjectAccessStatus
pasSuccess = 0
pasLockedByOther = 1
pasFileNotFound = 2
pasSystemError = 3
End Enum
‘ ==============================================================================
‘ 関数名: TestFileExclusiveAccess
‘ 概要: 指定されたファイルが他のユーザー/プロセスに占有されていないかをテストする
‘ ==============================================================================
Public Function TestFileExclusiveAccess(ByVal filePath As String) As ProjectAccessStatus
Dim hFile As LongPtr
Dim errCode As Long
‘ パスの存在確認(FileSystemObjectを使うまでもないが堅牢性のため)
If Dir(filePath) = “” Then
TestFileExclusiveAccess = pasFileNotFound
Exit Function
End If
‘ 書き込み権限を持たせて排他オープンを試みる
‘ 他者が開いている場合、FILE_SHARE_WRITE または GENERIC_WRITE で弾かれる
hFile = CreateFileW( _
StrPtr(filePath), _
GENERIC_READ Or GENERIC_WRITE, _
0, _ ‘ 共有なし(他のプロセスに読み書きを許さない)
0, _
OPEN_EXISTING, _
FILE_ATTRIBUTE_NORMAL, _
0)
If hFile = INVALID_HANDLE_VALUE Then
errCode = GetLastError()
If errCode = ERROR_SHARING_VIOLATION Or errCode = ERROR_LOCK_VIOLATION Then
TestFileExclusiveAccess = pasLockedByOther
Else
TestFileExclusiveAccess = pasSystemError
End If
Exit Function
End If
‘ ハンドルが取得できた=現在誰も排他ロックしていない(安全)
CloseHandle hFile
TestFileExclusiveAccess = pasSuccess
End Function
‘ ==============================================================================
‘ プロシージャ名: SafeOpenProjectWithBaseline
‘ 概要: 排他制御をクリアした上で、安全にプロジェクトを開きベースラインを設定する
‘ ==============================================================================
Public Sub SafeOpenProjectWithBaseline(ByVal filePath As String)
Dim accessStatus As ProjectAccessStatus
Dim prjApp As MSProject.Application
Dim targetProj As MSProject.Project
On Error GoTo ErrorHandler
‘ 1. OSレベルでの排他チェック
accessStatus = TestFileExclusiveAccess(filePath)
Select Case accessStatus
Case pasLockedByOther
MsgBox “指定されたプロジェクトファイルは現在、他のユーザーが編中です。” & vbCrLf & _
“データの破損を防ぐため、処理を中断します。”, vbCritical, “排他制御アラート”
Exit Sub
Case pasFileNotFound
MsgBox “プロジェクトファイルが見つかりません: ” & filePath, vbExclamation, “ファイルエラー”
Exit Sub
Case pasSystemError
MsgBox “予期せぬシステムエラーが発生しました。エラーコードを確認してください。”, vbCritical, “API Error”
Exit Sub
Case pasSuccess
‘ 処理続行
End Select
‘ 2. MS Project アプリケーションのインスタンス化 (COMのライフサイクル管理)
‘ 既存のインスタンスを掴むのではなく、独立したプロセスとして安全に制御する
Set prjApp = New MSProject.Application
prjApp.Visible = False ‘ バックグラウンド実行でパフォーマンスを最大化
‘ 3. ファイルオープン (ReadOnlyを明示的に解除した排他オープン)
‘ ※環境に応じてパスワード引数等を付加すること
prjApp.FileOpenEx Name:=filePath, ReadOnly:=False
Set targetProj = prjApp.ActiveProject
‘ 4. ベースライン設定などのクリティカルな処理
‘ 例: ベースライン 1 の保存
targetProj.SetBaseline Baseline:=pjBaseline1
‘ 変更の保存
targetProj.Save
MsgBox “プロジェクトの排他処理およびベースライン設定が正常に完了しました。”, vbInformation, “完了”
CleanUp:
‘ 5. メモリ解放とCOMオブジェクトの厳密な破棄 (最重要)
On Error Resume Next
If Not targetProj Is Nothing Then Set targetProj = Nothing
If Not prjApp Is Nothing Then
prjApp.FileCloseEx pjDoNotSave ‘ 二重保存防止
prjApp.Quit
Set prjApp = Nothing
End If
Exit Sub
ErrorHandler:
MsgBox “ランタイムエラーが発生しました: ” & Err.Description, vbCritical, “Critical Exception”
Resume CleanUp
End Sub
—
3. チーフアーキテクトが指摘する「メモリ管理とCOMの罠」
VBAにおける最大の悪習は、オブジェクト変数を `Nothing` に明示せず、ガベージコレクションの気まぐれに身を委ねることだ。特に MS Project や Excel などのOut-of-Process COMサーバーを背負うVBAコードでは、これをやるとプロセスリーク(ゾンビプロセスの残留)が確実に発生する。
オブジェクトライフサイクルの鉄則
1. `Set prjApp = Nothing` だけでは不十分:
`prjApp.Quit` を明示的にコールし、COMサーバー側のシャットダウンを強制すること。さもないと、タスクマネージャーのバックグラウンドプロセスに `WINPROJ.EXE` が亡霊のように居座り続け、次回実行時のファイルロックを引き起こす原因となる。
2. エラーハンドリング時の確実な脱出経路 (`CleanUp`):
いかなる例外(ネットワーク切断、権限剥奪など)が発生しようとも、`On Error GoTo CleanUp` を通る動線を確保し、OSハンドルとCOM参照を確実に破棄しなければならない。
—
4. レガシー環境・システム間連携における実戦的知見
共有フォルダ(SMB環境)におけるファイルロックは、ネットワークの遅延や、OSの oplock(オプティミスティック・ロック)機構の挙動によって、タイムラグが生じる。
- オプティミスティック・ロックの弊害:
WindowsクライアントOS間でのファイル共有では、クライアント側がファイルをキャッシュするため、リアルタイムな排他検知が機能しない瞬間がある。これを完全に防ぐには、ファイルサーバー側の設定でオプティミスティック・ロックを無効化するか、あるいは今回紹介したようなネイティブAPI(`CreateFileW`)の共有モード `0`(完全排他要求)による直接プローブが唯一にして最強の防衛策となる。
- RDBやWeb API連携への移行前段階としての価値:
本来、本格的なマルチユーザー環境であれば、MS Projectを共有フォルダで運用するべきではなく、Project Server / Project Online、あるいはデータベース(SQL Server)バックエンドへの移行が正解である。しかし、予算やレガシーな業務プロセスの制約により「どうしても共有フォルダの `.mpp` を使わざるを得ない」現場において、本稿の排他制御ロジックは、データ崩壊という致命傷を防ぎ、システムの延命を図るための最後の砦となる。
妥協のないコードとアーキテクチャ設計によってのみ、不安定なレガシーインフラは制御可能となる。現場のコードに今すぐこの排他制御を組み込み、夜間バグ報告の恐怖から解放されることを願う。
