diff --git a/Torch.API/Managers/IPluginManager.cs b/Torch.API/Managers/IPluginManager.cs
index 28caf87..2028e42 100644
--- a/Torch.API/Managers/IPluginManager.cs
+++ b/Torch.API/Managers/IPluginManager.cs
@@ -14,12 +14,12 @@ namespace Torch.API.Managers
///
/// Fired when plugins are loaded.
///
- event Action> PluginsLoaded;
+ event Action> PluginsLoaded;
///
/// Collection of loaded plugins.
///
- IList Plugins { get; }
+ IDictionary Plugins { get; }
///
/// Updates all loaded plugins.
diff --git a/Torch.API/Plugins/ITorchPlugin.cs b/Torch.API/Plugins/ITorchPlugin.cs
index 3e8f9aa..35a34df 100644
--- a/Torch.API/Plugins/ITorchPlugin.cs
+++ b/Torch.API/Plugins/ITorchPlugin.cs
@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
///
/// The version of the plugin.
///
- Version Version { get; }
+ string Version { get; }
///
/// The name of the plugin.
diff --git a/Torch.API/Plugins/PluginAttribute.cs b/Torch.API/Plugins/PluginAttribute.cs
index 7b60e32..4142ce3 100644
--- a/Torch.API/Plugins/PluginAttribute.cs
+++ b/Torch.API/Plugins/PluginAttribute.cs
@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
///
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
///
+ [Obsolete]
[AttributeUsage(AttributeTargets.Class)]
public class PluginAttribute : Attribute
{
diff --git a/Torch.Server/ViewModels/PluginManagerViewModel.cs b/Torch.Server/ViewModels/PluginManagerViewModel.cs
index ed43547..ec87acb 100644
--- a/Torch.Server/ViewModels/PluginManagerViewModel.cs
+++ b/Torch.Server/ViewModels/PluginManagerViewModel.cs
@@ -29,7 +29,7 @@ namespace Torch.Server.ViewModels
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
}
- private void PluginManager_PluginsLoaded(IList obj)
+ private void PluginManager_PluginsLoaded(ICollection obj)
{
Plugins.Clear();
foreach (var plugin in obj)
diff --git a/Torch/DispatcherExtensions.cs b/Torch/Extensions/DispatcherExtensions.cs
similarity index 100%
rename from Torch/DispatcherExtensions.cs
rename to Torch/Extensions/DispatcherExtensions.cs
diff --git a/Torch/Extensions/StringExtensions.cs b/Torch/Extensions/StringExtensions.cs
index 29c8a60..10e86d2 100644
--- a/Torch/Extensions/StringExtensions.cs
+++ b/Torch/Extensions/StringExtensions.cs
@@ -2,51 +2,21 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Torch
{
public static class StringExtensions
{
- public static string Truncate(this string s, int maxLength)
+ ///
+ /// Try to extract a 3 component version from the string. Format: #.#.#
+ ///
+ public static bool TryExtractVersion(this string version, out Version result)
{
- return s.Length <= maxLength ? s : s.Substring(0, maxLength);
- }
-
- public static IEnumerable ReadLines(this string s, int max, bool skipEmpty = false, char delim = '\n')
- {
- var lines = s.Split(delim);
-
- for (var i = 0; i < lines.Length && i < max; i++)
- {
- var l = lines[i];
- if (skipEmpty && string.IsNullOrWhiteSpace(l))
- continue;
-
- yield return l;
- }
- }
-
- public static string Wrap(this string s, int lineLength)
- {
- if (s.Length <= lineLength)
- return s;
-
- var result = new StringBuilder();
- for (var i = 0; i < s.Length;)
- {
- var next = i + lineLength;
- if (s.Length - 1 < next)
- {
- result.AppendLine(s.Substring(i));
- break;
- }
-
- result.AppendLine(s.Substring(i, next));
- i = next;
- }
-
- return result.ToString();
+ result = null;
+ var match = Regex.Match(version, @"(\d+\.)?(\d+\.)?(\d+)");
+ return match.Success && Version.TryParse(match.Value, out result);
}
}
}
diff --git a/Torch/Managers/PluginManager.cs b/Torch/Managers/PluginManager.cs
index 3f0108d..4f699a0 100644
--- a/Torch/Managers/PluginManager.cs
+++ b/Torch/Managers/PluginManager.cs
@@ -1,34 +1,37 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
+using System.IO.Compression;
using System.Linq;
+using System.Net;
using System.Reflection;
using System.Threading.Tasks;
+using System.Xml.Serialization;
using NLog;
+using Octokit;
using Torch.API;
using Torch.API.Managers;
using Torch.API.Plugins;
+using Torch.Collections;
using Torch.Commands;
-using VRage.Collections;
namespace Torch.Managers
{
///
public class PluginManager : Manager, IPluginManager
{
+ private GitHubClient _gitClient = new GitHubClient(new ProductHeaderValue("Torch"));
private static Logger _log = LogManager.GetLogger(nameof(PluginManager));
+ private const string MANIFEST_NAME = "manifest.xml";
public readonly string PluginDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
[Dependency]
- private UpdateManager _updateManager;
- [Dependency]
private CommandManager _commandManager;
///
- public IList Plugins { get; } = new ObservableList();
+ public IDictionary Plugins { get; } = new ObservableDictionary();
- public event Action> PluginsLoaded;
+ public event Action> PluginsLoaded;
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
{
@@ -41,7 +44,7 @@ namespace Torch.Managers
///
public void UpdatePlugins()
{
- foreach (var plugin in Plugins)
+ foreach (var plugin in Plugins.Values)
plugin.Update();
}
@@ -50,94 +53,257 @@ namespace Torch.Managers
///
public override void Detach()
{
- foreach (var plugin in Plugins)
+ foreach (var plugin in Plugins.Values)
plugin.Dispose();
Plugins.Clear();
}
- private void DownloadPlugins()
+ public void LoadPlugins()
{
- var folders = Directory.GetDirectories(PluginDir);
- var taskList = new List();
-
- //Copy list because we don't want to modify the config.
- var toDownload = Torch.Config.Plugins.ToList();
-
- foreach (var folder in folders)
+ DownloadPluginUpdates();
+ _log.Info("Loading plugins...");
+ var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
+ foreach (var item in pluginItems)
{
- var manifestPath = Path.Combine(folder, "manifest.xml");
- if (!File.Exists(manifestPath))
+ var path = Path.Combine(PluginDir, item);
+ var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
+ var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
+ if (manifest == null)
{
- _log.Debug($"No manifest in {folder}, skipping");
+ _log.Warn($"Item '{item}' is missing a manifest, skipping.");
continue;
}
- var manifest = PluginManifest.Load(manifestPath);
- toDownload.RemoveAll(x => string.Compare(manifest.Repository, x, StringComparison.InvariantCultureIgnoreCase) == 0);
- taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
+ if (Plugins.ContainsKey(manifest.Guid))
+ {
+ _log.Error($"The GUID provided by {manifest.Name} ({item}) is already in use by {Plugins[manifest.Guid].Name}");
+ continue;
+ }
+
+ if (isZip)
+ LoadPluginFromZip(path);
+ else
+ LoadPluginFromFolder(path);
}
- foreach (var repository in toDownload)
- {
- var manifest = new PluginManifest {Repository = repository, Version = "0.0"};
- taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest));
- }
-
- Task.WaitAll(taskList.ToArray());
+ Plugins.ForEach(x => x.Value.Init(Torch));
+ _log.Info($"Loaded {Plugins.Count} plugins.");
+ PluginsLoaded?.Invoke(Plugins.Values);
}
- ///
- public void LoadPlugins()
+ private void DownloadPluginUpdates()
{
- if (Torch.Config.ShouldUpdatePlugins)
- DownloadPlugins();
- else
- _log.Warn("Automatic plugin updates are disabled.");
-
- _log.Info("Loading plugins");
- var dlls = Directory.GetFiles(PluginDir, "*.dll", SearchOption.AllDirectories);
- foreach (var dllPath in dlls)
+ _log.Info("Checking for plugin updates...");
+ var count = 0;
+ var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir));
+ Parallel.ForEach(pluginItems, async item =>
{
- _log.Debug($"Loading plugin {dllPath}");
- var asm = Assembly.UnsafeLoadFrom(dllPath);
-
- foreach (var type in asm.GetExportedTypes())
+ var path = Path.Combine(PluginDir, item);
+ var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase);
+ var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path);
+ if (manifest == null)
{
- if (type.GetInterfaces().Contains(typeof(ITorchPlugin)))
+ _log.Warn($"Item '{item}' is missing a manifest, skipping update check.");
+ return;
+ }
+
+ manifest.Version.TryExtractVersion(out Version currentVersion);
+ var latest = await GetLatestArchiveAsync(manifest.Repository).ConfigureAwait(false);
+
+ if (currentVersion == null || latest.Item1 == null)
+ {
+ _log.Error($"Error parsing version from manifest or GitHub for plugin '{manifest.Name}.'");
+ return;
+ }
+
+ if (latest.Item1 <= currentVersion)
+ return;
+
+ _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}.");
+ await UpdatePlugin(path, latest.Item2).ConfigureAwait(false);
+ count++;
+ });
+
+ _log.Info($"Updated {count} plugins.");
+ }
+
+ private async Task> GetLatestArchiveAsync(string repository)
+ {
+ try
+ {
+ var split = repository.Split('/');
+ var latest = await _gitClient.Repository.Release.GetLatest(split[0], split[1]).ConfigureAwait(false);
+ if (!latest.TagName.TryExtractVersion(out Version latestVersion))
+ {
+ _log.Error($"Unable to parse version tag for the latest release of '{repository}.'");
+ }
+
+ var zipAsset = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip", StringComparison.CurrentCultureIgnoreCase));
+ if (zipAsset == null)
+ {
+ _log.Error($"Unable to find archive for the latest release of '{repository}.'");
+ }
+
+ return new Tuple(latestVersion, zipAsset?.BrowserDownloadUrl);
+ }
+ catch (Exception e)
+ {
+ _log.Error($"Unable to get the latest release of '{repository}.'");
+ _log.Error(e);
+ return default(Tuple);
+ }
+ }
+
+ private Task UpdatePlugin(string localPath, string downloadUrl)
+ {
+ if (File.Exists(localPath))
+ File.Delete(localPath);
+
+ if (Directory.Exists(localPath))
+ Directory.Delete(localPath, true);
+
+ var fileName = downloadUrl.Split('/').Last();
+ var filePath = Path.Combine(PluginDir, fileName);
+
+ return new WebClient().DownloadFileTaskAsync(downloadUrl, filePath);
+ }
+
+ private void LoadPluginFromFolder(string directory)
+ {
+ var assemblies = new List();
+ var files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToList();
+
+ var manifest = GetManifestFromDirectory(directory);
+ if (manifest == null)
+ {
+ _log.Warn($"Directory {directory} is missing a manifest, skipping load.");
+ return;
+ }
+
+ foreach (var file in files)
+ {
+ if (!file.Contains(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = File.OpenRead(file))
+ {
+ var data = new byte[stream.Length];
+ stream.Read(data, 0, data.Length);
+ assemblies.Add(Assembly.Load(data));
+ }
+ }
+
+ InstantiatePlugin(manifest, assemblies);
+ }
+
+ private void LoadPluginFromZip(string path)
+ {
+ PluginManifest manifest;
+ var assemblies = new List();
+ using (var zipFile = ZipFile.OpenRead(path))
+ {
+ manifest = GetManifestFromZip(path);
+ if (manifest == null)
+ {
+ _log.Warn($"Zip file {path} is missing a manifest, skipping.");
+ return;
+ }
+
+ foreach (var entry in zipFile.Entries)
+ {
+ if (!entry.Name.Contains(".dll", StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = entry.Open())
{
- if (type.GetCustomAttribute() == null)
- continue;
-
- try
- {
- var plugin = (TorchPluginBase)Activator.CreateInstance(type);
- if (plugin.Id == default(Guid))
- throw new TypeLoadException($"Plugin '{type.FullName}' is missing a {nameof(PluginAttribute)}");
-
- _log.Info($"Loading plugin {plugin.Name} ({plugin.Version})");
- plugin.StoragePath = Torch.Config.InstancePath;
- Plugins.Add(plugin);
-
- _commandManager.RegisterPluginCommands(plugin);
- }
- catch (Exception e)
- {
- _log.Error($"Error loading plugin '{type.FullName}'");
- _log.Error(e);
- throw;
- }
+ var data = new byte[entry.Length];
+ stream.Read(data, 0, data.Length);
+ assemblies.Add(Assembly.Load(data));
}
}
}
- Plugins.ForEach(p => p.Init(Torch));
- PluginsLoaded?.Invoke(Plugins.ToList());
+ InstantiatePlugin(manifest, assemblies);
+ }
+
+ private PluginManifest GetManifestFromZip(string path)
+ {
+ using (var zipFile = ZipFile.OpenRead(path))
+ {
+ foreach (var entry in zipFile.Entries)
+ {
+ if (!entry.Name.Equals(MANIFEST_NAME, StringComparison.CurrentCultureIgnoreCase))
+ continue;
+
+ using (var stream = new StreamReader(entry.Open()))
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ var manifest = (PluginManifest)ser.Deserialize(stream);
+ return manifest;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private PluginManifest GetManifestFromDirectory(string directory)
+ {
+ var path = Path.Combine(directory, MANIFEST_NAME);
+ return !File.Exists(path) ? null : PluginManifest.Load(path);
+ }
+
+ private void InstantiatePlugin(PluginManifest manifest, IEnumerable assemblies)
+ {
+ Type pluginType = null;
+ foreach (var asm in assemblies)
+ {
+ foreach (var type in asm.GetExportedTypes())
+ {
+ if (!type.GetInterfaces().Contains(typeof(ITorchPlugin)))
+ continue;
+
+ if (pluginType != null)
+ {
+ _log.Error($"The plugin '{manifest.Name}' has multiple implementations of {nameof(ITorchPlugin)}.");
+ return;
+ }
+
+ pluginType = type;
+ }
+ }
+
+ if (pluginType == null)
+ {
+ _log.Error($"The plugin '{manifest.Name}' does not have an implementation of {nameof(ITorchPlugin)}.");
+ return;
+ }
+
+ // Backwards compatibility for PluginAttribute.
+ var pluginAttr = pluginType.GetCustomAttribute();
+ if (pluginAttr != null)
+ {
+ _log.Warn($"Plugin '{manifest.Name}' is using the obsolete {nameof(PluginAttribute)}, using info from attribute if necessary.");
+ manifest.Version = manifest.Version ?? pluginAttr.Version.ToString();
+ manifest.Name = manifest.Name ?? pluginAttr.Name;
+ if (manifest.Guid == default(Guid))
+ manifest.Guid = pluginAttr.Guid;
+ }
+
+ _log.Info($"Loading plugin '{manifest.Name}' ({manifest.Version})");
+ var plugin = (TorchPluginBase)Activator.CreateInstance(pluginType);
+
+ plugin.Manifest = manifest;
+ plugin.StoragePath = Torch.Config.InstancePath;
+ plugin.Torch = Torch;
+ Plugins.Add(manifest.Guid, plugin);
+ _commandManager.RegisterPluginCommands(plugin);
}
public IEnumerator GetEnumerator()
{
- return Plugins.GetEnumerator();
+ return Plugins.Values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
diff --git a/Torch/Managers/UpdateManager.cs b/Torch/Managers/UpdateManager.cs
index 02d0a2a..62b8e25 100644
--- a/Torch/Managers/UpdateManager.cs
+++ b/Torch/Managers/UpdateManager.cs
@@ -44,7 +44,7 @@ namespace Torch.Managers
CheckAndUpdateTorch();
}
- private async Task> GetLatestRelease(string owner, string name)
+ private async Task> TryGetLatestArchiveUrl(string owner, string name)
{
try
{
@@ -53,56 +53,17 @@ namespace Torch.Managers
return new Tuple(new Version(), null);
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
- var versionName = Regex.Match(latest.TagName, "(\\d+\\.)+\\d+").ToString();
- if (string.IsNullOrWhiteSpace(versionName))
- {
- _log.Warn("Unable to parse tag {0} for {1}/{2}", latest.TagName, owner, name);
- versionName = "0.0";
- }
- return new Tuple(new Version(versionName), zip?.BrowserDownloadUrl);
+ if (zip == null)
+ _log.Error($"Latest release of {owner}/{name} does not contain a zip archive.");
+ if (!latest.TagName.TryExtractVersion(out Version version))
+ _log.Error($"Unable to parse version tag for {owner}/{name}");
+ return new Tuple(version, zip?.BrowserDownloadUrl);
}
catch (Exception e)
{
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
_log.Error(e);
- return new Tuple(new Version(), null);
- }
- }
-
- public async Task CheckAndUpdatePlugin(PluginManifest manifest)
- {
- if (!Torch.Config.GetPluginUpdates)
- return;
-
- try
- {
- var name = manifest.Repository.Split('/');
- if (name.Length != 2)
- {
- _log.Error($"'{manifest.Repository}' is not a valid GitHub repository.");
- return;
- }
-
- var currentVersion = new Version(manifest.Version);
- var releaseInfo = await GetLatestRelease(name[0], name[1]).ConfigureAwait(false);
- if (releaseInfo.Item1 > currentVersion)
- {
- _log.Warn($"Updating {manifest.Repository} from version {currentVersion} to version {releaseInfo.Item1}");
- var updateName = Path.Combine(_fsManager.TempDirectory, $"{name[0]}_{name[1]}.zip");
- var updatePath = Path.Combine(_torchDir, "Plugins");
- await new WebClient().DownloadFileTaskAsync(new Uri(releaseInfo.Item2), updateName).ConfigureAwait(false);
- UpdateFromZip(updateName, updatePath);
- File.Delete(updateName);
- }
- else
- {
- _log.Info($"{manifest.Repository} is up to date. ({currentVersion})");
- }
- }
- catch (Exception e)
- {
- _log.Error($"An error occured downloading the plugin update for {manifest.Repository}.");
- _log.Error(e);
+ return default(Tuple);
}
}
@@ -116,7 +77,7 @@ namespace Torch.Managers
try
{
- var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
+ var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false);
if (releaseInfo.Item1 > Torch.TorchVersion)
{
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
diff --git a/Torch/PluginManifest.cs b/Torch/PluginManifest.cs
index 6f13cb4..8329cfc 100644
--- a/Torch/PluginManifest.cs
+++ b/Torch/PluginManifest.cs
@@ -10,8 +10,11 @@ namespace Torch
{
public class PluginManifest
{
- public string Repository { get; set; } = "Jimmacle/notarealrepo";
- public string Version { get; set; } = "1.0";
+ public string Name { get; set; }
+ public Guid Guid { get; set; }
+ public string Repository { get; set; }
+ public string Version { get; set; }
+ public List Dependencies { get; } = new List();
public void Save(string path)
{
@@ -26,9 +29,20 @@ namespace Torch
{
using (var f = File.OpenRead(path))
{
- var ser = new XmlSerializer(typeof(PluginManifest));
- return (PluginManifest)ser.Deserialize(f);
+ return Load(f);
}
}
+
+ public static PluginManifest Load(Stream stream)
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ return (PluginManifest)ser.Deserialize(stream);
+ }
+
+ public static PluginManifest Load(TextReader reader)
+ {
+ var ser = new XmlSerializer(typeof(PluginManifest));
+ return (PluginManifest)ser.Deserialize(reader);
+ }
}
}
diff --git a/Torch/Torch.csproj b/Torch/Torch.csproj
index 77002b8..751c5fc 100644
--- a/Torch/Torch.csproj
+++ b/Torch/Torch.csproj
@@ -154,7 +154,7 @@
-
+
diff --git a/Torch/TorchBase.cs b/Torch/TorchBase.cs
index 3a40a23..58d9f8f 100644
--- a/Torch/TorchBase.cs
+++ b/Torch/TorchBase.cs
@@ -257,7 +257,7 @@ namespace Torch
try { Console.Title = $"{Config.InstanceName} - Torch {TorchVersion}, SE {GameVersion}"; }
catch
{
- ///Running as service
+ //Running as service
}
#if DEBUG
diff --git a/Torch/TorchPluginBase.cs b/Torch/TorchPluginBase.cs
index 0bead82..64859d9 100644
--- a/Torch/TorchPluginBase.cs
+++ b/Torch/TorchPluginBase.cs
@@ -16,30 +16,11 @@ namespace Torch
public abstract class TorchPluginBase : ITorchPlugin
{
public string StoragePath { get; internal set; }
- public Guid Id { get; }
- public Version Version { get; }
- public string Name { get; }
- public ITorchBase Torch { get; private set; }
- private static readonly Logger _log = LogManager.GetLogger(nameof(TorchPluginBase));
-
- protected TorchPluginBase()
- {
- var type = GetType();
- var pluginInfo = type.GetCustomAttribute();
- if (pluginInfo == null)
- {
- _log.Warn($"Plugin {type.FullName} has no PluginAttribute");
- Name = type.FullName;
- Version = new Version(0, 0, 0, 0);
- Id = default(Guid);
- }
- else
- {
- Name = pluginInfo.Name;
- Version = pluginInfo.Version;
- Id = pluginInfo.Guid;
- }
- }
+ public PluginManifest Manifest { get; internal set; }
+ public Guid Id => Manifest.Guid;
+ public string Version => Manifest.Version;
+ public string Name => Manifest.Name;
+ public ITorchBase Torch { get; internal set; }
public virtual void Init(ITorchBase torch)
{