.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