Shadow of the Lifecycle: PowerPoint Application Event Hooking による非同期監査証跡の実装
多くの開発者は、VBAを単なる「作業の自動化ツール」と考えている。しかし、エンタープライズ・アーキテクチャの視点に立てば、VBAは「エンドポイントにおける挙動の制御点」である。
特に、機密情報の塊であるプレゼンテーション資料のライフサイクル(生成・閲覧・改変・保存)を追跡する場合、ユーザーが意識することなく、かつユーザーの操作を阻害することなく、全てのログを中央データベースへ送出する仕組みが必要となる。
本稿では、PowerPointの`Application`オブジェクトをクラスモジュールで完全にフックし、Windowsの非同期的な性質を利用して、パフォーマンスを犠牲にすることなく監査ログを収集する、極限のアーキテクチャについて詳解する。
1. 致命的な設計ミス:同期処理によるUIのブロッキング
監査ログの実装において、シニアエンジニアが真っ先に避けるべきは、「イベントハンドラ内での直接的なDB接続」である。
' --- 避けるべきアンチパターン ---
Private Sub App_PresentationBeforeSave(ByVal Pres As Presentation)
' ここでSQL Serverに接続し、ネットワークの応答を待つと...
' ユーザーは「保存ボタン」を押した後、数秒間フリーズしたような体験をする。
' これはUXの崩壊であり、システムへの拒絶反応を招く。
Call SendLogToDatabase(Pres.Name)
End Sub
VBAはシングルスレッドで動作する。イベントハンドラ内でネットワークI/O(データベースへの接続やAPI呼び出し)が発生し、そのレスポンスが遅延すれば、PowerPointのメインスレッドはその間、完全に停止する。
我々が目指すべきは、「ログ情報のキャプチャは即座に完了させ、送出処理は別プロセスに委ねる(Fire-and-Forget)」という設計である。
2. クラスモジュールによる Application オブジェクトの完全掌握
PowerPointのイベントをグローバルに監視するには、標準モジュールではなく、`WithEvents`キーワードを用いたクラスモジュールが不可欠である。
クラスモジュールの設計:`clsAppHook`
まず、`clsAppHook` という名前のクラスモジュールを作成する。ここでは、アプリケーションレベルのイベントを定義し、オブジェクトのライフサイクルを厳格に管理する。
Option Explicit
' Applicationオブジェクトをイベント監視対象として宣言
Private WithEvents App As PowerPoint.Application
' ログ送出用の外部プロセス(あるいは軽量なスクリプト)へのパス
Private Const LOG_AGENT_PATH As String = "C:\Program Files\Corp\LogAgent.exe"
''' <summary>
''' フックの開始
''' </summary>
Public Sub Initialize(ByVal targetApp As PowerPoint.Application)
Set App = targetApp
End Sub
''' <summary>
''' フックの解除とメモリの明示的解放
''' </summary>
Public Sub Terminate()
Set App = Nothing
End Sub
' --- Event Handlers ---
''' <summary>
''' プレゼンテーションが開かれた時のイベント
''' </summary>
Private Sub App_PresentationOpen(ByVal Pres As Presentation)
Call DispatchLog("OPEN", Pres.Name, Pres.FullName)
End Sub
''' <summary>
''' 保存される直前のイベント
''' </summary>
Private Sub App_PresentationBeforeSave(ByVal Pres As Presentation)
Call DispatchLog("SAVE", Pres.Name, Pres.FullName)
End Sub
''' <summary>
''' ログを非同期で外部プロセスへ放り投げる
''' </summary>
Private Sub DispatchLog(ByVal action As String, ByVal fileName As String, ByVal filePath As String)
On Error Resume Next
Dim shell As Object
Set shell = CreateObject("WScript.Shell")
' ログデータの構築(カンマ区切り、あるいはJSON形式)
' ユーザー名、時刻、アクション、ファイル名、フルパス
Dim logData As String
logData = Environ("USERNAME") & "|" & Now & "|" & action & "|" & fileName & "|" & filePath
' Windows APIのShellExecuteに相当する挙動を非同期で行う
' 引数にログデータを渡し、別プロセスとして実行。
' 第3引数「True」ではなく「False」にすることで、終了を待たずに制御を戻す(非同期)
shell.Run """" & LOG_AGENT_PATH & """ """ & logData & """", 0, False
' オブジェクトの即時解放(メモリリーク防止)
Set shell = Nothing
End Sub
3. メモリ管理とオブジェクトのライフサイクル
VBAにおいて、クラスモジュールで定義した`WithEvents`変数は、そのクラスのインスタンスが破棄されると同時にイベント監視も終了する。
アドイン(.ppam)として実装する場合、このインスタンスをアプリケーションの生存期間中、メモリ上に保持し続けなければならない。逆に、不適切なタイミングでインスタンスを破棄したり、`Set App = Nothing` を忘れたりすると、PowerPointの終了時に「メモリ不整合」によるクラッシュを引き起こす原因となる。
標準モジュールでの管理:`modMain`
Option Explicit
' アプリケーション全体で唯一のインスタンスを保持
Private mAppHook As clsAppHook
''' <summary>
''' アドイン読み込み時に実行されるエントリーポイント
''' </summary>
Public Sub Auto_Open()
Set mAppHook = New clsAppHook
' 現在動作中のApplicationオブジェクトを注入
mAppHook.Initialize Application
End Sub
''' <summary>
''' アドイン解除時に実行されるクリーンアップ
''' </summary>
Public Sub Auto_Close()
If Not mAppHook Is Nothing Then
mAppHook.Terminate
Set mAppHook = Nothing
End If
End Sub
4. システム間連携の極致:非同期ログエージェント
前述の `shell.Run …, 0, False` は、VBAから「投げっぱなし」でコマンドを実行する強力な手法である。この設計を完成させるには、受け手となる軽量なエージェントが必要だ。
ここで、PythonやVB.NET、あるいは高度なPowerShellスクリプトを用いて、受信した引数をHTTP POSTで中央のWeb APIへ送信する仕組みを構築する。
設計思想の要点:
1. VBA側: 最小限の計算、最小限のI/O(`shell.Run`のみ)。
2. エージェント側: ネットワークの再試行(Retry logic)、バッファリング、プロキシ設定の処理。
これにより、たとえ社内ネットワークが一時的に不安定であっても、PowerPointの操作が止まることは決してない。
5. アーキテクトの結論
この実装は、単なる「ログ出力」ではない。「ユーザー体験(UX)とセキュリティ要件のトレードオフを、アーキテクチャによって解決する」というエンジニアリングの真髄である。
- クラスモジュールによるカプセル化: 状態管理を明確にし、グローバル変数の汚染を防ぐ。
- 非同期実行によるデカップリング: 業務アプリケーション(PowerPoint)と監視インフラを分離する。
- 明示的なメモリ管理: 長期間稼働するアドインとしての堅牢性を担保する。
レガシーなVBA環境であっても、Windows APIやプロセス制御を正しく理解していれば、現代的なマイクロサービス指向の監視システムと連携させることは十分に可能である。
技術の深淵を知る者は、ツールを単に使うのではない。ツールが動く「背景」を制御するのだ。
