![]() ![]() ![]() ![]() Windows サービスを柔軟に管理するサービスマネージャの製作:パート2この記事のURLhttp://japan.internet.com/developer/20051206/26.html
著者:J. Ambrose Little
海外internet.com発の記事
はじめにパート1では、.NET Service Managerとはいったい何をするものか、そしてそれの下で動作する被管理サービスのインストールと設定はどうすればよいか、という点を説明しました。パート2では、.NET Service Managerの働きの仕組みを詳細に見ていきます。つまり、ドラッグアンドドロップ配備などの便利な働きがなぜ可能なのか、ということです。こうした機能を背後で支えている諸概念には、他の.NETプロジェクトにも応用できるものが少なくありません。 .NET Service Managerは、いくつかの中核的技術の上に成り立っています。たとえば、.NET Remoting、AppDomain、Reflection、シャドウコピー、そしてクラスやインタフェースといった標準的なオブジェクト指向技術です。そのうち、アセンブリの動的ロードとアンロードでの中心となる技術がRemotingです。Remotingを利用すると、コードのロード、ホスト、実行を隔離されたメモリ領域(アプリケーションドメイン、もしくはAppDomainと呼び出されます)で行い、外見上は標準のWindowsプロセスとなんら変わりなく動作させることができます。 AppDomainとRemotingの関係AppDomainは、小型のプロセスのようなもので、信頼性と安全性の確保に必要な分離性を実現していますが、プロセッサやメモリのオーバーヘッドは完全なWin32プロセスよりも小さく抑えられています。1つの実プロセスの内部でいくつものAppDomainが実行されます。同じプロセス内にあるAppDomain間の通信は、当然、プロセス間通信より高速であると期待できますが、やはりRemoting境界を越えた往来が必要となるため、パフォーマンスには多少の影響が出ます。 リモート通信に必要なオーバーヘッドを理解するために、1つの比喩を考えましょう。いま、2人の人間が同じオフィスビル内の、異なる2つの部門にいるとします。オフィスビルがプロセス、部門がAppDomainに相当します。2人は社内便を使って、メッセージや荷物をやり取りします。そこで必要とされる時間や費用は、さほど大きなものではありません。 次に、同じ2人が国内の別地域に住んでいるとしましょう。この場合は、同じメッセージや荷物を送るにもかなりの郵便代がかかりますし、それを郵便局に持っていくための時間もかかります。時間や費用がかかることが、すなわちオーバーヘッドの増大です。Win32プロセス間の通信でも、同じことが言えます。 最後に、1人がアメリカ、もう1人がイギリスに住んでいる場合を考えましょう。メッセージや荷物を送るのにかかる時間は大幅に長くなります。また、それほどではないにせよ、費用も増大します。この状況は、2台のマシンをつなぐネットワーク経由の通信に似ています。 さて、この2人が同じ室内にいたらどうでしょう。通信はずっと速く、ずっと安上がりになります。とにかく、面と向かって話し合い、必要なものを手渡せばいいのですから、封筒も包装もいりません。AppDomain内通信もこれと同じことです。2つのオブジェクト間における最も効率の良い通信方法が、これであることは明らかでしょう。 ここまでの比喩は、主としてパフォーマンスだけを考えたものでした。今回の記事で実装する.NET Service ManagerではRemotingの別の機能――他のAppDomain内にオブジェクトのインスタンスを作り、それにStart(開始)やStop(停止)などのキーメッセージを送信するという働き――が重要な意味を持ちます。この機能には、 Remoting境界を越えてオブジェクトをマーシャリングする場合、既定では値単位でマーシャリングが実施されます。つまり、データ(値)の完全コピーがシリアル化され、送信されます。ただ、これをRemotingで正しく(つまり、適切に)行おうとすれば、実際にはクラスに とにかく、Remotingを使って、ある型のインスタンスを別のAppDomain内に作成します。コードでは、この型を 各アセンブリをそれぞれ別個のAppDomainにロードしておくと、それを それぞれの概念の実装 関係する概念と技術を高みから概観したところで、今度は具体的な実装を詳しく見ていくことにしましょう。このアプリケーションのエンジンはServiceBrokerアセンブリの中にあります。パート1で述べたとおり、ここには必要な エンジンの中核は リスト1
private HybridDictionary serviceNames = new HybridDictionary(10); private HybridDictionary serviceAppDomains = new HybridDictionary(10); private HybridDictionary services = new HybridDictionary(10); private HybridDictionary serviceLastModified = new HybridDictionary(10); サービスのロードと開始 次のリスト2に示すのは、アプリケーション機能の中核を成す リスト2
public void StartService(string filePath) { string serviceName; string fileName = filePath.Substring( filePath.LastIndexOf("") + 1); if (fileName.IndexOf("Microsoft.ApplicationBlocks") != -1 || fileName == "ServiceBroker.dll") return; try { AssemblyName asmName = null; try { asmName = AssemblyName.GetAssemblyName(filePath); } catch (Exception ex) { Logger.WriteToLog( String.Format("Could not get assembly name " +"from ’{0}’; bypassing that file.",filePath) + " Exception Details: " + ex.ToString() ,System.Diagnostics.EventLogEntryType.Warning); return; } ... 次にロード手順を開始します。まず、Reflectionの このメソッドの次のコードブロックをリスト3に示します。この部分の目的は、問題のアセンブリの最新バージョンがロード済みでないかどうかを確かめることです。ロード済みなら、これ以後の実行の一部を省略できます。確かめるには、まず、 リスト3
// check if assembly service already loaded if (this.serviceNames.Contains(filePath)) { serviceName = this.serviceNames[filePath].ToString(); DateTime curTime = File.GetLastWriteTime(filePath); // only add the service if it is not already running latest copy if (curTime.Ticks <= ((DateTime)this.serviceLastModified[serviceName]).Ticks) { Logger.WriteToLog( String.Format("Skipping ’{0}’ because it is already loaded.", serviceName), System.Diagnostics.EventLogEntryType.Information); return; } else// new copy, so stop current and clear out cache { // stop/unload service this.UnloadService(serviceName); // remove from file path cache this.serviceNames.Remove(filePath); } } 現在ディレクトリ中にあるバージョンがいまロードされているバージョンと同じか、それより古い場合は、再ロードは不要です。ロードを省略したことをログに記入して、関数を抜けます。それ以外の場合は、新しいバージョンがディレクトリにドロップされているので、現在のバージョンをアンロードし、キャッシュから取り除かなければなりません。 リスト4
private void UnloadService(string serviceName) { RemoteServiceHandler service = this.services[serviceName] as RemoteServiceHandler; if (service != null) { try { service.StopService(); } catch (Exception ex) { Logger.LogException(ex); } } this.services.Remove(serviceName); AppDomain svcDomain = this.serviceAppDomains[serviceName] as AppDomain; if (svcDomain != null) { try { AppDomain.Unload(svcDomain); } catch (Exception ex) { Logger.LogException(ex); } } this.serviceAppDomains.Remove(serviceName); this.serviceLastModified.Remove(serviceName); } 被管理サービスの新バージョンをロードする必要があるかどうか(既にロードされているときは、アンロードが必要かどうか)を調べた後で、AppDomainの作成と被管理サービスのリモートロードに進みます。 リスト5
AppDomain svcDomain = null; try { AppDomainSetup setup = new AppDomainSetup(); setup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; setup.PrivateBinPath = setup.ApplicationBase; setup.ApplicationName = asmName.FullName; setup.ShadowCopyDirectories = setup.ApplicationBase; setup.ShadowCopyFiles = "true"; svcDomain = AppDomain.CreateDomain(asmName.FullName, null, setup); } catch (Exception ex) { Logger.LogException( new ApplicationException( String.Format("Could not create an AppDomain for ’{0}’; " + "bypassing that assembly.", asmName.FullName), ex)); return; } これは、被管理サービスの実行に使う新しいAppDomainをセットアップするコードです。 さてこれで、被管理サービスを実行するための専用のAppDomainが作成できました。では、新しいAppDomainに被管理サービスをロードしましょう。型データをメインのAppDomainにロードせずにこれを行うには、上で触れた リスト6
RemoteServiceHandler svc = null; try { svc = (RemoteServiceHandler) svcDomain.CreateInstanceFromAndUnwrap( svcDomain.BaseDirectory + "ServiceBroker.dll", "ServiceBroker.RemoteServiceHandler"); } catch (Exception ex) { AppDomain.Unload(svcDomain); Logger.LogException( new AssemblyLoadException( "Could not load ServiceBroker remote service handler," + " bypassing that file.", asmName.FullName, ex)); return; } リスト6のコードでは、被管理サービスのドメインに置かれる 前出の比喩を使って表現すれば、この時点で、相手が近くのオフィスにいることを知っていて、その人宛てのオフィス間郵便のアドレスも知っている状態になります。これまでは、情報のやり取りに関わる人全員が同じ部屋の中にいました。ここで初めて、離れた場所にいる人を相手にできるようになります。 ここの例外ハンドラでは、まず 被管理サービスのAppDomain内にある リスト7
try { if (!svc.LoadService(asmName.FullName)) { AppDomain.Unload(svcDomain); Logger.WriteToLog( String.Format("No ServiceEntryPointAttribute was " + "found for assembly ’{0}’.", asmName.FullName), System.Diagnostics.EventLogEntryType.Warning); return; } } catch (Exception ex) { AppDomain.Unload(svcDomain); Logger.LogException(ex); return; } リスト8
try { this.service = (IService)assembly.CreateInstance( this.serviceEntryPointType, true); } catch (Exception ex) { throw new TypeInitializationException( this.serviceEntryPointType, ex); } 例外の発生もなくすべてが進めば、このメソッドからはtrueが返され(メッセージが リスト9
svc.StartService(); serviceName = svc.ServiceName; this.serviceNames.Add(filePath, serviceName); this.services.Add(serviceName, svc); this.serviceLastModified.Add( serviceName, File.GetLastWriteTime(filePath)); this.serviceAppDomains.Add(serviceName, svcDomain); リスト9に示すコードの実行が終わった時点で、サービスは既に開始されています。つまり、当該被管理サービスの作者が サービスの停止 もちろん、これまでやってきたことの裏返しとして、被管理サービスを停止させるための作業も必要です。しかし、困難な仕事はすべて終わっていて、停止にともなう作業はずっと簡単ですから安心してください。被管理サービスアセンブリが削除され、あるいはServicesアプレット中で.NET Service Managerが停止されると、 リスト10
public void StopService(string filePath) { if (this.serviceNames.Contains(filePath)) { this.UnloadService(Convert.ToString( this.serviceNames[filePath])); this.serviceNames.Remove(filePath); } } 以上で、.NET Service Managerの魔法の源泉をあらかた見終わりました。もちろん、そのほとんど(特に、AppDomain、Remoting、Reflectionを取り巻く諸概念)は、どのようなアプリケーションでも便利に使用でき、アセンブリの動的なロード、更新、アンロードといった便利な機能を実現できます。 残務整理 .NET Service Managerとは切っても切り離せないのに、説明が長くなるために本稿では取り上げなかった事柄がいくつかあります。その第一は、Windowsサービスコードそのものです。これについては既に多くの資料(.NETでWindowsサービスを構築する方法などを扱った記事の類)が出回っていて、私から価値ある何かを付け加えられるとは思えないという事情もありました。あえて言うならば、実際のWindowsサービスでは、 上記のとおりディレクトリを監視していて、1つまずいことに気がつきました。それは、ファイルをディレクトリにドロップしたとき、同じファイルに対して もう1つ簡単に触れておきたい事柄に、 .NET Service Managerは、既に私と仲間たちにとっては開発に役立つ有用なサービスとなっています。読者にとっても同様であることを願っています。このアプリケーション全体の課題として、なかなか解決が難しいだろうと思われる問題が1つあることを申し上げておきます。それは、従属DLLを持つ被管理サービスを配備するときには、参照されるDLLを先に配備しておかなければならないということです。そうしないと、被管理サービスに対して 参考資料
著者紹介J. Ambrose Little(J. Ambrose Little)
ASPAlliance編集長。ASPInsiderであり、Microsoft ASP.NET MVPでもある。現在はWebアーキテクトとして、フロリダ州タンパにある大手信用組合に勤務。これまでに、Verizon社のコンサルタントとしてXML Webサービスと中間層コンポーネントを開発し、BOk Financial社のWebサービス部門で同社イントラネット用にASP.NETアプリケーションを開発。.NET以前にも数年のプログラミング経験があり、ASPとVB COM/DCOMを用いたWebアプリケーション開発に従事。
中世ヨーロッパ史の研究が趣味で、この分野で学士号を取得。ソフトウェア開発以外では、映画鑑賞、読書、執筆、弁証論、ビリヤード、フーズボール、チェス、バドミントンなどを楽しむが、最大の楽しみは、もちろん、妻Christianeに娘Bridgetと過ごすことである。
japan.internet.comのウエブサイトの内容は全て、国際法、日本国内法の定める著作権法並びに商標法の規定によって保護されており、その知的財産権、著作権、商標の所有者はインターネットコム株式会社、インターネットコム株式会社の関連会社または第三者にあたる権利者となっています。
本サイトの全てのコンテンツ、テキスト、グラフィック、写真、表、グラフ、音声、動画などに関して、その一部または全部を、japan.internet.comの許諾なしに、変更、複製、再出版、アップロード、掲示、転送、配布、さらには、社内LAN、メーリングリストなどにおいて共有することはできません。 ただし、コンテンツの著作権又は所有権情報を変更あるいは削除せず、利用者自身の個人的かつ非商業的な利用目的に限ってのみ、本サイトのコンテンツをプリント、ダウンロードすることは認められています。 |