lformella / xdcc-grabscher

XG is an IRC Downloadmanager written in C#.
http://www.larsformella.de/lang/en/portfolio/programme-software/xg
65 stars 50 forks source link

Plugins via reflection or configuration #50

Open scottc opened 10 years ago

scottc commented 10 years ago

Horriblesubs does not allow for packlist command, so I wrote this plugin and it works nicely.

However it would be nice to have it in a seperate project that would automatically get detected and called if I drop the dll into the bin folder via reflection or configuration file. Rather then having to modify the Application project and referencing the dll and then adding the line of code app.AddWorker(new Plugin.Import.XDCCParser.Plugin());.

If you are curious about the plugin:

using System;
using System.Net;
using log4net;
using XG.Model.Domain;
using XG.Config.Properties;
using System.IO;

namespace XG.Plugin.Import.XDCCParser
{
    /// <summary>
    /// XDCC-Parser Plugin, to download packagelist from XDCC-Parser Web Server url.
    /// </summary>
    public class Plugin : APlugin
    {
        static readonly ILog _log = LogManager.GetLogger(typeof(Plugin));
        private static string SERVER_STRING = "irc.rizon.net";
        private static string CHANNEL_STRING = "HorribleSubs";
        private static string XDCC_PARSER_SEARCH_URL = "http://xdcc.horriblesubs.info/search.php";

        private Channel _channel;

        protected override void StartRun()
        {
            _log.Info("XDCC Parser Parser Plugin loaded");

            Servers.Add(SERVER_STRING);
            Servers.Server(SERVER_STRING).AddChannel(CHANNEL_STRING);

            _channel = Servers.Server(SERVER_STRING).Channel(CHANNEL_STRING);

            ParsePackages(_channel, GetSource(XDCC_PARSER_SEARCH_URL));
        }

        /// <summary>
        /// Parses the packages.
        /// </summary>
        /// <param name="doc">Document.</param>
        /// <remarks>
        /// p.k[0] = {b:"CR-ARCHIVE|1080p", n:1, s:92, f:"[HorribleSubs] AIURA - 01 [1080p].mkv"};
        /// </remarks>
        private void ParsePackages(Channel channel, string str)
        {
            var delimiters = new[]{"p.k[", "] = {b:\"", "\", n:", ", s:", ", f:\"", "\"};", "\n"};

            var packstr = str.ToString().Split(delimiters, StringSplitOptions.RemoveEmptyEntries);

            for(var i = 0; i < packstr.Length; i += 5)
            {
                var botName = packstr[i + 1];
                var packId = int.Parse(packstr[i + 2]);
                var packSize = int.Parse(packstr[i + 3]) * 1024 * 1024;
                var packName = packstr [i + 4];
                channel.AddBot(new Bot{ Name = botName });
                channel.Bot(botName).AddPacket(new Packet{ Id = packId, Name = packName, Size =  packSize});
            }
        }

        /// <summary>
        /// Gets the source.
        /// </summary>
        /// <returns>The source.</returns>
        /// <param name="url">URL.</param>
        private string GetSource(string url)
        {
            var webRequest = HttpWebRequest.Create(url);
            webRequest.Method = "GET";
            var webResponse = webRequest.GetResponse();
            var stream = new StreamReader(webResponse.GetResponseStream());
            var pageSource = stream.ReadToEnd();
            webResponse.Close();
            return pageSource;
        }
    }
}
lformella commented 10 years ago

Great idea. I hardcoded everything because until now I created all the plugins :)

If you have an idea how to load 3rd party plugins and configs dynamically, feel free to fork the project and replace the hardcoded plugin configuration.

Would it be better to put user plugins into the user bin? There they are surviving reinstalls or new versions.

scottc commented 10 years ago

Did a bit of reading seems using MEF is the best way to go, it's a part of the .net 4.0 framework.

Managed Extensibility Framework (MEF) http://msdn.microsoft.com/en-us/library/dd460648%28v=vs.110%29.aspx

Created Fork https://github.com/scottc/xdcc-grabscher-MEF-Plugins

I'll send you a push request whenever I get it done.

scottc commented 10 years ago

Made my first attempt https://github.com/scottc/xdcc-grabscher-MEF-Plugins/commit/47f5ad74c0065167e9c6e26cbfa9f00d67652bfa, it compiles and runs, but it's not detecting the plugins inside the plugins folder for some reason.

scottc commented 10 years ago

Found the problem, plugins are loading correctly.

When using the metadata, you must fill out all the attributes mentioned on the interface, otherwise MEF will not import it.

