Windows サービスアプリケーション

Last Updated 2011/09/21


私は .Net Framework クラスライブラリリファレンスを公開していますが、その記事を書く過程で Windows サービスアプリケーションの作成方法とインストールの方法について WEB サイトをあたっていたところ、Visual Studio の Standard 版では Windows サービスアプリケーションを作れないとの記事を見つけました。Standard 版には Windows サービスアプリケーションを作るためのデザイナ機能がないからです。私は俄然、燃えましたね。Standard 版であるがために Windows サービスアプリケーションの作成をあきらめる人がいてはいけないと考え、このページを作成することにしました。

なお、以下の記事は Visual Studio 2005 Standard 版に基づいています。使用言語は C# で、Windows サービスアプリケーションをコンソールアプリケーションとして作成します。もちろん、サービスをインストールするテクニックについても説明します。


Windows サービス

以前のバージョンの Windows では NT サービスと呼んでいたものですが、Windows XP ないし Windows Vista が標準になってきたころから、Windows サービスと呼ぶようになりました。

さて、Windows サービスがどんなものかを説明する前に、コントロールパネルの [管理ツール]-[サービス] を選択すると表示されるダイアログボックスを見てください。この中に、現在のシステムに登録済みの Windows サービスがリストアップされます。私の場合、100 を超えるサービスが登録されています。個々のサービスの説明はしませんが、表示されたダイアログボックスの [説明] を見ればだいたいの機能が分かります。

Windows サービスの代表的なものは Plug and Play です。これはシステム(パソコンのこと)へのハードウエアの増減を感知し、対応するものです。たとえば、USB メモリを接続すると自動的に有効になりますが、そのような機能を可能にするには常にシステムを監視しつづけなければなりません。Windows サービスとはこういう機能を実現するものです。

Windows サービスはシステムの起動時(パソコンに火を入れたとき)に自動的に起動させることができます。また、一時的に停止したり、再開することも可能です。Windows サービスは通常は UI(ユーザーインターフェース)を使わないので、サーバーでの運用に適していて、長時間の稼動にも耐えるという特徴があります(正確には、「耐えなければならない」でしょうね)。

ところで、WEB サイトをあたっていて気付いたことですが、サービスアプリケーションのサンプルコードは実用性のないものばかりですね。私もこのページのためにサービスアプリケーションのサンプルを作ろうと思っても、どんなものがいいかアイデアが浮かんできません。先に説明した登録済みのサービスを見ると、そのほとんどがネットワークに関係するものです。あれこれと思案していたところ、10 年以上前のことを思い出しました。

ある人からパソコンを起動している実働時間を記録したいがどうすればいいかとの質問を受けました。その当時は Windows 95 の時代でしたので、NT サービスは使えませんでした。そこで、何かアプリケーションを作ってパソコンの起動と同時に立ち上げて、そのときの時刻を初期化ファイル(.INI ファイル)かレジストリに記録し、シャットダウンの時にも同様に記録するようなことで対処したと思います。正確には覚えていませんが。そこで、この機能を実現するアプリケーションを Windows サービスとして作ってみたいと思います。

Windows サービスアプリケーションを作る

Windows サービスアプリケーションは通常のアプリケーションとはその扱い方がチョット異なります。通常のアプリケーションはユーザーが動かしますが、サービスアプリケーションはシステム、つまり、.Net Framework を介して Windows が動かします。サービスアプリケーションの実行プログラムは .exe ファイルの形式ですが、それを単独で起動することはできません。動かすのは Windows だからです。したがって、サービスアプリケーションは Windows が理解できる形態で作らなければなりません。つまり、サービスアプリケーションの形式は決まっています。詳しくはおいおい説明しますが、サービスアプリケーションは System.ServiceProcess.ServiceBase クラスから派生する決まりになっています。また、システムに登録するためのインストーラも System.Configuration.Install.Installer クラスから派生するクラスとして設計しなければなりません。

Note シバリがきついのはたしかですが、逆に言えば、型が決まっているともいえます。以下に示すコードを見れば分かりますが、アプリケーションのパターンは決まっています。したがって、ここで紹介するサンプルコードはすべてのサービスアプリケーションのテンプレートとして使うことができます。

