VB.NETアプリケーションのクラッシュレス化:AppDomain.CurrentDomain.UnhandledExceptionによる最終防衛ラインのエラーキャッチ
レガシーな業務システム、あるいは24時間稼働を要求されるプラントや基幹連携の常駐プロセスにおいて、「理由不明の突然死(強制終了)」ほど恐ろしいものはない。
ユーザーがどれほど丁寧な操作をしようとも、サードパーティ製DLLのメモリ違反、非同期スレッド(Task)内での未処理例外、あるいはCOM相互運用における深刻なスタック破損が発生すれば、CLR(共通言語ランタイム)は容赦なくプロセスを断絶させる。
「なぜ落ちたのか分からない」
このエンジニアにとって最大の屈辱を断ち切るために存在する最後の防衛ラインが、`AppDomain.CurrentDomain.UnhandledException` である。
本稿では、VB.NETアプリケーションが非業の死を遂げるのを防ぎ、最期に尊厳あるログとダンプを残して優美に散る(あるいは復帰を試みる)ための極限のアーキテクチャを解説する。
—
1. 致命的例外(Fatal Exception)の正体とCLRの挙動
通常、`Try-Catch` 構文で捕捉できるのは、同一スレッド内で発生したマネージ例外に限定される。.NET Framework 2.0以降、スレッドプールや `async/await` の背後で発生した未処理例外は、容赦なくプロセス全体をクラッシュさせる仕様に変更された。これは、アプリケーションが「不整合なメモリ状態」のまま動作し続け、データベースを破損させたりセキュリティホールを露出させたりするのを防ぐためのセーフティネットである。
しかし、エンタープライズの現場において、「黙って落ちる」ことは「何も分からないまま業務が止まる」と同義である。
私たちは、プロセスが死に至るまさにその瞬間(AppDomainの終焉)をフックし、黒いブラックボックスから最後のフライトレコーダーを回収しなければならない。
—
2. 最終防衛ラインの実装:`UnhandledException` の罠と真実
`AppDomain.CurrentDomain.UnhandledException` は、プロセスが終了する直前に発火するイベントだ。しかし、ここにはシニアエンジニアであっても見落としがちな極めて重大な仕様上の罠が存在する。
> ⚠️ 致命的な注意点
> このイベントハンドラ内で例外が発生した場合、または処理がモタついた場合、プロセスは即座に強制終了され、デバッグ情報は永遠に失われる。さらに、.NET 4.0以降では、このイベントが発生してもOSの設定や環境によってはプロセスが強制終了を免れないケースがある。
以下のコードは、この最終防衛ラインを極限まで堅牢に構築したVB.NETの実装例である。
Imports System
Imports System.IO
Imports System.Threading
Imports System.Windows.Forms
Namespace EnterpriseArchitecture.Fallbacks
Friend NotInheritable Class Program
Public Shared Sub Main(args As String())
‘ 1. UIスレッド全体の未処理例外ハンドラ (Windows Formsの場合)
AddHandler Application.ThreadException, AddressOf OnUIThreadException
‘ 2. 非UIスレッドおよびバックグラウンドタスクの未処理例外ハンドラ
AddHandler AppDomain.CurrentDomain.UnhandledException, AddressOf OnUnhandledException
Try
‘ アプリケーションのメインエントリポイント
Application.EnableVisualStyles()
Application.SetCompatibleTextRenderingDefault(False)
Application.Run(New MainForm())
Catch ex As Exception
‘ ここに到達するのはMainメソッド内の同期例外のみ
WriteEmergencyLog(ex, “MainSyncCatch”)
End Sub
End Sub
‘ — UIスレッド例外ハンドラ —
Private Shared Sub OnUIThreadException(sender As Object, e As ThreadExceptionEventArgs)
Dim ex As Exception = e.Exception
WriteEmergencyLog(ex, “UIThreadException”)
ShowFatalErrorDialog(ex)
‘ 必要に応じて安全に終了へ導く
Environment.Exit(1)
End Sub
‘ — 最終防衛ライン (AppDomain全体) —
Private Shared Sub OnUnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
Dim ex As Exception = TryCast(e.ExceptionObject, Exception)
‘ e.IsTerminating が True の場合、プロセスは確実に死亡する
Dim isTerminating As Boolean = e.IsTerminating
If ex IsNot Nothing Then
WriteEmergencyLog(ex, $”AppDomain_UnhandledException (IsTerminating: {isTerminating})”)
Else
WriteEmergencyLog(New Exception(“Unknown non-CLS compliant exception occurred.”), “AppDomain_NonCLS”)
End If
‘ 最後のあがき:メモリのフラッシュとファイルシステムの同期
System.IO.File.WriteAllText(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “CRITICAL_SHUTDOWN.lock”),
DateTime.Now.ToString(“yyyy-MM-dd HH:mm:ss”)
)
End Sub
‘ — 緊急ログ出力 (外部依存を極力排除した堅牢なI/O) —
Private Shared Sub WriteEmergencyLog(ex As Exception, context As String)
Try
Dim logPath As String = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, “emergency_crash.log”)
Dim logContent As String =
$”[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{context}]” & Environment.NewLine &
$”Type: {ex.GetType().FullName}” & Environment.NewLine &
$”Message: {ex.Message}” & Environment.NewLine &
$”StackTrace: {ex.StackTrace}” & Environment.NewLine &
New String(“-“c, 60) & Environment.NewLine
‘ 同時書き込み競合を防ぎつつ追記
File.AppendAllText(logPath, logContent)
Ca
