2014年1月20日 星期一

Windows Service 控制介面

ServiceController這個類別可以用來控制Service的狀態
詳細的資訊請參考MSDN上面的文件

新增一個Windows Form應用程式來當範例

首先要先參考System.ServiceProcess這個元件

簡單拉兩個按鈕來控制啟動或停止服務

在Button Click事件中,透過ServiceController來切換狀態
private void Btn_Start_Click(object sender, EventArgs e)
{
    // 設定服務
    using (ServiceController objSC = new ServiceController("Service1"))
    {
        // 設定一個 Timeout 時間,若超過 30 秒啟動不成功就宣告失敗
        TimeSpan timeout = TimeSpan.FromMilliseconds(1000 * 30);

        // 啟動服務
        objSC.Start();

        // 設定該服務必須在timeout時間內切換到Running狀態
        objSC.WaitForStatus(ServiceControllerStatus.Running, timeout);
    }
}

private void Btn_Stop_Click(object sender, EventArgs e)
{
    // 設定服務
    using (ServiceController objSC = new ServiceController("Service1"))
    {
        // 若該服務不是「停用」的狀態,才將其停止運作,否則會引發 Exception  
        if (objSC.Status != ServiceControllerStatus.Stopped && objSC.Status != ServiceControllerStatus.StopPending)
        {
            // 設定一個 Timeout 時間,若超過30秒停止不成功就宣告失敗!  
            TimeSpan timeout = TimeSpan.FromMilliseconds(1000 * 30);

            // 停止服務
            objSC.Stop();

            // 設定該服務必須在timeout時間內切換到Stopped狀態
            objSC.WaitForStatus(ServiceControllerStatus.Stopped, timeout);
        }
    }
}

再拉個按鈕來發送自訂命令

透過ExecuteCommand來發送命令,需要注意這裡面的CommandID必須在128~256這個範圍
private void Btn_CMD_128_Click(object sender, EventArgs e)
{
    // 設定服務
    using (ServiceController objSC = new ServiceController("Service1"))
    {
        if (objSC.Status == ServiceControllerStatus.Running)
        {
            objSC.ExecuteCommand(128);
        }
    }
}

在Service中去覆寫OnCustomCommand,就可以收到發送命令的通知
至於收到什麼編號要去做什麼行為,簡單地用個switch case就行了
protected override void OnCustomCommand(int command)
{
    switch (command)
    {
        case 128:
            break;
        case 129:
            break;
        case 130:
            break;
        default:
            break;
    }
}

Windows Service 啟動參數

Windows Service可以透過幾種方式來傳入啟動參數
可以利用服務的啟動參數

OnStart就可以收到參數

但需要注意這種方式只能設定一次,下次啟動還要再設定才行
透過命令列的話有兩種方式
net start service1 /3000 /yyyyMMddHHmmss
但這種方式收到的參數也會有斜線

sc start service1 3000 yyyyMMddHHmmss
這種方式就不會有斜線了

還有一種方式是設定在服務機碼的ImagePath後面
需要透過覆寫安裝程式的Install函式來完成
using System.Collections;
using System.ComponentModel;
using Microsoft.Win32;

namespace MyService
{
    [RunInstaller(true)]
    public partial class ProjectInstaller : System.Configuration.Install.Installer
    {
        public ProjectInstaller()
        {
            InitializeComponent();
        }

        public override void Install(IDictionary stateSaver)
        {
            base.Install(stateSaver);

            // 安裝的時後增加啟動參數
            RegistryKey System = Registry.LocalMachine.OpenSubKey("System");
            RegistryKey currentControlSet = System.OpenSubKey("CurrentControlSet");
            RegistryKey services = currentControlSet.OpenSubKey("Services");
            RegistryKey service = services.OpenSubKey(this.serviceInstaller1.ServiceName, true);
            string imagePath = service.GetValue("ImagePath") + " 3000 yyyyMMddHHmmss";
            service.SetValue("ImagePath", imagePath);
            service.Close();
        }
    }
}


服務的內容就會在執行路徑後面帶上參數
透過Environment.GetCommandLineArgs()來取得參數

只不過這種參數方式是需要修改機碼,所以設定上不太方便

最簡單好用的方式,莫過於透過app.config中的appSettings
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <configSections>
        <sectionGroup name="common">
            <section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging"/>
        </sectionGroup>
    </configSections>
    <common>
        <logging>
            <factoryAdapter type="Common.Logging.NLog.NLogLoggerFactoryAdapter, Common.Logging.NLog">
                <arg key="configType" value="FILE" />
                <arg key="configFile" value="~/NLog.config" />
            </factoryAdapter>
        </logging>
    </common>
    <appSettings>
        <add key="TimerInterval" value="3000" />
        <add key="DateTimeFormat" value="HH:mm:ss yyyy/MM/dd" />
    </appSettings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
    <runtime>
        <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
            <dependentAssembly>
                <assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral" />
                <bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" />
            </dependentAssembly>
            <dependentAssembly>
                <assemblyIdentity name="Common.Logging" publicKeyToken="af08829b84f0328e" culture="neutral" />
                <bindingRedirect oldVersion="0.0.0.0-2.1.2.0" newVersion="2.1.2.0" />
            </dependentAssembly>
        </assemblyBinding>
    </runtime>
