[Windows Service] 如何建立Windows Service應用程式

如何建立Windows Service應用程式

前言

Windows Service其實就是一種長期執行的應用程式,可以在電腦啟動時自動執行服務,且不顯示任何UI介面來影響使用者,非常適合背景長期執行功能時使用;筆者最近剛好有使用到Windows Service來處理一些資料面整合的問題,所以簡單紀錄自己實作過程。

 

實作介紹

建立Windows Service專案

image

建立專案後會產生預設服務Service1類別,該類別繼承自System.ServiceProcess.ServiceBase服務基礎類別。由於此預設服務僅能啟動與停止,因此僅需要複寫OnStart及OnStop()方法即可;若調整CanPauseAndContinue為true時則表示此服務可被暫停與繼續,所以就有複寫OnPause()及OnContinue()方法的必要。

image

 

建立服務基礎類別

一般而言會在OnStart方法中建立Timer/Thread來執行任務,並且在OnStop方法中停止Timer/Thread執行,而這些作業其實都是相同的,不需要在各服務中不斷撰寫;因此筆者建立一個Timer服務基礎類別來封裝這些動作,讓繼承此類別的服務僅需實作主要作業(MainTask)即可,其餘的事情就不需要多費心。

partial class TimerServiceBase : ServiceBase
{
    // Fields
    private Logger _logger;

    private int _mainTaskInterval;

    private Timer timer;


    // Constructors
    public TimerServiceBase() : this( NLog.LogManager.GetCurrentClassLogger())
    {
        InitializeComponent();
    }

    public TimerServiceBase(Logger logger, int mainTaskInterval = 60000)
        : base()
    {
        InitializeComponent();

        // set logger
        this._logger = logger;

        // set time interval
        this._mainTaskInterval = mainTaskInterval;
    }


    // Methods
    protected override void OnStart(string[] args)
    {
        // set interval by service parameters
        if (args != null && args.Length > 0)
        {
            int intervalParameter;
            if (int.TryParse(args[0], out intervalParameter))
            { _mainTaskInterval = intervalParameter; }
        }

        // Setup timer
        timer = new Timer();
        timer.Interval = _mainTaskInterval;
        timer.Elapsed += new System.Timers.ElapsedEventHandler(this.OnTimer);
        timer.Start();
        _logger.Info("Start service...");
    }

    protected override void OnStop()
    {
        timer.Stop();
        _logger.Info("Stop!!");
    }

    protected void OnTimer(object sender, System.Timers.ElapsedEventArgs args)
    {
        try
        {
            // avoid error to stop serivce
            MainTask();
        }
        catch (Exception ex)
        {
            _logger.Error("Main task exception", ex);
        }
    }

    protected virtual void MainTask()
    {
        // main job will be here
    }
}

 

建立服務

由於我們已經完成個人服務基礎類別(TimerServiceBase)來處理流程面的事務,因此只要建立類別並繼承TimerServiceBase後,即可專注地覆寫MainTask方法來執行服務主要作業。假設系統需分別同步User及ExchangeRate兩項資訊,但由於更新週期不同所以傾向建立兩個服務來處理,以下參考。

partial class SyncUserService : TimerServiceBase
{   
    // Fields
    private static Logger logger = NLog.LogManager.GetCurrentClassLogger();


    // Constructors
    public SyncUserService(int mainTaskInterval = 60000)
        : base(logger, mainTaskInterval)
    {
        InitializeComponent();
    }


    // Methods
    protected override void MainTask()
    {
        logger.Info("Sync User from DB");

        // do main job here ...
    }
}

 

partial class SyncExchangeRateService : TimerServiceBase
{
    // Fields
    private static Logger logger = NLog.LogManager.GetCurrentClassLogger();


    // Constructors
    public SyncExchangeRateService(int mainTaskInterval = 60000)
        : base(logger, mainTaskInterval)
    {
        InitializeComponent();
    }


    // Methods
    protected override void MainTask()
    {
        logger.Info("Sync Exchange Rate from WebApi");

        // do main job here ...
    }
}

 

調整程式進入點

可於此設定服務的執行週期,此處設定SyncUserService服務每10秒同步User資訊,而SyncExchangeRateService服務每1秒同步ExchangeRate資訊。服務開發完畢後若想要直接執行偵錯時,請參考如何對Windows Service進行除錯文章。

static class Program
{
    /// <summary>
    /// 應用程式的主要進入點。
    /// </summary>
    static void Main()
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[] 
        { 
            new SyncUserService(10000), // 10s
            new SyncExchangeRateService(1000) // 1s
        };
        ServiceBase.Run(ServicesToRun);
    }
}

 

建立ProjectIntasller安裝類別

首先在SyncUserService上按右鍵點選加入安裝程式

image

隨即產生ProjectInstaller檔案於專案中。可在serviceProcessInstaller中可以調整執行服務的帳戶類別。

image

在syncUserServiceInstaller除了可以設定描述(Description)即顯示名稱(DisplayName)外,亦可設定啟動方式(StartType)為自動或手動;另外,在ServicesDependedOn中可以設定此服務之相依服務名稱,表示在啟動服務時會檢查是否存在相依服務,若存在則一起啟動該服務,若否則無法啟動此服務。

image

設定完後如法炮製處理另一個服務,在SyncExchangeRateService上按右鍵點選加入安裝程式

image

最後查看一下ProjectInstaller是否有將上述2個服務列在安裝清單

image

 

安裝服務

此步驟就不贅述了,簡單的使用installutil.exe來部屬服務,指令如下。

安裝: InstallUtil.exe [服務應用程式位置]  

卸載: InstallUtil.exe /u [服務應用程式位置]

 

執行安裝

image

image

 

啟動服務

啟動服務後可以透過LogViewer來查看執行狀態;我們可以發現服務如預期般的執行,SyncUserService啟動後每10秒同步User資訊,而SyncExchangeRateService啟動後每1秒同步ExchangeRate資訊。

image

我們也可以透過啟動參數來設定執行作業週期 (右鍵點選服務之內容)

image

SyncUserService確實在啟動後改為每5秒同步User資訊一次

image

 

注意事項

Current Directory 陷阱

服務被啟動時,當前工作資料夾路徑會是在System32中,因此若在Service中使用相對路徑(Relative Path)來存取外部檔案時,會發生無法取得資料的錯誤。解決方式有兩種,第一種就是直接將所需檔案複製到System32資料夾中;另一種方式就是調整CurrentDirectory位置,如下所示。

static class Program
{
    /// <summary>
    /// 應用程式的主要進入點。
    /// </summary>
    static void Main()
    {
        // 將目前工作目錄設定為服務執行檔位置
        System.IO.Directory.SetCurrentDirectory(System.AppDomain.CurrentDomain.BaseDirectory);

        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[] 
        { 
            new MyService(10000)
        };
        ServiceBase.Run(ServicesToRun);
    }
}

 

參考資訊

https://msdn.microsoft.com/zh-tw/library/zt39148a(v=vs.110).aspx

http://haacked.com/archive/2004/06/29/current-directory-for-windows-service-is-not-what-you-expect.aspx/


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !