【テクニカル・上級編】【堅牢なログ設計】実行時エラー発生時に自動で「ソースコードの行番号」を特定・記録するデバッグ支援ロジック – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

序文: VBScriptという名の「沈黙する実行環境」

Windows Script Host (WSH) 環境下で、数十、数百のレガシーな自動化スクリプトを運用してきた者にとって、VBScriptの「エラー情報の乏しさ」は共通の苦痛である。

`Err.Number` と `Err.Description`。これらだけでは、数千行に及ぶ巨大なスパゲッティコードの、一体「どの行」で「どのようなコンテキスト」において致命的な例外が発生したのかを特定することは不可能に近い。現代的な言語であればスタックトレースが即座に提供されるところ、VBScriptは沈黙を守る。

しかし、真のアーキテクトは、言語の欠陥を嘆かない。言語の限界を理解し、その制約の中で「いかにして真実を炙り出すか」という設計思想を構築する。

本稿では、VBScript自身に自身のソースコードをパース(解析)させ、エラー発生時の行番号および周辺コンテキストを動的に特定する、極めて堅牢なデバッグ支援ロジックの設計パターンを提示する。

課題: 欠落したコンテキスト — なぜErr.Descriptionだけでは不十分なのか

一般的なスクリプト開発者が書くエラーハンドリングは、以下のレベルに留まっている。

On Error Resume Next
' ... 何らかの処理 ...
If Err.Number <> 0 Then
    WScript.Echo "Error: " & Err.Description
    Err.Clear
End If

これは、プロトタイプとしては十分だが、エンタープライズレベルの運用では「無能」なコードである。エラーが発生した際、エンジニアが知りたいのは「何が起きたか」だけでなく、「コードのどの位置で、どの関数が、どのような状態であったか」という多角的な情報である。

行番号が不明なエラーログは、原因究明のための「手がかり」ではなく、単なる「ノイズ」に過ぎない。

解決策: 自己参照的パースによる「行番号」の擬似的な特定

VBScriptには、実行中のプログラムカウンタ(PC)を取得する標準的な手段は存在しない。そこで、以下の戦略を採る。

1. 自己参照 (Self-Reflection): `WScript.ScriptFullName` を利用して、実行中のスクリプトファイル自身を `FileSystemObject` で読み込む。
2. コンテキスト・キャプチャ: エラー発生時に、現在実行中のプロシージャ名を特定し、そのプロシージャ内のソースコードをスキャンする。
3. ログの構造化: 単なる文字列ではなく、タイムスタンプ、エラーコード、行番号、およびエラー発生箇所のコード断片を構造化して出力する。

以下に、その核心となる `Logger` クラスの設計を示す。

実装: 極限の堅牢性を備えた Logger クラス

この実装では、メモリリークを防ぐためのオブジェクト解放(`Set obj = Nothing`)を徹底し、ファイルI/Oのオーバーヘッドを考慮して、エラー発生時のみパースが走る設計にしている。

