2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-07-02 08:53:42 -04:00

Initial Commit

This commit is contained in:
2024-04-13 15:54:59 +10:00
parent 8e178cbb7b
commit 63ed43f4af
459 changed files with 8039 additions and 20504 deletions

View File

@ -0,0 +1,29 @@
using Observatory.Framework;
namespace Pulsar.PluginManagement;
public class PlaceholderPlugin : IObservatoryNotifier
{
public PlaceholderPlugin(string name)
{
this.name = name;
}
public string Name => name;
private string name;
public string ShortName => name;
public string Version => string.Empty;
public PluginUI PluginUI => new PluginUI(PluginUI.UIType.None, null);
public object Settings { get => null; set { } }
public void Load(IObservatoryCore observatoryCore)
{ }
public void OnNotificationEvent(NotificationArgs notificationArgs)
{ }
}

View File

@ -0,0 +1,95 @@
using System.Reflection;
using Observatory.Framework;
using Observatory.Framework.Files;
using Pulsar.Utils;
using HttpClient = System.Net.Http.HttpClient;
namespace Pulsar.PluginManagement;
public class PluginCore : IObservatoryCore
{
public string Version => Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
public Action<Exception, string> GetPluginErrorLogger(IObservatoryPlugin plugin)
{
return (ex, context) =>
{
LoggingUtils.LogError(ex, $"from plugin {plugin.ShortName} {context}");
};
}
public Status GetStatus() => LogMonitor.GetInstance.Status;
public Guid SendNotification(string title, string text)
{
return SendNotification(new NotificationArgs { Title = title, Detail = text });
}
public Guid SendNotification(NotificationArgs notificationArgs)
{
throw new NotImplementedException();
}
public void CancelNotification(Guid notificationId)
{
throw new NotImplementedException();
}
public void UpdateNotification(Guid id, NotificationArgs notificationArgs)
{
throw new NotImplementedException();
}
/// <summary>
/// Adds an item to the datagrid on UI thread to ensure visual update.
/// </summary>
/// <param name="worker"></param>
/// <param name="item"></param>
public void AddGridItem(IObservatoryWorker worker, object item)
{
worker.PluginUI.DataGrid.Add(item);
}
public void AddGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items)
{
}
public void SetGridItems(IObservatoryWorker worker, IEnumerable<Dictionary<string,string>> items)
{
}
public HttpClient HttpClient
{
get => Utils.HttpClient.Client;
}
public LogMonitorState CurrentLogMonitorState
{
get => LogMonitor.GetInstance.CurrentState;
}
public bool IsLogMonitorBatchReading
{
get => LogMonitorStateChangedEventArgs.IsBatchRead(LogMonitor.GetInstance.CurrentState);
}
public event EventHandler<NotificationArgs> Notification;
internal event EventHandler<PluginMessageArgs> PluginMessage;
public string PluginStorageFolder
{
get
{
throw new NotImplementedException();
}
}
public void SendPluginMessage(IObservatoryPlugin plugin, object message)
{
PluginMessage?.Invoke(this, new PluginMessageArgs(plugin.Name, plugin.Version, message));
}
}

View File

