WindowsサービスをC#で書く

Windowsでデーモンっぽいものを作るとなると、やはりサービスを書いて、管理ツールから開始や停止の制御ができた方がWindowsっぽくてかっこいい。
VisualStudio にはそれを.NETの言語で書くためのテンプレートがあり、それを使えば難なくできると思ったら、いくつか引っかかった点があった。

そこで、その引っかかりやすい点をメモするついでに作り方を書いておこうと思う。

プロジェクトの作成、インストーラの設定

まず、VisualStudioの新規プロジェクト作成から「Windowsサービス」を選ぶ。

OKを押すと、Service1のデザイナが表示される。
ここで、早速サービスのコードを書き始めても良いのだけど、書いたところで「インストーラクラス」をプロジェクトに追加しないとビルドしても登録できない、つまりデバッグもできないんで、先にインストーラクラスを作っておく。
やり方は簡単。Service1のデザイナで何もないところをクリックして、プロパティウィンドウから「インストーラの追加」を選ぶ。*1

これでProjectInstallerのデザイナが表示される。serviceProcessInstaller1 と serviceInstaller1 があるとおもう。このファイルはこれ以上コンポーネントを増やさないので各々の末尾「1」はとっておこう。かっこわるいし。

このファイルでは、インストールするコンポーネントの名前や実行ユーザーを指定できる。設定すべき項目は以下の通り。

  • serviceInstaller
    • DisplayName:サービス一覧画面に表示する名前。(例:DDNS Update Service)
    • Description:サービス一覧画面に表示する説明。(例:DDNSの更新をします)
    • ServiceName:サービスの識別名。サービスのクラス名と合わせるのがよさそう。(例:DDNSUpdateService)
    • StartType:インストール直後の「スタートアップの種類」
  • serviceProcessInstaller
    • Account:サービスの実行アカウントの種類を選択する。

ここで、ソリューションエクスプローラでService1の名前を変えておく。*2
ここまでで、ソリューションエクスプローラはこんな感じになっている。

サービスの実装

次に、サービスのデザイナを開いて、サービスのプロパティから ServiceName を入力しておく。これはserviceInstaller.ServiceNameと合わせておく。

そして、必要なコンポーネントを配置する。System.Windows.Forms 名前空間以外の、UIがでないコンポーネントが利用できる。今回は EventLog と Timer を配置した。

注意点は、Timer は System.Windows.Forms.Timer は使えないので、System.Timers.Timer を使うようにすること。*3見分け方は時間経過イベントが Tick の方が System.Windows.Forms.Timer、Elapsed の方が System.Timers.Timer。
System.Timers.Timer.Enabled の初期値は true なので、プロパティウィンドウから false にしておくことも忘れずに。

あとは、ソースの実装。サービスのソースを開くと、OnStart と OnStop があるのが確認できる。OnStart がサービスが起動するときに呼ばれて、OnStop がサービスが停止するときに呼ばれることになっている。OnStart でスレッドを起動して、OnStop で停止させるのがセオリーらしい。
今回はタイマーの有効・無効だけで開始と停止を制御することにした。

今回書いたのは簡単なDDNS更新サービス。一定期間ごとに決まったURLにアクセスするだけの簡素なものです。

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.Net;

namespace DDNSUpdateService
{
    /// <summary>
    /// DDNSサービスのメインクラス
    /// </summary>
    public partial class DDNSUpdateService : ServiceBase
    {
        // ポーリング間隔
        private readonly TimeSpan pollingInterval = new TimeSpan(0, 30, 0);
        // ポーリング先URL
        private readonly Uri pollingTarget = new Uri("<<更新URLを書く>>");
        // ロックオブジェクト
        private readonly object lockObj = new object();

        // 次回ポーリング時刻
        private DateTime nextPoll = DateTime.MinValue;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public DDNSUpdateService()
        {
            InitializeComponent();
        }

        /// <summary>
        /// サービスの開始時に実行されます
        /// </summary>
        /// <param name="args">引数</param>
        protected override void OnStart(string[] args)
        {
            timer.Enabled = true;
            UpdateNextPoll();
        }

        /// <summary>
        /// サービスの停止時に実行される
        /// </summary>
        protected override void OnStop()
        {
            timer.Enabled = false;
        }

        /// <summary>
        /// タイマーイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            lock (lockObj)
            {
                if (DateTime.Now > nextPoll)
                {
                    try
                    {
                        WebClient client = new WebClient();
                        client.DownloadData(pollingTarget);
                        eventLog.WriteEntry("DDNSの更新は成功しました。", EventLogEntryType.Information);
                    }
                    catch (Exception ex)
                    {
                        eventLog.WriteEntry("DDNSの更新は失敗しました。\n" + ex.Message, EventLogEntryType.Warning);
                    }
                    finally
                    {
                        UpdateNextPoll();
                    }
                }
            }
        }

        /// <summary>
        /// 次回実行時刻を更新します
        /// </summary>
        private void UpdateNextPoll()
        {
            nextPoll = DateTime.Now.Add(pollingInterval);
        }
    }
}

あとはソリューションのビルド。bin 以下に exe ができる。

サービスのインストール

開発環境へのパスが通ったコマンドプロンプト*4を開いて、サービスのexeのあるディレクトリへ移動して、以下のコマンドを実行する。

installutil [登録するサービスのexeファイル]

「インストールが完了しました。」と出ればうまくいったはず。管理ツールの「サービス」をみてみるとうまく登録されている。


インストール直後は停止しているので、サービスを起動しておく。

サービスのデバッグ

サービスをデバッグしたいときは、VisualStudioのツールメニューから「プロセスにアタッチ」を選択する。
サービスのexeを一覧から選んで、「アタッチ」すれば、あとはいつものようにデバッグできる。

まとめてみると、インストーラ周りの設定に気づけばかなり簡単に作れることがわかった。オレオレサービスいっぱいかけるね!

*1:右クリックメニューにも同様の項目があるので、そちらでもOK

*2:ソリューションエクスプローラでファイル名を変えると中のクラス名も変えてくれる

*3:ツールボックス上になければ、ツールボックスで右クリックして「アイテムの追加と削除」から追加する

*4:スタート>プログラム>Microsoft Visual Studio 2005>Visual Studio Tools>Visual Studio コマンドプロンプト