さて、今回はサービスアプリケーションをコンソールアプリケーションとして作成します。メニューの [ファイル]-[新規作成]-[プロジェクト] で「コンソールアプリケーション」を選択してください。[プロジェクト名] には "ServiceSample" と入力します。これで以下に示すテンプレートコードがコードエディタ部に表示されます。

using System;
using System.Collections.Generic;
using System.Text;

namespace ServiceSample
{
  class Program
  {
    static void Main(string[] args)
    {
    }
  }
}

ソリューションエクスプローラの "Program.cs" にフォーカスを与えて、マウスの右クリックで表示されるポップアップメニューの [名前の変更] を選択し、ユニットのファイル名を "ServiceSample.cs" に変更します。このとき、「ファイル名を変更しようとしています。このプロジェクトのすべての参照コード要素 'Program' に名前を変更しますか?」と問い合わせてきます。この部分の 'Program' は本来、"ServiceSample" が正しいですよね。なぜなら、デフォルトの 'Program' になっているコード部分を "ServiceSample" に変更するかどうかを問い合わせているのですから。ともあれ、ここは「はい」にしておきましょう(どっちでも大差ありません)。すると、コードは以下のように変更されます。

namespace ServiceSample
{
  class ServiceSample
  {
    static void Main(string[] args)
    {
    }
  }
}

namespace は好みに合わせて変更してください。ここでは "emanual.Service" としておきます。class 宣言および main メソッドの属性を明らかにするために public を追加しましょう。また、コードの視認性をよくするため、閉じカッコ部にそれと分かる説明文を追加しておくことをすすめます。コードを追加していくと、開きカッコとの距離が大きくなってこのカッコがどこと対応しているかが分かりにくくなるからです。ここまでの結果は次のようになります。

namespace emanual.Service ← 変更
{
  public class ServiceSample ← class を追加
  {
    public static void Main(string[] args) ← public を追加
    {
    }
  } // end of ServiceSample class ← 追加
} // end of namespace             ← 追加

下準備ができたところで、ここからが本番です。

サービスアプリケーションは System.ServiceProcess.ServiceBase クラスから派生することが決まりであると先に説明しました。そこで、プロジェクトに以下の参照を追加しなければなりません。もちろん、using 文も必要です。

  System.Serviceprocess.dll

次に、クラスのコンストラクタとグローバルな変数などを保持するクラスを追加します。g クラスとは変なクラス名になっていますが、コードをコンパクトにするためのテクニックと理解してください。グローバル変数をクラスとしてまとめなければならないということはありませんが、コードを見やすくする効果があります。

namespace emanual.Service
{
  class ServiceSample : System.ServiceProcess.ServiceBase
  {
    public static void Main(string[] args)
    {
      ServiceBase.Run(new ServiceSample()); // サービスを起動するときの決まり手
    }

    //-----------------------------------------------------------------
    // コンストラクタ
    public ServiceSample()
    {
      this.AutoLog = true;
      this.CanShutdown = true;
      this.CanStop = true;
      this.ServiceName = g.SERVICE_NAME;
    }
  } // end of ServiceSample class

  //***************************************************************************
  // g class (global な変数を定義する)
  //***************************************************************************
  public class g
  {
    public const string SERVICE_NAME = "ServiceSample";
    public const string DISPLAY_NAME = "Service Sample";
    public const string DESCRIPTION = "ServiceSample の説明文";
    public const string PASSWORD = ""; // 今回は使わない
  } // end of g class
} // end of namespace

これで、最低限のサービスアプリケーションが出来上がりました。もちろん、これでは何もしませんので、以下のコードを追加します。これらのメソッドは Windows が呼び出します。

  // サービスを開始する時に呼び出されるメソッド
  protected override void OnStart(string[] args)
  {
    this.EventLog.WriteEntry(String.Format("OnStart: {0}", DateTime.Now));
  }

  // サービスを停止する時に呼び出されるメソッド
  protected override void OnStop()
  {
    this.RequestAdditionalTime(2000); // 必須ではないが、念のため!
    this.EventLog.WriteEntry(String.Format("OnStop: {0}", DateTime.Now));
    this.ExitCode = 0; // 正常終了するために必要らしい
  }

  // システムのシャットダウン時に呼び出されるメソッド
  protected override void OnShutdown()
  {
    this.EventLog.WriteEntry(String.Format("OnShutdown: {0}", DateTime.Now));
  }

