0

I need some help with IPlugin implementation for my app.

PluginContracts code:

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

Main App:

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

Plugin:

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

(The plugin app is a Dll with a tray icon in Class “MyContext”)

I have everything working, the plugin loads (with the Tray Icon), but I can't close it and load something else. I have a FileSystemWatcher that will close the plugin, update the Dll and then reopen it, but it closes the Main App and I can’t do anything else…

Thanks for the help

Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
user7406533
  • 73
  • 1
  • 9
  • Try running the code for that specific plugin in a separate `AppDomain`. It should make `Application.Exit()` only terminate that and not your entire application. – Visual Vincent May 18 '18 at 09:33
  • @VisualVincent, thanks for the reply, I tried to follow the example in the link, but I'm getting a MissingMethodException error "Entry point not found in assembly 'Plugin-test, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'" – user7406533 May 18 '18 at 14:38
  • I see now that the link I gave you wasn't really that good as it expects the loaded assembly to have a `Main()` entry point (which DLLs usually don't). What you need is to load each plugin assembly in a separate `AppDomain` and create an instance of their `Plugin` class. Then you just need to invoke the method of your choice. The following answer's **second** code block should work: https://stackoverflow.com/a/14184863 (it's in C# though, but it shouldn't be too hard to convert) – Visual Vincent May 18 '18 at 16:18
  • @VisualVincent, Thanks, now I'm getting "'Could not load type 'IPlugin' from assembly 'Plugintest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.' " – user7406533 May 22 '18 at 13:28
  • Make sure that the plugin can find the DLL. Try setting the [**`ApplicationBase`**](https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx) and [**`PrivateBinPath`**](https://msdn.microsoft.com/en-us/library/system.appdomainsetup.privatebinpath(v=vs.110).aspx) to that of your main application when creating each `AppDomain`: https://learn.microsoft.com/en-us/dotnet/framework/app-domains/how-to-configure-an-application-domain – Visual Vincent May 22 '18 at 14:58
  • I'm sorry but I can't write a more complete answer because I am currently having a very busy week and therefore don't have the time to try this on my own (I'm merely using Stack Overflow on the fly via my phone). – Visual Vincent May 22 '18 at 15:01
  • @VisualVincent, Now I'm getting ": 'Could not load type 'Plugin' from assembly 'Plugintest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.'" , It is not the same (notice the 'IPlugin' vs 'Plugin') – user7406533 May 23 '18 at 19:10

1 Answers1

1

Sorry for taking so long to answer. This turned out to be a lot more complicated than I thought, at least if you want to impose security restrictions on your plugin.

The unsafe way

If you're okay with your plugin executing in so-called full trust, meaning that it can do anything it wants, then you have to do three things:

  1. Tweak your code to launch the plugin in a separate AppDomain.

  2. Run the DoSomething() method in its own thread (this is necessary since Application.Run() tries to create a new UI thread).

  3. Change IPlugin from an Interface to a MustInherit class. This is because code that is marshalled between different AppDomains MUST run in an object that inherits from MarshalByRefObject.

New code:

EDIT (2018-05-29)

I figured the best way to wait for a plugin to exit in a windowless application would be to utilize a ManualResetEvent.

I have made a quick implementation for this in the code below. You must now call PluginClass.Exit() instead of DoExit() when closing your plugin.

I will add this to the safer solution after some more testing.

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

PluginBase.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

Your plugin:

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

However, as letting the plugin run in full trust is extremely unsafe I designed a solution which lets you control what the plugin can do. This required me to rewrite most of the code though, as it needed a different structure to work.


The safe(r) way

In my test case PluginContracts is a separate DLL (project) with only four classes:

  • PluginBase - The base class for all plugins.
  • PluginInfo - A wrapper containing info about a plugin (used by PluginManager).
  • PluginManager - A manager for loading, unloading and keeping track of plugins.
  • PluginUnloader - A class that runs with full trust in the plugin's restricted AppDomain. Only used to be able to call Application.Exit().

First and foremost, for everything to work the PluginContracts DLL needs to be signed with a Strong Name. Here's how that can be done:

  1. Right-click the PluginContracts project in the Solution Explorer and press Properties.

  2. Select the Signing tab.

  3. Check the check box that says Sign the assembly, leave Delay sign only unchecked.

  4. Open the drop down and press <New...>.

  5. Give the key a file name and password (be sure to remember this!).

  6. Done!

Now that that's fixed you need to make the PluginContracts DLL available to be called by partially trusted code. This is so that our plugin can use it since it will be running as untrusted code.

  1. Select the project in the Solution Explorer again.

  2. Press the button in the Solution Explorer that says Show all files.

  3. Expand the My Project node.

  4. Double-click AssemblyInfo.vb to edit it and add this line at the end of the file:

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

