Merge branch 'staging' into session-mgr-cmp
This commit is contained in:
50
Jenkinsfile
vendored
50
Jenkinsfile
vendored
@@ -1,3 +1,21 @@
|
|||||||
|
def packageAndArchive(buildMode, packageName, exclude) {
|
||||||
|
zipFile = "bin\\${packageName}.zip"
|
||||||
|
packageDir = "bin\\${packageName}\\"
|
||||||
|
|
||||||
|
bat "IF EXIST ${zipFile} DEL ${zipFile}"
|
||||||
|
bat "IF EXIST ${packageDir} RMDIR /S /Q ${packageDir}"
|
||||||
|
|
||||||
|
bat "xcopy bin\\x64\\${buildMode} ${packageDir}"
|
||||||
|
if (exclude.length() > 0) {
|
||||||
|
bat "del ${packageDir}${exclude}"
|
||||||
|
}
|
||||||
|
if (buildMode == "Release") {
|
||||||
|
bat "del ${packageDir}*.pdb"
|
||||||
|
}
|
||||||
|
powershell "Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\"\$PWD\\${packageDir}\", \"\$PWD\\${zipFile}\")"
|
||||||
|
archiveArtifacts artifacts: zipFile, caseSensitive: false, onlyIfSuccessful: true
|
||||||
|
}
|
||||||
|
|
||||||
node {
|
node {
|
||||||
stage('Checkout') {
|
stage('Checkout') {
|
||||||
checkout scm
|
checkout scm
|
||||||
@@ -16,12 +34,18 @@ node {
|
|||||||
|
|
||||||
stage('Build') {
|
stage('Build') {
|
||||||
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim()
|
currentBuild.description = bat(returnStdout: true, script: '@powershell -File Versioning/version.ps1').trim()
|
||||||
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=Release /p:Platform=x64"
|
if (env.BRANCH_NAME == "master") {
|
||||||
|
buildMode = "Release"
|
||||||
|
} else {
|
||||||
|
buildMode = "Debug"
|
||||||
|
}
|
||||||
|
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=${buildMode} /p:Platform=x64 /t:Clean"
|
||||||
|
bat "\"${tool 'MSBuild'}msbuild\" Torch.sln /p:Configuration=${buildMode} /p:Platform=x64"
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Test') {
|
stage('Test') {
|
||||||
bat 'IF NOT EXIST reports MKDIR reports'
|
bat 'IF NOT EXIST reports MKDIR reports'
|
||||||
bat "\"packages/xunit.runner.console.2.2.0/tools/xunit.console.exe\" \"bin-test/x64/Release/Torch.Tests.dll\" \"bin-test/x64/Release/Torch.Server.Tests.dll\" \"bin-test/x64/Release/Torch.Client.Tests.dll\" -parallel none -xml \"reports/Torch.Tests.xml\""
|
bat "\"packages/xunit.runner.console.2.2.0/tools/xunit.console.exe\" \"bin-test/x64/${buildMode}/Torch.Tests.dll\" \"bin-test/x64/${buildMode}/Torch.Server.Tests.dll\" \"bin-test/x64/${buildMode}/Torch.Client.Tests.dll\" -parallel none -xml \"reports/Torch.Tests.xml\""
|
||||||
step([
|
step([
|
||||||
$class: 'XUnitBuilder',
|
$class: 'XUnitBuilder',
|
||||||
thresholdMode: 1,
|
thresholdMode: 1,
|
||||||
@@ -38,29 +62,21 @@ node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stage('Archive') {
|
stage('Archive') {
|
||||||
bat '''IF EXIST bin\\torch-server.zip DEL bin\\torch-server.zip
|
archiveArtifacts artifacts: "bin/x64/${buildMode}/Torch*", caseSensitive: false, fingerprint: true, onlyIfSuccessful: true
|
||||||
IF EXIST bin\\package-server RMDIR /S /Q bin\\package-server
|
|
||||||
xcopy bin\\x64\\Release bin\\package-server\\
|
|
||||||
del bin\\package-server\\Torch.Client*'''
|
|
||||||
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-server\\\", \\\"\$PWD\\bin\\torch-server.zip\\\")\""
|
|
||||||
archiveArtifacts artifacts: 'bin/torch-server.zip', caseSensitive: false, onlyIfSuccessful: true
|
|
||||||
|
|
||||||
bat '''IF EXIST bin\\torch-client.zip DEL bin\\torch-client.zip
|
packageAndArchive(buildMode, "torch-server", "Torch.Client*")
|
||||||
IF EXIST bin\\package-client RMDIR /S /Q bin\\package-client
|
|
||||||
xcopy bin\\x64\\Release bin\\package-client\\
|
|
||||||
del bin\\package-client\\Torch.Server*'''
|
|
||||||
bat "powershell -Command \"Add-Type -Assembly System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::CreateFromDirectory(\\\"\$PWD\\bin\\package-client\\\", \\\"\$PWD\\bin\\torch-client.zip\\\")\""
|
|
||||||
archiveArtifacts artifacts: 'bin/torch-client.zip', caseSensitive: false, onlyIfSuccessful: true
|
|
||||||
|
|
||||||
archiveArtifacts artifacts: 'bin/x64/Release/Torch*', caseSensitive: false, fingerprint: true, onlyIfSuccessful: true
|
packageAndArchive(buildMode, "torch-client", "Torch.Server*")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (env.BRANCH_NAME == "master") {
|
||||||
gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim()
|
gitVersion = bat(returnStdout: true, script: "@git describe --tags").trim()
|
||||||
gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim()
|
gitSimpleVersion = bat(returnStdout: true, script: "@git describe --tags --abbrev=0").trim()
|
||||||
if (gitVersion == gitSimpleVersion) {
|
if (gitVersion == gitSimpleVersion) {
|
||||||
stage('Release') {
|
stage('${buildMode}') {
|
||||||
withCredentials([usernamePassword(credentialsId: 'torch-github', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
|
withCredentials([usernamePassword(credentialsId: 'torch-github', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]) {
|
||||||
powershell "& ./Jenkins/release.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")"
|
powershell "& ./Jenkins/${buildMode}.ps1 \"https://api.github.com/repos/TorchAPI/Torch/\" \"$gitSimpleVersion\" \"$USERNAME:$PASSWORD\" @(\"bin/torch-server.zip\", \"bin/torch-client.zip\")"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,12 +14,12 @@ namespace Torch.API.Managers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired when plugins are loaded.
|
/// Fired when plugins are loaded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<IList<ITorchPlugin>> PluginsLoaded;
|
event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Collection of loaded plugins.
|
/// Collection of loaded plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IList<ITorchPlugin> Plugins { get; }
|
IReadOnlyDictionary<Guid, ITorchPlugin> Plugins { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates all loaded plugins.
|
/// Updates all loaded plugins.
|
||||||
|
@@ -17,7 +17,7 @@ namespace Torch.API.Plugins
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The version of the plugin.
|
/// The version of the plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Version Version { get; }
|
string Version { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the plugin.
|
/// The name of the plugin.
|
||||||
|
@@ -10,6 +10,7 @@ namespace Torch.API.Plugins
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
|
/// Indicates that the given type should be loaded by the plugin manager as a plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("All plugin meta-information is now defined in the manifest.xml.")]
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
public class PluginAttribute : Attribute
|
public class PluginAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
@@ -29,7 +29,7 @@ namespace Torch.Server.ViewModels
|
|||||||
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
|
pluginManager.PluginsLoaded += PluginManager_PluginsLoaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PluginManager_PluginsLoaded(IList<ITorchPlugin> obj)
|
private void PluginManager_PluginsLoaded(IReadOnlyCollection<ITorchPlugin> obj)
|
||||||
{
|
{
|
||||||
Plugins.Clear();
|
Plugins.Clear();
|
||||||
foreach (var plugin in obj)
|
foreach (var plugin in obj)
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
</UserControl.DataContext>
|
</UserControl.DataContext>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="150"/>
|
<ColumnDefinition Width="200"/>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Grid Grid.Column="0">
|
<Grid Grid.Column="0">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListView.ItemTemplate>
|
</ListView.ItemTemplate>
|
||||||
</ListView>
|
</ListView>
|
||||||
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" IsEnabled="false"/>
|
<Button Grid.Row="1" Content="Open Folder" Margin="3" DockPanel.Dock="Bottom" Click="OpenFolder_OnClick"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
|
<Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SelectedPlugin.Control}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -15,6 +16,8 @@ using System.Windows.Navigation;
|
|||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
using NLog;
|
using NLog;
|
||||||
using Torch.API;
|
using Torch.API;
|
||||||
|
using Torch.API.Managers;
|
||||||
|
using Torch.Managers;
|
||||||
using Torch.Server.ViewModels;
|
using Torch.Server.ViewModels;
|
||||||
|
|
||||||
namespace Torch.Server.Views
|
namespace Torch.Server.Views
|
||||||
@@ -24,6 +27,9 @@ namespace Torch.Server.Views
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class PluginsControl : UserControl
|
public partial class PluginsControl : UserControl
|
||||||
{
|
{
|
||||||
|
private ITorchServer _server;
|
||||||
|
private PluginManager _plugins;
|
||||||
|
|
||||||
public PluginsControl()
|
public PluginsControl()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -31,8 +37,15 @@ namespace Torch.Server.Views
|
|||||||
|
|
||||||
public void BindServer(ITorchServer server)
|
public void BindServer(ITorchServer server)
|
||||||
{
|
{
|
||||||
var pluginManager = new PluginManagerViewModel(server.Plugins);
|
_server = server;
|
||||||
|
_plugins = _server.Managers.GetManager<PluginManager>();
|
||||||
|
var pluginManager = new PluginManagerViewModel(_plugins);
|
||||||
DataContext = pluginManager;
|
DataContext = pluginManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenFolder_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Process.Start("explorer.exe", _plugins.PluginDir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
@@ -10,28 +12,148 @@ using System.Windows.Threading;
|
|||||||
namespace Torch.Collections
|
namespace Torch.Collections
|
||||||
{
|
{
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ObservableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
|
public class ObservableDictionary<TKey, TValue> : ViewModel, IDictionary<TKey, TValue>, INotifyCollectionChanged
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
private IDictionary<TKey, TValue> _internalDict;
|
||||||
public new void Add(TKey key, TValue value)
|
|
||||||
|
public ObservableDictionary()
|
||||||
{
|
{
|
||||||
base.Add(key, value);
|
_internalDict = new Dictionary<TKey, TValue>();
|
||||||
var kv = new KeyValuePair<TKey, TValue>(key, value);
|
}
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, kv));
|
|
||||||
|
public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
_internalDict = new Dictionary<TKey, TValue>(dictionary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a <see cref="ObservableDictionary{TKey,TValue}"/> using the given dictionary by reference. The original dictionary should not be used after calling this.
|
||||||
|
/// </summary>
|
||||||
|
public static ObservableDictionary<TKey, TValue> ByReference(IDictionary<TKey, TValue> dictionary)
|
||||||
|
{
|
||||||
|
return new ObservableDictionary<TKey, TValue>
|
||||||
|
{
|
||||||
|
_internalDict = dictionary
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new bool Remove(TKey key)
|
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
|
||||||
{
|
{
|
||||||
if (!ContainsKey(key))
|
return _internalDict.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return ((IEnumerable)_internalDict).GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Add(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
Add(item.Key, item.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Remove(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return Remove(item.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_internalDict.Clear();
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||||
|
OnPropertyChanged(nameof(Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Contains(KeyValuePair<TKey, TValue> item)
|
||||||
|
{
|
||||||
|
return _internalDict.Contains(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
|
||||||
|
{
|
||||||
|
foreach (var kv in _internalDict)
|
||||||
|
{
|
||||||
|
array[arrayIndex] = kv;
|
||||||
|
arrayIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Count => _internalDict.Count;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsReadOnly => false;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool ContainsKey(TKey key)
|
||||||
|
{
|
||||||
|
return _internalDict.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Add(TKey key, TValue value)
|
||||||
|
{
|
||||||
|
_internalDict.Add(key, value);
|
||||||
|
var kv = new KeyValuePair<TKey, TValue>(key, value);
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, kv));
|
||||||
|
OnPropertyChanged(nameof(Count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Remove(TKey key)
|
||||||
|
{
|
||||||
|
if (!_internalDict.ContainsKey(key))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var kv = new KeyValuePair<TKey, TValue>(key, this[key]);
|
var kv = new KeyValuePair<TKey, TValue>(key, this[key]);
|
||||||
base.Remove(key);
|
if (!_internalDict.Remove(key))
|
||||||
|
return false;
|
||||||
|
|
||||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, kv));
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, kv));
|
||||||
|
OnPropertyChanged(nameof(Count));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetValue(TKey key, out TValue value)
|
||||||
|
{
|
||||||
|
return _internalDict.TryGetValue(key, out value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TValue this[TKey key]
|
||||||
|
{
|
||||||
|
get => _internalDict[key];
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var oldKv = new KeyValuePair<TKey, TValue>(key, _internalDict[key]);
|
||||||
|
var newKv = new KeyValuePair<TKey, TValue>(key, value);
|
||||||
|
_internalDict[key] = value;
|
||||||
|
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newKv, oldKv));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<TKey> Keys => _internalDict.Keys;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ICollection<TValue> Values => _internalDict.Values;
|
||||||
|
|
||||||
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
|
NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
|
||||||
@@ -52,12 +174,5 @@ namespace Torch.Collections
|
|||||||
nh.Invoke(this, e);
|
nh.Invoke(this, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -106,7 +106,7 @@ namespace Torch.Commands
|
|||||||
[Permission(MyPromoteLevel.None)]
|
[Permission(MyPromoteLevel.None)]
|
||||||
public void Plugins()
|
public void Plugins()
|
||||||
{
|
{
|
||||||
var plugins = Context.Torch.Managers.GetManager<PluginManager>()?.Plugins.Select(p => p.Name) ?? Enumerable.Empty<string>();
|
var plugins = Context.Torch.Managers.GetManager<PluginManager>()?.Plugins.Select(p => p.Value.Name) ?? Enumerable.Empty<string>();
|
||||||
Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}");
|
Context.Respond($"Loaded plugins: {string.Join(", ", plugins)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
54
Torch/Extensions/ICollectionExtensions.cs
Normal file
54
Torch/Extensions/ICollectionExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace Torch
|
||||||
|
{
|
||||||
|
public static class ICollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only wrapped <see cref="ICollection{T}"/>
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyCollection<T> AsReadOnly<T>(this ICollection<T> source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
return source as IReadOnlyCollection<T> ?? new ReadOnlyCollectionAdapter<T>(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only wrapped <see cref="IList{T}"/>
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
return source as IReadOnlyList<T> ?? new ReadOnlyCollection<T>(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a read-only wrapped <see cref="IDictionary{TKey, TValue}"/>
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
return source as IReadOnlyDictionary<TKey, TValue> ?? new ReadOnlyDictionary<TKey, TValue>(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ReadOnlyCollectionAdapter<T> : IReadOnlyCollection<T>
|
||||||
|
{
|
||||||
|
private readonly ICollection<T> _source;
|
||||||
|
|
||||||
|
public ReadOnlyCollectionAdapter(ICollection<T> source)
|
||||||
|
{
|
||||||
|
_source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _source.Count;
|
||||||
|
public IEnumerator<T> GetEnumerator() => _source.GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,51 +2,21 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Torch
|
namespace Torch
|
||||||
{
|
{
|
||||||
public static class StringExtensions
|
public static class StringExtensions
|
||||||
{
|
{
|
||||||
public static string Truncate(this string s, int maxLength)
|
/// <summary>
|
||||||
|
/// Try to extract a 3 component version from the string. Format: #.#.#
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryExtractVersion(this string version, out Version result)
|
||||||
{
|
{
|
||||||
return s.Length <= maxLength ? s : s.Substring(0, maxLength);
|
result = null;
|
||||||
}
|
var match = Regex.Match(version, @"(\d+\.)?(\d+\.)?(\d+)");
|
||||||
|
return match.Success && Version.TryParse(match.Value, out result);
|
||||||
public static IEnumerable<string> 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ namespace Torch.Managers
|
|||||||
CheckAndUpdateTorch();
|
CheckAndUpdateTorch();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Tuple<Version, string>> GetLatestRelease(string owner, string name)
|
private async Task<Tuple<Version, string>> TryGetLatestArchiveUrl(string owner, string name)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -53,56 +53,17 @@ namespace Torch.Managers
|
|||||||
return new Tuple<Version, string>(new Version(), null);
|
return new Tuple<Version, string>(new Version(), null);
|
||||||
|
|
||||||
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
|
var zip = latest.Assets.FirstOrDefault(x => x.Name.Contains(".zip"));
|
||||||
var versionName = Regex.Match(latest.TagName, "(\\d+\\.)+\\d+").ToString();
|
if (zip == null)
|
||||||
if (string.IsNullOrWhiteSpace(versionName))
|
_log.Error($"Latest release of {owner}/{name} does not contain a zip archive.");
|
||||||
{
|
if (!latest.TagName.TryExtractVersion(out Version version))
|
||||||
_log.Warn("Unable to parse tag {0} for {1}/{2}", latest.TagName, owner, name);
|
_log.Error($"Unable to parse version tag for {owner}/{name}");
|
||||||
versionName = "0.0";
|
return new Tuple<Version, string>(version, zip?.BrowserDownloadUrl);
|
||||||
}
|
|
||||||
return new Tuple<Version, string>(new Version(versionName), zip?.BrowserDownloadUrl);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
|
_log.Error($"An error occurred getting release information for '{owner}/{name}'");
|
||||||
_log.Error(e);
|
_log.Error(e);
|
||||||
return new Tuple<Version, string>(new Version(), null);
|
return default(Tuple<Version, string>);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +77,7 @@ namespace Torch.Managers
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var releaseInfo = await GetLatestRelease("TorchAPI", "Torch").ConfigureAwait(false);
|
var releaseInfo = await TryGetLatestArchiveUrl("TorchAPI", "Torch").ConfigureAwait(false);
|
||||||
if (releaseInfo.Item1 > Torch.TorchVersion)
|
if (releaseInfo.Item1 > Torch.TorchVersion)
|
||||||
{
|
{
|
||||||
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
|
_log.Warn($"Updating Torch from version {Torch.TorchVersion} to version {releaseInfo.Item1}");
|
||||||
|
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
327
Torch/Plugins/PluginManager.cs
Normal file
327
Torch/Plugins/PluginManager.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid, ITorchPlugin> _plugins = new ObservableDictionary<Guid, ITorchPlugin>();
|
||||||
|
[Dependency]
|
||||||
|
private CommandManager _commandManager;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyDictionary<Guid, ITorchPlugin> Plugins => _plugins.AsReadOnly();
|
||||||
|
|
||||||
|
public event Action<IReadOnlyCollection<ITorchPlugin>> PluginsLoaded;
|
||||||
|
|
||||||
|
public PluginManager(ITorchBase torchInstance) : base(torchInstance)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(PluginDir))
|
||||||
|
Directory.CreateDirectory(PluginDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates loaded plugins in parallel.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdatePlugins()
|
||||||
|
{
|
||||||
|
foreach (var plugin in _plugins.Values)
|
||||||
|
plugin.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unloads all plugins.
|
||||||
|
/// </summary>
|
||||||
|
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<Tuple<Version, string>> 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<Version, string>(latestVersion, zipAsset?.BrowserDownloadUrl);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_log.Error($"Unable to get the latest release of '{repository}.'");
|
||||||
|
_log.Error(e);
|
||||||
|
return default(Tuple<Version, string>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Assembly>();
|
||||||
|
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<Assembly>();
|
||||||
|
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<Assembly> 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<PluginAttribute>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IEnumerable.GetEnumerator"/>
|
||||||
|
public IEnumerator<ITorchPlugin> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _plugins.Values.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
67
Torch/Plugins/PluginManifest.cs
Normal file
67
Torch/Plugins/PluginManifest.cs
Normal file
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The display name of the plugin.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier for the plugin.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Guid { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A GitHub repository in the format of Author/Repository to retrieve plugin updates.
|
||||||
|
/// </summary>
|
||||||
|
public string Repository { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The plugin version. This must include a string in the format of #[.#[.#]] for update checking purposes.
|
||||||
|
/// </summary>
|
||||||
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of dependent plugin repositories. This may be updated to include GUIDs in the future.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Dependencies { get; } = new List<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -155,9 +155,10 @@
|
|||||||
<Link>Properties\AssemblyVersion.cs</Link>
|
<Link>Properties\AssemblyVersion.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Collections\ObservableList.cs" />
|
<Compile Include="Collections\ObservableList.cs" />
|
||||||
<Compile Include="DispatcherExtensions.cs" />
|
|
||||||
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
|
<Compile Include="Managers\ChatManager\ChatManagerClient.cs" />
|
||||||
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
|
<Compile Include="Managers\ChatManager\ChatManagerServer.cs" />
|
||||||
|
<Compile Include="Extensions\DispatcherExtensions.cs" />
|
||||||
|
<Compile Include="Extensions\ICollectionExtensions.cs" />
|
||||||
<Compile Include="Managers\DependencyManager.cs" />
|
<Compile Include="Managers\DependencyManager.cs" />
|
||||||
<Compile Include="Managers\KeenLogManager.cs" />
|
<Compile Include="Managers\KeenLogManager.cs" />
|
||||||
<Compile Include="Managers\PatchManager\AssemblyMemory.cs" />
|
<Compile Include="Managers\PatchManager\AssemblyMemory.cs" />
|
||||||
@@ -204,7 +205,7 @@
|
|||||||
<Compile Include="Managers\MultiplayerManagerBase.cs" />
|
<Compile Include="Managers\MultiplayerManagerBase.cs" />
|
||||||
<Compile Include="Managers\UpdateManager.cs" />
|
<Compile Include="Managers\UpdateManager.cs" />
|
||||||
<Compile Include="Persistent.cs" />
|
<Compile Include="Persistent.cs" />
|
||||||
<Compile Include="PluginManifest.cs" />
|
<Compile Include="Plugins\PluginManifest.cs" />
|
||||||
<Compile Include="Utils\Reflection.cs" />
|
<Compile Include="Utils\Reflection.cs" />
|
||||||
<Compile Include="Managers\ScriptingManager.cs" />
|
<Compile Include="Managers\ScriptingManager.cs" />
|
||||||
<Compile Include="Utils\TorchAssemblyResolver.cs" />
|
<Compile Include="Utils\TorchAssemblyResolver.cs" />
|
||||||
@@ -221,7 +222,7 @@
|
|||||||
<Compile Include="Extensions\StringExtensions.cs" />
|
<Compile Include="Extensions\StringExtensions.cs" />
|
||||||
<Compile Include="ViewModels\PlayerViewModel.cs" />
|
<Compile Include="ViewModels\PlayerViewModel.cs" />
|
||||||
<Compile Include="ViewModels\ViewModel.cs" />
|
<Compile Include="ViewModels\ViewModel.cs" />
|
||||||
<Compile Include="Managers\PluginManager.cs" />
|
<Compile Include="Plugins\PluginManager.cs" />
|
||||||
<Compile Include="ViewModels\PluginViewModel.cs" />
|
<Compile Include="ViewModels\PluginViewModel.cs" />
|
||||||
<Compile Include="Views\CollectionEditor.xaml.cs">
|
<Compile Include="Views\CollectionEditor.xaml.cs">
|
||||||
<DependentUpon>CollectionEditor.xaml</DependentUpon>
|
<DependentUpon>CollectionEditor.xaml</DependentUpon>
|
||||||
|
@@ -16,30 +16,11 @@ namespace Torch
|
|||||||
public abstract class TorchPluginBase : ITorchPlugin
|
public abstract class TorchPluginBase : ITorchPlugin
|
||||||
{
|
{
|
||||||
public string StoragePath { get; internal set; }
|
public string StoragePath { get; internal set; }
|
||||||
public Guid Id { get; }
|
public PluginManifest Manifest { get; internal set; }
|
||||||
public Version Version { get; }
|
public Guid Id => Manifest.Guid;
|
||||||
public string Name { get; }
|
public string Version => Manifest.Version;
|
||||||
public ITorchBase Torch { get; private set; }
|
public string Name => Manifest.Name;
|
||||||
private static readonly Logger _log = LogManager.GetLogger(nameof(TorchPluginBase));
|
public ITorchBase Torch { get; internal set; }
|
||||||
|
|
||||||
protected TorchPluginBase()
|
|
||||||
{
|
|
||||||
var type = GetType();
|
|
||||||
var pluginInfo = type.GetCustomAttribute<PluginAttribute>();
|
|
||||||
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 virtual void Init(ITorchBase torch)
|
public virtual void Init(ITorchBase torch)
|
||||||
{
|
{
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -20,6 +21,16 @@ namespace Torch
|
|||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propName = "")
|
||||||
|
{
|
||||||
|
if (backingField.Equals(value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
backingField = value;
|
||||||
|
// ReSharper disable once ExplicitCallerInfoArgument
|
||||||
|
OnPropertyChanged(propName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fires PropertyChanged for all properties.
|
/// Fires PropertyChanged for all properties.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
Reference in New Issue
Block a user