</configuration>

記得要引用System.Configuration參考

啟動的時後取出參數傳入就行了

Windows Service 遠端偵錯方式

通常在本機開發的時後都是沒有問題的,會發生問題都是佈署到正式環境後才會發生="=
而用一般遠端偵錯的方式,都是已經啟動服務成功才能下中斷點
如果問題出在啟動失敗的話,就中斷不到
這種情境可以加入System.Diagnostics.Debugger.Launch();
讓應用程式等待偵錯工具的連入後才會繼續執行下去
using System.ServiceProcess;

namespace MyService
{
    public partial class Service1 : ServiceBase
    {
        private NowTimeReporter reporter = new NowTimeReporter();

        public Service1()
        {
            InitializeComponent();

            // 偵錯中斷用
            System.Diagnostics.Debugger.Launch();
        }

        protected override void OnStart(string[] args)
        {
            this.reporter.Start();
        }

        protected override void OnStop()
        {
            this.reporter.Stop();
        }
    }
}

執行start.bat來啟動服務
net start Service1

就會跳出選擇偵錯工具的畫面,這裡先不選擇

回到本機,選擇工具->附加執行緒,選擇遠端和輸入IP,再選擇執行檔,然後附加就行了

到Server中上放開偵錯工具選擇畫面


就會停在System.Diagnostics.Debugger.Launch();這一行

在本機也可以用這種偵錯方式,就不用到應用程式進入點去動手腳
只要讓服務啟動,再選擇偵錯工具連入就行了


Windows Service 本機偵錯方式

Windows Service輸出類型雖然是Windows 應用程式

但開發的時後直接執行卻會出現啟動錯誤的提示訊息

原因在於程式進入點是用ServiceBase來啟動,而不是Application.Run
using System.ServiceProcess;

namespace MyService
{
    static class Program
    {
        /// <summary>
        /// 應用程式的主要進入點。
        /// </summary>
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
                { 
                    new Service1() 
                };
            ServiceBase.Run(ServicesToRun);
        }
    }
}

為了方便本機開發,就需要先動點手腳
首先先把邏輯拆到獨立的類別去,並公開Start和Stop方法
using System;
using System.Timers;
using Common.Logging;

namespace MyService
{
    internal class NowTimeReporter
    {
        private Timer timer;
        private ILog log;
        private double timerInterval = 1000;
        private string datetimeFormat = "yyyy/MM/dd HH:mm:ss";

        public NowTimeReporter()
        {
            this.log = LogManager.GetLogger(typeof(Service1));
            this.timer = new Timer();
            this.timer.Interval = this.timerInterval;
            this.timer.AutoReset = false;
            this.timer.Enabled = false;
            this.timer.Elapsed += Timer_Elapsed;
        }

        public void Start()
        {
            this.timer.Enabled = true;
        }

        public void Stop()
        {
            this.timer.Enabled = false;
        }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            this.timer.Stop();

            try
            {
                this.log.TraceFormat("now:{0}", DateTime.Now.ToString(this.datetimeFormat));
            }
            catch (Exception ex)
            {
                this.log.Error(ex);
            }

            this.timer.Start();
        }
    }
}


讓Service只是簡單的呼叫Start和Stop
using System.ServiceProcess;

namespace MyService
{
    public partial class Service1 : ServiceBase
    {
        private NowTimeReporter reporter = new NowTimeReporter();

        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            this.reporter.Start();
        }

        protected override void OnStop()
        {
            this.reporter.Stop();
        }
    }
}

把專案類型切換成主控台應用程式


接下來在程式進入點的地方利用執行時的使用者名稱來判斷要用那一種方式啟動
using System;
using System.ServiceProcess;

namespace MyService
{
    static class Program
    {
        /// <summary>
        /// 應用程式的主要進入點。
        /// </summary>
        static void Main()
        {
            if (Environment.UserName == "SYSTEM")
            {
                ServiceBase[] ServicesToRun;
                ServicesToRun = new ServiceBase[] 
                { 
                    new Service1() 
                };
                ServiceBase.Run(ServicesToRun);
            }
            else
            {
                NowTimeReporter reporter = new NowTimeReporter();
                reporter.Start();
                Console.ReadLine();
            }
        }
    }
}

然後在本機開發的時後,就可以直接下中斷點了







Windows Service 開發

Windows Service是一種沒有UI,開機後不需使用者登入就會執行的應用程式,通常會用來做一些定時排程的工作。
用C#開發Windows Service很簡單,以下用一個簡單的範例來介紹

