任意のコマンドをWindowsサービスにする

任意のコマンドをサブプロセスとして起動する.NETのコンソールアプリを作成し、それをWindowsサービスに登録することで実現します。

ユースケースとしては、.NETで作っていないWeb APIサーバーやgRPCサーバー等が考えられます。
※GUIアプリはWindowsサービスで起動できません。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314891/aeacf306-28b1-a7cd-8ba4-c07d7e25d511.png)





.NETの[汎用ホスト](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host)※で、以下のサービスをホストするアプリを作成します。

1. 指定するコマンドを実行
2. 1が終了したらアプリを終了させる 


※ 汎用ホストに馴染みがない方は、[こちら](https://qiita.com/matsumon-development/items/d66058f742a464ff0971)でできるだけやさしく解説したのでよければ御覧ください。


解説に用いる実行させるコマンドの例として、以下のpythonスクリプトを用意しました。
10秒経過したら終了するスクリプトです。

```python:$USERPROFILE\sample.py
#!/usr/bin/env python3
import os
import sys
from logging import (error, getLogger, StreamHandler, FileHandler, Formatter, Logger,  INFO)
from time import sleep

args = sys.argv

def main():
    #ログフォーマット
    fmt = Formatter('%(asctime)s [%(levelname)s] %(message)s')

    #コンソールログのハンドラー
    strhandler = StreamHandler()
    strhandler.setFormatter(fmt)

    #ロガーの定義
    logger: Logger  = getLogger(__name__)
    logger.setLevel(INFO)
    logger.addHandler(strhandler)
  
    logger.info('ファイル名は%sです'  %  args[0])
    logger.info('引数1は%sです'  %  args[1])
    logger.info('引数2は%sです'  %  args[2])
    try:
        # 10回出力する
        for i in range(10):
            logger.info('%s回目です', i+1)
            sleep(1)

    except KeyboardInterrupt:
        logger.error('Terminated by Ctrl + c\n\n\n')
        sys.exit

if __name__ == "__main__":
    main()

```

以下実行例です。
```
PS C:\User\Ken > python sample.py hoge fuga
2022-09-08 5:20:54,884 [INFO] ファイル名はsample.pyです
2022-09-08 5:20:54,884 [INFO] 引数1はhogeです
2022-09-08 5:20:54,885 [INFO] 引数2はfugaです
2022-09-08 5:20:54,885 [INFO] 1回目です
2022-09-08 5:20:55,890 [INFO] 2回目です
2022-09-08 5:20:56,896 [INFO] 3回目です
2022-09-08 5:20:57,901 [INFO] 4回目です
2022-09-08 5:20:58,901 [INFO] 5回目です
2022-09-08 5:20:59,907 [INFO] 6回目です
2022-09-08 5:21:00,910 [INFO] 7回目です
2022-09-08 5:21:01,915 [INFO] 8回目です
2022-09-08 5:21:02,916 [INFO] 9回目です
2022-09-08 5:21:03,919 [INFO] 10回目です
```




## 準備

### 開発環境の準備

アプリを作成するために必要なツールになります。

|ツール|用途|インストール先|
|-----|-|----------|
|.NET6 SDK|アプリのビルド|https://docs.microsoft.com/ja-jp/dotnet/core/install/windows?tabs=net60 |


### プロジェクトの作成

任意のshellのコマンドラインで、Workerサービスのテンプレートを使ってプロジェクトを作成します。

```bash
dotnet new worker -o SomethingAppLauncher
```

以下が作成されます。

```
SomethingAppLauncher
├── Program.cs
├── Properties
├── SomethingAppLauncher.csproj
├── Worker.cs
├── appsettings.Development.json
├── appsettings.json
└── obj
```

### [任意] 出来上がった実行ファイルが.NETランタイムがなしでも起動するようにする

1つのexeにパッケージされたビルドアーティファクトにします。
そしてその中に、.NETランタイムもパッケージします。

```diff_xml:SomethingAppLauncher.csproj
  <Project Sdk="Microsoft.NET.Sdk.Worker">

    <PropertyGroup>
      <TargetFramework>net6.0</TargetFramework>
      <Nullable>enable</Nullable>
      <ImplicitUsings>enable</ImplicitUsings>
+     <PublishSingleFile>true</PublishSingleFile>
+     <SelfContained>true</SelfContained>
      <UserSecretsId>dotnet-SomethingAppLauncher-1233bc4f-73e7-47fa-abcd-1234a5bc7d89</UserSecretsId>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
    </ItemGroup>
</Project>
```


## 1. 最低限動くもの

以下3点を変更します

- Worker.cs
- Program.cs
- SomethingAppLauncher.csproj


### 1.1. サブプロセスを実行するサービスを作成

```diff_c_sharp:Worker.cs
  namespace SomethingAppLauncher;

+ using System.Diagnostics;

  public class Worker : BackgroundService
  {
      private readonly ILogger<Worker> _logger;
+     private readonly IHostApplicationLifetime _applicationLifetime;

-     public Worker(ILogger<Worker> logger)
      //アプリをシャットダウンさせるメソッドを持つ、IHostApplicationLifetimeインターフェースをコンストラクタインジェクション
+     public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime)
      {
          _logger = logger;
+         _applicationLifetime = applicationLifetime;
      }

      protected override async Task ExecuteAsync(CancellationToken stoppingToken)
      {
+         try{
+             Process proc = new Process{
                  //サブプロセスとして実行させるコマンド
+                 StartInfo = new ProcessStartInfo("c:/Program Files/Python310/python.exe"){
+                     UseShellExecute = false,
+                     RedirectStandardOutput = false,
+                 }
+             };
              //サブプロセスのコマンドに引数を渡す
+             proc.StartInfo.ArgumentList.Add("c:/Users/Ken/sample.py"));
+             proc.StartInfo.ArgumentList.Add("hoge"));
+             proc.StartInfo.ArgumentList.Add("fuga"));
              //サブプロセスを実行する
+             proc.Start();
              //サブプロセスを終了を待機する
+             proc.WaitForExit();
              //サブプロセスを閉じる
+             proc.Close();
+          }finally{
               //サブプロセスが終了したらアプリをシャットダウンさせる
+              _applicationLifetime.StopApplication();
+          }
      }
  }
```

実行させるコマンドをハードコーディングしていますが、2章で直します。


### 1.2. Windowsサービスで動くアプリにする

```diff_c_sharp:Program.cs
  using SomethingAppLauncher;
  using System.Diagnostics;

  IHost host = Host.CreateDefaultBuilder(args)
      .ConfigureServices(services =>
      {
          //Workerサービスはすでに雛形でサービスコンテナで登録されるようになっている
          services.AddHostedService<Worker>();
      })
      //汎用ホストを実装したアプリをWindowsサービスとして起動させるためのメソッド
+     .UseWindowsService()
      .Build();

  await host.RunAsync();
```

[UseWindowsServiceメソッド](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.extensions.hosting.windowsservicelifetimehostbuilderextensions.usewindowsservice?view=dotnet-plat-ext-3.1)は、[Microsoft.Extensions.Hosting.IHostBuilderインターフェース](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.extensions.hosting.ihostbuilder?view=dotnet-plat-ext-6.0&viewFallbackFrom=net-6.0)の拡張メソッドです。
[Microsoft.Extensions.Hosting.WindowsServicesパッケージ](https://www.nuget.org/packages/Microsoft.Extensions.Hosting.WindowsServices)をインポートすることで使用できます。
PackageReferenceに追加します。

```diff_xml:SomethingAppLauncher.csproj
  <Project Sdk="Microsoft.NET.Sdk.Worker">
  
    <PropertyGroup>
      <TargetFramework>net6.0</TargetFramework>
      <Nullable>enable</Nullable>
      <ImplicitUsings>enable</ImplicitUsings>
      <PublishSingleFile>true</PublishSingleFile>
      <SelfContained>true</SelfContained>
      <UserSecretsId>dotnet-SomethingAppLauncher-1233bc4f-73e7-47fa-abcd-1234a5bc7d89</UserSecretsId>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
+     <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
    </ItemGroup>
  </Project>
```


### 1.3. 実行例

1. ビルドする

   ```
   PS C:\User\Ken\SomethingAppLauncher\ > dotnet publish -r win-x64 -o dist
   ```

3. 実行する


   ```
   PS C:\User\Ken\SomethingAppLauncher\ > .\dist\SomethingAppLauncher.exe
   2022-09-08 7:27:54,885 [INFO] 1回目です
   2022-09-08 7:27:55,890 [INFO] 2回目です
   2022-09-08 7:27:56,896 [INFO] 3回目です
   2022-09-08 7:27:57,901 [INFO] 4回目です
   2022-09-08 7:27:58,901 [INFO] 5回目です
   2022-09-08 7:27:59,907 [INFO] 6回目です
   2022-09-08 7:28:00,910 [INFO] 7回目です
   2022-09-08 7:28:01,915 [INFO] 8回目です
   2022-09-08 7:28:02,916 [INFO] 9回目です
   2022-09-08 7:28:03,919 [INFO] 10回目です
   info: Microsoft.Hosting.Lifetime[0]
         Application is shutting down...
   info: Microsoft.Hosting.Lifetime[0]
         Application started. Press Ctrl+C to shut down.
   info: Microsoft.Hosting.Lifetime[0]
         Hosting environment: Production
   info: Microsoft.Hosting.Lifetime[0]
         Content root path: C:\User\Ken\omethingAppLauncher\
   ```


## 2. ビルドしたアプリに設定ファイルから、実行させるコマンドを指定する

1章で作ったアプリでは、実行するコマンドをハードコーディングしていました。
変更するにはアプリを再ビルドしなけれななりません。
かなり不便なので、本章で実行するコマンドの指定を設定ファイルに変更します。


### 2.1. appsettings.jsonでサブプロセスのコマンドとその引数を指定

```diff_json:appsettings.json
  {
+   "SubProcess": {
+     "exe": "c:/Program Files/Python310/python.exe",
+     "args":
+     [
+       "c:/Users/Ken/sample.py",
+       "hoge",
+       "fuga"
+     ]
+   },
    "Logging": {
      "LogLevel": {
        "Default": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
```


### 2.2.　サービスにappsettings.jsonの特定のセクションを参照させる


```diff_c_sharp:Worker.cs
  namespace SomethingAppLauncher;

  using System.Diagnostics;

  public class Worker : BackgroundService
  {
      private readonly ILogger<Worker> _logger;
      private readonly IHostApplicationLifetime _applicationLifetime;
+     private readonly IConfiguration _configuration;

-     public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime)
      //IConfigurationインターフェースをコンストラクタでインジェクションする
+     public Worker(ILogger<Worker> logger, IHostApplicationLifetime applicationLifetime, IConfiguration configuration)
      {
          _logger = logger;
          _applicationLifetime = applicationLifetime;
+         _configuration = configuration;
      }

      protected override async Task ExecuteAsync(CancellationToken stoppingToken)
      {
          try{
              Process proc = new Process{
-                 StartInfo = new ProcessStartInfo("c:/Program Files/Python310/python.exe"){
                  //appsettings.jsonのSubProcess.exeセクションを、実行させるコマンドに指定する。
+                 StartInfo = new ProcessStartInfo(_configuration.GetSection("SubProcess").GetValue<String>("exe")){
                      UseShellExecute = false,
                      RedirectStandardOutput = false,
                  }
              };
-             proc.StartInfo.ArgumentList.Add("c:/Users/Ken/sample.py"));
-             proc.StartInfo.ArgumentList.Add("hoge"));
-             proc.StartInfo.ArgumentList.Add("fuga"));
              //appsettings.jsonのSubProcess.args[]セクションの値を順番にコマンドライン引数に指定する。
+             _configuration.GetSection("SubProcess").GetSection("args").Get<List<string>>().ForEach(x => proc.StartInfo.ArgumentList.Add(x));
              proc.Start();
              proc.WaitForExit();
              proc.Close();
           }finally{
               _applicationLifetime.StopApplication();
           }
      }
  }
```

### 2.3. 実行ファイルと同じフォルダにあるapplication.jsonが呼び出されるようにする

デフォルトでは、カレントディレクトリのapplication.jsonが呼び出されるようになっていますが、必ずビルドした実行ファイルと同じところにあるものが呼ばれるようにしておきます。

```
ディレクトリ: C:\Program File\SomethingAppLauncher

Mode       LastWriteTime     Length    Name
------     -------------     ------    ----
-a----     2022/09/01 14:00  69228308  SomethingAppLauncher.exe
-a----     2022/09/01 14:00  69228308  appsettings.json
```

Program.csを変更します。

```diff_c_sharp:Program.cs
  using SomethingAppLauncher;
  using System.Diagnostics;

  IHost host = Host.CreateDefaultBuilder(args)
+     .ConfigureAppConfiguration((hostContext, config) => config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName)))
      .ConfigureServices(services =>
      {
          services.AddHostedService<Worker>();
      })
      .Build();

  await host.RunAsync();
```

### 2.4. 実行例

1. ビルドする

   ```
   PS C:\User\Ken\SomethingAppLauncher\ > dotnet publish -r win-x64 -o dist
   ```

2. 設定ファイルとビルドした実行ファイルをデプロイする


   ```
   PS C:\User\Ken\SomethingAppLauncher\ > mv .\dist\SomethingAppLauncher.exe C:\Program File\SomethingAppLauncher\
   PS C:\User\Ken\SomethingAppLauncher\ > mv .\dist\appsettings.json C:\Program File\SomethingAppLauncher\
   ```


3. 実行する


   ```
   PS C:\User\Ken > C:\Program File\SomethingAppLauncher\SomethingAppLauncher.exe
   2022-09-08 7:27:54,884 [INFO] ファイル名はc:/Users/Ken/sample.pyです
   2022-09-08 7:27:54,884 [INFO] 引数1はhogeです
   2022-09-08 7:27:54,885 [INFO] 引数2はfugaです
   2022-09-08 7:27:54,885 [INFO] 1回目です
   2022-09-08 7:27:55,890 [INFO] 2回目です
   2022-09-08 7:27:56,896 [INFO] 3回目です
   2022-09-08 7:27:57,901 [INFO] 4回目です
   2022-09-08 7:27:58,901 [INFO] 5回目です
   2022-09-08 7:27:59,907 [INFO] 6回目です
   2022-09-08 7:28:00,910 [INFO] 7回目です
   2022-09-08 7:28:01,915 [INFO] 8回目です
   2022-09-08 7:28:02,916 [INFO] 9回目です
   2022-09-08 7:28:03,919 [INFO] 10回目です
   info: Microsoft.Hosting.Lifetime[0]
         Application is shutting down...
   info: Microsoft.Hosting.Lifetime[0]
         Application started. Press Ctrl+C to shut down.
   info: Microsoft.Hosting.Lifetime[0]
         Hosting environment: Production
   info: Microsoft.Hosting.Lifetime[0]
         Content root path: C:\User\Ken
   ```


## 3. 作成したアプリをサービスに登録する

以下参照してください。

https://docs.microsoft.com/ja-jp/dotnet/core/extensions/windows-service#create-the-windows-service