C# - Generic Host를 이용해 .NET 5로 리눅스 daemon 프로그램 만드는 방법
지난번에는 별다른 의존성 없이 단순하게 만드는 방법을 소개했는데요,
C# - .NET Core Console로 리눅스 daemon 프로그램 만드는 방법
; https://www.sysnet.pe.kr/2/0/11958
이번에는 ASP.NET Core의 Generic host를 이용한 방법으로 작성해 보겠습니다. 이에 대해서는 마이크로소프트의 문서에 친절하게 설명하고 있으니,
.NET Generic Host in ASP.NET Core
; https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host
위의 내용을 보셔도 충분합니다. ^^
시작은 .NET 5 Console 프로그램으로 만듭니다. 그다음 Generic Host를 위한 관련 패키지를 추가하면 되는데요, 혹은 그냥 간단하게 "
.NET Core 콘솔 프로젝트에서 Kestrel 호스팅 방법" 글에서 설명한 것처럼 Console csproj 파일의 Sdk 항목을 "Microsoft.NET.Sdk.Web"으로 변경해도 됩니다.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
이제 Program.cs의 Main 메서드를 다음과 같이 작성하고,
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace testd
{
class Program
{
static async Task Main(string[] args)
{
var host = new HostBuilder()
.ConfigureServices((hostContext, services) => services.AddHostedService<ShutdownService>())
.UseConsoleLifetime()
.Build();
await host.RunAsync();
}
}
}
실질적인 서비스 코드는 별도의 ShutdownService.cs 파일을 만들어 다음과 같이 만들어 두면 됩니다.
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
namespace testd
{
class ShutdownService : IHostedService
{
private bool pleaseStop;
private Task BackgroundTask;
private readonly IHostApplicationLifetime applicationLifetime;
public ShutdownService(IHostApplicationLifetime applicationLifetime)
{
this.applicationLifetime = applicationLifetime;
}
public Task StartAsync(CancellationToken _)
{
Console.WriteLine("[testd] Starting service");
BackgroundTask = Task.Run(async () =>
{
while (!pleaseStop)
{
await Task.Delay(50);
}
Console.WriteLine("[testd] Background task gracefully stopped");
});
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("[testd] Stopping service");
pleaseStop = true;
await BackgroundTask;
Console.WriteLine("[testd] Service stopped");
}
}
}
당연히, BackgroundTask 속성에 할당한 Task.Run 코드에는 여러분이 원하는 코드를 넣어야 합니다. 사실상 위의 코드가 전부이고 나머지 팁/트릭은 "
.NET Generic Host in ASP.NET Core" 글의 내용을 참고해 멋을 좀 더 부리시면 됩니다.
편의상, 서비스 등록/해제를 위한 코드를 "
C# - .NET Core Console로 리눅스 daemon 프로그램 만드는 방법" 글에서 소개한 방법처럼 사용해도 됩니다.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
namespace testd
{
class Program
{
static async Task Main(string[] args)
{
if (args.Length >= 1)
{
string netDllPath = typeof(Program).Assembly.Location;
if (args[0] == "--install" || args[0] == "-i")
{
InstallService(netDllPath, true);
}
else if (args[0] == "--uninstall" || args[0] == "-u")
{
InstallService(netDllPath, false);
}
return;
}
var host = new HostBuilder()
.ConfigureServices((hostContext, services) => services.AddHostedService<ShutdownService>())
.UseConsoleLifetime()
.Build();
await host.RunAsync();
}
static int InstallService(string netDllPath, bool doInstall)
{
// 2021-04-22 업데이트
// KillSignal=SIGINT 제거
// KillMode=mixed 추가
string serviceFile = @"
[Unit]
Description={0} running on {1}
[Service]
WorkingDirectory={2}
ExecStart={3} {4}
SyslogIdentifier={5}
KillMode=mixed
[Install]
WantedBy=multi-user.target
";
// ...[생략]...
}
static int ControlService(string serviceName, string mode)
{
// ...[생략]...
}
}
}
이후 서비스 등록/해제 및 systemctl 관련 명령어를 다음과 같이 수행할 수 있습니다.
[서비스 등록]
sudo dotnet ./testd.dll --install
[서비스 해제]
sudo dotnet ./testd.dll --uninstall
[서비스 시작]
sudo systemctl start dotnet-testd
[서비스 중지-1 SIGINT]
sudo systemctl stop dotnet-testd
[서비스 중지-2 SIGTERM]
sudo systemctl kill dotnet-testd
// 기타 Ctrl + '\'키로 발생하는 SIGQUIT
확인을 위해 서비스 등록을 하고 "tail -F /var/log/syslog"로 보면 다음과 같은 로그가 찍혀 있습니다.
Apr 21 22:27:48 testnix systemd[1]: Started testd.dll running on Unix 5.8.0.48.
Apr 21 22:27:48 testnix dotnet-testd[758407]: [testd] Starting service
그리고 서비스 중지(systemctl kill)를 하면 이렇게 로그가 남고!
Apr 21 22:28:30 testnix dotnet-testd[758407]: [testd] Stopping service
Apr 21 22:28:30 testnix dotnet-testd[758407]: [testd] Background task gracefully stopped
Apr 21 22:28:30 testnix dotnet-testd[758407]: [testd] Service stopped
Apr 21 22:28:30 testnix systemd[1]: dotnet-testd.service: Succeeded.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
그나저나, 제가 이 글을 쓴 이유는 "
.NET Core 콘솔 프로젝트에서 Kestrel 호스팅 방법" 글에 달린
덧글 때문입니다. ^^;
현재 .NET 5로 해당 프로그램을 만들면 Ctrl + C에 대해 잘 반응을 하지만 daemon으로 등록해 두면 systemctl stop(SIGINT)에 대해 반응하지 않고 곧바로 프로세스가 종료됩니다. 종료가 된다는 것은 다행이지만 Console.CancelKeyPress 이벤트 핸들러가 실행되지 않으므로 아쉽게도 프로세스 종료 시 수행해야 할 특정 작업이 있다면 더 이상 실행이 되지 않습니다.
그래서 혹시나 마이크로소프트 측에서 만든 서비스 관련 코드라면 이에 대한 반응을 준비하지 않았나 싶어 Generic Host를 이용해 다시 한번 동일한 daemon 예제 코드를 작성해 본 것인데요, 마찬가지로 "systemctl stop"에는 반응하지 않았습니다. (즉,"Background task gracefully stopped" 등의 로그가 남지 않습니다.)
혹시 이 원인에 대해 리눅스 환경 및 닷넷 코어를 잘 아시는 분은 덧글 부탁드립니다. ^^
2021-04-22 업데이트:
덧글에 주신 의견에 따라, 서비스 등록 시 기존의 KillSignal=SIGINT를 제외하고 KillMode=mixed를 추가하면 systemctl stop/kill에 대해 SIGTERM 신호를 받을 수 있어 ProcessExit 이벤트가 실행됩니다. (당분간이라고 해야 할지 모르겠지만) 일단은 이런 방법으로 stop/kill에 대해 수행할 코드가 있다면 대처하시면 되겠습니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]