[ImportMany]
IEnumerable<Lazy<IPlugin, IPluginMetaData>> _plugins;
public interface IPlugin
{
    void StartRun();
    void StopRun();
}
public interface IPluginMetaData
{
    string Name { get; }
    string Description { get; }
    string Version { get; }
    string Author { get; }
    string Website { get; }
}
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "ElasticSearch")]
[ExportMetadata("Description", "ElasticSearch")]
[ExportMetadata("Version", "1.0.0")]
[ExportMetadata("Author", "lformella")]
//[ExportMetadata("Website", "https://github.com/lformella/")] // will not work unless you include all 5 MetaData Attributes
public class Plugin : APlugin
{

But as you can see the meta data is quite handy.

foreach (var plugin in _plugins)
{
    Log.Info("Plugin '" + plugin.Metadata.Name + " " + plugin.Metadata.Version + "' Loaded");
    plugin.Value.StartRun();
}

Alternatively, we don't need to use metadata.

[ImportMany]
IEnumerable<IPlugin> _plugins;

Just something to be aware of if we are planning to use meta data.

I think the meta data will be helpful because we can provide the user some information about the plugin, even if it is not loaded. Before the user decides to enable it or not, like web browser plugins.

The Rrd and BotWatchdog plugins are assigned values, I'm not sure on the best way to pass db access to the plugins from the main application.

For the BotWatchdog worker, I have just made the property use the System.Settings class directly.

public override Int64 SecondsToSleep
{ 
    get
    { 
        return Settings.Default.BotOfflineCheckTime;
    }
    set
    {
        throw new NotImplementedException("Is readonly.");
    }
}

I also need to pass these:

Servers
Files
Searches
Notifications
ApiKeys

For the Rrd Worker, I'm not sure on the best way to pass an instance of RrdDB to the plugin.

lformella commented 10 years ago

You can ignore the plugins which extend the LoopPlugin class. I created a branch to use the Quartz-Scheduler to do tasks which have to run an sleep for a while: https://github.com/lformella/xdcc-grabscher/blob/quartz_scheduler/XG.Business/Job/BotWatchdog.cs https://github.com/lformella/xdcc-grabscher/blob/quartz_scheduler/XG.Business/Job/Rrd.cs These tasks fit a quartz job better than a real XG plugin.

Great you found a way to decouple plugins from the app. I like the use of the Metadata to be able have a description what the plugin does. What to do with values you have to fill out? Like jabber server informations? Can these values and their descriptions also exported via metadata? Something like this would be great:

[Export(typeof(IPlugin))]
[ExportMetadata("Name", "ElasticSearch")]
[ExportMetadata("Description", "ElasticSearch")]
[ExportMetadata("Version", "1.0.0")]
[ExportMetadata("Value.Server", "The server of the ES instance")]
[ExportMetadata("Value.Port", "The port of the ES instance")]

All values should be editable in the web gui and should be stored somewhere in the config. A way to validate user data is usefull too. A plugin should be able to describe the type of the value and controll the validation in the webgui. like:

[ExportMetadata("Value.Port", "The port of the ES instance")]
[ExportMetadata("Value.Port.Type", "Int")]
[ExportMetadata("Value.Port.Necessary", "True")]
[ExportMetadata("Value.Port.Min", "1")]
[ExportMetadata("Value.Port.Max", "65535")]
scottc commented 10 years ago

I believe there are other attributes for that purpose, like the configuration and validation ones. http://msdn.microsoft.com/en-us/library/system.configuration.configurationpropertyattribute%28v=vs.110%29.aspx http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.validationattribute%28v=vs.110%29.aspx

[ConfigurationProperty("url", DefaultValue = "http://www.contoso.com", IsRequired = true)]
[RegexStringValidator(@"\w+:\/\/[\w.]+\S*")]
public string Url
{
    get
    {
        return (string)this["url"];
    }
    set
    {
        this["url"] = value;
    }
}

[ConfigurationProperty("port", DefaultValue = (int)0, IsRequired = false)]
[IntegerValidator(MinValue = 0, MaxValue = 8080, ExcludeRange = false)]
public int Port
{
    get
    {
        return (int)this["port"];
    }
    set
    {
        this["port"] = value;
    }
}

Plugins could probably define their own strictly typed configuration sections, in the same or seperate config files. http://msdn.microsoft.com/en-us/library/system.configuration.configurationsection%28v=vs.110%29.aspx

The idea is to have a consistant interface that any plugin can use, so rather then specifying concrete configuration attributes directly in the meta data, we would be better off just specifying an abstract type.

public interface IPlugin
{
    void StartRun();
    void StopRun();
    ConfigurationSection Config { get; set; }
}

or better yet define it seperately, so configs are not mandatory but you can still decorate the plugin class with a configurable interface.

public interface IConfigurable<TConfigurationSection> where TConfigurationSection : ConfigurationSection
{
    TConfigurationSection Config { get; set; }
}
public abstract class AWorker : ANotificationSender, IPlugin, IConfigurable<AConfigSection>
{
var configurablePlugin = (new MyPlugin() as IPlugin) as IConfigurable;  
if (configurablePlugin != null)
{
    var foo = configurablePlugin.Config.ToString();
}
else
{
    //throw new Exception("This plugin does not support configuration.");
}

Or something like that, it's late and i need sleep.

The purpose of the metadata attributes is to provide generic information regarding the plugin, before it is loaded. In theory you could create many Interfaces, or extend the ExportMetadata attribute or even create your own attribute types, but I think it's outside of the scope which the MEF is trying to provide. Best of thinking of them, like ID3 tags on mp3 files.

I can't think of a good reason why you would need to access the config settings, if you are not going to load the plugin. However you can do it like this:

[ExportMetadata("Configuration", MyConfig )]

The ExportMetadata is also just a IDictionary<string, Object>, so you specify other values then just strings.

I believe it is baked into the dll as type information, so you can only specify data which is availible at compile time.

However i may be wrong.