[Topshelf ] 使用 Topshelf 取代 Windows Service 專案

VS IDE 有一個 Windows Service 範本,可以讓我們把應用程式變成 Windows Service,安裝、除錯需要額外的工具輔助,相較之下 Topshelf 在這兩點在使用上較為容易...

Windows Service 範本,需要額外的工具把應用程式裝在服務上,我列出我已知的幾種方式

  • InstallUtil.exe:編寫安裝服務腳本用。
  • Installer:寫自我安裝,琳瑯滿目的 Installer 供你選擇  AssemblyInstaller、TransactedInstaller、ServiceInstaller...etc

服務安裝後還需要,管理他的狀態,可以用 SC.exe 寫服務啟動、停止的腳本,下面列出以往的文章,對它有興趣的可以前往察看

[C#.NET][VB.NET] 如何建立 Windows 服務 Service 專案

[Windows Service] 替 Windows Service 專案加上 Debug 模式

[TFS 2017] 實作 Build vNext 自動部署 Windows Service

 

當改用 Topshelf 之後呢就不需要了,他本身就內建自我安裝服務、啟動服務、移除服務,接下來看看如何使用

本文連結

開發環境

  • VS 2019
  • Topshelf 4.2.1
  • Topshelf.NLog 4.2.1
  • NLog 4.5.11
  • NLog.Config 4.5.11
  • NLog.Schema 4.5.11

起手式

首先建立一個 Console 專案,安裝以下套件

Install-Package Topshelf -Version 4.2.1

建立一個 DoThing,裡面放一個 Timer ,每秒定期列印時間

public class DoThing
{
    private readonly Timer _timer;
 
    public DoThing()
    {
        this._timer         =  new Timer(1000) {AutoReset = true};
        this._timer.Elapsed += (sender, eventArgs) => Console.WriteLine($"Now Time:{DateTime.Now}");
    }
 
    public void Start()
    {
        this._timer.Start();
        Console.WriteLine($"Timer Start");
    }
 
    public void Stop()
    {
        this._timer.Stop();
        Console.WriteLine($"Timer Stop");
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WinService/Lab.TopshelfService/ConsoleApp1/DoThing.cs
 

HostFactory.Run 是用來建立 Windows Service 設定

x.Service<DoThing>:Windows Service 裡面要做的事,這裡放上 DoThing 物件

s.ConstructUsing(name => new DoThing()):建構物件

s.WhenStarted(tc => tc.Start()):啟動,按 F5/Ctrl+F5

s.whenstopped(tc => tc.stop()):停止,按下 Ctrl+C

internal class Program
{
    private static void Main(string[] args)
    {
        HostFactory.Run(x => 
                        {
                            x.Service<DoThing>(s => 
                                                 {
                                                     s.ConstructUsing(name => new DoThing()); 
                                                     s.WhenStarted(tc => tc.Start());          
                                                     s.WhenStopped(tc => tc.Stop());          
                                                 });
                            x.RunAsLocalSystem();
                            var assemblyName = Assembly.GetEntryAssembly().GetName().Name;
                            x.SetDescription("Sample Topshelf Host"); 
                            x.SetDisplayName(assemblyName);                
                            x.SetServiceName(assemblyName);                
                        });
    }
}

https://github.com/yaochangyu/sample.dotblog/blob/master/WinService/Lab.TopshelfService/ConsoleApp1/Program.cs
 

服務配置

x.RunAsLocalSystem():使用 LocalSystem 帳號

x.SetDescription("Sample Topshelf Host"):服務說明

x.SetDisplayName(assemblyName):服務顯示名稱

x.SetServiceName(assemblyName):服務名稱

 

對應到 Windows Service 的配置如下圖

 

以上,就是一個服務基本的配置,相當的簡單。

 

更多的配置

https://topshelf.readthedocs.io/en/latest/configuration/index.html

[料理佳餚] Topshelf - 一個完整接合 Windows 服務的套件

 

執行效果如下

 

服務管理

以我的範例,我的組件名稱 ConsoleApp1.exe,別照抄,這要看你的專案設定唷

安裝:ConsoleApp1 install

移除ConsoleApp1 uninstall

啟動:ConsoleApp1 start

停止:ConsoleApp1 stop

 

我把它弄成了腳本

@echo off
set batchFolder=%~dp0
set serviceName=ConsoleApp1.exe
set servicePatch=%batchFolder%%serviceName%
 
echo BatchFolder:%batchFolder%
echo Service:%servicePatch%
echo Installing %serviceName%...
echo ---------------------------------------------------
%servicePatch% install
%servicePatch% start
echo ---------------------------------------------------
echo Done.

https://github.com/yaochangyu/sample.dotblog/blob/master/WinService/Lab.TopshelfService/ConsoleApp1/install.bat
https://github.com/yaochangyu/sample.dotblog/blob/master/WinService/Lab.TopshelfService/ConsoleApp1/stop.bat
https://github.com/yaochangyu/sample.dotblog/blob/master/WinService/Lab.TopshelfService/ConsoleApp1/uninstall.bat

 

整合 NLog

Install-Package NLog.Config -Version 4.5.11
Install-Package Topshelf -Version 4.2.1

添加 x.UseNLog();

HostFactory.Run(x =>
                {
                    x.Service<DoThing>(s =>
                                       {
                                           s.ConstructUsing(name => new DoThing());
                                           s.WhenStarted(tc => tc.Start());
                                           s.WhenStopped(tc => tc.Stop());
                                       });
                    x.UseNLog();
                    x.RunAsLocalSystem();
                    var assemblyName = Assembly.GetEntryAssembly().GetName().Name;
                    x.SetDescription("Sample Topshelf Host");
                    x.SetDisplayName(assemblyName);
                    x.SetServiceName(assemblyName);
                });

 

設定 NLog Console target

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd"
      autoReload="true"
      throwExceptions="false"
      internalLogLevel="Off" internalLogFile="c:\temp\nlog-internal.log">
  <variable name="myvar" value="myvalue"/>
  <targets async="true">
    <target xsi:type="Console"
            name="ConsoleTarget"
            layout="${longdate} ${callsite} ${level} ${message}"
            encoding="utf-8"
            error="false"
            detectConsoleAvailable="false" />
  </targets>

  <rules>
    <logger name="*" minlevel="Trace" writeTo="ConsoleTarget"/>
  </rules>
</nlog>

 

這樣一來就把原本的 Console log,換了成 NLog Console target,執行結果如下圖

啟動多個服務

一個 HostFactory.Run 只能掛載一個 Service

HostFactory.Run(x =>
                {
                    x.Service<DoThing>(s =>
                                              {
                                                  s.ConstructUsing(name => new DoThing());
                                                  s.WhenStarted(tc => tc.Start());
                                                  s.WhenStopped(tc => tc.Stop());
                                              });
                     ...
                });

 

假如需要多個服務,需要一個物件把服務集中放在集合容器裡面,然後在一起 Start / Stop。

先建立一個抽象物件 IService

public interface IService
{
    void Start();
 
    void Stop();
}

 

Service1、Service2 通通實作 IService

public class Service1 : IService
{
    private static readonly ILogger s_logger;
    private readonly        Timer   _timer;
 
    static Service1()
    {
        if (s_logger == null)
        {
            s_logger = LogManager.GetCurrentClassLogger();
        }
    }
 
    public Service1()
    {
        this._timer         =  new Timer(1000) {AutoReset = true};
        this._timer.Elapsed += (sender, eventArgs) => Console.WriteLine($"I'm Service1");
    }
 
    public void Start()
    {
        this._timer.Start();
 
        s_logger.Trace("Timer Start");
    }
 
    public void Stop()
    {
        this._timer.Stop();
        s_logger.Trace("Timer Stop");
    }
}

 

ServiceContainer 紀錄那些 Service 被放進來

public class ServiceContainer
{
    internal readonly Dictionary<Type, IService> _serviceCaches;
 
    public ServiceContainer()
    {
        if (this._serviceCaches == null)
        {
            this._serviceCaches = new Dictionary<Type, IService>();
        }
    }
 
    public void Add<T>() where T : IService
    {
        var key     = typeof(T);
        var isExist = this._serviceCaches.TryGetValue(key, out var result);
        var service = Activator.CreateInstance(key) as IService;
        if (!isExist)
        {
            this._serviceCaches.Add(key, service);
        }
    }
}

 

ServiceManager.Start / Stop 就去 ServiceContainer._serviceCaches 字典集合拿東西,然後呼叫 Service.Start / Stop

public class ServiceManager
{
    private static ServiceContainer s_container;
 
    public static ServiceContainer Container
    {
        get
        {
            if (s_container == null)
            {
                s_container = new ServiceContainer();
            }
 
            return s_container;
        }
        set => s_container = value;
    }
 
    private bool _isStart;
 
    public void Start()
    {
        if (this._isStart)
        {
            return;
        }
 
        foreach (var service in Container._serviceCaches)
        {
            service.Value.Start();
        }
 
        this._isStart = true;
    }
 
    public void Stop()
    {
        if (!this._isStart)
        {
            return;
        }
 
        foreach (var service in Container._serviceCaches)
        {
            service.Value.Stop();
        }
 
        this._isStart = false;
    }
}

 

調用端代碼,跟上面都很像,改用 ServiceManager 建構服務和 ServiceManager.Container.Add<IService> 增加服務

HostFactory.Run(x =>
                {
                    x.Service<ServiceManager>(s =>
                                              {
                                                  ServiceManager.Container.Add<Service1>();
                                                  ServiceManager.Container.Add<Service2>();
 
                                                  s.ConstructUsing(name => new ServiceManager());
                                                  s.WhenStarted(tc => tc.Start());
                                                  s.WhenStopped(tc => tc.Stop());
                                              });
                     ...
                });

 

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/WinService/Lab.TopshelfService

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2019 .NET

Image result for microsoft+mvp+logo