4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-08

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

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

image.png

.NETの汎用ホスト※で、以下のサービスをホストするアプリを作成します。

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

※ 汎用ホストに馴染みがない方は、こちらでできるだけやさしく解説したのでよければ御覧ください。

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

$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サービスのテンプレートを使ってプロジェクトを作成します。

dotnet new worker -o SomethingAppLauncher

以下が作成されます。

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

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

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

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. サブプロセスを実行するサービスを作成

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サービスで動くアプリにする

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メソッドは、Microsoft.Extensions.Hosting.IHostBuilderインターフェースの拡張メソッドです。
Microsoft.Extensions.Hosting.WindowsServicesパッケージをインポートすることで使用できます。
PackageReferenceに追加します。

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
    
  2. 実行する

    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でサブプロセスのコマンドとその引数を指定

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の特定のセクションを参照させる

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を変更します。

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. 作成したアプリをサービスに登録する

以下参照してください。

4
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?