diff --git a/Torch.API/Managers/IPluginManager.cs b/Torch.API/Managers/IPluginManager.cs index 28caf87..2a2ee08 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; } + IReadOnlyDictionary 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..d8c1297 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("All plugin meta-information is now defined in the manifest.xml.")] [AttributeUsage(AttributeTargets.Class)] public class PluginAttribute : Attribute { diff --git a/Torch.Server/ViewModels/PluginManagerViewModel.cs b/Torch.Server/ViewModels/PluginManagerViewModel.cs index ed43547..d910cf8 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(IReadOnlyCollection obj) { Plugins.Clear(); foreach (var plugin in obj) diff --git a/Torch.Server/Views/PluginsControl.xaml b/Torch.Server/Views/PluginsControl.xaml index a9f343e..f0bc7a4 100644 --- a/Torch.Server/Views/PluginsControl.xaml +++ b/Torch.Server/Views/PluginsControl.xaml @@ -12,7 +12,7 @@ - + @@ -27,7 +27,7 @@ - public partial class PluginsControl : UserControl { + private ITorchServer _server; + private PluginManager _plugins; + public PluginsControl() { InitializeComponent(); @@ -31,8 +37,15 @@ namespace Torch.Server.Views public void BindServer(ITorchServer server) { - var pluginManager = new PluginManagerViewModel(server.Plugins); + _server = server; + _plugins = _server.Managers.GetManager(); + var pluginManager = new PluginManagerViewModel(_plugins); DataContext = pluginManager; } + + private void OpenFolder_OnClick(object sender, RoutedEventArgs e) + { + Process.Start("explorer.exe", _plugins.PluginDir); + } } } 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/ICollectionExtensions.cs b/Torch/Extensions/ICollectionExtensions.cs new file mode 100644 index 0000000..acc952c --- /dev/null +++ b/Torch/Extensions/ICollectionExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Torch +{ + public static class ICollectionExtensions + { + /// + /// Returns a read-only wrapped + /// + public static IReadOnlyCollection AsReadOnly(this ICollection source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + return source as IReadOnlyCollection ?? new ReadOnlyCollectionAdapter(source); + } + + /// + /// Returns a read-only wrapped + /// + public static IReadOnlyList AsReadOnly(this IList source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + return source as IReadOnlyList ?? new ReadOnlyCollection(source); + } + + /// + /// Returns a read-only wrapped + /// + public static IReadOnlyDictionary AsReadOnly(this IDictionary source) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + return source as IReadOnlyDictionary ?? new ReadOnlyDictionary(source); + } + + sealed class ReadOnlyCollectionAdapter : IReadOnlyCollection + { + private readonly ICollection _source; + + public ReadOnlyCollectionAdapter(ICollection source) + { + _source = source; + } + + public int Count => _source.Count; + public IEnumerator GetEnumerator() => _source.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + } +} \ No newline at end of file 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 deleted file mode 100644 index 3f0108d..0000000 --- a/Torch/Managers/PluginManager.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using NLog; -using Torch.API; -using Torch.API.Managers; -using Torch.API.Plugins; -using Torch.Commands; -using VRage.Collections; - -namespace Torch.Managers -{ - /// - public class PluginManager : Manager, IPluginManager - { - private static Logger _log = LogManager.GetLogger(nameof(PluginManager)); - 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 event Action> PluginsLoaded; - - public PluginManager(ITorchBase torchInstance) : base(torchInstance) - { - if (!Directory.Exists(PluginDir)) - Directory.CreateDirectory(PluginDir); - } - - /// - /// Updates loaded plugins in parallel. - /// - public void UpdatePlugins() - { - foreach (var plugin in Plugins) - plugin.Update(); - } - - /// - /// Unloads all plugins. - /// - public override void Detach() - { - foreach (var plugin in Plugins) - plugin.Dispose(); - - Plugins.Clear(); - } - - private void DownloadPlugins() - { - 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) - { - var manifestPath = Path.Combine(folder, "manifest.xml"); - if (!File.Exists(manifestPath)) - { - _log.Debug($"No manifest in {folder}, skipping"); - continue; - } - - var manifest = PluginManifest.Load(manifestPath); - toDownload.RemoveAll(x => string.Compare(manifest.Repository, x, StringComparison.InvariantCultureIgnoreCase) == 0); - taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest)); - } - - foreach (var repository in toDownload) - { - var manifest = new PluginManifest {Repository = repository, Version = "0.0"}; - taskList.Add(_updateManager.CheckAndUpdatePlugin(manifest)); - } - - Task.WaitAll(taskList.ToArray()); - } - - /// - public void LoadPlugins() - { - 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.Debug($"Loading plugin {dllPath}"); - var asm = Assembly.UnsafeLoadFrom(dllPath); - - foreach (var type in asm.GetExportedTypes()) - { - if (type.GetInterfaces().Contains(typeof(ITorchPlugin))) - { - 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; - } - } - } - } - - Plugins.ForEach(p => p.Init(Torch)); - PluginsLoaded?.Invoke(Plugins.ToList()); - } - - public IEnumerator GetEnumerator() - { - return Plugins.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return 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 deleted file mode 100644 index 6f13cb4..0000000 --- a/Torch/PluginManifest.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Xml.Serialization; - -namespace Torch -{ - public class PluginManifest - { - public string Repository { get; set; } = "Jimmacle/notarealrepo"; - public string Version { get; set; } = "1.0"; - - public void Save(string path) - { - using (var f = File.OpenWrite(path)) - { - var ser = new XmlSerializer(typeof(PluginManifest)); - ser.Serialize(f, this); - } - } - - public static PluginManifest Load(string path) - { - using (var f = File.OpenRead(path)) - { - var ser = new XmlSerializer(typeof(PluginManifest)); - return (PluginManifest)ser.Deserialize(f); - } - } - } -} diff --git a/Torch/Plugins/PluginManager.cs b/Torch/Plugins/PluginManager.cs new file mode 100644 index 0000000..25eb10c --- /dev/null +++ b/Torch/Plugins/PluginManager.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +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; + +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"); + private readonly ObservableDictionary _plugins = new ObservableDictionary(); + [Dependency] + private CommandManager _commandManager; + + /// + public IReadOnlyDictionary Plugins => _plugins.AsReadOnly(); + + public event Action> PluginsLoaded; + + public PluginManager(ITorchBase torchInstance) : base(torchInstance) + { + if (!Directory.Exists(PluginDir)) + Directory.CreateDirectory(PluginDir); + } + + /// + /// Updates loaded plugins in parallel. + /// + public void UpdatePlugins() + { + foreach (var plugin in _plugins.Values) + plugin.Update(); + } + + /// + /// Unloads all plugins. + /// + public override void Detach() + { + foreach (var plugin in _plugins.Values) + plugin.Dispose(); + + _plugins.Clear(); + } + + public void LoadPlugins() + { + DownloadPluginUpdates(); + _log.Info("Loading plugins..."); + var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir)); + foreach (var item in pluginItems) + { + var path = Path.Combine(PluginDir, item); + var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); + var manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); + if (manifest == null) + { + _log.Warn($"Item '{item}' is missing a manifest, skipping."); + continue; + } + + 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); + } + + _plugins.ForEach(x => x.Value.Init(Torch)); + _log.Info($"Loaded {_plugins.Count} plugins."); + PluginsLoaded?.Invoke(_plugins.Values.AsReadOnly()); + } + + private void DownloadPluginUpdates() + { + _log.Info("Checking for plugin updates..."); + var count = 0; + var pluginItems = Directory.EnumerateFiles(PluginDir, "*.zip").Union(Directory.EnumerateDirectories(PluginDir)); + Parallel.ForEach(pluginItems, async item => + { + PluginManifest manifest = null; + try + { + var path = Path.Combine(PluginDir, item); + var isZip = item.EndsWith(".zip", StringComparison.CurrentCultureIgnoreCase); + manifest = isZip ? GetManifestFromZip(path) : GetManifestFromDirectory(path); + if (manifest == null) + { + _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) + { + _log.Debug($"{manifest.Name} {manifest.Version} is up to date."); + return; + } + + _log.Info($"Updating plugin '{manifest.Name}' from {currentVersion} to {latest.Item1}."); + await UpdatePluginAsync(path, latest.Item2).ConfigureAwait(false); + count++; + } + catch (Exception e) + { + _log.Error($"An error occurred updating the plugin {manifest.Name}."); + _log.Error(e); + } + }); + + _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 UpdatePluginAsync(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()) + { + var data = new byte[entry.Length]; + stream.Read(data, 0, data.Length); + assemblies.Add(Assembly.Load(data)); + } + } + } + + 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())) + { + return PluginManifest.Load(stream); + } + } + } + + 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)}, not loading."); + return; + } + + pluginType = type; + } + } + + if (pluginType == null) + { + _log.Error($"The plugin '{manifest.Name}' does not have an implementation of {nameof(ITorchPlugin)}, not loading."); + 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.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Torch/Plugins/PluginManifest.cs b/Torch/Plugins/PluginManifest.cs new file mode 100644 index 0000000..13618a9 --- /dev/null +++ b/Torch/Plugins/PluginManifest.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Torch +{ + public class PluginManifest + { + /// + /// The display name of the plugin. + /// + public string Name { get; set; } + + /// + /// A unique identifier for the plugin. + /// + public Guid Guid { get; set; } + + /// + /// A GitHub repository in the format of Author/Repository to retrieve plugin updates. + /// + public string Repository { get; set; } + + /// + /// The plugin version. This must include a string in the format of #[.#[.#]] for update checking purposes. + /// + public string Version { get; set; } + + /// + /// A list of dependent plugin repositories. This may be updated to include GUIDs in the future. + /// + public List Dependencies { get; } = new List(); + + public void Save(string path) + { + using (var f = File.OpenWrite(path)) + { + var ser = new XmlSerializer(typeof(PluginManifest)); + ser.Serialize(f, this); + } + } + + public static PluginManifest Load(string path) + { + using (var f = File.OpenRead(path)) + { + 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 32e91d1..bab46f8 100644 --- a/Torch/Torch.csproj +++ b/Torch/Torch.csproj @@ -156,7 +156,8 @@ - + + @@ -200,7 +201,7 @@ - + @@ -217,7 +218,7 @@ - + CollectionEditor.xaml 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) {