Author Topic: Is it possible to support plugins contained in their own folders?  (Read 408 times)

deviluke

  • Newbie
  • *
  • Posts: 8
So I'm making a plugin and after build I get my own DLL and then about 24 other DLLs my plugin depends on.
I tried creating a folder for the plugin the usual place MB looks for plugins, but nothing gets picked up? Even the .zip option gets me "Can't find any .dll's".

Polluting users plugin folders with 24 unrelated DLLs seems kinda excessive?
I made a template for making MusicBee plugins.
Some conveniences have been added and should allow you to start developing right away.
https://github.com/iSplasher/musicbee-plugin

Steven

  • Administrator
  • Sr. Member
  • *****
  • Posts: 34369
the mb_plugin.dll file needs to be in the root Plugins folder but any associated files could be put in a separate sub-folder

deviluke

  • Newbie
  • *
  • Posts: 8
Thanks a lot for the response!
I'm quite new to .NET and having to deal with DLLs,. My understanding tells me those DLLs my plugin depends on need to be in the same folder. I'll try to look up if splitting the DLLs like that is possible, if not then I guess that's too bad!
Again, thanks a lot for your reply!
I made a template for making MusicBee plugins.
Some conveniences have been added and should allow you to start developing right away.
https://github.com/iSplasher/musicbee-plugin

deviluke

  • Newbie
  • *
  • Posts: 8
I managed to load DLLs put in a different folder with this snippet:

Code
public sealed class SetupDLLDependencies
    {

        static string DLLDirectory = "";

        static public void Run(string dependencyDirName)
        {
            DLLDirectory = Path.Combine(AppContext.BaseDirectory, dependencyDirName);

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveAssembly;
            AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
        }

        static private Assembly ResolveAssembly(Object sender, ResolveEventArgs e)
        {
            string name = Path.Combine(DLLDirectory, $"{e.Name.Split(',')[0]}.dll");
            Assembly res = null;
            try
            {
                res = System.Reflection.Assembly.LoadFile(name);
            }
            catch (Exception)
            {
                Console.WriteLine($"Failed to load {name}");
            }
            return res;
        }
    }

It works when I use the plugin DLL in an accompanying console app that I made, but I still can't get it to load in MB.
MB is able to detect it, but when I click Enable, I get the unhelpful message:
'Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information.'

Is it possible to include the retrieved info from LoaderExceptions  in this popup?

I'm of course able to load the plugin if I don't call any of the code requiring any dependencies, but that's basically all of my code (outside of MusicBeePlugin.Plugin setup)

EDIT:
I checked ErrorLog.dat, and only found this (it's in Danish but shouldn't be an issue)
Code
15-12-2023 15:33:20 - 10.0.22631.0 - 3.5.8698.34385D - System.Reflection.ReflectionTypeLoadException: En eller flere af de anmodede typer kan ikke indlæses. Hent egenskaben LoaderExceptions for at få flere oplysninger.
   ved System.Reflection.RuntimeModule.GetTypes(RuntimeModule module)
   ved System.Reflection.RuntimeModule.GetTypes()
   ved System.Reflection.Assembly.GetTypes()
   ved #=zu4Zx8AwXW_Fbz0RgnA==..ctor(#=zOPAFmqxyWq4nEsXBtA== #=zogaPBWQ=)

EDIT2:
I think I get what's going on now.
MB seems to call
Code
System.Reflection.Assembly.GetTypes()
before the snippet above gets to run. I tested this in the console app by trying to call Assembly.GetTypes() before and after the snippet ran. Obviously, it worked only if the snippet ran first.
Next, I tried to look for ways to get it to execute before MB uses Assembly.GetTypes(). So I changed the snippet to the following, in an attempt to get the code to execute upon load:
Code
public sealed class LibraryEntryPoint
    {

        static string DLLDirectory = "";

        // This static constructor will be called when the DLL is loaded
        static LibraryEntryPoint()
        {
            Assembly thisAssem = typeof(LibraryEntryPoint).Assembly;
            // Setup DLL dependencies
            SetupDllDependencies(thisAssem.GetCustomAttribute<AssemblyTitleAttribute>().Title);
        }

        static public void SetupDllDependencies(string dependencyDirName)
        {
            DLLDirectory = Path.Combine(AppContext.BaseDirectory, dependencyDirName);

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveAssembly;
            AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
        }

        static private Assembly ResolveAssembly(Object sender, ResolveEventArgs e)
        {
            string name = Path.Combine(DLLDirectory, $"{e.Name.Split(',')[0]}.dll");
            Assembly res = null;
            try
            {
                res = System.Reflection.Assembly.LoadFile(name);
            }
            catch (Exception)
            {
                Console.WriteLine($"Failed to load {name}");
            }
            return res;
        }
    }

It didn't work.
With no way to know how MB loads plugins, I think I might be on an impasse. There seems to be no way to be no way to ensure dependencies are loaded before MB uses Assembly.GetTypes(). And I guess I understand that you need to GetTypes() to check if the required types are there?

EDIT 3: It works
Disregard what I mentioned in the previous edit. I got it to work. The error I got was due to a wrong dll folder being set.
This is the correct code:
Code
public sealed class LibraryEntryPoint
    {

        static string DLLDirectory = "";

        // This static constructor will be called when the DLL is loaded
        static LibraryEntryPoint()
        {
            Assembly thisAssem = typeof(LibraryEntryPoint).Assembly;
            // Setup DLL dependencies
            string libFolder = Path.GetDirectoryName(thisAssem.Location);
            string libDepFolder = Path.Combine(libFolder, thisAssem.GetCustomAttribute<AssemblyTitleAttribute>().Title);
            SetupDllDependencies(libDepFolder);
        }

        static public void SetupDllDependencies(string dependencyDirPath)
        {
            DLLDirectory = dependencyDirPath;

            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += ResolveAssembly;
            AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
        }

        static private Assembly ResolveAssembly(Object sender, ResolveEventArgs e)
        {
            string name = Path.Combine(DLLDirectory, $"{e.Name.Split(',')[0]}.dll");
            Assembly res = null;
            try
            {
                res = System.Reflection.Assembly.LoadFile(name);
            }
            catch (Exception)
            {
                Console.WriteLine($"Failed to load {name}");
            }
            return res;
        }
    }

Also remember to add this class to the Plugin class so the code gets triggered:
Code
namespace MusicBeePlugin
{

    public partial class Plugin
    {
        // Required so entrypoint can be called
        static private LibraryEntryPoint entryPoint = new LibraryEntryPoint();
   
        //...
     }
}
Or you can just move everything in LibraryEntryPoint to Plugin.
Well, everything works, so my issue has been resolved!
Last Edit: December 16, 2023, 12:27:21 AM by deviluke
I made a template for making MusicBee plugins.
Some conveniences have been added and should allow you to start developing right away.
https://github.com/iSplasher/musicbee-plugin