前回はサービスプロジェクトの作成を行いました。
ここでは最低限の動作の仕組みについて確認し、プログラムを実装してみます。

サービスの作成方法についてはサービスプロジェクト作成をご確認ください。

概要
  • サービスの開始〜終了の流れを確認する
  • OnStart、OnStopについて知る
  • サービス起動後の処理について

サービスにおいてもアプリケーションの作成と同様、開始〜終了の流れがあります。
ただしサービスでは開始・終了に加えて、一時停止やセッションの変更などを通知まで管理することができます。

サービスとアプリケーションの主な比較について記述します。

機能サービスアプリケーション
使用するクラスServiceBase
クラス
Form
クラス
起動時ServiceBase.OnStart
関数
Form.OnLoad
イベント
終了時ServiceBase.OnStop
関数
Form.Closing
イベント
Form.Closed
イベント
一時停止 / 再開ServiceBase.OnPause
関数
ServiceBase.OnContinue
関数
なし
セッションの
変更
ServiceBase.OnSessionChange
関数
なし
シャットダウンServiceBase.OnShutdown
関数
なし
電源ステータス変更ServiceBase.OnPowerEvent
関数
なし

本格的なシステムの場合は考慮する必要がありますが、試験であれば起動と終了のみ考慮するだけでOKです。
起動の仕組について説明した後、OnStart関数とOnStop関数について説明します。

Mainプログラム

サービスプログラムにおいてもMain関数があります。
ここではMain関数の処理について確認してみます。

Main関数処理

Main関数はアプリケーションと同様にProgram.csファイルに自動生成されるため、分かりやすいです。

/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
static void Main()
{
    ServiceBase[] ServicesToRun;
    ServicesToRun = new ServiceBase[]
    {
        new Service1()
    };
    ServiceBase.Run(ServicesToRun);
}
  1. ServiceToRunというServiceBase配列のインスタンスを作成しています。
  2. この配列(ServiceToRun)の先頭要素をServiceBaseの派生クラス(Service1)で初期化しています。
  3. ServiceBase.Run関数よりServiceToRunを実行します。
    これによりService1で作成したサービスが起動します。

マイクロソフト社のリファレンスでは、ServiceBase.Run関数は「Service Control Managerに登録することでService Control ManagerからStartコマンドが発行され、結果起動する」といった表記がありますのでRun関数で直接起動している訳ではないようです。

Windowsフォームアプリケーションも同様にProgram.csファイルのMain関数から始まるのですが、こちらは、
Application.Run(new Form1());
といった形でApplicationクラスのRun関数からForm1インスタンスを実行しています。
Windowsフォームアプリケーションはこれでプロジェクト作成直後からフォーム画面を表示することができます。

Main関数の変更

Main関数の処理はこのままでも使えますが、複数のサービスを管理する前提?で配列定義されています。
多くの場合は1つのサービスの開発を行なうため、次のように変更してみます。

private static ServiceBase _servicesToRun;

/// <summary>
/// アプリケーションのメイン エントリ ポイントです。
/// </summary>
static void Main()
{
    _servicesToRun = new Service1();
    ServiceBase.Run(_servicesToRun);
}
  1. _serviceToRunというメンバ変数に変更します。配列にはしません。
  2. 後は_serviceToRunをService1クラスで初期化し、Run関数で開始します。

これで少しですが簡略化できました。
また、_serviceToRunをメンバ変更にすることでサービス起動中もこのサービス制御を行なうことができます。
こちらについてはOnStop関数にて説明します。

OnStart関数

Run関数によりサービスが開始されるとOnStart関数が呼び出されます。
この関数はプロジェクト作成時のサービスクラス(ここではService1)にてオーバーライドされています。

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

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

    protected override void OnStop()
    {
    }
}

OnStart関数の中に初期化したい処理を記載します。
ここではログファイルを作成するインスタンスを初期化してみます。

        /// <summary>
        /// ログ書き込み用
        /// </summary>
        private StreamWriter _log = null;

        /// <summary>
        /// OnStart関数
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            // 初期化
            _log = new StreamWriter("D:log.txt");

            // OnStartログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStartの呼び出し"));
            _log.Flush();
        }
  1. StreamWriterクラスのメンバ変数(_log)を作成します。nullで初期化しておきます。
  2. OnStart関数の先頭で_log変数を初期化します。ファイル名:log.txt
  3. 次にOnStartが呼び出されたことをログファイルに記録します。

