如何建立Windows Service應用程式
前言
Windows Service其實就是一種長期執行的應用程式,可以在電腦啟動時自動執行服務,且不顯示任何UI介面來影響使用者,非常適合背景長期執行功能時使用;筆者最近剛好有使用到Windows Service來處理一些資料面整合的問題,所以簡單紀錄自己實作過程。
實作介紹
建立Windows Service專案
建立專案後會產生預設服務Service1類別,該類別繼承自System.ServiceProcess.ServiceBase服務基礎類別。由於此預設服務僅能啟動與停止,因此僅需要複寫OnStart及OnStop()方法即可;若調整CanPauseAndContinue為true時則表示此服務可被暫停與繼續,所以就有複寫OnPause()及OnContinue()方法的必要。
建立服務基礎類別
一般而言會在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上按右鍵點選加入安裝程式
隨即產生ProjectInstaller檔案於專案中。可在serviceProcessInstaller中可以調整執行服務的帳戶類別。
在syncUserServiceInstaller除了可以設定描述(Description)即顯示名稱(DisplayName)外,亦可設定啟動方式(StartType)為自動或手動;另外,在ServicesDependedOn中可以設定此服務之相依服務名稱,表示在啟動服務時會檢查是否存在相依服務,若存在則一起啟動該服務,若否則無法啟動此服務。
設定完後如法炮製處理另一個服務,在SyncExchangeRateService上按右鍵點選加入安裝程式
最後查看一下ProjectInstaller是否有將上述2個服務列在安裝清單
安裝服務
此步驟就不贅述了,簡單的使用installutil.exe來部屬服務,指令如下。
安裝: InstallUtil.exe [服務應用程式位置]
卸載: InstallUtil.exe /u [服務應用程式位置]
執行安裝
啟動服務
啟動服務後可以透過LogViewer來查看執行狀態;我們可以發現服務如預期般的執行,SyncUserService啟動後每10秒同步User資訊,而SyncExchangeRateService啟動後每1秒同步ExchangeRate資訊。
我們也可以透過啟動參數來設定執行作業週期 (右鍵點選服務之內容)
SyncUserService確實在啟動後改為每5秒同步User資訊一次
注意事項
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
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !