The Backyard - CreateAndDestroyAppDomain Diff
- Added parts are displayed like this.
- Deleted parts are displayed
like this.
この記事は、「WEB+DB PRESS Vol.14」(技術評論社)に掲載していただいた記事の元原稿(必要に応じて加筆/修正/削除をしています)を公開するものです。
!AppDomainの生成
NetLauncherは、AppDomainの生成とアプリケーションの起動を3段階で行います。
1.AppDomain間通信用のシリアライズ可能クラスを生成する(リスト:WebLauncher.cs:Luanchメソッド)
AppDomainの実行中は待機する必要があることと、AppDomain間の通信のために(後述)、ここではワーカスレッド用に内部クラスを作成しています。
2. AppDomainを作成し、現在実行中のアセンブリをロードする(リスト:WebLauncher.cs:Launcherコンストラクタ)
NetLauncherは特定ホスト上に構成された専用アプリケーションのローンチャであり、インターネットから不特定のアプリケーションを起動する汎用ローンチャではありません。このことは起動するアプリケーションのセキュリティチェックを行う必要がないことを意味します。そのため実行するアプリケーションのエビデンスを自分自身と同等に設定します(リスト:Aの箇所)(注)。これにより結果的にマイコンピュータゾーンで実行されることになります。
""(注)AppDomain#ExecuteAssemblyメソッドに対してURLを与えるとURLモニカーによって処理が行われ、デフォルトで本来のエビデンスが設定されてしまいます。ExecuteAssembly呼び出し時点でもエビデンスの設定は可能ですが、その後にURLで示されたホスト上をプローブして参照しているアセンブリを取得すると、再度本来のエビデンスが復活してしまいます。アセンブリに一度設定されたエビデンスの削除は(当然)できないため、結局、他のアセンブリの名前解決の時点で自力でプローブ相当処理が必要となってしまいます。AssemblyFileクラスで行っているように事前に参照しているアセンブリもローカルへコピーするのは、この処理の前倒しと、アプリケーション実行時にオフラインであっても処理が継続できるようにすることの2点からです。
また、作成したAppDomainに自分自身のアセンブリをロードし、アプリケーション起動処理を実行可能とします(リスト:Bの箇所)。
3.ワーカスレッドから作成したAppDomain内に入りtryブロック内でアプリケーションを実行する(リスト:WebLauncher.cs:Launcher#ExecuteとLauncher#ExecuteAssemblyメソッド)
AppDomain間の通信にはAppDomain.DoCallBackメソッドを使用します(リスト:Cの箇所)。DoCallBackメソッドは引数のCrossAppDomainDelegateデリゲートで指定したメソッドをリモーティングで呼び出します。このとき、無属性のクラスのインスタンスメソッドを指定するとインスタンスの参照がマーシャリングされるため、呼び出し先のアセンブリがデリゲートを作成したインスタンスの参照を呼び出し、結局呼び出し元のAppDomainで実行されることになってしまいます。
ところがここでは、実行するアプリケーションの例外をフックするために、Windows.Forms.Application.ThreadExceptionイベントにデリゲートを登録する必要があります(リスト:Dの箇所)。このイベントは見てのとおり、スタティックですので、参照がマーシャリングされては期待した動作は得られません。その場合、NetLauncherを実行しているAppDomain内のApplicationクラスに対してデリゲートを登録することになるからです。したがって、値マーシャリングを行い、呼び出し先のAppDomainで実行されるようにする必要があります。そのためには、種々のオブジェクトを保持しているWebLauncherクラスとは別に軽量なLauncherクラスを[Serializable]属性付きで定義し、実行時に生成したインスタンスのメソッドのデリゲートをDoCallBackの引数とします。これにより、呼び出し先のアセンブリがデリゲートを作成したインスタンスをアンマーシャルしそのまま実行することが可能となります。なお、処理上、アウタークラスのインスタンスの保持が必要となるため、このフィールドについては[NonSerialized]属性を設定し、マーシャリング対象から外しています。このようなフィールドはアンマーシャリング後nullが設定されます。(注)
注)実際、すべてのプログラマがこのような処理を記述する必要は無いので、何が書いてあるか理解できなくても構いませんが、このような処理が筆者は大好きです。
なお、ThreadExceptionイベントハンドラを設定しないと、未処理の例外が発生すると、規定の例外ハンドラがメッセージボックスを出して処理の継続/終了の選択を要求します(図:defaultexception.png)。業務アプリケーションのメッセージとしては不適切ですし、障害資料も取れないので、ここでは例外情報を管理者にメールしアプリケーションそのものは終了するように実装しています(リスト:Eの箇所と引用外のOnThreadExceptionメソッド)。現実のアプリケーションでは、ここでお詫びのメッセージを表示すべきでしょう。
リスト:WebLauncher.cs:Luanchメソッド(抜粋)
private void Launch(Application app)
{
Launcher l = new Launcher(app.Name, baseDirectory, app.AssemblyFile, this);
Thread t = new Thread(new System.Threading.ThreadStart(l.Execute));
t.Start();
}
リスト:WebLauncher.cs:Launcherコンストラクタ(抜粋)
internal Launcher(string appName, string baseDirectory, AssemblyFile asm, WebLauncher initOuter)
{
outer = initOuter; // Javaと異なりインナークラスはスタティック相当
assemblyFile = asm.LocalFileName;
...省略...
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = baseDirectory;
setup.ApplicationName = appName;
setup.PrivateBinPath = asm.LocalDirectory;
setup.ShadowCopyFiles = "false";
Evidence baseEvidence = AppDomain.CurrentDomain.Evidence;
evidence = new Evidence(baseEvidence); // (A)
appDomain = AppDomain.CreateDomain(appName, evidence, setup);
appDomain.Load(Assembly.GetExecutingAssembly().GetName(), evidence); // (B)
appDomain.DomainUnload += new EventHandler(UnloadHandler);
}
リスト:WebLauncher.cs:Launcherクラス(抜粋)
internal void Execute()
{
...省略...
try
{ // (C)
appDomain.DoCallBack(new CrossAppDomainDelegate(ExecuteAssembly));
}
catch (Exception)
{
}
...省略...
}
public void ExecuteAssembly()
{
System.Windows.Forms.Application.ThreadException += // (D)
new ThreadExceptionEventHandler(OnThreadException);
try
{
AppDomain.CurrentDomain.ExecuteAssembly(assemblyFile, evidence);
}
catch (Exception e)
{ // (E)
if (sendto != null && sendto != String.Empty)
{
SendMail(sendto, (e is ForceExitException) ? e.InnerException : e);
}
}
}
!AppDomainの強制終了
AppDomainはUnloadスタティックメソッドを呼び出すことで、強制的にアンロードすることができます。
NetLauncherではこの機能を利用して、実行中のアプリケーションの強制終了処理を実装しています(リスト:LauncherForm.cs:domainKill_ClickとKillDomainメソッド)。
リスト:LauncherForm.cs:domainKill_ClickとKillDomainメソッド
private void domainKill_Click(object sender, System.EventArgs e)
{
ListViewItem item = domainView.SelectedItems[0];
AppDomain domain = (AppDomain)item.Tag;
DomainHandler dh = new DomainHandler(KillDomain);
dh.BeginInvoke(domain, null, null); // (A)
}
private delegate void DomainHandler(AppDomain dom);
private void KillDomain(AppDomain dom)
{
try
{
AppDomain.Unload(dom);
}
catch (Exception)
{
}
}
なお、Unloadメソッドは非常に時間がかかる場合があるため、ここでは非同期デリゲートを利用してスレッドプールのワーカスレッドで実行するように実装しています(リスト:A)。このようにデリゲートを作成してBeginInvokeメソッドを呼び出すことで、Threadクラスを使用せずに別スレッドでメソッドの実行が可能となります。
ただし、非同期デリゲートを多用すると、ソースが読みにくくなるため、ここで使用しているような単純なメソッドに限定した利用に留めるべきでしょう。引数が3個以上になったり、呼び出したメソッドがさらに複数のメソッドを呼び出す場合には、引数をインスタンス変数として保持したクラスとして独立させたほうがユニットテストの便や後からのメンテナンス性に優れていると感じます。
!AppDomainの生成
NetLauncherは、AppDomainの生成とアプリケーションの起動を3段階で行います。
1.AppDomain間通信用のシリアライズ可能クラスを生成する(リスト:WebLauncher.cs:Luanchメソッド)
AppDomainの実行中は待機する必要があることと、AppDomain間の通信のために(後述)、ここではワーカスレッド用に内部クラスを作成しています。
2. AppDomainを作成し、現在実行中のアセンブリをロードする(リスト:WebLauncher.cs:Launcherコンストラクタ)
NetLauncherは特定ホスト上に構成された専用アプリケーションのローンチャであり、インターネットから不特定のアプリケーションを起動する汎用ローンチャではありません。このことは起動するアプリケーションのセキュリティチェックを行う必要がないことを意味します。そのため実行するアプリケーションのエビデンスを自分自身と同等に設定します(リスト:Aの箇所)(注)。これにより結果的にマイコンピュータゾーンで実行されることになります。
""(注)AppDomain#ExecuteAssemblyメソッドに対してURLを与えるとURLモニカーによって処理が行われ、デフォルトで本来のエビデンスが設定されてしまいます。ExecuteAssembly呼び出し時点でもエビデンスの設定は可能ですが、その後にURLで示されたホスト上をプローブして参照しているアセンブリを取得すると、再度本来のエビデンスが復活してしまいます。アセンブリに一度設定されたエビデンスの削除は(当然)できないため、結局、他のアセンブリの名前解決の時点で自力でプローブ相当処理が必要となってしまいます。AssemblyFileクラスで行っているように事前に参照しているアセンブリもローカルへコピーするのは、この処理の前倒しと、アプリケーション実行時にオフラインであっても処理が継続できるようにすることの2点からです。
また、作成したAppDomainに自分自身のアセンブリをロードし、アプリケーション起動処理を実行可能とします(リスト:Bの箇所)。
3.ワーカスレッドから作成したAppDomain内に入りtryブロック内でアプリケーションを実行する(リスト:WebLauncher.cs:Launcher#ExecuteとLauncher#ExecuteAssemblyメソッド)
AppDomain間の通信にはAppDomain.DoCallBackメソッドを使用します(リスト:Cの箇所)。DoCallBackメソッドは引数のCrossAppDomainDelegateデリゲートで指定したメソッドをリモーティングで呼び出します。このとき、無属性のクラスのインスタンスメソッドを指定するとインスタンスの参照がマーシャリングされるため、呼び出し先のアセンブリがデリゲートを作成したインスタンスの参照を呼び出し、結局呼び出し元のAppDomainで実行されることになってしまいます。
ところがここでは、実行するアプリケーションの例外をフックするために、Windows.Forms.Application.ThreadExceptionイベントにデリゲートを登録する必要があります(リスト:Dの箇所)。このイベントは見てのとおり、スタティックですので、参照がマーシャリングされては期待した動作は得られません。その場合、NetLauncherを実行しているAppDomain内のApplicationクラスに対してデリゲートを登録することになるからです。したがって、値マーシャリングを行い、呼び出し先のAppDomainで実行されるようにする必要があります。そのためには、種々のオブジェクトを保持しているWebLauncherクラスとは別に軽量なLauncherクラスを[Serializable]属性付きで定義し、実行時に生成したインスタンスのメソッドのデリゲートをDoCallBackの引数とします。これにより、呼び出し先のアセンブリがデリゲートを作成したインスタンスをアンマーシャルしそのまま実行することが可能となります。なお、処理上、アウタークラスのインスタンスの保持が必要となるため、このフィールドについては[NonSerialized]属性を設定し、マーシャリング対象から外しています。このようなフィールドはアンマーシャリング後nullが設定されます。(注)
注)実際、すべてのプログラマがこのような処理を記述する必要は無いので、何が書いてあるか理解できなくても構いませんが、このような処理が筆者は大好きです。
なお、ThreadExceptionイベントハンドラを設定しないと、未処理の例外が発生すると、規定の例外ハンドラがメッセージボックスを出して処理の継続/終了の選択を要求します(図:defaultexception.png)。業務アプリケーションのメッセージとしては不適切ですし、障害資料も取れないので、ここでは例外情報を管理者にメールしアプリケーションそのものは終了するように実装しています(リスト:Eの箇所と引用外のOnThreadExceptionメソッド)。現実のアプリケーションでは、ここでお詫びのメッセージを表示すべきでしょう。
リスト:WebLauncher.cs:Luanchメソッド(抜粋)
private void Launch(Application app)
{
Launcher l = new Launcher(app.Name, baseDirectory, app.AssemblyFile, this);
Thread t = new Thread(new System.Threading.ThreadStart(l.Execute));
t.Start();
}
リスト:WebLauncher.cs:Launcherコンストラクタ(抜粋)
internal Launcher(string appName, string baseDirectory, AssemblyFile asm, WebLauncher initOuter)
{
outer = initOuter; // Javaと異なりインナークラスはスタティック相当
assemblyFile = asm.LocalFileName;
...省略...
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = baseDirectory;
setup.ApplicationName = appName;
setup.PrivateBinPath = asm.LocalDirectory;
setup.ShadowCopyFiles = "false";
Evidence baseEvidence = AppDomain.CurrentDomain.Evidence;
evidence = new Evidence(baseEvidence); // (A)
appDomain = AppDomain.CreateDomain(appName, evidence, setup);
appDomain.Load(Assembly.GetExecutingAssembly().GetName(), evidence); // (B)
appDomain.DomainUnload += new EventHandler(UnloadHandler);
}
リスト:WebLauncher.cs:Launcherクラス(抜粋)
internal void Execute()
{
...省略...
try
{ // (C)
appDomain.DoCallBack(new CrossAppDomainDelegate(ExecuteAssembly));
}
catch (Exception)
{
}
...省略...
}
public void ExecuteAssembly()
{
System.Windows.Forms.Application.ThreadException += // (D)
new ThreadExceptionEventHandler(OnThreadException);
try
{
AppDomain.CurrentDomain.ExecuteAssembly(assemblyFile, evidence);
}
catch (Exception e)
{ // (E)
if (sendto != null && sendto != String.Empty)
{
SendMail(sendto, (e is ForceExitException) ? e.InnerException : e);
}
}
}
!AppDomainの強制終了
AppDomainはUnloadスタティックメソッドを呼び出すことで、強制的にアンロードすることができます。
NetLauncherではこの機能を利用して、実行中のアプリケーションの強制終了処理を実装しています(リスト:LauncherForm.cs:domainKill_ClickとKillDomainメソッド)。
リスト:LauncherForm.cs:domainKill_ClickとKillDomainメソッド
private void domainKill_Click(object sender, System.EventArgs e)
{
ListViewItem item = domainView.SelectedItems[0];
AppDomain domain = (AppDomain)item.Tag;
DomainHandler dh = new DomainHandler(KillDomain);
dh.BeginInvoke(domain, null, null); // (A)
}
private delegate void DomainHandler(AppDomain dom);
private void KillDomain(AppDomain dom)
{
try
{
AppDomain.Unload(dom);
}
catch (Exception)
{
}
}
なお、Unloadメソッドは非常に時間がかかる場合があるため、ここでは非同期デリゲートを利用してスレッドプールのワーカスレッドで実行するように実装しています(リスト:A)。このようにデリゲートを作成してBeginInvokeメソッドを呼び出すことで、Threadクラスを使用せずに別スレッドでメソッドの実行が可能となります。
ただし、非同期デリゲートを多用すると、ソースが読みにくくなるため、ここで使用しているような単純なメソッドに限定した利用に留めるべきでしょう。引数が3個以上になったり、呼び出したメソッドがさらに複数のメソッドを呼び出す場合には、引数をインスタンス変数として保持したクラスとして独立させたほうがユニットテストの便や後からのメンテナンス性に優れていると感じます。