墓穴を掘るな:未処理例外時のビジュアル証拠を掴む「画面キャプチャ自動保存」の極意
現場から上がってくる「なんかエラーが出ました」という問い合わせほど、開発者を絶望させるものはない。再現性なし、環境依存、ユーザーの記憶違いの三拍子が揃ったバグほどリソースを食いつぶす癌はないのだ。
特に、いまだに現役で稼働し続けるWindows Forms(WinForms)アプリケーションにおいて、UIスレッドの崩壊やバックグラウンドワーカーでの致命的例外は、ユーザーがダイアログを「OK」で閉じた瞬間に消え去る。ログには `NullReferenceException` のスタックトレースが虚しく残るのみで、その時ユーザーがどの画面のどの状態(どの入力値、どのグリッドの選択状態)でそれを踏んだのかは、永遠の闇の中だ。
この不毛なデバッグ地獄に終止符を打つ。未処理例外(Unhandled Exception)を捕捉したその瞬間、UIスレッドが息を引き取るコンマ数秒の隙を突いて画面全体を高精度にキャプチャし、メモリリークを完全に回避しながらダンプ保存する仕組みを、VB.NETの極限の知見とともに授けよう。
—
1. アーキテクチャの全体像:なぜ「例外発生時」なのか
画面のキャプチャ自体は、`Graphics.CopyFromScreen` を使えば数行で書ける。しかし、「未処理例外時」という極限状態においてこれを安全に実行するには、以下のハードルをクリアしなければならない。
1. UIスレッドのハング・フリーズ対策: 例外発生源がUIスレッドである場合、描画コンテキストがロックされている可能性がある。
2. GDI+リソースの枯渇: キャプチャ画像(Bitmap)の生成はマネージドとアンマネージドの境界に位置するため、解放を怠ると `OutOfMemoryException` の連鎖を引き起こす。
3. マルチモニタ環境の完全網羅: プライマリ画面だけでなく、拡張デスクトップ全体の解像度と座標系を動的に解決する必要がある。
これらをすべて満たすシニアレベルのコードを構築する。
—
2. 実装:メモリ最適化とAPI連携による堅牢なキャプチャエンジン
以下のコードは、アプリケーション全体の `AppDomain.CurrentDomain.UnhandledException` および `Application.ThreadException` をフックし、例外ダイアログが表示される直前に画面全体を無音でキャプチャ、ローカル(またはネットワーク共有)へタイムスタンプ付きでPNG保存するモジュールである。
Imports System
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.IO
Imports System.Windows.Forms
Imports System.Threading
Namespace Enterprise.Diagnostics
”’
”’
Public NotInheritable Class CrashCaptureEngine
Private Sub New()
‘ 静的クラスのためインスタンス化させない
End Sub
”’
”’
Public Shared Sub Initialize()
‘ UIスレッド上の未処理例外
AddHandler Application.ThreadException, AddressOf OnUIThreadException
‘ 非UIスレッド(バックグラウンドタスク等)の未処理例外
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf OnNonUIThreadException
End Sub
Private Shared Sub OnUIThreadException(sender As Object, e As ThreadExceptionEventArgs)
CaptureAndLog(“UI_Thread”, e.Exception)
End Sub
Private Shared Sub OnNonUIThreadException(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
CaptureAndLog(“Background_Thread”, ex)
End Sub
”’
”’
Private Shared Sub CaptureAndLog(contextTag As String, ex As Exception)
SyncLock GetType(CrashCaptureEngine)
Dim screenRect As Rectangle = Rectangle.Empty
‘ マルチモニタ環境を考慮した仮想画面全体の領域を算出
For Each screen As Screen In Screen.AllScreens
screenRect = Rectangle.Union(screenRect, screen.Bounds)
Next
‘ アンマネージドリソース(GDI+ Bitmap)を確実に破棄するため Using を強制
Using bmp As New Bitmap(screenRect.Width, screenRect.Height, PixelFormat.Format32bppArgb)
Using g As Graphics = Graphics.FromImage(bmp)
Try
‘ スクリーン全体のピクセル情報を高速コピー
g.CopyFromScreen(screenRect.Left, screenRect.Top, 0, 0, screenRect.Size, CopyPixelOperation.SourceCopy)
Catch drawEx As Exception
‘ 描画権限剥奪やデスクトップロック中の例外は握りつぶし、ログのみ出力
System.Diagnostics.Debug.WriteLine($”キャプチャ失敗: {drawEx.Message}”)
Return
End Try
End Using
‘ 保存先パスの生成(環境変数やUNCパスを指定可能に)
Dim logDir As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “CrashDumps”)
If Not Directory.Exists(logDir) Then
Directory.CreateDirectory(logDir)
End If
Dim timeStamp As String = DateTime.Now.ToString(“yyyyMMdd_HHmmss_fff”)
Dim filePath As String = Path.Combine(logDir, $”Crash_{contextTag}_{timeStamp}.png”)
‘ 最適化された形式でPNG保存(ファイルロック競合を防ぐため即座にストリームを閉じる)
bmp.Save(filePath, ImageFormat.Png)
‘ 同時に例外スタックトレースをテキストとして書き出す
Dim stackLogPath As Path = Path.ChangeExtension(filePath, “.log”)
File.WriteAllText(stackLogPath,
$”[Timestamp] {DateTime.Now}{Environment.NewLine}” &
$”[Context] {contextTag}{Environment.NewLine}” &
$”[Exception] {If(ex IsNot Nothing, ex.ToString(), “Unknown Exception”)}”)
End Using
End SyncLock
End Sub
End Class
End Namespace
—
3. チーフアーキテクトが解説する「実装の急所」
上記のコードは一見シンプルに見えるが、レガシーかつ高負荷なWinFormsアプリで確実独走するためのノウハウが凝縮されている。
A. マルチモニタ(Virtual Screen)の完全制覇
単に `Screen.PrimaryScreen` をキャプチャするだけでは、デュアルディスプレイ環境でサブ画面に表示されていた業務フォームの重要エラーを取りこぼす。`Rectangle.Union` を用いて全ディスプレイの領域を包括する仮想デスクトップ空間を計算し、一網打尽にキャプチャしている点に注目してほしい。
B. `SyncLock` による多重キャプチャの防止
致命的例外(Out of Memoryや深刻なスレッド競合)が発生した場合、ほぼ同時に複数のスレッドから未処理例外イベントが火を噴くことがある。ディスクI/Oの競合やGDI+のデッドロックを防ぐため、`SyncLock` によって排他制御をかけ、先着1件のみが確実にディスク書き込みを行う設計にしている。
C. マネージド・アンマネージドの境界管理 (`Using` の徹底)
`Bitmap` オブジェクトや `Graphics` オブジェクトの背後には、WindowsのGDI+によるネイティブヒープがガッツリ絡んでいる。これをガベージコレクション(GC)の気まぐれに委ねてはならない。`Using` ステートメントを二重にネストさせることで、例外がスローされた直後のスタックアンワインドの過程で、即座にOSへハンドルを返却する構造を担保している。これにより、クラッシュ寸前のメモリプレッシャー下でも `OutOfMemoryException` の連鎖を回避できる。
—
4. 現場へ導入する際の実践的アドバイス
1. 保存先の選定:
ローカルの `AppDomain.CurrentDomain.BaseDirectory` 配下は、ユーザー権限(UAC)によっては書き込み権限がない場合がある。その場合は `Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)` などのユーザープロファイル領域、または社内UNCパス(ファイルサーバー)への非同期転送を検討せよ。
2. 個人情報(PII)のマスク処理:
顧客名や金額、パスワードが生々しく画面に映っている状態でキャプチャされるため、セキュリティポリシーとの兼ね合いが重要となる。保守性とのトレードオフになるが、「社内LAN環境限定のクローズドなシステム」でのみこの全画面キャプチャを有効にし、外販パッケージソフト等では特定コントロールのマスク処理を入れるなどの配慮が必要だ。
総括
「エラーが起きましぇん、再現しましぇん」というユーザーからのチャットにイライラさせられる時代は、今日で終わりにしよう。
例外発生の断末魔に画面の全容をスナップショットとして切り取り、証拠として残す。この泥臭くも極めてエンジニアリング的な実装こそが、レガシーシステムの保守性を神域へと引き上げる唯一の現実解である。
