Windowsサービスの作り方講座 その4

これまでにUPしたエントリ。
Windowsサービスの作り方講座 その0
Windowsサービスの作り方講座 その1
Windowsサービスの作り方講座 その2
Windowsサービスの作り方講座 その3


さて、続き。今回は、エラーハンドリングの追加をやってみよう。
まず、作る時点で実行時エラーが発生しないように細心の注意を払うのは当然の事だけども、それでも予期しない事態というのは発生するもので、その時にどうするかを考えておかないと、実際の運用で困るプログラムができてしまう。
じゃあ、その、予期しない事態が起こってしまったらどうするのか?だけども、エラーログを外部に吐くのが一番いいと思う。具体的には、スタックトレース。あと、可能なら主要な変数の値。予期しない何かが起こった時、何が起こったのかを知る手掛かりを残しておかないと対応もできないので、その時への備えを実装する。

まず、実行時の例外がメインのスレッドで発生するとどうなるのか?を知る必要があるのでその仕込み。仕込んだコードはこんな。

    Private Sub Runner()
        Dim currentTime As DateTime = Now()
        Dim previousActionTime As DateTime = currentTime - (New TimeSpan(0, My.Settings.IntervalMinutes, 0))
        Dim stateWriter As IO.StreamWriter = My.Computer.FileSystem.OpenTextFileWriter(My.Settings.TargetIP & " state.txt", True)
        Dim targetIsAlive As Boolean = False
        Dim stateReport As String = ""
        Dim deadCounter As Integer = 0
        WriteLog("Enter Runner")
        Do Until goingToDie
            '何かやりたいこと
            currentTime = Now()
            If currentTime - previousActionTime > (New TimeSpan(0, My.Settings.IntervalMinutes, 0)) Then
                targetIsAlive = My.Computer.Network.Ping(My.Settings.TargetIP)
                deadCounter += IIf(targetIsAlive, 0, 1)
                If deadCounter >= 3 Then
                    Throw New Exception("DEAD! DEAD! DEAD!")
                End If
                stateReport = String.Format("{0}:{1} is {2}.", Now(), My.Settings.TargetIP, IIf(targetIsAlive, "alive", "dead"))
                stateWriter.WriteLine(stateReport)
                stateWriter.Flush()
                previousActionTime = currentTime
            End If
            Threading.Thread.Sleep(1)
        Loop
        stateWriter.Close()
        stateWriter.Dispose()
        WriteLog("Exit Runner")
    End Sub

赤字のところが追加した行。3回pingで応答がなかったら例外を発生させるコードを仕込んだ。こいつを発生させるために、pingを投げるIPを変えておく。

こんな感じで。で、ビルドしなおして、実行。すると…

Visual Studioが入っていれば、サービス起動から2分後こんな窓がでる。入っていないとWindows Error Reportingが動きます。ちょっと今回はそこは保留。というか私にもよくわかってませんw ここんところは今後の宿題ということで。
まあ、上の窓が出た場合、「デバッグします!」という操作をすると、こんな画面になる。

で、だ。
Visual Studioが入っているPCで、開発者が例外発生したときそこにいりゃこれでいいんだけど、そんなことはないわけで。なので、外部に何が起こったかを記録しないといけない。その為には発生した例外をトラップして外部に記録する処理を追加する。追加したコードはこんな。

    Private Sub ExceptionHandler(ByVal sender As Object, ByVal args As UnhandledExceptionEventArgs)
        WriteLog(CType(args.ExceptionObject, Exception).ToString)
        End
    End Sub

    Protected Overrides Sub OnStart(ByVal args() As String)
        ' サービスを開始するコードをここに追加します。このメソッドによって、
        ' サービスが正しく実行されるようになります
        WriteLog = AddressOf New Diagnostics.EventLog("Application", My.Computer.Name, Me.ServiceName).WriteEntry
        WriteLog("OnStart")
        Me.args = args
        AddHandler Threading.Thread.GetDomain().UnhandledException, AddressOf ExceptionHandler
        mainThread = New Threading.Thread(AddressOf Runner)
        mainThread.Start()
    End Sub

