Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

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에 대해 수행할 코드가 있다면 대처하시면 되겠습니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/26/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-04-21 11시12분
아래의 글은 Generic Host와 docker 환경에서의 종료 관계를 설명하고 있습니다.

Graceful Shutdown C# Apps
; https://medium.com/@rainer_8955/gracefully-shutdown-c-apps-2e9711215f6d
정성태
2021-04-22 11시12분
[----] 글의 아래 부분이 잘못된 내용이라 댓글 남깁니다.

> [서비스 중지-1 SIGINT]
> sudo systemctl stop dotnet-testd

> [서비스 중지-2 SIGTERM]
> sudo systemctl kill dotnet-testd

https://www.freedesktop.org/software/systemd/man/systemd.kill.html#Options 를 참고했을때,

systemctl 의 기본 KillMode는 control-group입니다. 제가 이해한 바, 이 경우 `systemctl stop`이 실행되었을때 다음이 실행됩니다.

1. ExecStop 에 명시된 명령을 실행
2. 이후 종료되지 않은 프로세스가 있다면 프로세스를 강제 종료

아래 인용에 의해, systemctl 의 KillMode를 mixed로 변경하면 이 글의 문제가 해결될것으로 추측합니다.

> If set to mixed, the SIGTERM signal (see below) is sent to the main process while the subsequent SIGKILL signal (see below) is sent to all remaining processes of the unit's control group

또한 systemctl stop 과 systemctl kill 의 차이는 http://0pointer.de/blog/projects/systemd-for-admins-4.html 의 아래 인용이 잘 설명해주고 있으니 참고하시면 될 것 같습니다.

> How does this relate to systemctl stop? kill goes directly and sends a signal to every process in the group, however stop goes through the official configured way to shut down a service, i.e. invokes the stop command configured with ExecStop= in the service file. Usually stop should be sufficient.
[guest]
2021-04-22 11시22분
[----] 제 댓글 중 오해를 부를 수 있는 문장이 있어 다시 댓글 남깁니다.
> systemctl 의 KillMode를 mixed로 변경하면 이 글의 문제가 해결될것으로 추측합니다.

위는

"dotnet-testd 서비스의 KillMode를 mixed로 변경하면 이 글의 문제가 해결될것으로 추측합니다."

로 수정되야 합니다.
[guest]
2021-04-22 05시21분
의견 정말 감사합니다. 테스트를 해보니, 기존의 dotnet-testd.service 파일에 있던 KillSignal=SIGINT를 지우고 KillMode=mixed를 추가해 ProcessExit 이벤트가 모두 실행이 되는 것을 확인했습니다.

한 가지 의문이 있는데요, 아래와 같이 제가 주석을 달았던 것은,

> [서비스 중지-1 SIGINT]
> sudo systemctl stop dotnet-testd

stop 명령어의 경우 다음의 문서 내용과 함께,

systemd.service — Service unit configuration
; https://www.freedesktop.org/software/systemd/man/systemd.service.html

"
ExecStop =

If this option is not specified, the process is terminated by sending the signal specified in KillSignal= or RestartKillSignal= when service stop is requested.
"

이 글의 원래 시작이었던 daemon 소스 코드를 보면,

C# - .NET Core Console로 리눅스 daemon 프로그램 만드는 방법
; https://www.sysnet.pe.kr/2/0/11958

ExecStop은 비어 있고, KillSignal=SIGINT로 설정했기 때문에 "systemctl stop" 명령은 SIGINT를 보내는 것이 맞습니다. 실제로 동일한 소스 코드를 .NET Core 3.1 이하의 환경에서 실행하면 SIGINT (Ctrl+C)에 반응해 Console.CancelKeyPress 이벤트 핸들러가 잘 실행이 됩니다.

즉, 원칙적으로 보면 KillMode의 mixed/control-group 설정에 관계없이 (.NET Core 3.1 이하에서 그랬듯이) 잘 동작해야 하는 것이 아닌가... 라는 것이 의문입니다.
정성태
2021-04-22 06시38분
[----] > C# - .NET Core Console로 리눅스 daemon 프로그램 만드는 방법
> ; https://www.sysnet.pe.kr/2/0/11958

> 즉, 원칙적으로 보면 KillMode의 mixed/control-group 설정에 관계없이 (.NET Core 3.1 이하에서 그랬듯이) 잘 동작해야 하는 것이 아닌가... 라는 것이 의문입니다.

과연 그렇네요, 제가 "C# - .NET Core Console로 리눅스 daemon 프로그램 만드는 방법" 글을 보지 않아 KillSignal=SIGINT 로 되어 있다는 사실을 몰랐네요. 성태님의 말씀이 옳습니다.

https://github.com/dotnet/runtime/issues/51221#issuecomment-823043104 에 이 글과 유사한 문제제기가 있네요.

> Feels like this should be reverted to 3.1 behavior and focus on this #50527 for signal handling improvements. Using SIGTERM to shutdown currently in .NET Core requires more code and blocking code in process exit in order to let other code run (and can result in deadlocks if done incorrectly).


좋은 글 올려주셔서 늘 감사해하고 있습니다. 감사합니다.
[guest]
2021-04-22 09시24분
저도 감사드립니다. ^^
정성태
2021-05-29 05시54분
아래의 이슈가 해결되었다고 나옵니다.

.NET 5 apps can no longer intercept SIGINT signals (receive CancelKeyPress events) when running under Docker #51221
https://github.com/dotnet/runtime/issues/51221

따라서, 다음 버전의 .NET 5 업그레이드에서는 아마도 이 글에서 다룬 문제가 해결될 것입니다.
정성태
2022-06-30 10시19분
Running .NET Core Applications as a Windows Service
; https://code-maze.com/aspnetcore-running-applications-as-windows-service/

Story about graceful termination with modern .NET
; https://blog.kbegiedza.eu/dotnet-and-story-about-graceful-termination
정성태
2023-02-03 09시01분
A Noob Introduction to Hosted Services in ASP.NET Core
; https://mbarkt3sto.hashnode.dev/a-noob-introduction-to-hosted-services-in-aspnet-core

How to start using .NET Background Services
; https://blog.jetbrains.com/dotnet/2023/05/09/dotnet-background-services/

--------------------

Concurrent Hosted Service in .NET 8 | .NET Conf 2023
; https://youtu.be/sD_-XwauabE
정성태

1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13503정성태12/27/20232191Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232298닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232098개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232179디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232857닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232296오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232311Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232322Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232502Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232563닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232265개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232233Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232349개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232136개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232069오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232383개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232196개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232088오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232162개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232301닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232877닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232271개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232611개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232309개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232484닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232211닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...