ネットワークの戦場を制する:Windows APIによる排他制御を用いた、堅牢なるPowerPoint PDF一括変換アーキテクチャ
共有ネットワークドライブ上で動く自動化スクリプト。それは、多くのエンジニアにとって「いつ、誰の操作によってクラッシュするか分からない」という、時限爆弾のような存在だ。
「ファイルが開けません」「書き込み権限がありません」……。
標準的なVBAの `Dir` 関数や `FileSystemObject` (FSO) だけを用いたチェックでは、コンマ数秒の間に発生するレースコンディション(競合状態)を防ぐことはできない。ファイルが存在することを確認した直後、他ユーザーがファイルを開く。この絶望的な隙間を埋めるには、VBAの抽象化されたレイヤーを飛び越え、Windowsカーネルの排他制御に直接介入するしかない。
本稿では、エンタープライズ環境での運用に耐えうる、Windows APIとFSOを高度に融合させた「極限のバッチ変換スクリプト」の設計思想とその実装を詳解する。
1. なぜ「ファイル存在チェック」では不十分なのか
多くの開発者が陥る罠がある。それは、`If fso.FileExists(path) Then` という条件分岐を、ファイル操作の正当性の担保だと誤認することだ。
ファイルシステムにおける「存在」と「利用可能(Lock-free)」は全く別次元の話である。
共有環境において、対象のPPTXファイルが他者の編集プロセスに掴まれている場合、`Open` メソッドは即座に実行時エラーを吐き出し、あるいは最悪の場合、PowerPointプロセス自体をゾンビ化(バックグラウンドでメモリを食いつぶしながらハングアップ)させる。
我々が目指すべきは、「ファイルが物理的に存在し、かつ、他のプロセスによる共有モード(Share Mode)が許可されていない状態」を、OSレベルで確定させることだ。
2. Windows APIによる低レイヤーでの排他制御
この課題を解決するために、`CreateFile` APIを使用する。
`CreateFile` は、単にファイルを開くだけの関数ではない。第3引数の `dwShareMode` に `0` を渡すことで、「他のプロセスによる読み取り・書き込み・削除を一切許可しない」という、極めて強力な排他ロックを要求することができる。
もし、既に誰かがファイルを開いていれば、このAPIは `ERROR_SHARING_VIOLATION` を返してくる。これこそが、我々が求める「真実」だ。
API宣言と定数の定義
まず、64bit/32bit両環境に対応した宣言を定義する。現代のエンタープライズ環境では、Officeの64bit化は避けられないため、`PtrSafe` と `LongPtr` の使い分けは必須知識である。
Option Explicit
' --- Windows API Declarations ---
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 Long
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 ---
Private Const GENERIC_READ As Long = &H80000000
Private Const CREATE_ALWAYS As Long = 2
Private Const OPEN_EXISTING As Long = 3
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80
Private Const ERROR_SHARING_VIOLATION As Long = 32
Private Const INVALID_HANDLE_VALUE As LongPtr = -1
3. 実装:堅牢なるPDF一括変換エンジン
以下に、FSOによるディレクトリ走査、APIによる排他制御、そしてPowerPointオブジェクトのメモリ管理を統合したクラスモジュールの核となるロジックを示す。
核心となるロック検知ロジック
''' <summary>
''' ファイルが他のプロセスによってロックされているかを確認する。
''' </summary>
''' <param name="filePath">対象ファイルのフルパス</param>
''' <returns>ロックされている場合はTrue</returns>
Private Function IsFileLocked(ByVal filePath As String) As Boolean
#If VBA7 Then
Dim hFile As LongPtr
#Else
Dim hFile As Long
#End If
' dwShareMode を 0 に設定することで、排他的なアクセスを試みる
' 他のプロセスが既に開いている場合、ここで失敗する
hFile = CreateFileW(StrPtr(filePath), GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0)
If hFile = INVALID_HANDLE_VALUE Then
If GetLastError() = ERROR_SHARING_VIOLATION Then
IsFileLocked = True
Exit Function
End If
End If
' 成功した場合は、即座にハンドルを閉じる
If hFile <> INVALID_HANDLE_VALUE Then CloseHandle hFile
IsFileLocked = False
End Function
メイン・バッチ処理プロセス
ここで重要なのは、PowerPoint Applicationオブジェクトのライフサイクル管理だ。ループ内で `New PowerPoint.Application` を繰り返すような愚行は、メモリリークとプロセス残存の元凶となる。Applicationオブジェクトは一度だけ生成し、処理終了時に確実に解放(`Set = Nothing`)しなければならない。
Public Sub BatchConvertPptxToPdf(ByVal targetFolder As String)
Dim fso As Object
Dim folder As Object
Dim file As Object
Dim pptApp As Object ' PowerPoint.Application
Dim pres As Object ' PowerPoint.Presentation
Dim retryCount As Integer
Const MAX_RETRIES As Integer = 3
Const RETRY_DELAY_SEC As Double = 2#
Set fso = CreateObject("Scripting.FileSystemObject")
If Not fso.FolderExists(targetFolder) Then
Err.Raise 53, , "Target folder not found: " & targetFolder
End If
' PowerPointインスタンスを一度だけ生成(シングルトン的な運用)
On Error Resume Next
Set pptApp = GetObject(Class:="PowerPoint.Application")
If pptApp Is Nothing Then
Set pptApp = CreateObject("PowerPoint.Application")
End If
On Error GoTo ErrorHandler
' 表示を抑制してパフォーマンスを最適化
pptApp.Visible = msoFalse
Set folder = fso.GetFolder(targetFolder)
For Each file In folder.Files
' 拡張子チェック
If LCase(fso.GetExtensionName(file.Path)) = "pptx" Then
' --- 排他制御フェーズ ---
retryCount = 0
Do While IsFileLocked(file.Path) And retryCount < MAX_RETRIES
Debug.Print "File locked: " & file.Name & ". Retrying..."
Sleep (RETRY_DELAY_SEC * 1000) ' Windows APIのSleepを使用推奨
retryCount = retryCount + 1
Loop
If IsFileLocked(file.Path) Then
Debug.Print "Skipping (Locked): " & file.Name
GoTo NextFile
End If
' --- 変換フェーズ ---
On Error Resume Next
' ファイルを読み取り専用で開く(編集競合の防止)
Set pres = pptApp.Presentations.Open(FileName:=file.Path, ReadOnly:=msoTrue, WithWindow:=msoFalse)
If Err.Number = 0 Then
Dim pdfPath As String
pdfPath = fso.GetParentFolderName(file.Path) & "\" & fso.GetBaseName(file.Path) & ".pdf"
' PDFとして保存
pres.SaveAs pdfPath, 32 ' 32 = ppSaveAsPDF
Debug.Print "Success: " & file.Name
' オブジェクトの明示的解放(重要:メモリリーク防止)
pres.Close
Set pres = Nothing
Else
Debug.Print "Error opening " & file.Name & ": " & Err.Description
End If
On Error GoTo ErrorHandler
End If
NextFile:
Next file
CleanUp:
' リソースの完全解放
On Error Resume Next
If Not pres Is Nothing Then pres.Close: Set pres = Nothing
If Not pptApp Is Nothing Then pptApp.Quit: Set pptApp = Nothing
Set fso = Nothing
Debug.Print "Process Completed."
Exit Sub
ErrorHandler:
MsgBox "Critical Error: " & Err.Description, vbCritical
Resume CleanUp
End Sub
4. アーキテクトの視点:パフォーマンスと堅牢性の極致
このスクリプトが、単なる「動くコード」ではなく「プロフェッショナルなツール」である理由は、以下の3点に集約される。
I. メモリ管理の厳格化
VBAにおいて、COMオブジェクト(PowerPointやFSO)は「参照カウント」によって管理されている。`Set pres = Nothing` を怠ることは、メモリ上に幽霊のようなプレゼンテーションオブジェクトを残留させ、長時間稼働するサーバー環境において、最終的に `Out of Memory` を引き起こす。本実装では、各ループの終わりに明示的な `Close` と `Nothing` を徹底している。
II. エラー・レジリエンス(回復力)
ネットワークの瞬断や、他ユーザーによるロックは「例外」ではなく「日常」である。`MAX_RETRIES` を設けた再試行ロジックと、`ReadOnly:=msoTrue` でのオープンにより、システムが停止することなく、可能な限りのタスクを完遂する設計にしている。
III. 低レイヤーへのアクセス
`CreateFileW` を用いたロック判定は、ファイルシステムが提供する最も信頼性の高いシグナルである。これにより、ファイルが存在するのに開けないという「中途半端なエラー状態」を事前に検知し、処理を安全にスキップ、あるいは待機させることが可能となる。
結語
自動化エンジニアの真価は、ハッピーパス(正常系)のコードを書くことではなく、エッジケース(異常系)をいかにエレガントに、かつ確実に制御するかで決まる。
Windows APIという強力な武器を、VBAという使い慣れた環境に組み込む。この「ハイブリッドなアプローチ」こそが、レガシーな資産を現代のエンタープライズ要件に適応させるための、唯一にして最強の解である。