ここまでで、サービスアプリケーションの本体は一応完成です。

サービスのインストーラを作る

サービスアプリケーションはシステム(Windows のこと)が認識できるように登録しなければなりません。サービスアプリケーションの本体と同様に、インストーラの形式は System.Configuration.Install.Installer クラスから派生することに決まっています。以下の参照をプロジェクトに追加します。もちろん、using 文もです。

  System.Configuration.Install.dll

Note ここで重要な注意をします。以下に示すインストーラクラスはインストールに必要な手続きを定義するだけの機能しか持たないので、このクラスがあれば自動的にシステムに登録されるわけではありません。システムのインストール機構はサービスアプリケーションをインストールするとき、Installer クラスから派生するオブジェクトを探して、それを呼び出して使います。つまり、実際のインストールは .Net Framework を通じて Windows が行います。

以下のコードは、サービスアプリケーションのためのインストーラクラスの定義だけを示します。このクラスはサービスアプリケーション用インストーラクラスのテンプレートとして再利用できます。というより、基本的には同じものを使うことができます。ポイントは、ServiceProcessInstaller クラスの Account プロパティと ServiceInstaller クラスの ServiceNamme プロパティです。

namespace emanual.Service
{
  class ServiceSample : System.ServiceProcess.ServiceBase
  {
    .... (省略)
  } // end of ServiceSample class

  //***************************************************************************
  // SampleServiceInstaller class
  //***************************************************************************
  [RunInstaller(true)] ← アセンブリのインストール時にインストーラを起動するように指定する
  public class ServiceSampleInstaller : Installer ← お約束
  {
    public ServiceSampleInstaller()
    {
      ServiceProcessInstaller spi = new ServiceProcessInstaller();
      spi.Username = Environment.UserName;
 //     spi.Password = PASSWORD;
      spi.Account = ServiceAccount.LocalService; // 必須

      ServiceInstaller si = new ServiceInstaller();
      si.ServiceName = g.SERVICE_NAME; // 必須
      si.DisplayName = g.DISPLAY_NAME;
      si.Description = g.DESCRIPTION;

      this.Installers.Add(spi);
      this.Installers.Add(si);
    }
  } // end of ServiceSampleInstaller class
} // end of namespace

サービスアプリケーションのインストール

サービスアプリケーションはコンソールアプリケーションではありますが、単独で実行することはできません。サービスは Windows の機能の一部として動作しますので、Windows に登録しなければならないからです。つまり、サービスのインストールとは、Windows への登録を意味します。

一方、サービスに関係するインストーラクラスはたくさんありますが、それらを使っても Windows への登録、つまり、サービスをインストールすることはできません。サービスを実行するだけでなく、サービスのインストールも Windows の守備範囲にあるからです。インストーラクラスの役割は Windows の動作を補助するだけの機能でしかありません。

さて、サービスを Windows に登録する一般的な手順は .NET Framework に付属の InstallUtil.exe を使う方法です。ちなみに、InstallUtil.exe は私の環境の場合、以下に示すディレクトリ内にあります。

  C:\Windows\Microsoft.NET\Framework\v2.0.50727\InstallUtil.exe

  // Visual Studio 2010 の場合
  C:\Windows\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe

このソフトはサービスに組み込まれたインストーラクラスからインストールに必要なデータを読み込んでインストールを実行します。

InstallUtil.exe の使い方

Visual Studio のヘルプをキーワード "InstallUtil.exe" で検索すると、「インストーラツール(InstallUtil.exe)」があります。この中に InstallUtil.exe の使い方を示す構文の説明がありますが、例によって親切な説明とはいえません。そこで、通常はこれで間に合うという手順だけを説明します。

● インストールするとき

path はサービスアプリケーション(.exe ファイル)のフルパスを指定します。もちろん、InstallUtil.exe とサービスアプリケーションが同じディレクトリ内にある場合はファイル名だけでもかまいません。

    installutil path

● アンインストールするとき

オプションの "/u" を追加するほかはインストールの場合と同じです。

    installutil /u path

ManagedInstallerClass クラス

