无法在不关闭整个应用程序的情况下关闭/卸载插件



我需要一些关于我的应用程序的IPlugin实现的帮助。

插件合约代码:

Public Interface IPlugin
ReadOnly Property Name() As String
Sub DoSomething()
Sub DoExit()
End Interface

主应用:

Imports PluginContracts
Module Program
Sub Main()
LoadTray()
End Sub
Sub LoadTray()
Dim dll As Assembly = Assembly.LoadFrom(GetDLL.TrayDll)
For Each t As Type In dll.GetTypes
If Not t.GetInterface("IPlugin") = Nothing Then
Try
Dim PluginClass As IPlugin =  Type(Activator.CreateInstance(t), IPlugin)
PluginClass.DoSomething()
Catch ex As Exception
MessageBox.Show(ex.ToString())
End Try
End If
Next
End Sub
End Module

插件:

Imports PluginContracts
Imports System.Windows.Forms
Public Class Plugin
Implements IPlugin
Public Sub DoSomething() Implements IPlugin.DoSomething
Application.Run(New MyContext)
End Sub
Public Sub New()
End Sub
Public Sub DoExit() Implements IPlugin.DoExit
Application.Exit()
End Sub
Public ReadOnly Property Name As String Implements IPlugin.Name
Get
Name = "First Plugin"
End Get
End Property
End Class

(插件应用程序是一个 Dll,在类"我的上下文"中带有托盘图标)

我一切正常,插件加载(带有托盘图标),但我无法关闭它并加载其他内容。 我有一个文件系统观察器,它将关闭插件,更新 Dll,然后重新打开它,但它关闭了主应用程序,我无法执行任何其他操作......

感谢您的帮助

很抱歉花了这么长时间才回答。事实证明,这比我想象的要复杂得多,至少如果您想对插件施加安全限制。

不安全的方式

如果你对你的插件在所谓的完全信任下执行感到满意,这意味着它可以做任何它想做的事情,那么你必须做三件事:

  1. 调整您的代码以在单独的AppDomain中启动插件。

  2. 在其
  3. 自己的线程中运行DoSomething()方法(这是必需的,因为Application.Run()尝试创建新的 UI 线程)。

  4. IPluginInterface更改为MustInherit类。这是因为在不同AppDomains之间封送的代码必须在继承自MarshalByRefObject的对象中运行。

新代码:

编辑 (2018-05-29)

我认为在无窗口应用程序中等待插件退出的最佳方法是使用ManualResetEvent

我在下面的代码中为此进行了快速实现。现在,您必须在关闭插件时调用PluginClass.Exit()而不是DoExit()

经过更多测试后,我会将其添加到更安全的解决方案中。

Dim PluginDomain As AppDomain = Nothing
Dim PluginClass As PluginBase = Nothing
Sub Main()
LoadTray()
If PluginClass IsNot Nothing Then
PluginClass.WaitForExit()
End If
End Sub
Sub LoadTray()
Dim dll As Assembly = Assembly.LoadFrom(GetDLL.TrayDll)
For Each t As Type In dll.GetTypes
If GetType(PluginBase).IsAssignableFrom(t) = True Then
Try
Dim PluginDomainSetup As New AppDomainSetup() With {
.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
.PrivateBinPath = "Plugins"
}
PluginDomain = AppDomain.CreateDomain("MyPluginDomain", Nothing, PluginDomainSetup)
PluginClass = CType(PluginDomain.CreateInstanceFromAndUnwrap(GetDLL.TrayDll, t.FullName), PluginBase)
Dim PluginThread As New Thread(AddressOf PluginClass.DoSomething)
PluginThread.IsBackground = True
PluginThread.Start()
Catch ex As Exception
MessageBox.Show(ex.ToString())
End Try
Exit For 'Don't forget to exit the loop.
End If
Next
End Sub

插件库.vb:

Public MustInherit Class PluginBase
Inherits MarshalByRefObject
Private IsExitingEvent As New ManualResetEvent(False)
Public MustOverride ReadOnly Property Name As String
Public MustOverride Sub DoSomething()
Protected MustOverride Sub OnExit()
Public Sub [Exit]()
Me.OnExit()
Me.IsExitingEvent.Set()
End Sub
Public Sub WaitForExit()
Me.IsExitingEvent.WaitOne(-1)
End Sub
End Class

您的插件:

