概要
.NET(Core)になってから、Windowsサービスの作り方は.NET Frameworkの時とはだいぶ違うものになりました。ASP.NETをベースにしたGenericHostを使用するのが基本になりましたし、インストール方法も.NET Framework以前のものに戻っています。
Windowsサービス関連の話題にも何度か触れていますが、意外にサービス自体の作り方をまとめていなかったので、ここで書いてみようと思います。
最初にまとめ
大雑把に言えば、テンプレート「ワーカーサービス」を選んで、NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加して、GenericHostの初期化処理にAddWindowsService()の呼び出しを追加。これだけです。
あとは必要に応じてイベントハンドラやプロパティを設定します。Windowsサービスのライフサイクルに対応させる場合は、WindowsServiceLifeTimeを継承したクラスをさらに追加します。
これを、コードサンプルを添えながら説明していきます。説明に使ったコードはGitHubにも置いておきます。
最小限のWindowsサービス
次の手順で、Windowsサービスとしてインストールできる最低限のものが作れます。
- テンプレート「ワーカー サービス」を選んでプロジェクト作成
- NuGetの参照に「Microsoft.Extensions.Hosting.WindowsServices」を追加
- AddWindowsServiceでサービス追加
- Worker.ExecuteAsync()を抜けるようにする(抜けるとサービスのStartが完了したことになる)
- 発行したら、「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#のコードで完結できてだいぶ楽になったものの、昔の書き方とはだいぶ違うので知らないとだいぶ戸惑いそうです。この記事にまとめたような基本的なポイントを抑えておくと、いざ新しく作ろうとした時にスムーズに行くと思います。