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

これまでにUPしたエントリ。

これまでUPしたエントリ。

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


えーと、昨日の続き。その2です。その2でやることをまず箇条書きにしてみる。

  • サービス名をMyService変更する。
  • サービスの機能を実装するにはどうしたらいいのか説明。
  • どんな風に実装・分離するのがいいのか考える。*1

うーん、ひとまずこんな範囲にしておこう。残りはその3に先延ばし。

サービス名をデフォルトのService1からMyServiceに変更する

昨日までの作業の続きという前提で説明します。昨日の終わりの時点では、サービス名がService1だったので、これを任意のものに変える手順。今回はMyServiceに変えてみます。
まず、ソリューションを開いてVisual Studioを起動。Service1.vbのデザイナを表示させて、デザイナ画面を右クリックして「プロパティ」を選択。

ちょっと注意が必要なのは、ソリューションエクスプローラでService1.vbを選んで右クリックしても「プロパティ」って項目があるんだけども、これはまた別なものが表示される点。

間違うとこんな窓がでてきちゃうので必ずデザイナ画面から右クリック→プロパティで!

で、プロパティが表示されたらServiceNameって項目の値をService1からMyServiceに変更。Nameに入る値はクラスの名前でサービスの名前ではないです。ここもMyServiceに変更。ソースファイルの名前もService1.vbからMyService.vbに変更してみる。名前は私の趣味。で変更後はこうなる。

ここでエラー発生。

'Sub Main'が、'MyService.Service1'に見つかりませんでした。

これはService1ってクラス名をMyServiceに変えてしまったから。このエラーのところをwクリックすると↓の窓がでてくる。

MyService.MyServiceを選んでOKボタンを押せばOK。
よしサービス名変わった、これでOK!と思うと実は間違い。どーゆー事情でかはわからないけど、もう一か所別なところにサービス名を持っていて、そっちを直さないとサービス名が変わりません。変えずにビルド→インストールするとService1が登録されます。このように。

orz
で、直さなければいけないもう一か所は、ServiceInstallerに持っている。ソリューションエクスプローラから右クリック→デザイナの表示を選んで、デザイナ画面が表示されたらServiceInstaller1を右クリックしてプロパティを選択。

ここのServiceNameをMyServiceに変更する。これでビルド→インストールすると…

きちんとMyServiceという名前に変わった。

ん?まてよ、最初のプロパティ変更いらなかったんじゃ? という疑問がフツフツと湧いたので最初のMyService.vbのデザイナのプロパティを変更しなかったら何が起こるかを確認してみた。サービス名はMyServiceで動くんだけど、イベントログにサービス起動した旨書かれるときの名前がService1になってしまう事が判明。↓はService1のままにした時のイベントログの出力。

↓はMyServiceに直した時のイベントログの出力。

なんで別々なのかを小一時間問い詰めたくなる… ま、これはそーゆーモノだとあきらめて受け入れるしかない。

さて、ここまでをおさらい。

  • サービス名を変更するには2か所のServiceNameプロパティを変更すること。
  • ひとつはMyService.vbのデザイナのプロパティ。こちらはイベントログにサービス起動の旨書かれる時のソース名に対応する。
  • もうひとつはServiceInstallerのプロパティ。こちらが実際のサービス名に対応する。

あと、ServiceInstallerのプロパティにはStartTypeやDisplayName、Descriptionといったサービスコントロールパネルで表示される値や設定のデフォルト値がある。この辺は適宜直してほしい。

とりあえずここまで直してビルド→インストールすればMyServiceという名前のからっぽのサービスを起動・停止できるところまで来たはず。次にサービスの機能を実現するメインルーチンをどう実装したらいいのかを説明します。

サービスの機能をどこに実装すればいいのか?

サービスのプログラムのテンプレートをもう一度見直してみる。

Public Class MyService

    Protected Overrides Sub OnStart(ByVal args() As String)
        ' サービスを開始するコードをここに追加します。このメソッドによって、
        ' サービスが正しく実行されるようになります
    End Sub

    Protected Overrides Sub OnStop()
        ' サービスを停止するのに必要な終了処理を実行するコードをここに追加します。
    End Sub

End Class

クラス名をMyServiceに書き換えたので、そこだけ変わっている。この二か所のサブルーチンが、コントロールパネルの開始、終了ボタン、コマンドプロンプトから入力するnet start,net stopに対応しているのは前回説明したとおり。じゃあ、これにサービス開始から終了までの間、何らかの処理をするメインループを実装するとしたらどうなるだろうか? Windows Formアプリケーションを書いた事のある人だったら、こんなコードを書くかもしれない。

