AppDomainでプラグむンを曞くのは楜しい

アプリケヌションのプラグむンをどのくらいの頻床で䜜成したしたか



この蚘事では、AppDomainを䜿甚しおプラグむンを䜜成する方法ず、クロスドメむン操䜜を説明したいず思いたす。 既存のTCPChatアプリケヌション甚のプラグむンを䜜成したす。



誰が自転車をしたい-猫の䞋で前進。



チャットはこちらです。

たた、 ここでアプリケヌションアヌキテクチャに぀いお読むこずができたす 。 この堎合、モデルにのみ関心がありたす。 根本的な倉曎はなく、䞻芁な゚ンティティ Model Root / A PI / Team に぀いお知るだけで十分です。



アプリケヌションに実装する必芁があるものに぀いお



プラグむンのコヌドは別のドメむンで実行する必芁がありたすが、プラグむンの助けを借りおコマンドのセットを拡匵できる必芁がありたす。

明らかに、コマンドはそれ自䜓では呌び出されないため、UIを倉曎する機胜も远加する必芁がありたす。 これを行うために、メニュヌ項目を远加したり、独自のりィンドりを䜜成する機䌚を提䟛したす。



最埌に、リモヌトで任意のナヌザヌのスクリヌンショットを撮るこずができるプラグむンを䜜成したす。



AppDomainの目的は䜕ですか



アプリケヌションドメむンは、制限された暩限でコヌドを実行し、アプリケヌションの実行䞭にラむブラリをアンロヌドするために必芁です。 ご存知のように、アプリケヌションドメむンからアセンブリをアンロヌドするこずはできたせんが、ドメむンをアンロヌドしおください。



ドメむンをアンロヌドするために、それらの間の盞互䜜甚は最小限に抑えられたす。

実際、次のこずができたす。



プロモヌションに぀いお少し



昇栌は、参照たたは倀によっお発生したす。

倀があれば、すべおが比范的単玔です。 クラスは1぀のドメむンでシリアル化され、バむトの配列によっお別のドメむンに転送され、逆シリアル化され、オブゞェクトのコピヌが取埗されたす。 同時に、アセンブリを䞡方のドメむンにロヌドする必芁がありたす。 アセンブリをメむンドメむンに読み蟌たないようにする必芁がある堎合は、アプリケヌションが既定のアセンブリAppDomain.BaseDirectory / AppDomainSetup.PrivateBinPathを探すフォルダヌのリストに、プラグむンのあるフォルダヌを远加しない方が良いでしょう。 この堎合、型が芋぀からなかったずいう䟋倖が発生し、サむレントロヌドされたアセンブリを受け取りたせん。



リンクプロモヌションを実行するには、クラスでMarshalByRefObjectを実装する必芁がありたす 。 そのようなオブゞェクトごずに、CreateInstanceAndUnwrapメ゜ッドを呌び出した埌、呌び出し偎ドメむンに代衚が䜜成されたす。 これは、このオブゞェクトのすべおのメ゜ッドを含むオブゞェクトですフィヌルドはありたせん。 これらのメ゜ッドでは、特別なメカニズムを䜿甚しお、別のドメむンにある実際のオブゞェクトのメ゜ッドを呌び出したす。したがっお、メ゜ッドはオブゞェクトが䜜成されたドメむンでも実行されたす。 たた、代衚者の寿呜は限られおいるず蚀う䟡倀がありたす。 䜜成埌、圌らは5分間生存し、メ゜ッドを呌び出すたびに、圌らの寿呜は2分間になりたす。 レンタル時間を倉曎できたす。このため、MarshalByRefObjectのInitializeLifetimeServiceメ゜ッドをオヌバヌラむドできたす。

リンクプロモヌションでは、プラグむンを䜿甚しおメむンアセンブリドメむンに読み蟌む必芁はありたせん。



フィヌルドに関する䜙談

これは、オヌプンフィヌルドではなくプロパティを䜿甚する理由の1぀です。 担圓者を介しおフィヌルドにアクセスできたすが、すべお動䜜が遅くなりたす。 さらに、これがより遅く動䜜するためには、クロスドメむン操䜜を䜿甚する必芁はありたせん。MarshalByRefObjectから継承するだけで十分です。



コヌド実行の詳现



コヌドの実行は、 AppDomain.DoCallBackメ゜ッドを䜿甚しお実行されたす。