@ -0,0 +1,192 @@
using System.Timers;
using Observatory.Framework;
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
using Pulsar.Utils;
using Timer = System.Timers.Timer;
namespace Pulsar.PluginManagement;
class PluginEventHandler
{
private IEnumerable<IObservatoryWorker> observatoryWorkers;
private IEnumerable<IObservatoryNotifier> observatoryNotifiers;
private HashSet<IObservatoryPlugin> disabledPlugins;
private List<(string error, string detail)> errorList;
private Timer timer;
public PluginEventHandler(IEnumerable<IObservatoryWorker> observatoryWorkers, IEnumerable<IObservatoryNotifier> observatoryNotifiers)
{
this.observatoryWorkers = observatoryWorkers;
this.observatoryNotifiers = observatoryNotifiers;
disabledPlugins = new();
errorList = new();
InitializeTimer();
}
private void InitializeTimer()
{
// Use a timer to delay error reporting until incoming errors are "quiet" for one full second.
// Should resolve issue where repeated plugin errors open hundreds of error windows.
timer = new();
timer.Interval = 1000;
timer.Elapsed += ReportErrorsIfAny;
}
public void OnJournalEvent(object source, JournalEventArgs journalEventArgs)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.JournalEvent((JournalBase)journalEventArgs.journalEvent);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, journalEventArgs.journalType.Name, ((JournalBase)journalEventArgs.journalEvent).Json);
}
ResetTimer();
}
}
public void OnStatusUpdate(object sourece, JournalEventArgs journalEventArgs)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.StatusChange((Status)journalEventArgs.journalEvent);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, journalEventArgs.journalType.Name, ((JournalBase)journalEventArgs.journalEvent).Json);
}
ResetTimer();
}
}
internal void OnLogMonitorStateChanged(object sender, LogMonitorStateChangedEventArgs e)
{
foreach (var worker in observatoryWorkers)
{
if (disabledPlugins.Contains(worker)) continue;
try
{
worker.LogMonitorStateChanged(e);
}
catch (Exception ex)
{
RecordError(ex, worker.Name, "LogMonitorStateChanged event", ex.StackTrace ?? "");
}
}
}
public void OnNotificationEvent(object source, NotificationArgs notificationArgs)
{
foreach (var notifier in observatoryNotifiers)
{
if (disabledPlugins.Contains(notifier)) continue;
try
{
notifier.OnNotificationEvent(notificationArgs);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch (Exception ex)
{
RecordError(ex, notifier.Name, notificationArgs.Title, notificationArgs.Detail);
}
ResetTimer();
}
}
public void OnPluginMessageEvent(object _, PluginMessageArgs messageArgs)
{
foreach (var plugin in observatoryNotifiers.Cast<IObservatoryPlugin>().Concat(observatoryWorkers))
{
if (disabledPlugins.Contains(plugin)) continue;
try
{
plugin.HandlePluginMessage(messageArgs.SourceName, messageArgs.SourceVersion, messageArgs.Message);
}
catch (PluginException ex)
{
RecordError(ex);
}
catch(Exception ex)
{
RecordError(ex, plugin.Name, "OnPluginMessageEvent event", "");
}
}
}
public void SetPluginEnabled(IObservatoryPlugin plugin, bool enabled)
{
if (enabled) disabledPlugins.Remove(plugin);
else disabledPlugins.Add(plugin);
}
private void ResetTimer()
{
timer.Stop();
try
{
timer.Start();
}
catch
{
// Not sure why this happens, but I've reproduced it twice in a row after hitting
// read-all while also playing (ie. generating journals).
InitializeTimer();
timer.Start();
}
}
private void RecordError(PluginException ex)
{
errorList.Add(($"Error in {ex.PluginName}: {ex.Message}", ex.StackTrace ?? ""));
}
private void RecordError(Exception ex, string plugin, string eventType, string eventDetail)
{
errorList.Add(($"Error in {plugin} while handling {eventType}: {ex.Message}", eventDetail));
}
private void ReportErrorsIfAny(object sender, ElapsedEventArgs e)
{
if (errorList.Any())
{
ErrorReporter.ShowErrorPopup($"Plugin Error{(errorList.Count > 1 ? "s" : "")}", errorList);
timer.Stop();
}
}
}
internal class PluginMessageArgs
{
internal string SourceName;
internal string SourceVersion;
internal object Message;
internal PluginMessageArgs(string sourceName, string sourceVersion, object message)
{
SourceName = sourceName;
SourceVersion = sourceVersion;
Message = message;
}
}

View File