.Net Framework クラスライブラリの中に ManagedInstallerClass クラスがありますが、その内容は非公開となっています。したがって、Installutil.exe がこのクラスを内部的に使っていることは間違いありませんが、将来も使えるかどうかの保証はありません。

Note 将来なくなる可能性は 0 ではありませんが、まあ大丈夫だと思います。わずか 1 行で済む便利な機能をなくす理由はありません。

ともあれ、このクラスの使い方は簡単なので、説明はしておきましょう。なお、このクラスを使う場合はプロジェクトに System.Configuration.Install.dll への参照を追加しなければなりません。

using System.Configuration.Install; // ManagedInstallerClass

// サービスアプリケーションのフルパス
private string PATH = @"C:\Service\ServiceSample.exe";

// インストールする
private void button1_Click(object sender, EventArgs e)
{
  string[] args = { PATH };
  ManagedInstallerClass.InstallHelper(args);
}

// アンインストールする
private void button2_Click(object sender, EventArgs e)
{
  string[] args = { "/u", PATH };
  ManagedInstallerClass.InstallHelper(args);
}

自己インストール型サービスアプリケーション

サービスアプリケーションをシステムに登録する手順はすでに説明しました。しかし、サービスアプリケーション自身にその機能を持たせることができればそのほうが望ましいですね。そこで、すでに説明したサービスアプリケーションを改造することにしましょう。主な改造点は、main メソッドと g クラスです。コードの変更箇所はコード内に書き込みましたので、あらためてコードの説明はしません。

このアプリケーションの使い方は、インストールするときは引数として "/i" を、アンインストールするときは "/u" を指定するだけです。引数の指定をしないで起動するとサービスアプリケーションとして認識されるので、システムから「Installutil.exe を使ってインストール」しろと怒られます。

● インストールするとき

    ServiceSample /i

● アンインストールするとき

    ServiceSample /u
using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel; //RunInstallerAttribute
using System.Configuration.Install; // Installer
using System.ServiceProcess; // ServiceBase
using System.Configuration.Install; // ManagedInstallerClass
using System.Windows.Forms; // Application

namespace emanual.Service
{
  public class ServiceSample : ServiceBase
  {
    static void Main(string[] args)
    {
      if (args.Length < 1) // 引数がないとき、サービスとして実行する
      {
        ServiceBase.Run(new ServiceSample()); // サービスを実行する
        return;
      }

      // 引数があるとき、インストールまたはアンインストールを実行する
      if (String.Compare(args[0], "/u") == 0) // 引数が "/u" のとき、アンインストールする
      {
        string[] s = { "/u", g.SERVICE_NAME };

        // サービスがすでに存在するとき、アンインストールを実行する
        if (g.ServiceExists(g.SERVICE_NAME))
          ManagedInstallerClass.InstallHelper(s);
      }
      else // 引数が "/u" 以外のとき、インストールする
      {
        string[] s = { g.SERVICE_PATH };

        // サービスが存在しないとき、インストールを実行する
        if (!g.ServiceExists(g.SERVICE_NAME))
          ManagedInstallerClass.InstallHelper(s);
      }
    }

    //-------------------------------------------------------------------------
    // コンストラクタ
    public ServiceSample()
    {
      this.AutoLog = true;
      this.CanShutdown = true;
      this.CanStop = true;
      this.ServiceName = g.SERVICE_NAME;
    }

    // サービスを開始する時に呼び出されるメソッド
    protected override void OnStart(string[] args)
    {
      this.EventLog.WriteEntry(String.Format("OnStart: {0}", DateTime.Now));
    }

    // サービスを停止する時に呼び出されるメソッド
    protected override void OnStop()
    {
    this.RequestAdditionalTime(2000); // 必須ではないが、念のため!
    this.EventLog.WriteEntry(String.Format("OnStop: {0}", DateTime.Now));
    this.ExitCode = 0; // 正常終了するために必要らしい
    }

    // システムのシャットダウン時に呼び出されるメソッド
    protected override void OnShutdown()
    {
      this.EventLog.WriteEntry(String.Format("OnShutdown: {0}", DateTime.Now));
    }
  } // end of ServiceSample class

