VBScriptでログ監視ツール

# はじめに
　前回、[VBScriptをお手軽にデバッグして開発する方法](https://qiita.com/saeki4n/items/fc1e19c8b2348230d6b6) にて、VBScriptの開発方法を書いてみましたので、開発の例としてログ監視ツールを作ってみようと思いました。

# 仕様
- 特定のテキストの中身を監視する
- テキスト中に、特定の文字列が新規で検知したらアラートを上げる。
- 検知アラートはメッセージボックスを使用する。任意の設定で音が鳴るようにする。
- 検知アラートのメッセージボックスのOKボタンを押すと、そこから監視を継続する。
- ファイル中の文字数が前と比べて少なくなっていたら、ファイルの最初から確認する（ログローテーション対応）
- インターフェースを作成し監視終了をできるようにする
- 検知アラートの時間をログに出力するようにする
- 想定外のエラーが発生したらメッセージボックスを上げる

# 中身の解説

　コードの全文は後ろの方にありますので、説明抜きで理解出来る方はこの辺りは飛ばしてどうぞ。

## 設定箇所
　ユーザーが設定する必要が箇所は冒頭で設定可能なようにします。

```VB:設定箇所
Const MONITORED_FILE_PATH = "D:\test.log"   ' 監視ファイルの設定
Const MONITORING_STRING = "ErrorString"     ' 監視する文字列の設定
Const ALERT_SOUND_FILE = "Alert.wav"        ' 検知時に鳴らすサウンド設定.
Const ERROR_SOUND_FILE = "Error.wav"        ' スクリプトのエラー発生時に鳴らすサウンド設定.
                                            ' サウンドファイルは同一フォルダにある必要があります)
                                            ' 鳴らさない場合は、空白指定。例： ALERT_SOUND_FILE = ""
                                            ' 使用するプレイヤーは拡張子に紐づけられているプレイヤー
Const logFileName = "監視ログ.txt"           ' 監視ログのファイル名
```

## 設定確認箇所
　設定した内容は起動時に確認できるようにします。キャンセルボタンで終了するようにします。

```VB:設定確認箇所
    ' 監視開始メッセージを出力する
    temp = "監視をスタートします。" & vbCrLf & vbCrLf _
            & "監視ファイル：" & MONITORED_FILE_PATH & vbCrLf _
            & "監視文字：" & MONITORING_STRING & vbCrLf & vbCrLf _
            & "終了する際は「監視終了」ボタンを押してください。"
    If MsgBox(temp, vbInformation + vbOKCancel, "スタート") = vbCancel Then
        WScript.Quit
    End If
```

## 監視ファイルの確認箇所
　監視対象ファイルが存在しなければエラーを出して終了します。
　存在したなら中身を全部読み取り、現時点の文字数を記録しておきます。この文字数がどこまで監視したかを示す値となります。ファイルの中身が空っぽならばカウントはゼロです。

```VB:監視ファイルの確認箇所
    ' ファイルの存在確認
    If fso.FileExists(MONITORED_FILE_PATH) = False Then
        temp = "対象ファイル『" & MONITORED_FILE_PATH & "』が存在しません。" & vbCrLf _
                & "設定を確認してください。" & vbCrLf & "終了します。"
        MsgBox temp, vbCritical, "エラー"
        WScript.Quit
    End If
    
    ' 初回のファイル中身の取得
    Set objFile = fso.OpenTextFile(MONITORED_FILE_PATH, 1)
    If objFile.AtEndOfStream = False Then
        fileText = objFile.ReadAll      ' ファイル内容をすべて読み込み
        objFile.Close                   ' ファイルをクローズ
        monitoredLength = Len(fileText)
    Else
        monitoredLength = 0
    End If
```

## 監視開始時のファイルの更新日時の取得箇所
　更新日時を確認し、開始時の更新日時を覚えておきます。

```VB:監視開始時のファイルの更新日時の取得
    ' 監視前のファイルの更新日時の取得
    preFileUpdateTime = ""
    If fso.FileExists(MONITORED_FILE_PATH) Then
        preFileUpdateTime = fso.GetFile(MONITORED_FILE_PATH).DateLastModified
    End If
```


## インターフェースを作成
　インターフェースとしてIEオブジェクトを使用します。長いので関数化してあります。

```VB:インターフェースを作成
    ' インターフェース用にIEのオブジェクト作成します。
    Set ie = CreateInterfaceIE()
```

```VB:インターフェース作成関数
' インターフェース用のIEを作成する
Function CreateInterfaceIE()
' IEのオブジェクトを作り、画面の設定をして表示
    Dim ie
    Set ie = CreateObject("InternetExplorer.Application")
    ie.Width = 300
    ie.Height = 170
    ie.Toolbar = False
    ie.StatusBar = False
    ie.Resizable = False
    ie.Visible = True
    ie.Navigate "about:blank"
    
    Do While ie.Busy
        WScript.Sleep (100)
    Loop
    
    Dim HTML   
    HTML = "" _
    + "<form>" _
    + "  <input type=""hidden"" name=""CLICKED"" value=""false""/>" _
    + "  監視対象ファイル:　" & MONITORED_FILE_PATH & "<br/>" _
    + "  監視対象文字列:　" & MONITORING_STRING & "<br/>" _
    + "  <input type=""button"" " _
    + "    onClick = ""getElementsByName('CLICKED')(0).value = 'true';""" _
    + "    value=""監視終了"" />" _
    + "</form>"
    ie.Document.Body.InnerHtml = HTML
    ie.Document.Title = "監視中."    
    Set CreateInterfaceIE = ie
End Function
```

## 監視のためのループ部分

　監視内容はループ文で実装します。
　ループを抜ける条件は、インターフェースの「監視終了」ボタンが押された時と、エラーが発生した時です。

```VB:監視のためのループ部分
' エラーが起こっても無視して進む
On Error Resume Next
    ' 監視実行（無限ループ）
    Do While ie.Busy Or Not is_clicked(ie)                
        If Err.Number <> 0 Then ' エラーが発生したら終了
            PlaySound(ERROR_SOUND_FILE)       ' 音を鳴らす
            MsgBox "エラーが発生しました。監視を停止します。", vbCritical, "エラー"
            Exit Do
        End If        
        WScript.Sleep (1000)  ' 1秒待機

        ''' (中略)

    Loop    
```


## 監視内容の部分

　ファイルの更新日時が変化していたら（新しくなっていたらでは無い）、ファイルの中身を確認しに行きます。
　既に監視済みの文字数と、現在のファイルの文字数から、まだ監視していない文字数を確定します。マイナス（ログローテーションなどの理由で文字が削除された）ならば全ての文字が監視対象とします。
　監視テキストを確定し、その中に特定のキーワードが存在したら、アラートを発生させます。

```VB:監視内容（ループ内の抜粋）
        ' 現在の監視ファイルの更新日時の取得
        curFileUpdateTime = ""
        If fso.FileExists(MONITORED_FILE_PATH) Then
            curFileUpdateTime = fso.GetFile(MONITORED_FILE_PATH).DateLastModified
        End If
    
        ' 変更前後のファイル更新日時を比較
        If preFileUpdateTime <> curFileUpdateTime Then
            '変更日時が変わっているならば中身を取得
             Set objFile = fso.OpenTextFile(MONITORED_FILE_PATH, 1)
             fileText = objFile.ReadAll ' ファイルを読み込む
             objFile.Close              ' ファイルを閉じる
             
             num = Len(fileText) - monitoredLength     ' まだ監視していない文字数を取得する
             If num <= 0 Then               ' もし文字数がマイナスになったならば（ログローテーションなどでファイルの文字が削除された場合）
                 num = Len(fileText)        ' 監視対象文字数は全てとする
             End If
             
             monitoredText = Right(fileText, num)  ' 監視対象テキストを確定させる
             If InStr(monitoredText, MONITORING_STRING) > 0 Then
                ' 監視文字が存在している場合は
                WriteLog        ' ログに残す
                PlaySound(ALERT_SOUND_FILE)       ' 音を鳴らす
                
                ' アラートを出す
                MsgBox "発見：" & MONITORING_STRING, vbExclamation, "注意："
            End If
            monitoredLength = Len(fileText)           ' 監視済み文字数をセット
        End If
        preFileUpdateTime = curFileUpdateTime   ' 変更後日時を変更前日時として設定
```


# 監視ツールのスクリプト全文

　スクリプトの全文を記載します。
　不明点や不備な点がありましたら本ページのコメントに書いていただけますと出来る限り対応しますのでお気軽にどうぞ。

```VB:監視ツール.vbs
Option Explicit

 MonitoringFile                'VBSの場合

''***************************************************************************
''監視ツール.vbs  : 指定ファイルを監視するスクリプト
''************************************************************************
Const MONITORED_FILE_PATH = "D:\test.log"   ' 監視ファイルの設定
Const MONITORING_STRING = "ErrorString"     ' 監視する文字列の設定
Const ALERT_SOUND_FILE = "Alert.wav"        ' 検知時に鳴らすサウンド設定.
Const ERROR_SOUND_FILE = "Error.wav"        ' スクリプトのエラー発生時に鳴らすサウンド設定.
                                            ' サウンドファイルは同一フォルダにある必要があります)
                                            ' 鳴らさない場合は、空白指定。例： ALERT_SOUND_FILE = ""
                                            ' 使用するプレイヤーは拡張子に紐づけられているプレイヤー
Const logFileName = "監視ログ.txt"          ' 監視ログのファイル名

Dim thisScriptPath                  ' 現Scriptのパス
    
' 監視を実行
Public Sub MonitoringFile()
    Dim fso                 ' ファイル入出力オブジェクト
    Dim objFile             ' ファイルオブジェクト
    Dim fileText            ' ファイルの中身
    Dim monitoredLength     ' 監視済みの文字数
    Dim curFileUpdateTime   ' 現在のファイルの更新日時
    Dim preFileUpdateTime   ' 一つの前のファイルの更新日時
    Dim monitoredText       ' 監視対象テキスト
    Dim num                 ' 数値の一時的変数
    Dim temp                ' 一時的変数
    Dim ie                  ' IEオブジェクト(インターフェース用)
    
    ' ファイルパスを記録する
    thisScriptPath = Replace(WScript.ScriptFullName, WScript.ScriptName, "")

    ' 監視開始メッセージを出力する
    temp = "監視をスタートします。" & vbCrLf & vbCrLf _
            & "監視ファイル：" & MONITORED_FILE_PATH & vbCrLf _
            & "監視文字：" & MONITORING_STRING & vbCrLf & vbCrLf _
            & "終了する際は「監視終了」ボタンを押してください。"
    If MsgBox(temp, vbInformation + vbOKCancel, "スタート") = vbCancel Then
        WScript.Quit
    End If
    
    ' ファイルシステムオブジェクトセット
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    ' ファイルの存在確認
    If fso.FileExists(MONITORED_FILE_PATH) = False Then
        temp = "対象ファイル『" & MONITORED_FILE_PATH & "』が存在しません。" & vbCrLf & "設定を確認してください。" & vbCrLf & "終了します。"
        MsgBox temp, vbCritical, "エラー"
        WScript.Quit
    End If
    
    ' 初回のファイル中身の取得
    Set objFile = fso.OpenTextFile(MONITORED_FILE_PATH, 1)
    If objFile.AtEndOfStream = False Then
        fileText = objFile.ReadAll      ' ファイル内容をすべて読み込み
        objFile.Close                   ' ファイルをクローズ
        monitoredLength = Len(fileText)
    Else
        monitoredLength = 0
    End If
        
    ' 監視前のファイルの更新日時の取得
    preFileUpdateTime = ""
    If fso.FileExists(MONITORED_FILE_PATH) Then
        preFileUpdateTime = fso.GetFile(MONITORED_FILE_PATH).DateLastModified
    End If
        
    ' インターフェース用にIEのオブジェクト作成する
    Set ie = CreateInterfaceIE()
           
' エラーが起こっても無視して進む
On Error Resume Next
    ' 監視実行（無限ループ）
    Do While ie.Busy Or Not is_clicked(ie)                
        If Err.Number <> 0 Then ' エラーが発生したら終了
            PlaySound(ERROR_SOUND_FILE)       ' 音を鳴らす
            MsgBox "エラーが発生しました。監視を停止します。", vbCritical, "エラー"
            Exit Do
        End If        
        WScript.Sleep (1000)  ' 1秒待機
    
        ' 現在の監視ファイルの更新日時の取得
        curFileUpdateTime = ""
        If fso.FileExists(MONITORED_FILE_PATH) Then
            curFileUpdateTime = fso.GetFile(MONITORED_FILE_PATH).DateLastModified
        End If
    
        ' 変更前後のファイル更新日時を比較
        If preFileUpdateTime <> curFileUpdateTime Then
            '変更日時が変わっているならば中身を取得
             Set objFile = fso.OpenTextFile(MONITORED_FILE_PATH, 1)
             fileText = objFile.ReadAll ' ファイルを読み込む
             objFile.Close              ' ファイルを閉じる
             
             num = Len(fileText) - monitoredLength     ' まだ監視していない文字数を取得する
             If num <= 0 Then               ' もし文字数がマイナスになったならば（ログローテーションなどでファイルの文字が削除された場合）
                 num = Len(fileText)        ' 監視対象文字数は全てとする
             End If
             
             monitoredText = Right(fileText, num)  ' 監視対象テキストを確定させる
             If InStr(monitoredText, MONITORING_STRING) > 0 Then
                ' 監視文字が存在している場合は
                WriteLog        ' ログに残す
                PlaySound(ALERT_SOUND_FILE)       ' 音を鳴らす
                
                ' アラートを出す
                MsgBox "発見：" & MONITORING_STRING, vbExclamation, "注意："
            End If
            monitoredLength = Len(fileText)           ' 監視済み文字数をセット
        End If
        preFileUpdateTime = curFileUpdateTime   ' 変更後日時を変更前日時として設定
    Loop    
    ie.Quit
    Set fso = Nothing
End Sub

' ログを残す
Sub WriteLog()
    Dim fso         ' ファイル入出力オブジェクト
    Dim objFile     ' ファイルオブジェクト
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    ' ログに残す
    ' ファイルを追記モードで開く（ファイルが無ければ作成）
    Set objFile = fso.OpenTextFile(thisScriptPath & "\" & logFileName, 8, True)    
    objFile.WriteLine Now & "," & MONITORING_STRING & "," & MONITORED_FILE_PATH     ' ログを書き込む      
    objFile.Close    ' ファイルをクローズ
    
    ' 使用したオブジェクトをクリア
    Set objFile = Nothing
    Set fso = Nothing
End Sub

' 音を鳴らす
Sub PlaySound(soundFile)
    Dim ws  ' Wscript shellのオブジェクト
    If soundFile<> "" Then
        '音源が設定されていた場合は鳴らす
        Set ws = CreateObject("Wscript.Shell")
        ws.Run thisScriptPath & "\" & soundFile, 1, False
    End If
End Sub

' インターフェース用のIEを作成する
Function CreateInterfaceIE()
' IEのオブジェクトを作り、画面の設定をして表示
    Dim ie
    Set ie = CreateObject("InternetExplorer.Application")
    ie.Width = 300
    ie.Height = 170
    ie.Toolbar = False
    ie.StatusBar = False
    ie.Resizable = False
    ie.Visible = True
    ie.Navigate "about:blank"
    
    Do While ie.Busy
        WScript.Sleep (100)
    Loop
    
    Dim HTML   
    HTML = "" _
    + "<form>" _
    + "  <input type=""hidden"" name=""CLICKED"" value=""false""/>" _
    + "  監視対象ファイル:　" & MONITORED_FILE_PATH & "<br/>" _
    + "  監視対象文字列:　" & MONITORING_STRING & "<br/>" _
    + "  <input type=""button"" " _
    + "    onClick = ""getElementsByName('CLICKED')(0).value = 'true';""" _
    + "    value=""監視終了"" />" _
    + "</form>"
    ie.Document.Body.InnerHtml = HTML
    ie.Document.Title = "監視中."    
    Set CreateInterfaceIE = ie
End Function

' クリックされたかの判定を行う
Function is_clicked(ie)
  is_clicked = ie.Document.GetElementsByName("CLICKED")(0).Value <> "false"
End Function
```


# 参考サイト
- [VBScriptでGUI](https://s0u.hatenadiary.org/entry/20080308/1205000627)