개요

mono-service 는 .NET Framework 로 작성된 윈도우 서비스(Windows Services)mono 환경에서 구동할 수 있는 도구이다. 윈도우 서비스는 일반적으로 GUI 가 없는 백그라운드로 동작하는 실행 바이너리로 윈도우 운영체제가 서비스를 안정적으로 동작하도록 지원해 준다.

mono-service 는 .NET Framework 로 컴파일된 바이너리 및 실행 파일을 AppDomain 을 생성한 후 로드한다. mono-service 가 하는 역할은 일반적으로 POSIX 가 정의하는 유닉스 시그널(Unix Signals) 를 받아 처리하기 위한 용도이다. 맥 또는 리눅스 운영체제는 윈도우 운영체제가 제공하는 서비스의 시작/중지 명령을 이해할 수 없기 때문에 SIGINT, SIGKILL 신호 등을 받아서 서비스를 중지하도록 해야 한다.

필자가 회사에서 담당하고 있는 오픈소스 프로젝트인 크레마(게임 데이터 개발 도구)는 전반적으로 플러그인 아키텍처로 여러 가지 기능을 제공한다. 크레마 서버는 다양한 운영체제에서 동작이 가능하도록 mono 에서 실행할 수 있는데, 맥의 launchctl 과 리눅스의 systemd 로 서비스를 제공하기 위해 mono-service 로 서비스 바이너리 파일을 호스팅하도록 한다.

문제 발생

일반적으로 .NET Framework 에서 AppDomain.CurrentDomain.BaseDirectory 속성의 반환되는 결과는 경로 마지막에 / 문자열 붙여준다. 반면 Environment.CurrentDirectory 속성은 마지막에 / 문자열을 붙이지 않는다.

간단한 아래의 콘솔 프로그램의 결과를 보면 쉽게 알 수 있다.

using System;

class MainClass {
  public static void Main (string[] args) {
    Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
    Console.WriteLine (Environment.CurrentDirectory);
  }
}

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug

Path.GetDirectoryName와 조합하면 기대하지 않은 결과가 나올 수 있다. Path.GetDirectoryName/ 가 없는 경로의 마지막은 파일로 인식하여 그 부모의 디렉토리 이름까지의 경로를 반환하는데에서 발생한다.

Console.WriteLine (AppDomain.CurrentDomain.BaseDirectory);
Console.WriteLine (Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory));

Console.WriteLine (Environment.CurrentDirectory);
Console.WriteLine (Path.GetDirectoryName(Environment.CurrentDirectory));

// Results
// ...생략.../bin/debug/
// ...생략.../bin/debug
// ...생략.../bin/debug
// ...생략.../bin/

https://repl.it/@powerumc/BaseDirectory-vs-EnvironmentCurrentDirectory

mono-service 버그

처음 언급한 것처럼 mono-serviceAppDomain 을 생성하여 서비스를 실행한 바이너리를 로드한다. 여기에서 AppDomainApplicationBase 디렉토리를 Environment.CurrentDirectory 로 설정하는 바람에 서비스로 실행되는 컨텍스트에서 AppDomain.CurrentDomain.BaseDirectory 값이 기대한 값과 다르게 반환된다.

아래의 링크는 이런 문제를 해결하기 위해 Pull Request 를 보냈고, 정상적으로 메인 저장소에 머지가 되었다.

Fixed a bug in mono-service.cs by powerumc · Pull Request #17095 · mono/mono

아래의 코드는 간단하게 만든 mono-service 에서 구동할 예제 코드이다.

using System;
using System.ComponentModel;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;

namespace MonoServiceTest
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceBase.Run(new MonoServiceTest());
        }
    }

    public class MonoServiceTest : ServiceBase
    {
        protected override void OnStart(string[] args)
        {
            // Debugger.Launch();
            var baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            var directoryPath = Path.GetDirectoryName(baseDirectory);
            var logFilePath = Path.Combine(directoryPath, "log.txt");
            Console.WriteLine($"AppDomain.BaseDirectory = {baseDirectory}");
            Console.WriteLine($"DirectoryPath = {directoryPath}");
            Console.WriteLine($"LogFile Path = {logFilePath}");
        }
    }

    [RunInstaller(true)]
    public class MonoServiceInstaller : Installer
    {
        public MonoServiceInstaller()
        {
            this.Installers.AddRange(new Installer[]
            {
                new ServiceProcessInstaller
                {
                    Username = null,
                    Password = null,
                    Account = ServiceAccount.LocalSystem
                },
                new ServiceInstaller
                {
                    DisplayName = nameof(MonoServiceTest),
                    ServiceName = nameof(MonoServiceTest),
                    StartType = ServiceStartMode.Automatic
                }
            });
        }
    }
}

위의 코드를 컴파일 한 후 mono-service 로 실행하면 아래와 같이 잘못된 결과를 반환하는 것을 알 수 있다.

mono-service --no-daemon mono_service_test.exe

// Results
// AppDomain.BaseDirectory = ...생략.../bin/Debug
// DirectoryPath = ...생략.../bin
// LogFile Path = ...생략.../bin/log.txt

마무리

크로스 플랫폼을 지원하기 위해 기존 레거시를 .NET Core 로 전환하기엔 기술적인 부분과 운영적인 이슈도 있어서 mono-service 를 검토해 보았다. 그 중 기술적인 부분으로는 더 이상 WCF 서버는 .NET Core 에서 지원하지 않고, gRPC 사용을 권장하고 있다. 이를 위해 gRPC 를 WCF 와 대응되도록 호환 레이어를 만들어 전환하기 위해서 수 많은 테스트를 해야 하고 또 장애에 대응해야 한다.

기존 코드를 이식성이 좋은 mono 를 통해 여러 운영체제를 지원하고자 하였지만, mono-service 의 버그로 인해 버그를 조사하고 PR 를 보내기까지 많은 시간이 소요되었다.

위 버그 픽스 코드가 바로 릴리즈 되는 것은 아니기에 우선적으로 AppDomain.BaseDirectory 의 기대하지 않은 경로에 대해서도 올바르게 동작하도록 개발 중인 소스 코드를 수정해야 했다. 아마 차기 mono 릴리즈 버전에서는 이 문제가 수정될 것이니 가능하면 최신 버전의 mono 를 유지하는 것이 좋을 것 같다.

Posted by 땡초 POWERUMC

댓글을 달아 주세요