There is a backside to doing this: All code inside the PluginContracts DLL will now run with rather low permissions. To make it run with the standard permissions again you have to decorate each class with the SecurityCritical attribute, except for the PluginBase class (in the code below I've already fixed all this, so you don't need to change anything). Due to this I recommend that you only have below four classes in the PluginContracts project:

PluginBase.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

PluginInfo.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

PluginManager.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

PluginUnloader.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


Example usage

Main code (currently used in a form):

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("Plugins\TestPlugin.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

Example plugin:

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


How permissions work

When you call the PluginManager.LoadPlugin() method you pass it a path to the plugin which to load, however you can also pass it a set of permissions that you want to apply to the plugin (if you want to, this is optional).

By default all plugins are loaded only with these permissions:

  • It can execute its own managed code.

  • It has read access to the plugin file itself and its directory.

This means that a plugin:

  • Can not execute any unmanaged (also known as native) code. This is for example DllImport or Declare Function declarations.

  • Can not read/write/create/delete any files.

  • Can not have a User Interface (open any windows, use notify icons, etc.).

  • Does not have internet access.

  • Can not run any code other than its own and the framework's (within the boundaries of its restrictions).

  • ...and so on, and so forth...

This can be changed by specifying which permissions the plugin should be granted, when you're loading it. For instance if you want your plugin to be able to read and write files in a certain directory you could do:

PluginManager.LoadPlugin("Plugins\TestPlugin.dll", 
    New FileIOPermission(FileIOPermissionAccess.AllAccess, "C:\some\folder"))

Or if you want it to be able to access any folder or file:

PluginManager.LoadPlugin("Plugins\TestPlugin.dll", 
    New FileIOPermission(PermissionState.Unrestricted))

Multiple permissions can be added by just keep adding arguments:

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

For a list of all available permission types see:
https://msdn.microsoft.com/en-us/library/h846e9b3(v=vs.110).aspx

Visual Vincent
  • 18,045
  • 5
  • 28
  • 75
  • If you have any questions or need help with permissions feel free to ask! – Visual Vincent May 27 '18 at 00:08
  • First, Thank you for taking the time to write this. 2nd, WOW, I didn't realize that it is this complicated... 3rd, I tried the first (Unsafe) way because I don't really need it to be safe... but it just closes the application (I think it is the "If GetType(PluginBase.PluginBase).IsAssignableFrom(t) = True Then" line, I guess it is False for some reason...). – user7406533 May 28 '18 at 15:48
  • Then I tried the Safer way, I get "An unhandled exception of type 'System.TypeInitializationException' occurred in Unknown Module. Additional information: The type initializer for 'ATTBase.Program' threw an exception." when I run it in a Module (Formless app), and "An unhandled exception of type 'System.InvalidOperationException' occurred in test.exe Additional information: An error occurred creating the form. See Exception.InnerException for details. The error is: Method 'PluginContracts.PluginManager.Dispose()' is security transparent, but is a member of a security critical type." – user7406533 May 28 '18 at 15:51
  • In a Formed app. I figured out the the error is caused by "Dim PluginManager As New PluginManager" line – user7406533 May 28 '18 at 15:53
  • @user7406533 : The reason it closes your application is because the plugin runs entirely on its own now, thus it won't stop your app from exiting `Main()`. I have to update the code for it to work in your specific case. Also please note that you should preferably create the `PluginManager` at class level. – Visual Vincent May 28 '18 at 18:06
  • It finally worked (the unsafe way) in a test application with a Form... Button1 loads the plugin and button2 has "AppDomain.Unload(PluginDomain)", it unloads it, but after a min I get an error "An unhandled exception of type 'System.CannotUnloadAppDomainException' occurred in mscorlib.dll Additional information: Error while unloading appdomain. (Exception from HRESULT: 0x80131015)". I put "Dim PluginDomain As AppDomain = Nothing" at the top – user7406533 May 28 '18 at 19:40
  • @user7406533 : You need to call `Application.Exit()` inside the plugin's `AppDomain` before you can call `AppDomain.Unload()`. Put `Dim PluginClass As PluginBase = Nothing` at the top as well, then call `PluginClass.DoExit()` before you call `AppDomain.Unload()`. || In the meantime I'm working with updating the safe code so that it'll work properly for you as well. – Visual Vincent May 28 '18 at 19:45
  • Thanks for that... But the only way (that I found) to keep the app from closing after unloading the domain is to load a new domain with the new (updated) plugin into a new domain, and only then unload the old domain... I'm sure there is a better way to do this... – user7406533 May 28 '18 at 21:36
  • @user7406533 : I think I have fixed the problems in the safer code now. Try it out. I also added a way for the unsafe code to wait for the plugin to exit. I will add this to the safe way as well, but it requires some more testing. – Visual Vincent May 28 '18 at 22:25
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/172013/discussion-between-user7406533-and-visual-vincent). – user7406533 May 29 '18 at 16:49