サービスを実行し、ログに以下の呼び出し記録が行われます。

日時は実行した時間が記録されます。

保存先がCドライブの場合はWindowsのセキュリティにより保存できない場合があります。
そのような場合、他のドライブやUSBメモリなどに保存します。
私は外付けハードディスク(Dドライブ)に保存しました。

OnStop関数

サービスの停止時にはOnStop関数が呼び出されます。
こちらもOnStart関数と同様にプロジェクト作成時にオーバーライドされています。

同様にログを追加してみます。

        /// <summary>
        /// OnStop関数
        /// </summary>
        protected override void OnStop()
        {
            // OnStopログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStopの呼び出し"));
            _log.Flush();

            // ファイルを閉じてリソース解放
            _log.Close();
            _log.Dispose();
        }
  1. 開始と同様、OnStopの呼び出しログを記載します。
  2. OnStop関数の末尾にて_logインスタンスを解放します。

サービスの開始と停止後ログにそれぞれ記録されます。

サービスの処理実装

開始と終了について説明しました。
ここからはサービス処理をどのように記載するか説明します。

OnStart関数内に実装

試しにOnStart関数でループさせ、サービスの処理を行なうプログラムを作成してみます。
NG例のため実行しないでください。

        /// <summary>
        /// OnStart関数
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            // 初期化
            _log = new StreamWriter("D:log.txt");

            // OnStartログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStartの呼び出し"));
            _log.Flush();

            while(true)
            {
                // ここにサービスで行いたい処理を入れる

                // 負荷高くなるためSleep
                Thread.Sleep(500);
            }
        }
  1. OnStartの末尾でループさせ、その中で行いたい処理を実装します。(ここでは未実装)
  2. 500ms間隔で処理を行なうためにSleep関数を追加します。

このプログラムを実行すると一見動作しているように見えますが、タスクマネージャーを確認するとサービスが「開始中」のまま進んでおりません。

これはOnStart関数がサービス起動の初期化を行なう関数であり、この関数が完了しないとサービスは「実行中」になりません。

OnStart関数でループした場合はサービスの停止を受け付けません。
終了するにはタスクマネージャーからサービスの実態(.exe)を強制終了してください。
この場合は異常終了のためOnStop関数も呼ばれません。

このようにサービスで行いたい処理はOnStart関数が完了後に処理する必要があります。

タイマーによる実装

サービスの処理を実装する方法はいくつかあります。

実装方法
  • タイマーを作成し、その中で実装する
  • Taskクラスなどの非同期処理を行い、その中で実装する
  • コールバック関数を作成し、その中で実装する

ここでは簡単に作成できるタイマーを使ってみます。
最初にタイマーを定義します。

        /// <summary>
        /// サービス処理を行うタイマー
        /// </summary>
        private System.Timers.Timer _timer;

        /// <summary>
        /// OnStart関数
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            // 初期化
            _log = new StreamWriter("D:log.txt");

            // OnStartログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStartの呼び出し"));
            _log.Flush();

            // タイマーを初期化
            _timer = new System.Timers.Timer();
            _timer.Elapsed += OnTimedEvent;
            _timer.Interval = 1000;
            _timer.AutoReset = false;
            _timer.Start();
        }
  1. Timerクラスのメンバ変数(_timer)を作成します。
  2. タイマーの処理を行なう関数(OnTimedEvent)を作成します。
  3. OnStartの末尾でタイマーを初期化から開始まで行います。
    ここでは初期化後にIntervalを1000msに設定し、1秒間隔で処理を行います。
    AutoResetはタイマー処理に時間がかかる場合、処理中に再度呼び出しが発生するため1度だけ実行(false)にします。繰り返しはタイマーの最後で命令します。

タイマーについて

C#ではタイマーの種類が多いです。主に3つのタイマーを切り分けします。

System.Windows.Forms.Timer
Windowsフォームアプリケーションを作成時、コントロールとして貼り付けできるタイマーです。
サービスプロジェクトは画面が存在しないためFormクラスのオブジェクトは基本使えません。

System.Timers.Timer
サービスでも使用できるタイマーです。
今回はこちらを使用しました。

System.Threading.Timer
こちらもサービスで使用することができます。


一長一短ありますが、今回はSystem.Timers.Timerを使います。
Timerだけで宣言すると名前空間が同じで判断できない場合があります。その場合はSystemから入力して変数宣言する必要があります。

