はじめに
この記事は2部構成の前半です。パート1では、.NET Frameworkにおける「サービス」の起動方法や使い方、プラグアンドプレイサービスの追加方法などについて詳しく説明します。パート2はサービスの仕組みの詳細です。.NET Remotingやリフレクションを使用したアプリケーションドメインのローディングとアンローディングなど、高度なトピックを取り上げます。
こんなことを考えたのは私が最初ではなく、また最後でもないでしょう。事実、リソースセクションには同様の目的で書かれた記事が2つあり、私もそれを参考にしました。しかし、使いやすさと柔軟性では、僭越ながら私の実装がそれらに勝ると思います。
まず、用語について一言触れておきましょう。私たちはよくWindowsサービスとか、Webサービス、サービス指向アーキテクチャなどと言いますが、ここで使われる「サービス(service)」という単語は、これらの概念の本当の意味を表すのに必ずしも適切な単語ではありません。英語には代わりに使用できる単語があまりないのですが、ITの世界で手垢にまみれていない代替語を探すと、奉仕とか世話を意味する「ミニストレーション(ministration)」あたりが最適でしょうか。私自身は、難しげに聞こえる言葉を使いたがる性格ですが、ここは従来の業界用語を踏襲し、特に概念を明確化する必要があるときは、適切な修飾語の助けを借りることにします。
ここで紹介しようとしている新概念は、今回取り上げるWindowsサービスの名前「.NET Service Manager」と関係があります。このWindowsサービスは、平たく言えば、本来なら独立したWindowsサービスとしてインストールすべきサービスを、ドラッグアンドドロップやXCOPYでインストールするサービスです。基本的には、ユーザーがログインしていない間も実行しておきたい長時間稼動のプロセスや、時間限定のプロセスなどが、Windowsサービスの候補となります。
残念ながら、.NETでもWindowsサービスの実装は簡単ではありません。Windowsサービスのインストールや開始/停止は[サービス]MMCスナップインで手動で行わなければなりません。これは非常に面倒な作業であり、Windowsサービスがいくつもあると、サービスリストが手に負えなくなります。
そこで、今回の.NET Service Managerの出番となります。このアプリケーションでは、Windowsサービスを書き、インストールし、(MMCスナップインを使って)開始する、という作業が一度だけで済みます。その後は、ASP.NETなどと同等(またはそれ以上)のXCOPYを使用して実際の処理を行えるようになります。アセンブリ属性を適用し、簡単なインターフェイスを実装するだけで、どのような.NETコードライブラリ(DLL)アセンブリでも.NET Service Managerの下で実行できます。このようなアセンブリを、私は「被管理サービス」と呼んでいます。「Windowsサービスのように動作するものの、実は.NET Service Managerの管理下にあるサービス」の意味です。
この記事(パート1、パート2とも)で「サービス」に言及するときは、修飾語を付け、Windowsサービス(従来の「NTサービス」)なのか、被管理サービス(上記パラグラフで説明したサービス)なのかを示すことにします。パート1では、被管理サービスの実装方法について詳しく説明します。この説明に従えば、.NET Service Managerの働きの仕組みを知らなくても、.NET Service Managerとその機能を利用することができます。働きの仕組みについては、パート2で説明します。
.NET Service Managerを使えば、サービスをプロセスディレクトリにコピーするだけでインストールと開始ができます。また、サービスの更新も同じ配備メカニズムで簡単に行うことができ、その際に.NET Service Manager自体を停止・開始する必要はありません。すべての被管理サービスは独自のアプリケーションドメインで動作しているので、あるサービスを更新しても、他のサービスには何の影響もありませんし、.NET Service Manager自体を停止させる必要もありません。同様に、ある被管理サービスを停止させたいときは、そのDLLをプロセスディレクトリから取り除く(削除もしくは移動する)だけで済み、インストール同様、アンインストールも簡単にできます。
.NETがコードライブラリのconfigファイルをサポートしていないことはご存じでしょう。それにはちゃんとした理由がありますが、私自身は「その機能があればすごく便利なのに」と思うケースを何度も経験しています。被管理サービスもそうしたケースの1つです。そこで、設定値を「app.config」のようなファイル(たとえば、「SampleService.dll.config」)に格納するための簡単なインターフェイスを作ってみました。このconfigファイルは、アプリケーション設定値を持つ典型的なconfigファイルと同じ規則に従っています。この技術の実装例も後で紹介します。
あるコードを被管理サービスとして動作させるために必要な手順は、次の2つだけです。
- 1つの型に
ServiceBroker.IService
インターフェイスを実装します。 - アセンブリに
ServiceBroker.ServiceEntryPointAttribute
を適用します。
これを使用して、サービスの表示名と、サービスエントリポイントとなる型(ステップ1でインターフェイスを実装した型)を指定します。
もう1つ、configファイルの機能を利用するためのステップ3を加えることもできますが、これが必要なのは、その被管理サービスが変更可能な設定値を必要としている場合だけです。この3つのステップのそれぞれについて、以下で説明していきます。
もちろん、「ServiceBroker.dll」アセンブリへの参照も必要ですが、これは.NET Service Managerとともにインストールされるので、インストール先ディレクトリ(たとえば、「C:\Program Files\Littlechip\.NET Service Manager\」)で参照すればよいでしょう。あるいは、記事「Where's My Assembly?」のように共通のアセンブリ参照ディレクトリにコピーして、そこで参照することもできます。
ServiceBroker.IServiceを実装する
ServiceBrokerは.NET Service Managerに付随するアセンブリで、.NET Service Managerのエンジンとして働くと同時に、.NET Service Managerと被管理サービスとの対話に必要な型を提供します。IService
インターフェイスの詳細をリスト1に示します。
using System; namespace ServiceBroker { ///<summary> /// Interface that all services using the /// ServiceBroker must implement on the type specified /// by the <see cref= /// "ServiceEntryPointAttribute.EntryPointTypeName"/>. ///</summary> public interface IService { ///<summary> /// Starts the service functionality. ///</summary> void StartService(); ///<summary> /// Stops the service functionality. ///</summary> void StopService(); } }
ご覧のとおり、被管理サービスのインターフェイスは実に簡単で、開始メソッドと停止メソッドが必要とされるだけです。これは、もちろん、[サービス]MMCスナップインで使用できる典型的な操作に対応しています。Windowsサービスの再開という操作では、StopService
メソッドが呼ばれ、次いでStartService
メソッドが呼ばれるだけですから、メソッドとしては2つあれば足ります。
例として、きわめて単純な被管理サービスを実装してみました。これは実装方法を示すためだけの実装であり、名前はSampleServiceです。このアセンブリ中でIService
インターフェイスを実装している型を、リスト2に示します。
using System; namespace SampleService { ///<summary> /// Summary description for Test. ///</summary> public class Test : ServiceBroker.IService { #region IService Members public void StartService() { ServiceBroker.Logger.WriteToLog(AppSettings["StartText"], System.Diagnostics.EventLogEntryType.Information); } public void StopService() { ServiceBroker.Logger.WriteToLog(AppSettings["StopText"], System.Diagnostics.EventLogEntryType.Information); } #endregion } }
AppSettings
メンバを参照していることにお気づきでしょう。ここでは、リストを簡単にするためにこのメンバを省いていますが、後であらためて取り上げます。ここで重要なのは、IService
インターフェイスを実装していることと、ごく基本的な機能(イベントログへのメッセージ書き出し)を各メソッドに用意していることです。
もちろん、サービスを実装するのであれば、System.Threading.Timer
インスタンスをいくつか使い、何らかの活動が定期的に行われるようにしたり、System.IO.FileSystemWatcher
を実装して、ファイルシステム活動を監視したりすることが必要になるでしょう。この記事を読んでいるような読者なら、当然、必要なWindowsサービス機能について具体的な考えをお持ちでしょうから、私の方から何をすべきだとは申し上げません。例を見ていただければ、何をどうすればよいかがわかるはずです。
属性を適用する
もちろん、属性を適用せず、リフレクションで済ませる方法もあります。アセンブリ中の型を調べ、ServiceBroker.IService
インターフェイスを実装している最初の型を探してもよかったでしょう。ただ、そのような型探しの方法には、時間がかかる危険があります。また、私としては、被管理サービスに親しみやすい表示名を指定できるようにしたいという思いがあって、カスタム属性を使うことにしました。当該サービスに関するログ項目を書き込むときには、その表示名が使われます。
アセンブリに対するカスタム属性の適用はきわめて簡単です。Visual Studioを使っている場合、コードライブラリプロジェクトでは既定で「Assembly.cs」(VBなら「Assembly.vb」)ファイルが生成されます。そこで、アセンブリレベル属性を指定することが求められます。実際にアセンブリレベル属性を指定する場所はどのコードファイルでもかまいませんが、using
ステートメントより後ろ、型や名前空間の宣言より前であることが必要です。このサンプルでは、「Assembly.cs」ファイルを使用しています。
[assembly: ServiceBroker.ServiceEntryPoint("SampleService","SampleService.Test")] [assembly: System.Reflection.AssemblyVersion("1.0.*")]
生成される「Assembly.cs」ファイルには、普通、大量のコメントと少量の標準アセンブリレベル属性(タイトル、内容記述、会社、著作権など)が含まれています。私は、using
ステートメント(VBではImports
ステートメント)を含むすべてを取り除き、AssemblyVersionAttribute
だけを残しています。何を設定しなくても、アセンブリバージョンだけは設定するべきだと思います。
また、既にお気づきのことと思いますが、私はServiceBrokerアセンブリからのカスタム属性を適用しています。この属性のコンストラクタは引数を2つとります。1つは被管理サービスの表示名、もう1つは、サービスの開始・停止の際に.NET Service Managerに使用させる型の完全型名です。2つ目の引数に指定する型が、IService
インターフェイスを実装する型(たとえば、リスト2のTest
型)でなければならないのは明らかです。ここでは、SampleService.Test
型を使用するよう指定しています。
変更可能なアプリケーション設定値が特に必要ない場合は、ここで作業は終わりです。これをコンパイルして、.NET Service Managerのプロセスディレクトリ(つまり、.exeが置かれているディレクトリ)にドロップします。後は.NET Service Managerを実行すれば、それがアセンブリを自動的にロードし、サービスエントリポイント型を見つけてインスタンスを作成し、その型のIService.StartService
メソッド実装を呼んでくれます。
被管理サービスを更新するときは、必要な変更を施し、コンパイルした新バージョンを同じディレクトリにドロップします。.NET Service Managerは、稼動中に新バージョンを検出すると、旧バージョンを停止してアンロードし、新バージョンをロードして開始します。同様に、DLLファイルをディレクトリから移動または削除しても、.NET Service Managerにそれを停止してアンロードさせることができます。
DLLのconfigファイルを使用して柔軟性を強化する
ServiceBrokerのConfig
クラスのおかげで、被管理サービスに簡単に.configファイルを追加できます。残念ながら、Windowsフォーム/サービスアプリケーションの場合と異なり、「app.config」ファイルを用いて「<assemblyName>.dll.config」ファイルを生成することはできません(Visual Studioがそのようにセットアップされていません)。そのため、configファイルのコピーという多少余分な作業が必要です。つまり、アセンブリより先(または同時)にconfigファイルをコピーし、.NET Service Managerがアセンブリをロードしたときにconfigファイルが使えるようにしておきます。
いずれにせよ、configファイルのフォーマットは標準の「app.config」ファイルと同じです。ただし、ServiceBrokerのconfigハンドラが認識するのはアプリケーション設定値だけです。SampleServiceアセンブリで使用するconfigファイルをリスト4に示します。
<?xmlversion="1.0"encoding="utf-8"?> <configuration> <appSettings> <add key="StartText" value="This is where you would implement logicto start your service."/> <add key="StopText" value="This is where you would implement logic
to stop your service."/> </appSettings> </configuration>
configファイルを使用するには、Test
クラスに数行分のコード(リスト2では除いていた数行)を追加するだけです。リスト5に、configファイル関連のコードを含めた完全なTest
クラスを示します。
using System; namespace SampleService { ///<summary> /// Summary description for Test. ///</summary> public class Test : ServiceBroker.IService { internal static ServiceBroker.Config AppSettings = new ServiceBroker.Config(); #region IService Members public void StartService() { ServiceBroker.Logger.WriteToLog(AppSettings["StartText"], System.Diagnostics.EventLogEntryType.Information); } public void StopService() { ServiceBroker.Logger.WriteToLog(AppSettings["StopText"], System.Diagnostics.EventLogEntryType.Information); } #endregion } }
結局、追加するのは、ServiceBroker.Config
のstatic
(VBではShared
)な識別子を宣言するための1行です。この識別子はアセンブリ内部の型に見えればいいだけなので、internal
(VBではFriend
)と宣言します。これで、Test.AppSettings
フィールドを用いて、コード中のアプリケーション設定値へのアクセスを開始できます。このフィールドには、System.Configuration.ConfigurationSettings.AppSettings
によく似た文字列インデクサがあるので、使い方は標準のアプリケーション設定値のアクセスに非常によく似ています。実は、ConfigurationSettings
を作成し、AppSettings
メンバをそこに置いて、いっそう似せることもできますが、あまり似せるのも考えものです。というのは、System.Configuration
名前空間に同じクラスがある場合、その名前空間へのusing
ステートメント(VBではImports
ステートメント)があると、クラスの衝突が起こりやすいからです。
さあ、終わりました。これで、XCOPYで配備でき、ホット更新が可能で、簡単に設定できるWindowsサービスが得られました。他の「app.config」ファイルとは違い、ServiceBrokerはユニークです。configファイルを監視して変更の有無を察知し、configファイルが更新されたときは、キャッシュに記憶されている設定値(アプリケーションの実行中、高速アクセスのためにメモリにロードされています)を更新できます。しかも被管理サービスを再開する必要はありません。
.NET Service Managerのインストーラは、.NET Service ManagerをLocal Systemアカウントの下で実行されるものとしてインストールします。しかし、事情が許せば、これをもっと権限の小さなアカウントに変更することをお勧めします。実は、すべての被管理サービスが同じWindowsサービスのパーミッションの下で実行されることが、.NET Service Managerの弱点でもあります。もっときめ細かな管理が必要なときは、さらに別のWindowsサービスを開発しなければなりません。
ダウンロードサンプルに含まれているインストーラを実行し、.NET Service Manager Windowsサービスを開始してください。これでいつでも被管理サービスの作成と配備ができます。パート2では、なぜ.NET Service Managerの下でこれが可能になるのかを説明します。
参考資料
- 15 seconds 『Creating an Extensible Windows Service』 Rob Chartier 著、2002年10月
- FTPOnline.com 『Build a Plug 'n' Play XML Windows Service』 Dan Wahlin 著、2002年12月
- MSDN Library 『Adding and Removing References』
- MSDN Library 『Implementing Existing Interfaces』
- MSDN Library 『Applying Attributes』
- MSDN Library 『<appSettings> Element』