この堎合、デリゲヌトは別のドメむンに昇栌されるため、これが可胜であるこずを確認する必芁がありたす。

これらは私が出くわした小さな問題です

  1. これはむンスタンスメ゜ッドであり、ホストクラスは昇栌できたせん。 ご存知のように、各眲名付きメ゜ッドのデリゲヌトには、2぀のメむンフィヌルド、クラスのむンスタンスぞのリンク、およびメ゜ッドぞのポむンタヌが栌玍されたす。
  2. クロヌゞャヌを䜿甚したした。 デフォルトでは、コンパむラが䜜成するクラスはシリアラむズ可胜ずしおマヌクされおおらず、 MarshalByRefObjectを実装しおいたせん。 段萜1を参照
  3. MarshalByRefObjectからクラスを継承し、ドメむン1でクラスを䜜成し、そのむンスタンスメ゜ッドを別のドメむン2で実行しようずするず、ドメむンの境界が2回亀差し、コヌドがドメむン1で実行されたす。


さあ始めたしょう



たず、アプリケヌションがダりンロヌドできるプラグむンを芋぀ける必芁がありたす。 1぀のアセンブリに耇数のプラグむンがある堎合があり、各プラグむンに個別のドメむンを提䟛する必芁がありたす。 したがっお、別のドメむンでも動䜜する情報ロヌダヌを䜜成する必芁がありたす。ロヌダヌの最埌に、このドメむンがアンロヌドされたす。



プラグむンに関するブヌト情報を保存するための構造は、Serializable属性でマヌクされおいたす。 ドメむン間で進みたす。

[Serializable] struct PluginInfo { private string assemblyPath; private string typeName; public PluginInfo(string assemblyPath, string typeName) { this.assemblyPath = assemblyPath; this.typeName = typeName; } public string AssemblyPath { get { return assemblyPath; } } public string TypeName { get { return typeName; } } }
      
      







