F#でWindows Serviceを実装する。

2017/06/23

.NET FrameworkでWindowsのサービスを実装するのは簡単だろうと高をくくっていたらいろいろとはまったので記録を残しておく。

大きくやらなければならないのは、

  • Serviceとして実行される処理の実装
  • インストーラの実装

の2つ。

そのうち、インストーラの実装の方が、大体「Visual StudioでWindowsサービス用のテンプレートを使って……云々」という記載だけで誤魔化しているものばかりで、ほとんどまともな説明がない。わかってないなら、偉そうに書くなと。

(1)Serviceの実装

Serviceの実装は、

  1. System.ServiceProcess.ServiceBaseクラスを継承したサブクラスを実装する。
  2. System.ServiceProcess.ServiceBase.Runメソッドで、サービスを起動してやる。

2つをやる必要がある。

なので、以下のようなプログラムを記述する。

module File1

open System.ServiceProcess
open System.IO

// Windowsサービスに関連するイベントを処理するクラス
type ServiceTest() =
    inherit ServiceBase()
    do
        base.ServiceName <- "ServiceTest"
        base.CanStop <- true
        base.CanPauseAndContinue <- true
        base.AutoLog <- true

    override this.OnContinue () =
        File.AppendAllText( @"d:\a.txt", "OnContinue\r\n" )

    override this.OnCustomCommand command =
        File.AppendAllText( @"d:\a.txt", "OnCustomCommand " + command.ToString () + "\r\n" )

    override this.OnPause () =
        File.AppendAllText( @"d:\a.txt", "OnPause\r\n" )

    override this.OnPowerEvent powerStatus =
        File.AppendAllText( @"d:\a.txt", "OnPowerEvent " + powerStatus.ToString () + "\r\n" )
        true

    override this.OnSessionChange changeDescription =
        File.AppendAllText( @"d:\a.txt", "OnSessionChange " + changeDescription.ToString () + "\r\n" )

    override this.OnShutdown () =
        File.AppendAllText( @"d:\a.txt", "OnShutdown\r\n" )

    override this.OnStart args =
        File.AppendAllText( @"d:\a.txt", "OnStart ArgCnt=" + args.Length.ToString() + "\r\n" )

// エントリポイント
[<EntryPoint>]
let main args =
    File.AppendAllText( @"d:\a.txt", "main start\r\n" )
    // 起動してやる
    ServiceBase.Run( new ServiceTest() )
    File.AppendAllText( @"d:\a.txt", "main end\r\n" )
    0

この辺については、MSDNの以下を見るとおおむね記載されている。

https://msdn.microsoft.com/ja-jp/library/76477d2t(v=vs.110).aspx

併せて、参照設定で以下を追加しなければならない。

・System.ServiceProcess

(2)インストーラの実装

.NET Frameworkでサービスを実装する場合、インストールするのに「InstallUtil.exe」を使えと指定されている。それについては、MSDNの以下のページに記載がある。

https://msdn.microsoft.com/ja-jp/library/sd8zc8ha(v=vs.110).aspx

問題は、このコマンドでインストールされるサービスのプログラムには、いくつかのおまじないが必要になるということである。でもって、その唱えるべき呪文が正しく記載された情報が見当たらないのが腹が立つ。

まず、実装すべき内容そのものは、以下に記載がある。

https://msdn.microsoft.com/ja-jp/library/system.serviceprocess.serviceprocessinstaller(v=vs.110).aspx

だが、ここに書かれている情報は不完全で、これだけでは動かない。糞が。

概ね、やらなければならないこととしては

  1. System.Configuration.Install.Installerクラスを継承したクラスを実装する
  2. 上記のクラスのメンバとして、System.ServiceProcess.ServiceProcessInstallerと、System.ServiceProcess.ServiceInstallerのインスタンスを持たせる。
  3. コンストラクタで、上記メンバの初期化とSystem.Configuration.Install.Installer.Installersへの追加を行う

