.NET Framework で Windows Service を作るときは、Windows Service のために地道に実装するのは大変.... なので、TopShelf を使うことが定番でした。以前 Nancy を Windows Service でホストする記事を書いたこともあります。
では、.NET Core ではどうでしょうか? TopShelf は .NET Standard 2.0 に対応しているので利用できます。
しかしGeneric Host は Windows Service も想定されており、かなり簡単に作成できるので見てみましょう。
前回の記事から関連させて、Windows Service + Web Jobs でホスティングすることを目標としてみます。
目次
- 目次
- TL;DR;
- 事前に読んでおきたい
- Windows Service とは
- Windows Service はめんどうごとが多い
- Windows Service + WebJobs は可能なのか
- Windows Service をホストする
- Ref
TL;DR;
.NET Core + Generic Host でもWindows Serviceを作れます。 ここではその作り方とコツを見ていきましょう。
事前に読んでおきたい
Windows Service とは
これです。
Windows Service はめんどうごとが多い
Windows Service といえば作るのがめんどくさい筆頭です。TopShelf を使えばかなり楽ですが、Windows Service自体のハンドルが面倒なことには変わりありません。
特に開始、終了、(停止と再開は置いておいて)、実行ユーザー(プロファイル/アクセス権限)ははまりどころが多いでしょう。
.NET Core でもこの面倒さは変わらず存在します。
Windows Service + WebJobs は可能なのか
一見すると機能が上手く成り立たないように思えますが可能です。
実際に実装して使っています。先のエントリーがされていれば問題ありません。
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 と同じです。
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