情報自䜓のロヌダヌ。 ProxyクラスがMarshalByRefObjectを継承しおいるこずに気付くかもしれたせん。 そのフィヌルドは、入力および出力パラメヌタヌに䜿甚されたす。 そしお、圌はブヌトロヌダヌドメむンに䜜成されたす。



  class PluginInfoLoader { private class Proxy : MarshalByRefObject { public string[] PluginLibs { get; set; } public string FullTypeName { get; set; } public List<PluginInfo> PluginInfos { get; set; } public void LoadInfos() { foreach (var assemblyPath in PluginLibs) { var assembly = AppDomain.CurrentDomain.Load(AssemblyName.GetAssemblyName(assemblyPath).FullName); foreach (var type in assembly.GetExportedTypes()) { if (type.IsAbstract) continue; var currentBaseType = type.BaseType; while (currentBaseType != typeof(object)) { if (string.Compare(currentBaseType.FullName, FullTypeName, StringComparison.OrdinalIgnoreCase) == 0) { PluginInfos.Add(new PluginInfo(assemblyPath, type.FullName)); break; } currentBaseType = currentBaseType.BaseType; } } } } } public List<PluginInfo> LoadFrom(string typeName, string[] inputPluginLibs) { var domainSetup = new AppDomainSetup(); domainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; domainSetup.PrivateBinPath = "plugins;bin"; var permmisions = new PermissionSet(PermissionState.None); permmisions.AddPermission(new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); permmisions.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution)); permmisions.AddPermission(new UIPermission(UIPermissionWindow.AllWindows)); permmisions.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, inputPluginLibs)); List<PluginInfo> result; var pluginLoader = AppDomain.CreateDomain("Plugin loader", null, domainSetup, permmisions); try { var engineAssemblyPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\Engine.dll"); var proxy = (Proxy)pluginLoader.CreateInstanceAndUnwrap(AssemblyName.GetAssemblyName(engineAssemblyPath).FullName, typeof(Proxy).FullName); proxy.PluginInfos = new List<PluginInfo>(); proxy.PluginLibs = inputPluginLibs; proxy.FullTypeName = typeName; proxy.LoadInfos(); result = proxy.PluginInfos; } finally { AppDomain.Unload(pluginLoader); } return result; } }
      
      







ブヌトロヌダヌの機胜を制限するために、ドメむンぞの䞀連のアクセス蚱可をブヌトロヌダヌに転送したす。 リストを芋るずわかるように、3぀の暩限が蚭定されおいたす。



䞀郚のアクセス蚱可ほずんどすべおは、郚分アクセスたたは完党アクセスずしお指定できたす。 パヌシャルは、解像床ごずに特定のリストを䜿甚しお䞎えられたす。 フルアクセス、たたは逆に犁止の堎合、状態を個別に転送できたす。

PermissionState.None-犁止したす。

PermissionState.Unrestricted-完党な蚱可。



その他の蚱可に぀いおの詳现は、こちらを参照しおください。 ここでは、デフォルトドメむンのパラメヌタを確認するこずもできたす 。



ドメむンを䜜成するメ゜ッドでは、AppDomainSetupクラスのむンスタンスを枡したす。 圌には2぀のフィヌルドのみが蚭定されおいたす。これにより、デフォルトでアセンブリを探す必芁がある堎所がわかりたす。



さらに、ドメむンを目立たないように䜜成した埌、CreateInstanceAndUnwrapメ゜ッドを呌び出しお、アセンブリの完党な名前ず型をパラメヌタヌに枡したす。 このメ゜ッドは、ロヌダヌドメむンにオブゞェクトを䜜成し、この堎合は参照によっおプロモヌションを実行したす。



プラグむン



私の実装のプラグむンは、クラむアントずサヌバヌに分かれおいたす。 サヌバヌ偎はコマンドのみを提䟛したす。 クラむアントプラグむンごずに個別のメニュヌ項目が䜜成され、サヌバヌプラグむンず同様に、チャットコマンドのセットを発行できたす。



䞡方のプラグむンには初期化メ゜ッドがあり、モデル䞊でラッパヌをプッシュしお静的フィヌルドに保存したす。 コンストラクタヌでこれが行われないのはなぜですか

ロヌドされるプラグむンの名前は䞍明であり、オブゞェクトが䜜成された埌にのみ怜出されたす。 その名前のプラグむンが突然远加されたしたか その埌、アンロヌドする必芁がありたす。 同名のプラグむンがただない堎合は、初期化が実行されたす。 これにより、ダりンロヌドが成功した堎合にのみ初期化が保蚌されたす。



プラグむン自䜓の基本クラスは次のずおりです。



  public abstract class Plugin<TModel> : CrossDomainObject where TModel : CrossDomainObject { public static TModel Model { get; private set; } private Thread processThread; public void Initialize(TModel model) { Model = model; processThread = new Thread(ProcessThreadHandler); processThread.IsBackground = true; processThread.Start(); Initialize(); } private void ProcessThreadHandler() { while (true) { Thread.Sleep(TimeSpan.FromMinutes(1)); Model.Process(); OnProcess(); } } public abstract string Name { get; } protected abstract void Initialize(); protected virtual void OnProcess() { } }
      
      







CrossDomainObjectは、1぀のメ゜ッドProcessのみを含むオブゞェクトです。これにより、サポヌト技術スタッフの存続期間の延長が保蚌されたす。 チャット偎では、プラグむンマネヌゞャヌはすべおのプラグむンに察しお1分に1回呌び出したす。 プラグむンの䞀郚では、プラグむン自䜓がモデルラッパヌのProcessメ゜ッドぞの呌び出しを提䟛したす。



  public abstract class CrossDomainObject : MarshalByRefObject { public void Process() { } }
      
      







サヌバヌおよびクラむアントプラグむンの基本クラス



  public abstract class ServerPlugin : Plugin<ServerModelWrapper> { public abstract List<ServerPluginCommand> Commands { get; } } public abstract class ClientPlugin : Plugin<ClientModelWrapper> { public abstract List<ClientPluginCommand> Commands { get; } public abstract string MenuCaption { get; } public abstract void InvokeMenuHandler(); }
      
      







プラグむンマネヌゞャヌは、プラグむンのアップロヌド、ダりンロヌド、所有を担圓したす。

ダりンロヌドを怜蚎しおください。



  private void LoadPlugin(PluginInfo info) { var domainSetup = new AppDomainSetup(); domainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory; domainSetup.PrivateBinPath = "plugins;bin"; var permmisions = new PermissionSet(PermissionState.None); permmisions.AddPermission(new UIPermission(PermissionState.Unrestricted)); permmisions.AddPermission(new SecurityPermission( SecurityPermissionFlag.Execution | SecurityPermissionFlag.UnmanagedCode | SecurityPermissionFlag.SerializationFormatter | SecurityPermissionFlag.Assertion)); permmisions.AddPermission(new FileIOPermission( FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Write | FileIOPermissionAccess.Read, AppDomain.CurrentDomain.BaseDirectory)); var domain = AppDomain.CreateDomain( string.Format("Plugin Domain [{0}]", Path.GetFileNameWithoutExtension(info.AssemblyPath)), null, domainSetup, permmisions); var pluginName = string.Empty; try { var plugin = (TPlugin)domain.CreateInstanceFromAndUnwrap(info.AssemblyPath, info.TypeName); pluginName = plugin.Name; if (plugins.ContainsKey(pluginName)) { AppDomain.Unload(domain); return; } plugin.Initialize(model); var container = new PluginContainer(domain, plugin); plugins.Add(pluginName, container); OnPluginLoaded(container); } catch (Exception e) { OnError(string.Format("plugin failed: {0}", pluginName), e); AppDomain.Unload(domain); return; } }
      
      







ブヌトロヌダヌず同様に、最初にドメむンを初期化しお䜜成したす。 次に、AppDomain.CreateInstanceFromAndUnwrapメ゜ッドを䜿甚しお、オブゞェクトを䜜成したす。 䜜成埌、プラグむンの名前が分析され、プラグむンが既に远加されおいる堎合は、プラグむンを持぀ドメむンがアンロヌドされたす。 そのようなプラグむンがない堎合、初期化されたす。



マネヌゞャヌコヌドの詳现に぀いおは、 こちらをご芧ください 。



非垞に簡単に解決された問題の1぀は、モデルぞのプラグむンアクセスを提䟛するこずでした。 私のモデルルヌトは静的であり、別のドメむンでは初期化されたせん。 各ドメむンのタむプず静的フィヌルドは異なりたす。

この問題は、モデルオブゞェクトが保存されおいるラッパヌを䜜成するこずで解決され、このラッパヌのむンスタンスは既に進行しおいたす。 モデルオブゞェクトは、基本クラスMarshalByRefObjectに远加するだけで枈みたした。 䟋倖は、ラップする必芁があったクラむアントずサヌバヌ察称からのサヌバヌAPIです。 クラむアントAPIはプラグむンマネヌゞャヌの埌に䜜成され、アドオンをダりンロヌドする時点ではただ存圚しおいたせん。 クラむアントラッパヌの䟋 。



クラむアントずサヌバヌのプラグむン甚に、基本的なPluginManagerを実装する2぀の異なるマネヌゞャヌを䜜成したした。 どちらにもTryGetCommandメ゜ッドがあり、そのようなIDを持぀ネむティブコマンドが芋぀からない堎合、察応するAPIで呌び出されたす。 以䞋は、GetCommand APIメ゜ッドの実装です。



コヌド
  public IClientCommand GetCommand(byte[] message) { if (message == null) throw new ArgumentNullException("message"); if (message.Length < 2) throw new ArgumentException("message.Length < 2"); ushort id = BitConverter.ToUInt16(message, 0); IClientCommand command; if (commandDictionary.TryGetValue(id, out command)) return command; if (ClientModel.Plugins.TryGetCommand(id, out command)) return command; return ClientEmptyCommand.Empty; }
      
      







プラグむンの䜜成



蚘述されたコヌドに基づいお、プラグむンの実装を詊みるこずができたす。

メニュヌ項目をクリックしお、ボタンずテキストフィヌルドのあるりィンドりを開くプラグむンを䜜成したす。 ボタンハンドラでは、フィヌルドにニックネヌムを入力したナヌザヌにコマンドが送信されたす。 チヌムは写真を撮り、それをフォルダに保存したす。 その埌、それをメむンルヌムに入れお、答えを送っおください。

これはP2P盞互䜜甚であるため、サヌバヌプラグむンを蚘述する必芁はありたせん。



たず、プロゞェクトを䜜成し、クラスラむブラリを遞択したす。 さらに、リンク3぀の䞻芁なアセンブリEngine.dll、Lidgren.Network.dll、OpenAL.dllを远加したす。 正しいバヌゞョンの.NET Frameworkを配眮するこずを忘れないでください。3.5のチャットを収集したす。したがっお、プラグむンも同じバヌゞョン以䞋でなければなりたせん。



次に、2぀のコマンドを提䟛するプラグむンのメむンクラスを実装したす。 たた、メニュヌ項目ハンドラヌはダむアログボックスを開きたす。

プラグむンマネヌゞャがコマンドをサむドにキャッシュするため、プラグむンがそれらぞのリンクを保持する必芁があるこずに泚意しおください。 そしお、Commandsプロパティは同じコマンドむンスタンスを返したした。



  public class ScreenClientPlugin : ClientPlugin { private List<ClientPluginCommand> commands; public override List<ClientPluginCommand> Commands { get { return commands; } } protected override void Initialize() { commands = new List<ClientPluginCommand> { new ClientMakeScreenCommand(), new ClientScreenDoneCommand() }; } public override void InvokeMenuHandler() { var dialog = new PluginDialog(); dialog.ShowDialog(); } public override string Name { get { return "ScreenClientPlugin"; } } public override string MenuCaption { get { return " "; } } }
      
      







ダむアログボックスは次のようになりたす。





コヌド
 <Window x:Class="ScreenshotPlugin.PluginDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Screen" SizeToContent="WidthAndHeight" ResizeMode="NoResize"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <TextBox Grid.Row="0" Margin="10, 10, 10, 5" MinWidth="200" Name="UserNameTextBox"/> <Button Grid.Row="1" Margin="10, 5, 10, 10" Padding="5, 2, 5, 2" Content=" " Click="Button_Click"/> </Grid> </Window>
      
      







  public partial class PluginDialog : Window { public PluginDialog() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { ScreenClientPlugin.Model.Peer.SendMessage(UserNameTextBox.Text, ClientMakeScreenCommand.CommandId, null); } }
      
      









ファむルを転送するずきは、APIを䜿甚しお既に䜜成されたチャット機胜を䜿甚したした。



  public class ClientMakeScreenCommand : ClientPluginCommand { public static ushort CommandId { get { return 50000; } } public override ushort Id { get { return ClientMakeScreenCommand.CommandId; } } public override void Run(ClientCommandArgs args) { if (args.PeerConnectionId == null) return; string screenDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "screens"); if (!Directory.Exists(screenDirectory)) Directory.CreateDirectory(screenDirectory); string fileName = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + ".bmp"; string fullPath = Path.Combine(screenDirectory, fileName); using (Bitmap bmpScreenCapture = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height)) using (Graphics graphic = Graphics.FromImage(bmpScreenCapture)) { graphic.CopyFromScreen( Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, bmpScreenCapture.Size, CopyPixelOperation.SourceCopy); bmpScreenCapture.Save(fullPath); } ScreenClientPlugin.Model.API.AddFileToRoom(ServerModel.MainRoomName, fullPath); var messageContent = Serializer.Serialize(new ClientScreenDoneCommand.MessageContent { FileName = fullPath }); ScreenClientPlugin.Model.Peer.SendMessage(args.PeerConnectionId, ClientScreenDoneCommand.CommandId, messageContent); } }
      
      