Imports PluginContracts
Imports System.Windows.Forms
Public Class Plugin
Inherits PluginBase
Protected Overrides Sub DoSomething()
Application.Run(New MyContext)
End Sub
Protected Overrides Sub OnExit()
Application.Exit()
End Sub
Public Overrides ReadOnly Property Name As String
Get
Return "First Plugin" 'Use "Return" instead of "Name = ..."
End Get
End Property
End Class

但是,由于让插件在完全信任下运行是非常不安全的,我设计了一个解决方案,让您控制插件可以做什么。这需要我重写大部分代码,因为它需要不同的结构才能工作。


安全的方式

在我的测试用例中PluginContracts是一个单独的 DLL(项目),只有四个类:

  • PluginBase- 所有插件的基类。
  • PluginInfo- 包含有关插件的信息的包装器(由PluginManager使用)。
  • PluginManager- 用于加载,卸载和跟踪插件的管理器。
  • PluginUnloader- 在插件的受限AppDomain中以完全信任方式运行的类。以前只能打电话给Application.Exit()

首先,要使一切正常工作,需要使用强名称对PluginContractsDLL 进行签名。这是如何完成的:

  1. 右键单击Solution Explorer中的PluginContracts项目,然后按Properties键。

  2. 选择"Signing"选项卡。

  3. 选中显示Sign the assemblyDelay sign only选中

    的复选框。
  4. 打开下拉菜单,然后按<New...>

  5. 为密钥提供文件名和密码(请务必记住这一点!

  6. 做!

修复后,您需要使PluginContractsDLL 可供部分受信任的代码调用。这样我们的插件就可以使用它,因为它将作为不受信任的代码运行。

  1. 再次选择Solution Explorer中的项目。

  2. 按下Solution Explorer中显示Show all files的按钮。

  3. 展开My Project节点。

  4. 双击AssemblyInfo.vb进行编辑,并在文件末尾添加以下行:

    <Assembly: AllowPartiallyTrustedCallers(PartialTrustVisibilityLevel:=Security.PartialTrustVisibilityLevel.NotVisibleByDefault)> 
    

这样做有一个不利的一面:PluginContractsDLL 中的所有代码现在都将以相当低的权限运行。要使其再次以标准权限运行,您必须使用SecurityCritical属性装饰每个类,除了PluginBase(在下面的代码中,我已经修复了所有这些,因此您无需更改任何内容)。因此,我建议您在PluginContracts项目中只有以下四个类:

插件库.vb:

''' <summary>
''' A base class for application plugins.
''' </summary>
''' <remarks></remarks>
Public MustInherit Class PluginBase
Inherits MarshalByRefObject
Public MustOverride ReadOnly Property Name As String
Public MustOverride Sub DoSomething()
Public MustOverride Sub OnExit()
End Class

插件信息.vb:

Imports System.Security
Imports System.Runtime.Remoting
''' <summary>
''' A class holding information about a plugin.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public Class PluginInfo
Private _file As String
Private _plugin As PluginBase
Private _appDomain As AppDomain
Private Unloader As PluginUnloader
Friend Unloaded As Boolean = False
''' <summary>
''' Gets the AppDomain that this plugin runs in.
''' </summary>
''' <remarks></remarks>
Friend ReadOnly Property AppDomain As AppDomain
Get
Return _appDomain
End Get
End Property
''' <summary>
''' Gets the full path to the plugin assembly.
''' </summary>
''' <remarks></remarks>
Public ReadOnly Property File As String
Get
Return _file
End Get
End Property
''' <summary>
''' Gets the underlying plugin.
''' </summary>
''' <remarks></remarks>
Public ReadOnly Property Plugin As PluginBase
Get
Return _plugin
End Get
End Property
''' <summary>
''' DO NOT USE! See PluginManager.UnloadPlugin() instead.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Friend Sub Unload()
Me.Unloader.Unload()
Me.Unloaded = True
End Sub
''' <summary>
''' Initializes a new instance of the PluginInfo class.
''' </summary>
''' <param name="File">The full path to the plugin assembly.</param>
''' <param name="Plugin">The underlying plugin.</param>
''' <param name="AppDomain">The AppDomain that the plugin runs in.</param>
''' <remarks></remarks>
<SecurityCritical()>
Friend Sub New(ByVal File As String, ByVal Plugin As PluginBase, ByVal AppDomain As AppDomain)
_file = File
_plugin = Plugin
_appDomain = AppDomain
'Create an instance of PluginUnloader inside the plugin's AppDomain.
Dim Handle As ObjectHandle = Activator.CreateInstanceFrom(Me.AppDomain, GetType(PluginUnloader).Module.FullyQualifiedName, GetType(PluginUnloader).FullName)
Me.Unloader = CType(Handle.Unwrap(), PluginUnloader)
End Sub
End Class

插件管理器.vb:

'-------------------------------------------------------------------------------
'Copyright (c) 2018, Vincent Bengtsson
'All rights reserved.
'
'Redistribution and use in source and binary forms, with or without
'modification, are permitted provided that the following conditions are met:
'1. Redistributions of source code must retain the above copyright notice, this
'   list of conditions and the following disclaimer.
'2. Redistributions in binary form must reproduce the above copyright notice,
'   this list of conditions and the following disclaimer in the documentation
'   and/or other materials provided with the distribution.
'
'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
'ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
'WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
'DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
'ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
'(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
'LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
'ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
'(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
'SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'-------------------------------------------------------------------------------
Imports System.Collections.ObjectModel
Imports System.Reflection
Imports System.IO
Imports System.Security
Imports System.Security.Permissions
Imports System.Security.Policy
''' <summary>
''' A class for managing application plugins.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public NotInheritable Class PluginManager
Implements IDisposable
Private PluginLookup As New Dictionary(Of String, PluginInfo)
Private PluginList As New List(Of String)
Private CurrentAppDomain As AppDomain = Nothing
Private _loadedPlugins As New ReadOnlyCollection(Of String)(Me.PluginList)
''' <summary>
''' Gets a list of all loaded plugins' names.
''' </summary>
''' <remarks></remarks>
Public ReadOnly Property LoadedPlugins As ReadOnlyCollection(Of String)
Get
Return _loadedPlugins
End Get
End Property
''' <summary>
''' Returns the plugin with the specified name (or null, if the plugin isn't loaded).
''' </summary>
''' <param name="Name">The name of the plugin to get.</param>
''' <remarks></remarks>
<SecurityCritical()>
Public Function GetPluginByName(ByVal Name As String) As PluginInfo
Dim Plugin As PluginInfo = Nothing
Me.PluginLookup.TryGetValue(Name, Plugin)
Return Plugin
End Function
''' <summary>
''' Checks whether a plugin by the specified name is loaded.
''' </summary>
''' <param name="Name">The name of the plugin to look for.</param>
''' <remarks></remarks>
<SecurityCritical()>
Public Function IsPluginLoaded(ByVal Name As String) As Boolean
Return Me.PluginLookup.ContainsKey(Name)
End Function
''' <summary>
''' Loads a plugin with the specified permissions (or no permissions, if omitted).
''' </summary>
''' <param name="File">The path to the plugin assembly to load.</param>
''' <param name="Permissions">Optional. A list of permissions to give the plugin (default permissions that are always applied: SecurityPermissionFlag.Execution).</param>
''' <remarks></remarks>
<SecurityCritical()>
Public Function LoadPlugin(ByVal File As String, ByVal ParamArray Permissions As IPermission()) As PluginInfo
Dim FullPath As String = Path.GetFullPath(File)
If System.IO.File.Exists(FullPath) = False Then Throw New FileNotFoundException()
'Check if the plugin file has already been loaded. This is to avoid odd errors caused by Assembly.LoadFrom().
If Me.PluginLookup.Values.Any(Function(info As PluginInfo) info.File.Equals(FullPath, StringComparison.OrdinalIgnoreCase)) = True Then
Throw New ApplicationException("Plugin """ & FullPath & """ is already loaded!")
End If
'Load assembly and look for a type derived from PluginBase.
Dim PluginAssembly As Assembly = Assembly.LoadFrom(FullPath)
Dim PluginType As Type = PluginManager.GetPluginType(PluginAssembly)
If PluginType Is Nothing Then Throw New TypeLoadException("""" & FullPath & """ is not a valid plugin!")
'Set up the application domain.
'Setting PartialTrustVisibleAssemblies allows our plugin to make partially trusted calls to the PluginBase DLL.
Dim PluginDomainSetup As New AppDomainSetup() With {
.ApplicationBase = Me.CurrentAppDomain.BaseDirectory,
.PartialTrustVisibleAssemblies = New String() {GetType(PluginUnloader).Assembly.GetName().Name & ", PublicKey=" & BitConverter.ToString(GetType(PluginUnloader).Assembly.GetName().GetPublicKey()).ToLower().Replace("-", "")}
}
'Set up the default (necessary) permissions for the plugin:
'   SecurityPermissionFlag.Execution     - Allows our plugin to execute managed code.
'   FileIOPermissionAccess.Read          - Allows our plugin to read its own assembly.
'   FileIOPermissionAccess.PathDiscovery - Allows our plugin to get information about its parent directory.
Dim PluginPermissions As New PermissionSet(PermissionState.None) 'No permissions to begin with.
PluginPermissions.AddPermission(New SecurityPermission(SecurityPermissionFlag.Execution))
PluginPermissions.AddPermission(New FileIOPermission(FileIOPermissionAccess.Read Or FileIOPermissionAccess.PathDiscovery, FullPath))
'Load all additional permissions (if any).
For Each Permission As IPermission In Permissions
PluginPermissions.AddPermission(Permission)
Next
'Get the strong name for the assembly containing PluginUnloader and create the AppDomain.
'The strong name is used so that PluginUnloader may bypass the above added restrictions.
Dim TrustedAssembly As StrongName = GetType(PluginUnloader).Assembly.Evidence.GetHostEvidence(Of StrongName)()
Dim PluginDomain As AppDomain = AppDomain.CreateDomain(File, Nothing, PluginDomainSetup, PluginPermissions, TrustedAssembly)
'Create an instance of the plugin.
Dim Plugin As PluginBase = CType(PluginDomain.CreateInstanceFromAndUnwrap(FullPath, PluginType.FullName), PluginBase)
Dim PluginInfo As New PluginInfo(FullPath, Plugin, PluginDomain)
'Is a plugin by this name already loaded?
If Me.IsPluginLoaded(Plugin.Name) = True Then
Dim Name As String = Plugin.Name
Me.UnloadPlugin(PluginInfo)
Throw New ApplicationException("A plugin by the name """ & Name & """ is already loaded!")
End If
'Add the plugin to our lookup table and name list.
Me.PluginLookup.Add(Plugin.Name, PluginInfo)
Me.PluginList.Add(Plugin.Name)
'Return the loaded plugin to the caller.
Return PluginInfo
End Function
''' <summary>
''' Unloads a plugin.
''' </summary>
''' <param name="Name">The name of the plugin to unload.</param>
''' <remarks></remarks>
<SecurityCritical()>
Public Sub UnloadPlugin(ByVal Name As String)
Dim Plugin As PluginInfo = Me.GetPluginByName(Name)
If Plugin Is Nothing Then Throw New ArgumentException("No plugin by the name """ & Name & """ is loaded.", "Name")
Me.UnloadPlugin(Plugin)
End Sub
''' <summary>
''' Unloads a plugin.
''' </summary>
''' <param name="PluginInfo">The plugin to unload.</param>
''' <remarks></remarks>
<SecurityCritical()>
Public Sub UnloadPlugin(ByVal PluginInfo As PluginInfo)
If PluginInfo Is Nothing Then Throw New ArgumentNullException("PluginInfo")
If PluginInfo.Unloaded = True Then Return
Dim PluginName As String = PluginInfo.Plugin.Name
Dim Permission As New SecurityPermission(SecurityPermissionFlag.ControlAppDomain)
Permission.Assert()
PluginInfo.Plugin.OnExit()
PluginInfo.Unload()
AppDomain.Unload(PluginInfo.AppDomain)
CodeAccessPermission.RevertAssert()
Me.PluginLookup.Remove(PluginName)
End Sub
''' <summary>
''' Attempts to get a class derived from PluginBase in the specified assembly.
''' </summary>
''' <param name="PluginAssembly">The assembly to check.</param>
''' <remarks></remarks>
<SecurityCritical()>
Private Shared Function GetPluginType(ByVal PluginAssembly As Assembly) As Type
For Each t As Type In PluginAssembly.GetTypes()
If GetType(PluginBase).IsAssignableFrom(t) = True Then Return t
Next
Return Nothing
End Function
''' <summary>
''' Initializes a new instance of the PluginManager class.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public Sub New()
Me.CurrentAppDomain = AppDomain.CurrentDomain
End Sub
#Region "IDisposable Support"
Private disposedValue As Boolean ' To detect redundant calls
' IDisposable
<SecurityCritical()>
Protected Sub Dispose(disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
' TODO: dispose managed state (managed objects).
'Unload all plugins.
For Each PluginPair As KeyValuePair(Of String, PluginInfo) In Me.PluginLookup
Try : Me.UnloadPlugin(PluginPair.Value) : Catch : End Try
Next
End If
' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
' TODO: set large fields to null.
End If
Me.disposedValue = True
End Sub
' TODO: override Finalize() only if Dispose(ByVal disposing As Boolean) above has code to free unmanaged resources.
'Protected Overrides Sub Finalize()
'    ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
'    Dispose(False)
'    MyBase.Finalize()
'End Sub
' This code added by Visual Basic to correctly implement the disposable pattern.
<SecurityCritical()>
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region
End Class

插件卸载.vb:

Imports System.Windows.Forms
Imports System.Security
Imports System.Security.Permissions
''' <summary>
''' A class for unloading plugins from within their AppDomains.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public NotInheritable Class PluginUnloader
Inherits MarshalByRefObject
''' <summary>
''' Calls Application.Exit(). This must be called inside the plugin's AppDomain.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public Sub Unload()
'Request permission to execute managed code (required to call Application.Exit()).
Dim Permission As New SecurityPermission(SecurityPermissionFlag.UnmanagedCode)
Permission.Assert()
'Exits the plugin's UI threads (if any exist).
Application.Exit()
'Revert UnmanagedCode privilege.
CodeAccessPermission.RevertAssert()
End Sub
''' <summary>
''' Initializes a new instance of the PluginUnloader class.
''' </summary>
''' <remarks></remarks>
<SecurityCritical()>
Public Sub New()
End Sub
End Class


示例用法

主代码(当前在表单中使用):

Dim PluginManager As New PluginManager
Dim PluginClass As PluginInfo
Private Sub RunPluginButton_Click(sender As System.Object, e As System.EventArgs) Handles RunPluginButton.Click
'Load our plugin and give it UI permissions.
'The "UIPermissionWindow.AllWindows" flag allows our plugin to create a user interface (display windows, notify icons, etc.).
PluginClass = PluginManager.LoadPlugin("PluginsTestPlugin.dll", New UIPermission(UIPermissionWindow.AllWindows))
'IMPORTANT: Each plugin must run in its own thread!
Dim tr As New Thread(AddressOf PluginClass.Plugin.DoSomething)
tr.IsBackground = True
tr.Start()
End Sub
Private Sub ExitPluginButton_Click(sender As System.Object, e As System.EventArgs) Handles ExitPluginButton.Click
If PluginClass Is Nothing Then
MessageBox.Show("Plugin not loaded!", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Return
End If
PluginManager.UnloadPlugin(PluginClass)
PluginClass = Nothing
End Sub

示例插件:

Public Class TestPlugin
Inherits PluginBase
Public Overrides ReadOnly Property Name As String
Get
Return "Test Plugin"
End Get
End Property
Public Overrides Sub DoSomething()
Application.Run(New MyContext)
End Sub
Public Overrides Sub OnExit()
'Do some cleanup here, if necessary.
'No need to call Application.Exit() - it is called automatically by PluginUnloader.
End Sub
End Class


权限的工作原理

当您调用PluginManager.LoadPlugin()方法时,您会向它传递要加载的插件的路径,但是您也可以向其传递一组要应用于插件的权限(如果需要,这是可选的)。

默认情况下,所有插件具有以下权限加载:

  • 它可以执行自己的托管代码。

  • 它具有对插件文件本身及其目录的读取访问权限。

这意味着插件:

  • 无法执行任何非托管(也称为本机)代码。例如,这是DllImportDeclare Function声明。

  • 无法读取/写入/创建/删除任何文件

  • 不能有用户界面(打开任何窗口,使用通知图标等)。

  • 没有互联网接入。

  • 不能运行除其自身和框架以外的任何代码(在其限制范围内)。

  • 。等等,等等...

这可以通过指定在加载插件时应授予插件的权限来更改。例如,如果您希望插件能够在某个目录中读取和写入文件,您可以执行以下操作:

PluginManager.LoadPlugin("PluginsTestPlugin.dll", 
New FileIOPermission(FileIOPermissionAccess.AllAccess, "C:somefolder"))

或者,如果您希望它能够访问任何文件夹或文件:

PluginManager.LoadPlugin("PluginsTestPlugin.dll", 
New FileIOPermission(PermissionState.Unrestricted))

只需不断添加参数即可添加多个权限:

PluginManager.LoadPlugin("PluginsTestPlugin.dll", 
New FileIOPermission(PermissionState.Unrestricted), 
New UIPermission(UIPermissionWindow.AllWindows), 
New WebPermission(PermissionState.Unrestricted))

有关所有可用权限类型的列表,请参阅:
https://msdn.microsoft.com/en-us/library/h846e9b3(v=vs.110).aspx

相关内容