首先開一個新專案,名稱為MyService

在方案總管中可以看到,只有Program.cs和Service1.cs兩個檔案

先來看看Program.cs,這是應用程式的進入點,主要是透過ServiceBase.Run來啟動服務
using System.ServiceProcess;

namespace MyService
{
    static class Program
    {
        /// <summary>
        /// 應用程式的主要進入點。
        /// </summary>
        static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] 
            { 
                new Service1() 
            };
            ServiceBase.Run(ServicesToRun);
        }
    }
}

再來看一下Service1.cs,可以看到是繼承自ServiceBase的類別,再自行覆寫要處理的事件
using System.ServiceProcess;

namespace MyService
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
        }

        protected override void OnStop()
        {
        }
    }
}

可覆寫的方法如下
OnContinue 指定暫停服務後要繼續正常運作所要執行的動作
OnCustomCommand 指定在具有指定參數值的命令發生時所要執行的動作
需要注意命令編號為128~256
OnPause 指定在服務暫停時所要執行的動作
OnPowerEvent 這適用於攜帶型電腦,當它們進入暫停模式的時候,不同於系統關閉
OnSessionChange 當從 Terminal Server 工作階段接收到變更事件時執行
OnShutdown 指定緊接在系統關閉之前應該發生的處理
OnStart 指定在服務啟動時所要執行的動作
OnStop 指定在服務停止執行時所要執行的動作

可設定的屬性如下,最常用的還是ServiceName這個屬性

更詳細的內容可以參考MSDN關於ServiceBase的介紹

先來簡單地寫個定時功能
透過Log元件輸出目前時間到Log2Console
相關內容可以參考之前的筆記
Common.Logging
好用的LogViewer

using System;
using System.ServiceProcess;
using System.Timers;
using Common.Logging;

namespace MyService
{
    public partial class Service1 : ServiceBase
    {
        private Timer timer;
        private ILog log;
        private string datetimeFormat = "yyyy/MM/dd HH:mm:ss";
        private double timerInterval = 1000;

        public Service1()
        {
            InitializeComponent();
            this.log = LogManager.GetLogger(typeof(Service1));
            this.timer = new Timer();
            this.timer.Interval = this.timerInterval;
            this.timer.AutoReset = false;
            this.timer.Enabled = false;
            this.timer.Elapsed += Timer_Elapsed;
        }

        protected override void OnStart(string[] args)
        {
            this.timer.Enabled = true;
        }

        protected override void OnStop()
        {
            this.timer.Enabled = false;
        }

        private void Timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            this.timer.Stop();

            try
            {
                this.log.TraceFormat("now:{0}", DateTime.Now.ToString(this.datetimeFormat));
            }
            catch (Exception ex)
            {
                this.log.Error(ex);
            }

            this.timer.Start();
        }
    }
}



接下來要增加安裝程式,在Service1.cs的設計畫面按右鍵,選擇加入安裝程式

會增加一個繼承自System.Configuration.Install.Installer的ProjectInstaller類別

先設定服務執行時使用的帳號,比較安全的帳號是NetworkService,如果有需要存取資源的話,再去允許權限或是改用LocalSystem這個帳號

接下來設定服務的啟動方式,最常用的當然是自動啟動

DisplayName是名稱,Description描述

設定好後就完成這個Service,編譯後產生執行檔,接下來就要把這個服務安裝的到作業系統中
比較正規的作法是去新增一個安裝專案,但為了方便開發起見,以下用批次檔的方式來安裝

新增幾個批次檔,並改成永遠複製

install.bat用來安裝
@echo off
set InstallUtil=%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe
if not exist %InstallUtil% set InstallUtil=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe
if not exist %InstallUtil% set InstallUtil=%WINDIR%\Microsoft.NET\Framework\v3.5\InstallUtil.exe
if not exist %InstallUtil% (
 echo InstallUtil.exe not found
 exit
)
 
%InstallUtil% -i MyService.exe

restart.bat用來重新啟動服務
net stop Service1
net start Service1

start.bat用來啟動服務
net start Service1

stop.bat用來停止服務
net stop Service1

uninstall.bat用來反安裝
@echo off
set InstallUtil=%WINDIR%\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe
if not exist %InstallUtil% set InstallUtil=%WINDIR%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe
if not exist %InstallUtil% set InstallUtil=%WINDIR%\Microsoft.NET\Framework\v3.5\InstallUtil.exe
if not exist %InstallUtil% (
 echo InstallUtil.exe not found
 exit
)

%InstallUtil% -u MyService.exe

注意檔案編碼為ANSI格式,UTF8格式在command pro中執行會有亂碼
批次檔是透過InstallUtil.exe這個工具程式來安裝和反安裝,詳細的參數請參考MSDN的說明

點擊install.bat,安裝成功

點繫start.bat,啟動服務

順利的話就可以在Log2Console中定時收到目前時間



要移除服務就點擊uninstall.bat,反安裝成功