ということである。

なので、プログラム的には以下となる。

// 名前空間にはProject.WindowsServiceを指定しなければならない。
namespace Project.WindowsService

open System.ServiceProcess
open System.Configuration.Install
open System.ComponentModel

 // System.Configuration.Install.Installerクラスを継承したサブクラスを実装する。
// その際、このクラスにはRunInstaller(true)の属性を指定してやらなければならない。
// さらに、このクラスはProject.WindowsServiceの名前空間に所属していなければならない。
[<RunInstaller(true)>]
type public MyProjectInstaller() =
    inherit Installer()

    let processInstaller =
        new ServiceProcessInstaller(
            Account = ServiceAccount.LocalSystem
        )

    let serviceInstaller1 =
        new ServiceInstaller(
            StartType = ServiceStartMode.Manual,
            ServiceName = "ServiceTest",
            DisplayName = "ServiceTest",
            Description = "Some string that descript this service desuwa."
        )

    do
        base.Installers.AddRange
            [|
                processInstaller :> Installer;
                serviceInstaller1 :> Installer
            |]

ここで、赤背景で塗ってある箇所は、MSDNにも真っ当に記載されていないポイントなので気を付ける必要がある。

併せて、System.Configuration.Installへの参照設定を追加してやる必要もある。

上記2つのプログラムを適当にコンパイルしてやって(一方はProject.WindowsServiceの名前空間を指定する必要があり、もう一方はmain関数が存在する必要があるから、モジュールにならざるを得ない。そのため、少なくともソースコードのファイルは2つに別れなければならないはず)、「installutil EXEファイル名」と指定してやれば、サービスに登録できるはず。

とりあえず、64bit環境なので管理者権限で起動したPowerShellから、以下のように実行してやる。

------------------------------------------------------------------
Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.
PS C:\Windows\system32> $Env:Path += ";C:\Windows\Microsoft.NET\Framework64\v4.0.30319"
PS C:\Windows\system32> cd D:\MO\F#\ServiceTest\ServiceTest\obj\Debug
PS D:\MO\F#\ServiceTest\ServiceTest\obj\Debug> installutil ServiceTest.exe
Microsoft(R) .NET Framework Installation utility Version 4.7.2046.0
Copyright (C) Microsoft Corporation. All rights reserved.

トランザクションのインストールを実行中です。
インストール段階を開始しています。
D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe アセンブリの進行状態については、ログ ファイルの内容を参照してください。
ファイルは D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog にあります。
アセンブリ 'D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe' をインストールしています。
該当するパラメーター:
   logtoconsole =
   assemblypath = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe
   logfile = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog
サービス 'ServiceTest' をインストールしています...
サービス 'ServiceTest' は正常にインストールされました。
EventLog ソース ServiceTest をログ Application に作成しています...
インストール段階が正常に完了しました。コミット段階を開始しています。
D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe アセンブリの進行状態については、ログ ファイルの内容を参照してください。
ファイルは D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog にあります。
アセンブリ 'D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe' をコミットしています。
該当するパラメーター:
   logtoconsole =
   assemblypath = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe
   logfile = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog
コミット段階が正常に終了しました。
トランザクション インストールが完了しました。
PS D:\MO\F#\ServiceTest\ServiceTest\obj\Debug>
------------------------------------------------------------------

そうすると、以下のようにサービスに登録される。

ソースに記載している通り、d:\a.txtにデバッグ用の文字列を出力させているので、「開始」-「一時停止」-「再開」-「停止」と操作してやると、以下のような出力が得られる。

------------------------------------------------------------------
main start
OnStart ArgCnt=0
OnPause
OnContinue
main end
------------------------------------------------------------------

また、登録したサービスを削除するには「installutil /u EXEファイル名」と指定してやればいい。




0 Comments:

コメントを投稿

Links to this post:

リンクを作成

<< Home