'**
' Class: ErrorLogger
' Description: 自己参照的パース機能を持つ堅牢なログ出力モジュール
'**
Class ErrorLogger
    Private fso
    Private scriptPath
    Private logPath

    ' コンストラクタ
    Private Sub Class_Initialize()
        Set fso = CreateObject("Scripting.FileSystemObject")
        scriptPath = WScript.ScriptFullName
        ' ログファイルはスクリプトと同じディレクトリに生成
        logPath = fso.GetParentFolderName(scriptPath) & "\execution_error.log"
    End Sub

    ' デストラクタ: オブジェクトの明示的解放は必須
    Private Sub Class_Terminate()
        Set fso = Nothing
    End Sub

    '**
    ' Method: LogFatalError
    ' Parameter: procName - 現在実行中のプロシージャ名
    ' Parameter: errObj   - Errオブジェクト
    '**
    Public Sub LogFatalError(procName, errObj)
        Dim ts, lineNum, contextCode
        
        ' 1. エラー発生箇所の行番号と周辺コードを特定するためのパース処理
        ' VBScriptには行番号取得APIがないため、プロシージャ名をキーにファイルをスキャンする
        lineNum = "Unknown"
        contextCode = "N/A"
        
        On Error Resume Next ' パース自体のエラーでメイン処理を止めないための防壁
        
        Dim tsFile, lineContent, currentLine
        Set tsFile = fso.OpenTextFile(scriptPath, 1) ' 読み取り専用
        
        currentLine = 0
        Do Until tsFile.AtEndOfStream
            currentLine = currentLine + 1
            lineContent = tsFile.ReadLine
            
            ' プロシージャの開始行を特定し、そこからの行数をカウントする簡易的なロジック
            ' 本来はより高度な構文解析が必要だが、実務的には「Sub/Function」の出現で判定
            If InStr(1, lineContent, "Sub " & procName, vbTextCompare) > 0 Or _
               InStr(1, lineContent, "Function " & procName, vbTextCompare) > 0 Then
                
                ' プロシージャ開始後、エラーが発生したであろう行を特定する試み
                ' ※厳密な行特定は困難だが、プロシージャ内のコード断片をログに含めることで
                ' 開発者が視覚的にエラー箇所を特定できるようにする
                contextCode = "[Line " & currentLine & "] " & lineContent
            End If
        Loop
        tsFile.Close
        Set tsFile = Nothing
        
        On Error GoTo 0

        ' 2. ログファイルへの書き出し
        Set ts = fso.OpenTextFile(logPath, 8, True) ' 8 = ForAppending
        ts.WriteLine "======================================================================"
        ts.WriteLine "TIMESTAMP : " & Now
        ts.WriteLine "PROCEDURE : " & procName
        ts.WriteLine "ERROR NO  : " & errObj.Number
        ts.WriteLine "DESCRIPTION: " & errObj.Description
        ts.WriteLine "CONTEXT   : " & contextCode
        ts.WriteLine "======================================================================"
        ts.Close
        Set ts = Nothing
        
        ' 3. ユーザーへの通知(WSH環境の場合)
        WScript.Echo "【致命的なエラー】" & vbCrLf & _
                     "詳細な情報はログを確認してください: " & logPath
    End Sub
End Class

'**
' メイン実行ロジック (Test Implementation)
'**
Dim logger
Set logger = New ErrorLogger

' テスト用のエラー発生プロシージャ
Sub CriticalProcess()
    On Error Resume Next
    
    Dim objDivisionByZero
    Set objDivisionByZero = 10 / 0 ' 意図的なゼロ除算エラー
    
    If Err.Number <> 0 Then
        ' エラー発生時にコンテキストと共にログ出力
        logger.LogFatalError "CriticalProcess", Err
        Err.Clear
    End If
    
    ' メモリ解放の徹底
    Set objDivisionByZero = Nothing
End Sub

' 実行
CriticalProcess()

' オブジェクトの解放
Set logger = Nothing

運用上の鉄則: オブジェクトのライフサイクル管理とメモリリークの回避

この設計を実戦投入するにあたり、シニアエンジニアが守るべき鉄則が二つある。

1. ゾンビプロセスの根絶

WSH(特に `wscript.exe` や `cscript.exe`)を長時間稼働させる自動化ジョブにおいて、最も恐ろしいのは、エラーハンドリングの不備によってオブジェクトが解放されず、プロセスがメモリを食いつぶしながら残り続ける「ゾンビプロセス」である。

VBScriptにおける `Set obj = Nothing` は単なる作法ではない。これは、COMオブジェクトの参照カウントを適切にデクリメントし、OSのリソースを解放するための生存戦略である。特に `FileSystemObject` や `ADODB.Connection` などの外部リソースを扱うクラスでは、必ず `Class_Terminate` または明示的な破棄を実装せよ。

2. パース処理のコスト管理

本稿で示した「自己参照的パース」は、ファイルサイズが数メガバイトに及ぶような巨大なスクリプトにおいては、エラー発生時のオーバーヘッドが無視できなくなる。
しかし、エラーは「例外」である。通常の正常系フロー(Happy Path)においてパース処理が走ることはない。したがって、「エラーが起きた瞬間に、最大限の情報を得るためにコストを支払う」という設計思想は、実務において極めて合理的である。

結言

VBScriptは、もはや最新の言語ではない。しかし、Windowsエコシステムの中に深く根を張ったレガシー資産は、今後も我々の目の前で動き続ける。

「言語が対応していないからできない」という言い訳を捨て、スクリプト自身に自己を解析させるというメタ的なアプローチを取ることで、保守性は劇的に向上する。この「極限の知見」を手に、混沌としたレガシー環境を秩序あるシステムへと昇華させてほしい。

タイトルとURLをコピーしました