  //***************************************************************************
  // ServiceSampleInstaller class (インストーラクラス)
  //***************************************************************************
  [RunInstaller(true)] // ← アセンブリのインストール時にインストーラを起動するように指定する
  public class ServiceSampleInstaller : Installer
  {
    public ServiceSampleInstaller()
    {
      ServiceProcessInstaller spi = new ServiceProcessInstaller();
      spi.Username = Environment.UserName;
      spi.Account = ServiceAccount.LocalService;

      ServiceInstaller si = new ServiceInstaller();
      si.ServiceName = g.SERVICE_NAME;
      si.DisplayName = g.DISPLAY_NAME;
      si.Description = g.DESCRIPTION;
      si.StartType = ServiceStartMode.Automatic; // システム起動時にサービスを開始する

      this.Installers.Add(spi);
      this.Installers.Add(si);
    }
  } // end of ServiceSampleInstaller class

  //**************************************************************************************
  // g class (global な変数を定義する)
  //**************************************************************************************
  public class g
  {
    public const string SERVICE_NAME = "ServiceSample";
    public const string DISPLAY_NAME = "Service Sample2";
    public const string DESCRIPTION = "ServiceSample の説明文";
    public const string PASSWORD = "";
    public static string SERVICE_PATH = Application.ExecutablePath; // サービスアプリケーションのフルパス

    // 指定のサービスがインストール済みかどうかをチェックする
    public static bool ServiceExists(string serviceName)
    {
      bool check = false;

      ServiceController[] services = ServiceController.GetServices();

      foreach (ServiceController service in services)
      {
        if (String.Compare(service.ServiceName, serviceName, true) == 0)
        {
          check = true;
          break;
        }
      }

      return check;
    }
  } // end of g class
} // end of namespace

これで目的のアプリケーションは完成です。しかし、1 つ問題があります。OnShutdown メソッドが呼び出されなかったのか、呼び出されたけれどログへの出力ができなかったのかは分かりませんが、このメソッドからログへの出力がありません。原因は不明です。しかし、システムをシャットダウンすると OnStop メソッドが呼び出されるので、このサービスの目的は達成できます。

以上の結果を盛り込んだサービスアプリケーションのソースコードは以下をダウンロードしてください。コンソールアプリケーションとしてのソースコードだけで、プロジェクトファイルは含みません。

ServiceSample.lzh (2,004 bytes)


カスタムコマンド

ServiceBase クラスの以下に示すメソッドは標準のコマンドに呼応して呼び出されるものですが、独自のコマンドを定義することもできます。

  OnContinue
  OnPause
  OnPowerEvent
  OnSessionChange
  OnShutdown
  OnStart
  OnStop

コマンドの送り手は、ServiceController クラスです。このクラスの ExecuteCommand メソッドを使ってコマンドをあらわす数値を送ると、サービス側の OnCustomCommand メソッドがコマンドを受け取るので、そのメソッド内で必要な処理を実行します。


イベントログを見る

コントロールパネルの [管理ツール]-[イベントビューア] を選択すると表示されるダイアログボックスを使ってイベントログを見ることができます。

デフォルトのログ名 "Application" のログは [Windows ログ]-[アプリケーション] の中にあります。しかし、特定のサービスのログを見るにはとうてい便利なツールとはいえません。そこで、自作することをすすめます。

フォームに button1 と textBox1 を配置します。button1 をクリックすると、指定のサービス名のログのメッセージを textBox1 に表示します。

private void button1_Click(object sender, EventArgs e)
{
  const string LOG_NAME = "application";
  const string SERVICE_NAME = "ServiceSample";

  if (!EventLog.Exists(LOG_NAME))
  {
    textBox1.Text = "イベントログがない。";
    return;
  }

  EventLog log = new EventLog(LOG_NAME);

  EventLogEntryCollection col = log.Entries;

//  どれぐらいのログがあるか確かめる場合は以下のコードを生かす
//  textBox1.Text = col.Count.ToString() + "\r\n";
//  Application.DoEvents();

  StringBuilder sb = new StringBuilder();

  for (int i = 0; i < col.Count; ++i)
  {
    if (String.Compare(SERVICE_NAME, col[i].Source, true) == 0)
    {
      sb.Append(String.Format("{0} {1}\r\n", col[i].Index, col[i].Message);
    }
  }

  textBox1.Text = sb.ToString();
}

−以上−