@ -0,0 +1,329 @@
using System.IO.Compression;
using System.Reflection;
using System.Runtime.Loader;
using Observatory.Framework;
using Pulsar.Utils;
namespace Pulsar.PluginManagement;
public class PluginManager
{
public static PluginManager GetInstance
{
get
{
return _instance.Value;
}
}
private static readonly Lazy<PluginManager> _instance = new Lazy<PluginManager>(NewPluginManager);
private static PluginManager NewPluginManager()
{
return new PluginManager();
}
public readonly List<(string error, string? detail)> errorList;
public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins;
public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins;
private readonly PluginCore core;
private readonly PluginEventHandler pluginHandler;
private PluginManager()
{
errorList = LoadPlugins(out workerPlugins, out notifyPlugins);
pluginHandler = new PluginEventHandler(workerPlugins.Select(p => p.plugin), notifyPlugins.Select(p => p.plugin));
var logMonitor = LogMonitor.GetInstance;
logMonitor.JournalEntry += pluginHandler.OnJournalEvent;
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
core = new PluginCore();
List<IObservatoryPlugin> errorPlugins = new();
foreach (var plugin in workerPlugins.Select(p => p.plugin))
{
try
{
LoadSettings(plugin);
plugin.Load(core);
}
catch (PluginException ex)
{
errorList.Add((FormatErrorMessage(ex), ex.StackTrace));
errorPlugins.Add(plugin);
}
}
workerPlugins.RemoveAll(w => errorPlugins.Contains(w.plugin));
errorPlugins.Clear();
foreach (var plugin in notifyPlugins.Select(p => p.plugin))
{
// Notifiers which are also workers need not be loaded again (they are the same instance).
if (!plugin.GetType().IsAssignableTo(typeof(IObservatoryWorker)))
{
try
{
LoadSettings(plugin);
plugin.Load(core);
}
catch (PluginException ex)
{
errorList.Add((FormatErrorMessage(ex), ex.StackTrace));
errorPlugins.Add(plugin);
}
catch (Exception ex)
{
errorList.Add(($"{plugin.ShortName}: {ex.Message}", ex.StackTrace));
errorPlugins.Add(plugin);
}
}
}
notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin));
core.Notification += pluginHandler.OnNotificationEvent;
core.PluginMessage += pluginHandler.OnPluginMessageEvent;
if (errorList.Any())
ErrorReporter.ShowErrorPopup("Plugin Load Error" + (errorList.Count > 1 ? "s" : string.Empty), errorList);
}
private static string FormatErrorMessage(PluginException ex)
{
return $"{ex.PluginName}: {ex.UserMessage}";
}
private void LoadSettings(IObservatoryPlugin plugin)
{
throw new NotImplementedException();
}
public static Dictionary<PropertyInfo, string> GetSettingDisplayNames(object settings)
{
var settingNames = new Dictionary<PropertyInfo, string>();
if (settings != null)
{
var properties = settings.GetType().GetProperties();
foreach (var property in properties)
{
var attrib = property.GetCustomAttribute<SettingDisplayName>();
if (attrib == null)
{
settingNames.Add(property, property.Name);
}
else
{
settingNames.Add(property, attrib.DisplayName);
}
}
}
return settingNames;
}
public void SaveSettings(IObservatoryPlugin plugin, object settings)
{
throw new NotImplementedException();
}
public void SetPluginEnabled(IObservatoryPlugin plugin, bool enabled)
{
pluginHandler.SetPluginEnabled(plugin, enabled);
}
private static List<(string, string?)> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers)
{
observatoryWorkers = new();
observatoryNotifiers = new();
var errorList = new List<(string, string?)>();
var pluginPath = $"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins";
if (Directory.Exists(pluginPath))
{
ExtractPlugins(pluginPath);
var pluginLibraries = Directory.GetFiles($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins", "*.dll");
foreach (var dll in pluginLibraries)
{
try
{
var pluginStatus = PluginStatus.SigCheckDisabled;
var loadOkay = true;
if (loadOkay)
{
var error = LoadPluginAssembly(dll, observatoryWorkers, observatoryNotifiers, pluginStatus);
if (!string.IsNullOrWhiteSpace(error))
{
errorList.Add((error, string.Empty));
}
}
}
catch (Exception ex)
{
errorList.Add(($"ERROR: {new FileInfo(dll).Name}, {ex.Message}", ex.StackTrace ?? string.Empty));
LoadPlaceholderPlugin(dll, PluginStatus.InvalidLibrary, observatoryNotifiers);
}
}
}
return errorList;
}
private static void ExtractPlugins(string pluginFolder)
{
var files = Directory.GetFiles(pluginFolder, "*.zip")
.Concat(Directory.GetFiles(pluginFolder, "*.eop")); // Elite Observatory Plugin
foreach (var file in files)
{
try
{
ZipFile.ExtractToDirectory(file, pluginFolder, true);
File.Delete(file);
}
catch
{
// Just ignore files that don't extract successfully.
}
}
}
private static string LoadPluginAssembly(string dllPath, List<(IObservatoryWorker plugin, PluginStatus signed)> workers, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers, PluginStatus pluginStatus)
{
var recursionGuard = string.Empty;
AssemblyLoadContext.Default.Resolving += (context, name) => {
if ((name?.Name?.EndsWith("resources")).GetValueOrDefault(false))
{
return null;
}
// Importing Observatory.Framework in the Explorer Lua scripts causes an attempt to reload
// the assembly, just hand it back the one we already have.
if ((name?.Name?.StartsWith("Observatory.Framework")).GetValueOrDefault(false) || name?.Name == "ObservatoryFramework")
{
return context.Assemblies.Where(a => (a.FullName?.Contains("ObservatoryFramework")).GetValueOrDefault(false)).First();
}
var foundDlls = Directory.GetFileSystemEntries(new FileInfo($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}deps").FullName, name.Name + ".dll", SearchOption.TopDirectoryOnly);
if (foundDlls.Any())
{
return context.LoadFromAssemblyPath(foundDlls[0]);
}
if (name.Name != recursionGuard && name.Name != null)
{
recursionGuard = name.Name;
return context.LoadFromAssemblyName(name);
}
throw new Exception("Unable to load assembly " + name.Name);
};
var pluginAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName);
Type[] types;
var err = string.Empty;
var pluginCount = 0;
try
{
types = pluginAssembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.OfType<Type>().ToArray();
}
catch
{
types = Array.Empty<Type>();
}
IEnumerable<Type> workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker)));
foreach (var worker in workerTypes)
{
var constructor = worker.GetConstructor(Array.Empty<Type>());
if (constructor != null)
{
var instance = constructor.Invoke(Array.Empty<object>());
workers.Add(((instance as IObservatoryWorker)!, pluginStatus));
if (instance is IObservatoryNotifier)
{
// This is also a notifier; add to the notifier list as well, so the work and notifier are
// the same instance and can share state.
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
}
pluginCount++;
}
}
// Filter out items which are also workers as we've already created them above.
var notifyTypes = types.Where(t =>
t.IsAssignableTo(typeof(IObservatoryNotifier)) && !t.IsAssignableTo(typeof(IObservatoryWorker)));
foreach (var notifier in notifyTypes)
{
var constructor = notifier.GetConstructor(Array.Empty<Type>());
if (constructor != null)
{
var instance = constructor.Invoke(Array.Empty<object>());
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
pluginCount++;
}
}
if (pluginCount == 0)
{
err += $"ERROR: Library '{dllPath}' contains no suitable interfaces.";
LoadPlaceholderPlugin(dllPath, PluginStatus.InvalidPlugin, notifiers);
}
return err;
}
private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers)
{
PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name);
notifiers.Add((placeholder, pluginStatus));
}
/// <summary>
/// Possible plugin load results and signature statuses.
/// </summary>
public enum PluginStatus
{
/// <summary>
/// Plugin valid and signed with matching certificate.
/// </summary>
Signed,
/// <summary>
/// Plugin valid but not signed with any certificate.
/// </summary>
Unsigned,
/// <summary>
/// Plugin valid but not signed with valid certificate.
/// </summary>
InvalidSignature,
/// <summary>
/// Plugin invalid and cannot be loaded. Possible version mismatch.
/// </summary>
InvalidPlugin,
/// <summary>
/// Plugin not a CLR library.
/// </summary>
InvalidLibrary,
/// <summary>
/// Plugin valid but executing assembly has no certificate to match against.
/// </summary>
NoCert,
/// <summary>
/// Plugin signature checks disabled.
/// </summary>
SigCheckDisabled
}
}