任意のコマンドをサブプロセスとして起動する.NETのコンソールアプリを作成し、それをWindowsサービスに登録することで実現します。
ユースケースとしては、.NETで作っていないWeb APIサーバーやgRPCサーバー等が考えられます。
※GUIアプリはWindowsサービスで起動できません。
.NETの汎用ホスト※で、以下のサービスをホストするアプリを作成します。
- 指定するコマンドを実行
- 1が終了したらアプリを終了させる
※ 汎用ホストに馴染みがない方は、こちらでできるだけやさしく解説したのでよければ御覧ください。
解説に用いる実行させるコマンドの例として、以下のpythonスクリプトを用意しました。
10秒経過したら終了するスクリプトです。
#!/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ランタイムもパッケージします。
<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. サブプロセスを実行するサービスを作成
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サービスで動くアプリにする
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に追加します。
<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. 実行例
-
ビルドする
PS C:\User\Ken\SomethingAppLauncher\ > dotnet publish -r win-x64 -o dist
-
実行する
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でサブプロセスのコマンドとその引数を指定
{
+ "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の特定のセクションを参照させる
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を変更します。
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. 実行例
-
ビルドする
PS C:\User\Ken\SomethingAppLauncher\ > dotnet publish -r win-x64 -o dist
-
設定ファイルとビルドした実行ファイルをデプロイする
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\
-
実行する
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. 作成したアプリをサービスに登録する
以下参照してください。