Public Class MyService
    Private goingToDie As Boolean = False
    Protected Overrides Sub OnStart(ByVal args() As String)
        ' サービスを開始するコードをここに追加します。このメソッドによって、
        ' サービスが正しく実行されるようになります
        Do Until goingToDie
            'なにかやりたいこと。
        Loop
    End Sub

    Protected Overrides Sub OnStop()
        ' サービスを停止するのに必要な終了処理を実行するコードをここに追加します。
        goingToDie = True
    End Sub

End Class

このコードをビルドしてサービスとして実行すると、こんな事になる。

つまり、サービスとして正常起動したよということをWindowsに教えてあげるにはOnStartを所定の時間内で抜けてこないといけない。なので、ここにメインループは書けないのです! がーん!
結構ここでつまづいた人も多いんじゃないかと思う。じゃあ一体どうしたらいいんだよ?と。
答えを一口に言ってしまうと、メインループは別スレッドとして実行する、だ。OnStartはそのスレッドをキックする処理だけ書けばいい。具体的には、こんな感じで。

Public Class MyService
    Private goingToDie As Boolean = False
    Private mainThread As Threading.Thread
    Private args() As String
    Private Sub Runner()
        Do Until goingToDie
            '何かやりたいこと
            Threading.Thread.Sleep(1)
        Loop
    End Sub

    Protected Overrides Sub OnStart(ByVal args() As String)
        ' サービスを開始するコードをここに追加します。このメソッドによって、
        ' サービスが正しく実行されるようになります
        Me.args = args
        mainThread = New Threading.Thread(AddressOf Runner)
        mainThread.Start()
    End Sub

    Protected Overrides Sub OnStop()
        ' サービスを停止するのに必要な終了処理を実行するコードをここに追加します。
        goingToDie = True
        mainThread.Join()
    End Sub

End Class

Runnerってサブルーチン*2を作って、それを別スレッドとしてStartしている。OnStopでメインループを終わるようにしておいて、最後はスレッドが終わる所で同期するようにJoinする。スレッドのAbortの方がいいという考え方もあるかもしれないけど、そこはまあケースバイケースで。私はこの場合、Runnerのメインスレッドの中の処理で一貫性が保たれるための何らかの処理があるだろうという前提を置いて、より堅い実装にしてみた。こうすれば、OnStartで開始のきっかけだけもらいつつ、メインループに突入し、なおかつOnStartを抜けていくことができる。ここまで書ければあとは、本当にやりたい事をメインループの中に埋め込むだけだ。個別の要件を除外した感じで、Windowsサービスとして動くためのひな型は、これで完成と言ってもいい。

でも、もうちょっと続けます。テストやVisual Studioを使ったデバッグをどうやってやったらいいかなんてところを、その3で説明しようと思う。

えーと、あと最後にもうちょっと直したコードを。↑で示したコードだけだと本当にRunnerが動いているのかわからないので、通ったところがわかるようにイベントログに足跡を残すようにしてみた。

Public Class MyService
    Private goingToDie As Boolean = False
    Private mainThread As Threading.Thread
    Private args() As String
    Private WriteLog As Action(Of String)
    Private Sub Runner()
        WriteLog("Entry Runner")
        Do Until goingToDie
            '何かやりたいこと
            Threading.Thread.Sleep(1)
        Loop
        WriteLog("Exit Runner")
    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
        mainThread = New Threading.Thread(AddressOf Runner)
        mainThread.Start()
    End Sub

    Protected Overrides Sub OnStop()
        ' サービスを停止するのに必要な終了処理を実行するコードをここに追加します。
        WriteLog("OnStop")
        goingToDie = True
        mainThread.Join()
    End Sub

End Class

ちょっと補足。EventLogクラスのコンストラクタの第3引数にMe.ServiceNameとか指定しているけど、ここに入る値はクラスの名前のようです。なのでこのコードではMyServiceが第3引数のSourceに設定される。このWriteLogにEventLog.WriteEntryのアドレスを指定する部分をPrivateスコープで宣言しているところへ初期値として設定する所に移動したらうまく動きませんでした。多分、Main関数の中でMe.ServiceNameが組み立てられているんじゃないかなーとか妄想。そんな感じ。

最後にここまで作ったソリューションファイルをzipで置きます。
MyService (2).zip 直

この続きは週末にでも。

*1:後日にします。

*2:Mainサブルーチンは既に予約済みなので使えない。やむなしでRunner。