次にタイマーの処理を行なう関数(OnTimedEvent)を実装します。

        /// <summary>
        /// タイマー処理
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
        {
            // OnTimedEventログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnTimedEventの呼び出し"));
            _log.Flush();

            // タイマー開始
            ((System.Timers.Timer)source).Start();
        }
  1. OnTimedEvent関数が呼び出しされる度に、ログを出力します。
    本来はここにサービスで行いたい処理を実装します。
  2. OnTimedEventの末尾でStart関数を呼び、タイマーを再開します。

これでサービス起動後に繰り返し処理が行えるようになりました。

サービスの終了

サービスを終了するにはタスクマネージャーなどの外部から「サービスの停止」を行うのが一般的ですが、自身でも終了したい場合があります。
Windowsフォームアプリケーションではメインのフォームをclose()する処理に相当します。

終了するにはServiceBase.Stop関数を使用します。
今回はOnTimedEvent関数を30回実行後、自動終了してみます。

まずはProgram.cs側に停止関数を追加します。

        /// <summary>
        /// サービス終了
        /// </summary>
        public static void ServiceStop()
        {
            _servicesToRun.Stop();
        }
  1. ServiceStop関数を作成します。
  2. 関数の中でメンバ変数(_serviceToRun)のStop関数を実行します。
    この関数は呼び出しされると、OnStop関数が呼び出されサービスが終了します。

次にOnTimedEvent関数を実装します。

     /// <summary>
     /// 終了までのカウント
     /// </summary>
     private int _count = 30;

     /// <summary>
     /// タイマー処理
     /// </summary>
     /// <param name="source"></param>
     /// <param name="e"></param>
     private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
     {
         // OnTimedEventログ書き込み
         _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnTimedEventの呼び出し"));
         _log.Flush();

         if ((--_count) > 0)
         {
             // タイマー開始
             ((System.Timers.Timer)source).Start();
         }
         else
         {
             // サービス終了
             Program.ServiceStop();
         }
     }
  1. _count変数を初期値30で宣言します。
  2. OnTimedEvent関数実行時に_countをデクリメントし、0になったらServiceStop関数を呼び出しサービスを終了します。

30回実行後、サービスが自動停止しました。

以下、ソースコードです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace service_project
{
    internal static class Program
    {
        private static ServiceBase _servicesToRun;

        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        static void Main()
        {
            _servicesToRun = new Service1();
            ServiceBase.Run(_servicesToRun);
        }

        /// <summary>
        /// サービス終了
        /// </summary>
        public static void ServiceStop()
        {
            _servicesToRun.Stop();
        }
    }
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Threading;

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

        /// <summary>
        /// ログ書き込み用
        /// </summary>
        private StreamWriter _log = null;

        /// <summary>
        /// サービス処理を行うタイマー
        /// </summary>
        private System.Timers.Timer _timer;

        /// <summary>
        /// OnStart関数
        /// </summary>
        /// <param name="args"></param>
        protected override void OnStart(string[] args)
        {
            // 初期化
            _log = new StreamWriter("D:log.txt");

            // OnStartログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStartの呼び出し"));
            _log.Flush();

            // タイマーを初期化
            _timer = new System.Timers.Timer();
            _timer.Elapsed += OnTimedEvent;
            _timer.Interval = 1000;
            _timer.AutoReset = false;
            _timer.Start();
        }

        /// <summary>
        /// OnStop関数
        /// </summary>
        protected override void OnStop()
        {
            // OnStopログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnStopの呼び出し"));
            _log.Flush();

            // ファイルを閉じてリソース解放
            _log.Close();
            _log.Dispose();
        }

        /// <summary>
        /// 終了までのカウント
        /// </summary>
        private int _count = 30;

        /// <summary>
        /// タイマー処理
        /// </summary>
        /// <param name="source"></param>
        /// <param name="e"></param>
        private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e)
        {
            // OnTimedEventログ書き込み
            _log.WriteLine(string.Format("{0}_{1}", DateTime.Now.ToString(), "OnTimedEventの呼び出し"));
            _log.Flush();

            if ((--_count) > 0)
            {
                // タイマー開始
                ((System.Timers.Timer)source).Start();
            }
            else
            {
                // サービス終了
                Program.ServiceStop();
            }
        }
    }
}

まとめ

プロジェクト作成と開始・終了まで動かすことができました。
処理にオリジナルの実装を行なうことができますので、アイデア出てきましたらソフト作成についても紹介したいと思います。