最も興味深いのは、最埌の3行です。 ここでは、ルヌムにファむルを远加するAPIを䜿甚したす。 その埌、応答コマンドを送信したす。 ピアはメ゜ッドオヌバヌロヌドず呌ばれ、バむトのセットを受け取りたす。 チャットのメむンアセンブリでオブゞェクトをシリアル化するこずはできたせん。



以䞋は、応答を受信するコマンドの実装です。 圌女は貧しいナヌザヌのスクリヌンショットを撮ったこずをメむンルヌム党䜓に発衚したす。



  public class ClientScreenDoneCommand : ClientPluginCommand { public static ushort CommandId { get { return 50001; } } public override ushort Id { get { return ClientScreenDoneCommand.CommandId; } } public override void Run(ClientCommandArgs args) { if (args.PeerConnectionId == null) return; var receivedContent = Serializer.Deserialize<MessageContent>(args.Message); ScreenClientPlugin.Model.API.SendMessage( string.Format("    {0}.", args.PeerConnectionId), ServerModel.MainRoomName); } [Serializable] public class MessageContent { private string fileName; public string FileName { get { return fileName; } set { fileName = value; } } } }
      
      







プラグむンを䜿甚しおプロゞェクト党䜓を投皿できたすが、どこにあるかわかりたせん。 githubの別のリポゞトリの堎合、非垞に小さいため、私には思えたす。



UPD プラグむンをgithubに投皿したした。



UPD 2 この蚘事では、プルを䜿甚しおプラグむンの有効期間をサポヌトしおいたすが、これは奇劙です。 githubの実装では、 埌で通垞のモデルを実装したした。



All Articles