新しもの好きプログラマの耳より情報ブログ

仕事でもあるプログラミングについて役に立ちそうな情報を発信していこうというブログです。役に立たなそうな情報はfacebookで。

.NET 8でのWindowsサービスの作り方

概要

.NET(Core)になってから、Windowsサービスの作り方は.NET Frameworkの時とはだいぶ違うものになりました。ASP.NETをベースにしたGenericHostを使用するのが基本になりましたし、インストール方法も.NET Framework以前のものに戻っています。

Windowsサービス関連の話題にも何度か触れていますが、意外にサービス自体の作り方をまとめていなかったので、ここで書いてみようと思います。

最初にまとめ

大雑把に言えば、テンプレート「ワーカーサービス」を選んで、NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加して、GenericHostの初期化処理にAddWindowsService()の呼び出しを追加。これだけです。

あとは必要に応じてイベントハンドラやプロパティを設定します。Windowsサービスのライフサイクルに対応させる場合は、WindowsServiceLifeTimeを継承したクラスをさらに追加します。

これを、コードサンプルを添えながら説明していきます。説明に使ったコードはGitHubにも置いておきます

最小限のWindowsサービス

次の手順で、Windowsサービスとしてインストールできる最低限のものが作れます。

  1. テンプレート「ワーカー サービス」を選んでプロジェクト作成
  2. NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加
  3. AddWindowsServiceでサービス追加
  4. Worker.ExecuteAsync()を抜けるようにする(抜けるとサービスのStartが完了したことになる)
  5. 発行したら、「sc create サービス名 binPath=”~.exe”」でサービスインストール。「sc start サービス名」でサービス開始

テンプレートとNuGetは省略して説明します。

AddWindowsServiceでサービス追加

.NET 8のテンプレートならば、Program.csに次のようなコードが有ると思います。

var builder = Host.CreateApplicationBuilder(args);
var host = builder.Build();
host.Run();

GenericHostは、ここに必要な物を足していく使い方になります。

Windowsサービスにしたい場合、次のようにAddWindowsServiceを追加して、パラメータでサービス名を設定します。

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "sample-windows-service-dotnet8";
});

Worker.ExecuteAsync()を抜けるようにする

ワーカーサービスのテンプレートは、起動するとWorkerクラスのExecuteAsyncメソッドが呼ばれ、そこで無限ループするように作られています。コンソールアプリとしてそのまま起動する場合、ExecuteAsyncを抜けたらプロセスが終了します。

しかし、Windowsサービスとして起動する場合は、「サービス開始時にExecuteAsyncが呼ばれ、それを抜けるとサービスがRunning状態になる」という動きに変わります。そのため、ExecuteAsyncの無限ループをやめる必要があります。例えばこんな感じです。

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
    await Task.CompletedTask;
}

発行

サービスとしてインストールするために、発行をします。これはWindowsサービスでなくても同じなので、この記事では説明しません。

サービスインストール・開始

サービスインストールは、.NET Frameworkでは専用のインストール方法が必要でした。しかし.NET(Core)では、ネイティブC++のサービスと同様にsc.exeでインストールする方式になっています。

インストールはsc createです。色々なパラメータ指定はコマンド仕様を見た方が速いですが、最低限のインストールであれば、サービス名とexeのフルパスを指定すればOKです。

sc create "(サービス名)" binPath="(サービスのフルパス).exe"

例:
sc create "sample-windows-service-dotnet8" binPath="C:\Service\sample-windows-service-dotnet8.exe"

開始はsc startです。

sc start "(サービス名)"

例:
sc start "sample-windows-service-dotnet8"

動作確認

ワーカーサービス(というよりGenericHost)はデフォルトでは、コンソールアプリの標準出力へログを出す設定になっています。しかし、サービスとして動かす場合はこのログが見れません。

サービスの動作ログを手軽に見るには、AddDebugでデバッグメッセージへの出力を追加したり、AddEventLogでイベントログへの出力を追加すると便利です。これはGenericHostのILoggerの使い方の説明となるため、本記事ではこれ以上は説明しません。

var builder = Host.CreateApplicationBuilder(args);
(略)
builder.Logging.AddDebug();
builder.Logging.AddEventLog();

Windowsサービスのライフサイクルを追加

最低限のままだと、WindowsサービスのStart・Stopといったライフサイクルに対応できていません。Worker(BackgrondService)から、Windowsサービスのライフサイクルへ置き換えてみます。

ライフサイクルを実装するクラスを追加

WindowsServiceLifeTimeを継承したクラスを作成します。最低限の実装はコンストラクタだけです。次のように、最低でもILoggerは入れておくと便利だと思います。

    internal class SampleServiceLifetime : WindowsServiceLifetime
    {
        public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
        {
            _logger = logger;
        }

        public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
        {
              _logger = logger;
        }
        private readonly ILogger<SampleServiceLifetime> _logger;
    }    

このクラスを、IHostLifetimeをキーにしてシングルトンでDIコンテナに登録します。デバッグ実行の時に呼ばれないように、IsWindowsServiceで判定して、サービス起動した時だけ登録するようにするとより良いです。(DIはWindowsサービスと直接関係ないので説明省略します)

var builder = Host.CreateApplicationBuilder(args);
(略)
if (WindowsServiceHelpers.IsWindowsService())
{
    builder.Services.AddSingleton<IHostLifetime, SampleServiceLifetime>();
}

ライフサイクルのイベントの実装

WindowsServiceLifetimeを継承したクラスに戻り、必要なメソッドをオーバーライドします。例えばStartのイベントの処理をしたい場合は、次のようにOnStartをオーバーライドします。(動作確認用にログを入れています)

internal class SampleServiceLifetime : WindowsServiceLifetime
{
    protected override void OnStart(string[] args)
    {
        _logger.LogInformation("OnStart");
        base.OnStart(args);
    }
}

ライフサイクルのパラメータの設定

WindowsServiceLifetimeを継承したクラスに戻り、必要なプロパティをコンストラクタで設定します。例えばStopを禁止したい場合はこんな感じになります。

internal class SampleServiceLifetime : WindowsServiceLifetime
{
    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    private void CommonSetParams()
    {
        CanStop = false;
    }

}

ちなみにユーザーセッション情報を取る場合は

ユーザーセッション情報を取りたい場合はOnSessionChangeをオーバーライドすることで、SessionLogonなどの情報が撮れます。ただし、CanHandleSessionChangeEventプロパティをtrueにしないとイベントが来ないので注意です。

internal class SampleServiceLifetime : WindowsServiceLifetime
{
    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    public SampleServiceLifetime(IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory, IOptions<HostOptions> optionsAccessor, IOptions<WindowsServiceLifetimeOptions> windowsServiceOptionsAccessor, ILogger<SampleServiceLifetime> logger) : base(environment, applicationLifetime, loggerFactory, optionsAccessor, windowsServiceOptionsAccessor)
    {
        _logger = logger;
        CommonSetParams();
    }

    private void CommonSetParams()
    {
        CanHandleSessionChangeEvent = true;
    }

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    {
        base.OnSessionChange(changeDescription);
    }
 }

まとめ

Windowsサービスの作り方はほとんどC#のコードで完結できてだいぶ楽になったものの、昔の書き方とはだいぶ違うので知らないとだいぶ戸惑いそうです。この記事にまとめたような基本的なポイントを抑えておくと、いざ新しく作ろうとした時にスムーズに行くと思います。