赤字のところが追加した行。技術的にはmsdnここ@ITここを参考に。ThreadExceptionのハンドラ追加がないのは、Windows Formアプリケーションではないため。msdnには.NET Framework 4からはstack overflowとか、processが汚染される(という表現もすごいが)状況での例外に対応するには属性をつけろと書いてあるので使っている.NET Frameworkのバージョンに注意! ま、今回の前提では.NET Framework 3.5なので、問題ないっぽいです。
で、これを動かすと2分後にこんなログがWindowsのイベントログにのる。

ログの名前:         Application
ソース:           MyService
日付:            2011/01/23 15:15:30
イベント ID:       0
タスクのカテゴリ:      なし
レベル:           情報
キーワード:         クラシック
ユーザー:          N/A
コンピューター:       rinta-PC
説明:
System.Exception: DEAD! DEAD! DEAD!
   場所 MyService.MyService.Runner() 場所 D:\Users\rinta\Documents\Visual Studio 2008\Projects\MyService\MyService\MyService.vb:行 22
   場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   場所 System.Threading.ThreadHelper.ThreadStart()
イベント XML:

  
    
    0
    4
    0
    0x80000000000000
    
    17922
    Application
    rinta-PC
    
  
  
    System.Exception: DEAD! DEAD! DEAD!
   場所 MyService.MyService.Runner() 場所 D:\Users\rinta\Documents\Visual Studio 2008\Projects\MyService\MyService\MyService.vb:行 22
   場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   場所 System.Threading.ThreadHelper.ThreadStart()
  

MyServiceのRunnerサブルーチン内、ソースコードMyService.vbの22行目でダメになったというところまでこれでわかるようになった。ちなみに、この時の22行目というのはここ。

例外を発生させた行そのものではない、という点が若干不満かつ不安になるけど、まあ、現状ではこうなってしまいますよということで。
んで、例外をトラップすると、End文で終了していて、こうなるとサービスのプロセスが止まります。サービスは動いているのがあたりまえで、ひっそりと止まっているといろいろと困る。実行時に例外が発生した時の分析用資料の採取はひとまずできたけど、運用で困らないようにするにはどうしたらいいのか? ここでサービスのプロパティにある「回復」というタブの設定が生きてくる。

この窓。最初の方で、サービスが止まった時の処理を1回目、2回目、3回目以降に分けて、「何もしない」以外には下の3つから指定できるようになっている。

  • サービスを再起動する
  • プログラムを実行する
  • コンピューターを再起動する

一番単純で手っ取り早いのは「サービスを再起動する」だ。食べたデータに起因する問題でなければ、これで運用続行にできるかもしれない。2番目のは、異常終了扱いになった場合の後始末の処理+サービス再起動をしたい場合なんかにいい。3番目のは鉄板だけど、再起動しているあいだ、そのコンピューターの他の機能はアウトになってしまうので、そこのところと折り合いをつけないといけない。エンドユーザ一人が操作しているPCの再起動だったら、そんなに困らないかもしれないけど、何百という機能をサービスしているサーバなんかだったりすると、そう簡単に再起動はできない。これはもう、運用に応じてケースバイケースで考えるしかない。でも3つの選択肢があるので、落とし所は見つけられるかな、と、思います。

さて、今回はこんなところにしよう。エラートラップとイベントログへのスタックトレースの書き出しを実装したけど、反省が少々。

  • Visual Studioの入っていない環境ではコアダンプが取られているんだから、そっちの方が分析に足る資料が取れているんじゃないの? わざわざトラップしてスタックトレースだけにしちゃうメリットは? どうするの? バカなの? 死ぬの?

この問題がある。ここはちょっと、宿題にします。調べないとわからないw

次回は、できれば次の週末に。その時宿題ができていれば宿題をテーマに。そうでなければPCのシャットダウン対応について書こうかなと思います。
MyService(4).zip 直