tech.guitarrapc.cóm

Technical updates

.NET CoreのGeneric Host で Windows Service を作成する

.NET Framework で Windows Service を作るときは、Windows Service のために地道に実装するのは大変.... なので、TopShelf を使うことが定番でした。以前 Nancy を Windows Service でホストする記事を書いたこともあります。

tech.guitarrapc.com

では、.NET Core ではどうでしょうか? TopShelf は .NET Standard 2.0 に対応しているので利用できます。

github.com

しかしGeneric Host は Windows Service も想定されており、かなり簡単に作成できるので見てみましょう。

前回の記事から関連させて、Windows Service + Web Jobs でホスティングすることを目標としてみます。

目次

TL;DR;

.NET Core + Generic Host でもWindows Serviceを作れます。 ここではその作り方とコツを見ていきましょう。

事前に読んでおきたい

tech.guitarrapc.com

tech.guitarrapc.com

Windows Service とは

これです。

Windows Service はめんどうごとが多い

Windows Service といえば作るのがめんどくさい筆頭です。TopShelf を使えばかなり楽ですが、Windows Service自体のハンドルが面倒なことには変わりありません。

特に開始、終了、(停止と再開は置いておいて)、実行ユーザー(プロファイル/アクセス権限)ははまりどころが多いでしょう。

.NET Core でもこの面倒さは変わらず存在します。

Windows Service + WebJobs は可能なのか

一見すると機能が上手く成り立たないように思えますが可能です。

実際に実装して使っています。先のエントリーがされていれば問題ありません。

tech.guitarrapc.com

Windows Service をホストする

順にみていきましょう。

Packageの追加

Windows Servie をホストするには、次のパッケージを追加します。

    <PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
    <PackageReference Include="System.ServiceProcess.ServiceController" Version="4.5.0" />

Windows Service処理時の実装

Windows Service で Start や Stop した時の処理を書きます。感覚的には TopShelf と同じです。

gist.github.com

HostBuilder への拡張メソッド追加

Windows Serviceは、VSなどではコンソールとして起動して、サービスホスティング時だけ先ほどの実装を使ってほしいです。 そこで、サービスホスティング時の処理を拡張メソッドで定義します。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

    public static class ServiceBaseLifetimeHostExtensions
    {
        public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
        }
    }

Mainからの呼び出し

(VS上など)デバッガーアタッチされている、あるいは引数に--console コンソール時にコンソール実行するようにします。

RunConsoleAsync で Ctrl + C を待ち受け SIGTERM でカットしてくれるので便利です。

        static async Task Main(string[] args)
        {
            var isService = !(Debugger.IsAttached || args.Contains("--console"));
            var builder = CreateHostBuilder(args);
            if (isService)
            {
                await builder.RunAsServiceAsync();
            }
            else
            {
                await builder.RunConsoleAsync();
            }
        }

appsettings.json のパス解決

アプリのビルド時にappsettings.json のパスがサービス実行時だけ見つからないケースがあります。 この場合は、SetBasePathしておくといいでしょう。

Assembly からではなく、Processからとるのがオススメです。

                .ConfigureAppConfiguration((hostContext, configApp) =>
                {
                    configApp.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
                })

ビルド時にランタイムを含める

サービスホスト時の注意が、.NET Core Runtime の解決です。 経験した限りでは、.NET Core Runtime に対して System ユーザーでのPATH解決はうまくいかないことが多く困りどころになりそうです。 そのため現在のところランタイムを同梱させてビルドするほうが安定しており良いと判断しています。

dotnet ビルドするときにランタイムを指定するか、あるいはcsproj に指定しましょう。

dotnet publish -r win10-x64

csproj に指定するなら dotnet ビルド時にランタイム指定が不要です。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
  </PropertyGroup>

</Project>

これで、ランタイム同梱でビルドされます。 通常パブリッシュ向けビルドはdotnet publish のみですが、ランタイム同梱などの場合はdotnet build をしてからやるといいでしょう。

dotnet build
dotnet publish

サービス登録

さぁ準備完了です。sc コマンドでサービスを登録しましょう。

sc create myservice binPath=ビルドした.exeへのフルパス

次のメッセージが出れば登録完了です。

[SC] CreateService SUCCESS

サービスを開始しましょう。

sc start myservice

うまく起動できれば成功です!

SERVICE_NAME: myservice
        TYPE               : 10  WIN32_OWN_PROCESS
        STATE              : 2  START_PENDING
                                (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
        WIN32_EXIT_CODE    : 0  (0x0)
        SERVICE_EXIT_CODE  : 0  (0x0)
        CHECKPOINT         : 0x0
        WAIT_HINT          : 0x7d0
        PID                : 31760
        FLAGS

Ref

The Background Tasks Based On Generic Host In .NET Core

Creating Windows service and Linux daemon with the same code base in .NET

Running a .NET Core Generic Host App as a Windows Service

Host .NET Core console application like Windows Service

NetCore 2.1 Generic Host as a service