Skip to content

Commit 398c959

Browse files
committed
Load plugins from Plugins directory, refine publish command args
1 parent 8b895b8 commit 398c959

File tree

10 files changed

+100
-46
lines changed

10 files changed

+100
-46
lines changed

docs-src/commands.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,14 @@ The **InEngine.Core** plugin's command to clear the InEngine.NET queues produces
219219
InEngine 3.x
220220
Copyright © 2017 Ethan Hann
221221
222-
--command-plugin Required. The name of a command plugin file, e.g.
222+
--plugin Required. The name of a command plugin file, e.g.
223223
InEngine.Core
224224
225-
--command-verb A plugin command verb, e.g. echo
225+
--command A command name, e.g. echo
226226
227-
--command-class A command class name, e.g.
227+
--class A command class, e.g.
228228
InEngine.Core.Commands.AlwaysSucceed. Takes precedence
229-
over --command-verb if both are specified.
229+
over --command if both are specified.
230230
231231
--args An optional list of arguments to publish with the
232232
command.

docs-src/configuration.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ The **-c, --configuration** argument can also be used to specify an alternate co
77
```json
88
{
99
"InEngine": {
10-
"Plugins": [
11-
"path/to/MyCommandPlugin"
12-
],
10+
"Plugins": {
11+
"MyPlugin": "/path/to/plugin/assembly"
12+
},
1313
"ExecWhitelist": {
1414
"foo": "/path/to/foo.exe"
1515
},
@@ -37,10 +37,10 @@ The **-c, --configuration** argument can also be used to specify an alternate co
3737

3838
## Top-level Settings
3939

40-
| Setting | Type | Description |
41-
| ------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------- |
42-
| Plugins | array of strings | A list of paths of plugin assemblies, with ".dll" omitted from the assembly name. |
43-
| ExecWhitelist | object | A set of key/value pairs, where the value is the file system path of an executable and the key is a command alias. |
40+
| Setting | Type | Description |
41+
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
42+
| Plugins | object | A set of key/value pairs, where the value is the directory where the plugin is located and the key is the plugin name sans .dll extension. |
43+
| ExecWhitelist | object | A set of key/value pairs, where the value is the file system path of an executable and the key is a command alias. |
4444

4545

4646
## Mail Settings

docs-src/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Want to queue our example echo command to run in the background or possibly on a
3737
Use the core plugin's **queue:publish** command:
3838

3939
```bash
40-
inengine.exe queue:publish --command-plugin=InEngine.Core --command-verb=echo --args "text=Hello, world"
40+
inengine.exe queue:publish --plugin=InEngine.Core --command=echo --args "text=Hello, world"
4141
```
4242

4343
How do we consume that queued echo command?
@@ -86,5 +86,5 @@ It opens up the possibility of running shell scripts, ETLs, Java programs, etc.
8686
The example python script can be queued:
8787

8888
```bash
89-
inengine queue:publish --command-plugin=InEngine.Core --command-verb=exec --args="executable=helloworld"
89+
inengine queue:publish --plugin=InEngine.Core --command=exec --args="executable=helloworld"
9090
```

docs-src/queuing.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ Enqueue.Command(() => Console.WriteLine("Hello, world!"))
9898
The **exec** command allows for external programs to be executed.
9999

100100
```bash
101-
inengine queue:publish --command-plugin=InEngine.Core --command-verb=exe --args="command=/usr/bin/python" "args=--version"
101+
inengine queue:publish --plugin=InEngine.Core --command=exe --args="command=/usr/bin/python" "args=--version"
102102
```
103103

104104
!!! note "Do not include "--" for the command and args parameters."
@@ -171,19 +171,19 @@ Note that all queue commands reside in the **InEngine.Core** plugin.
171171
This is an example of how to publish a command from the CLI by specifying the command's plugin, class name, and arguments:
172172

173173
```bash
174-
inengine.exe queue:publish --command-plugin=MyCommandPlugin --command-class=MyCommand --args "text=bar"
174+
inengine.exe queue:publish --plugin=MyCommandPlugin --class=MyCommand --args "text=bar"
175175
```
176176

177177
There is an "Echo" command in the *InEngine.Core* package. It is useful for end-to-end testing with the queue feature.
178178

179179
```bash
180-
inengine.exe queue:publish --command-plugin=InEngine.Core --command-class=InEngine.Core.Commands.Echo --args "text=foo"
180+
inengine.exe queue:publish --plugin=InEngine.Core --class=InEngine.Core.Commands.Echo --args "text=foo"
181181
```
182182

183183
The command verb can also be specified instead of the full class name:
184184

185185
```bash
186-
inengine.exe queue:publish --command-plugin=InEngine.Core --command-verb=echo--args "text=foo"
186+
inengine.exe queue:publish --plugin=InEngine.Core --command=echo--args "text=foo"
187187
```
188188

189189
## Consuming Commands
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
3+
namespace InEngine.Core.Exceptions
4+
{
5+
public class PluginNotRegisteredException : Exception
6+
{
7+
public PluginNotRegisteredException(string message = "") : base(message)
8+
{}
9+
10+
public PluginNotRegisteredException(string message, Exception innerException) : base(message, innerException)
11+
{}
12+
}
13+
}

src/InEngine.Core/InEngineSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public class InEngineSettings
1212
{
1313
public static string BasePath { get; set; } = Directory.GetCurrentDirectory();
1414
public static string ConfigurationFile { get; set; } = "appsettings.json";
15-
public List<string> Plugins { get; set; } = new List<string>();
15+
public IDictionary<string, string> Plugins { get; set; } = new Dictionary<string, string>();
1616
public QueueSettings Queue { get; set; } = new QueueSettings();
1717
public MailSettings Mail { get; set; } = new MailSettings();
1818
public IDictionary<string, string> ExecWhitelist { get; set; } = new Dictionary<string, string>();

src/InEngine.Core/PluginAssembly.cs

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Reflection;
6+
using System.Threading;
67
using CommandLine;
78
using InEngine.Core.Exceptions;
89

@@ -14,50 +15,48 @@ public class PluginAssembly
1415
public string Name { get { return Assembly.GetName().Name; } }
1516
public string Version { get { return Assembly.GetName().Version.ToString(); } }
1617
public List<AbstractPlugin> Plugins { get; set; }
18+
public volatile static bool isAssemblyResolverRegistered = false;
19+
public static Mutex assemblyResolverLock = new Mutex();
20+
1721
public PluginAssembly(Assembly assembly)
1822
{
1923
Assembly = assembly;
2024
Plugins = Make<AbstractPlugin>();
2125
}
2226

23-
public static PluginAssembly LoadFrom(string assemblyPath)
27+
public static PluginAssembly LoadFrom(string pluginName)
2428
{
25-
var path = Path.Combine(InEngineSettings.BasePath, assemblyPath);
29+
RegisterPluginAssemblyResolver();
30+
var path = MakeFullPluginAssemblyPath(pluginName);
2631
try
2732
{
28-
return new PluginAssembly(Assembly.LoadFrom(path));
33+
return new PluginAssembly(Assembly.LoadFrom(path));
2934
}
3035
catch (Exception exception)
3136
{
3237
throw new PluginNotFoundException($"Plugin not found at {path}", exception);
3338
}
3439
}
3540

36-
public List<T> Make<T>() where T : class, IPlugin
37-
{
38-
return Assembly
39-
.GetTypes()
40-
.Where(x => x.IsClass && typeof(T).IsAssignableFrom(x) && !x.IsAbstract)
41-
.Select(x => Assembly.CreateInstance(x.FullName) as T)
42-
.ToList();
43-
}
44-
45-
public static List<PluginAssembly> Load<T>() where T : IPlugin
41+
public static List<PluginAssembly> Load<T>(bool shouldLoadCorePlugin = true) where T : IPlugin
4642
{
43+
RegisterPluginAssemblyResolver();
4744
var pluginList = new List<PluginAssembly>();
45+
4846
try
4947
{
50-
pluginList.Add(new PluginAssembly(Assembly.GetExecutingAssembly()));
51-
}
48+
if (shouldLoadCorePlugin)
49+
pluginList.Add(new PluginAssembly(Assembly.GetExecutingAssembly()));
50+
}
5251
catch (Exception exception)
5352
{
5453
throw new PluginNotFoundException("Could not load InEngine.Core plugin.", exception);
5554
}
56-
55+
5756
var assemblies = InEngineSettings
5857
.Make()
5958
.Plugins
60-
.Select(x => Assembly.LoadFrom($"{x}.dll"));
59+
.Select(x => Assembly.LoadFrom(Path.Combine(x.Value, $"{x.Key}.dll")));
6160
foreach (var assembly in assemblies)
6261
{
6362
try
@@ -75,6 +74,48 @@ public static List<PluginAssembly> Load<T>() where T : IPlugin
7574
return pluginList.OrderBy(x => x.Name).ToList();
7675
}
7776

77+
public static void RegisterPluginAssemblyResolver()
78+
{
79+
assemblyResolverLock.WaitOne();
80+
if (!isAssemblyResolverRegistered) {
81+
isAssemblyResolverRegistered = true;
82+
AppDomain.CurrentDomain.AssemblyResolve += LoadPluginEventHandler;
83+
}
84+
assemblyResolverLock.ReleaseMutex();
85+
}
86+
87+
static Assembly LoadPluginEventHandler(object sender, ResolveEventArgs args)
88+
{
89+
var assemblyName = new AssemblyName(args.Name).Name;
90+
var pluginName = assemblyName.Substring(0, assemblyName.Length - 4);
91+
var assemblyPath = MakeFullPluginAssemblyPath(pluginName);
92+
if (!File.Exists(assemblyPath))
93+
return null;
94+
return Assembly.LoadFrom(assemblyPath);
95+
}
96+
97+
static string MakeFullPluginAssemblyPath(string pluginName)
98+
{
99+
var plugins = InEngineSettings.Make().Plugins;
100+
if (!plugins.ContainsKey(pluginName))
101+
throw new PluginNotRegisteredException(pluginName);
102+
103+
return Path.Combine(
104+
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
105+
plugins[pluginName],
106+
$"{pluginName}.dll"
107+
);
108+
}
109+
110+
public List<T> Make<T>() where T : class, IPlugin
111+
{
112+
return Assembly
113+
.GetTypes()
114+
.Where(x => x.IsClass && typeof(T).IsAssignableFrom(x) && !x.IsAbstract)
115+
.Select(x => Assembly.CreateInstance(x.FullName) as T)
116+
.ToList();
117+
}
118+
78119
public AbstractCommand CreateCommandFromClass(string fullCommandName)
79120
{
80121
return Assembly.CreateInstance(fullCommandName) as AbstractCommand;
@@ -96,7 +137,7 @@ public AbstractCommand CreateCommandFromVerb(string verbName)
96137
throw new AmbiguousCommandException(verbName);
97138
if (commandCount == 0)
98139
throw new CommandNotFoundException(verbName);
99-
return Assembly.CreateInstance(commandClassNames.First()) as AbstractCommand;
140+
return Assembly.CreateInstance(commandClassNames.First()) as AbstractCommand;
100141
}
101142
}
102143
}

src/InEngine.Core/Queuing/Commands/Publish.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ namespace InEngine.Core.Queuing.Commands
77
{
88
public class Publish : AbstractCommand
99
{
10-
[Option("command-plugin", Required = true, HelpText = "The name of a command plugin file, e.g. InEngine.Core")]
11-
public string CommandPlugin { get; set; }
10+
[Option("plugin", Required = true, HelpText = "The name of a command plugin file, e.g. InEngine.Core")]
11+
public string PluginName { get; set; }
1212

13-
[Option("command-verb", HelpText = "A plugin command verb, e.g. echo.")]
13+
[Option("command", HelpText = "A command name, e.g. echo.")]
1414
public string CommandVerb { get; set; }
1515

16-
[Option("command-class", HelpText = "A command class name, e.g. InEngine.Core.Commands.AlwaysSucceed. Takes precedence over --command-verb if both are specified.")]
16+
[Option("class", HelpText = "A command class, e.g. InEngine.Core.Commands.AlwaysSucceed. Takes precedence over --command if both are specified.")]
1717
public string CommandClass { get; set; }
1818

1919
[OptionArray("args", HelpText = "An optional list of arguments to publish with the command.")]
@@ -29,8 +29,8 @@ public override void Run()
2929
{
3030
var command = Command;
3131

32-
if (command == null && !string.IsNullOrWhiteSpace(CommandPlugin)) {
33-
var plugin = PluginAssembly.LoadFrom($"{CommandPlugin}.dll");
32+
if (command == null && !string.IsNullOrWhiteSpace(PluginName)) {
33+
var plugin = PluginAssembly.LoadFrom(PluginName);
3434
if (!string.IsNullOrWhiteSpace(CommandClass))
3535
command = plugin.CreateCommandFromClass(CommandClass);
3636
else if (!string.IsNullOrWhiteSpace(CommandVerb)) {

src/InEngine.Core/Queuing/QueueAdapter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Reflection;
34
using InEngine.Core.Exceptions;
45
using InEngine.Core.Queuing.Clients;
56
using InEngine.Core.Queuing.Message;

src/InEngine/appsettings.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
22
"InEngine": {
3-
"Plugins": [
4-
"InEngine.Commands"
5-
],
3+
"Plugins": {
4+
},
65
"ExecWhitelist": {
76
},
87
"Mail": {
@@ -14,7 +13,7 @@
1413
"UseCompression": false,
1514
"PrimaryQueueConsumers": 16,
1615
"SecondaryQueueConsumers": 4,
17-
"QueueDriver": "redis",
16+
"QueueDriver": "file",
1817
"QueueName": "InEngineQueue",
1918
"RedisHost": "localhost",
2019
"RedisPort": 6379,

0 commit comments

Comments
 (0)