mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-07-01 08:23:42 -04:00
Initial Commit
This commit is contained in:
84
Pulsar/App.config
Normal file
84
Pulsar/App.config
Normal file
@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="Observatory.Properties.Core" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<userSettings>
|
||||
<Observatory.Properties.Core>
|
||||
<setting name="JournalFolder" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="AllowUnsigned" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="NativeNotify" serializeAs="String">
|
||||
<value>True</value>
|
||||
</setting>
|
||||
<setting name="NativeNotifyFont" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="NativeNotifyColour" serializeAs="String">
|
||||
<value>4294944000</value>
|
||||
</setting>
|
||||
<setting name="NativeNotifyCorner" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="NativeNotifyScreen" serializeAs="String">
|
||||
<value>-1</value>
|
||||
</setting>
|
||||
<setting name="TryPrimeSystemContextOnStartMonitor" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="CoreVersion" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="PluginSettings" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="VoiceNotify" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="VoiceSelected" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="VoiceVolume" serializeAs="String">
|
||||
<value>75</value>
|
||||
</setting>
|
||||
<setting name="VoiceRate" serializeAs="String">
|
||||
<value>0</value>
|
||||
</setting>
|
||||
<setting name="MainWindowSize" serializeAs="String">
|
||||
<value>800, 500</value>
|
||||
</setting>
|
||||
<setting name="MainWindowPosition" serializeAs="String">
|
||||
<value>100, 100</value>
|
||||
</setting>
|
||||
<setting name="NativeNotifyScale" serializeAs="String">
|
||||
<value>100</value>
|
||||
</setting>
|
||||
<setting name="NativeNotifyTimeout" serializeAs="String">
|
||||
<value>8000</value>
|
||||
</setting>
|
||||
<setting name="StartMonitor" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="ExportFolder" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="StartReadAll" serializeAs="String">
|
||||
<value>False</value>
|
||||
</setting>
|
||||
<setting name="Theme" serializeAs="String">
|
||||
<value>Dark</value>
|
||||
</setting>
|
||||
<setting name="ColumnSizing" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
<setting name="PluginsEnabled" serializeAs="String">
|
||||
<value />
|
||||
</setting>
|
||||
</Observatory.Properties.Core>
|
||||
</userSettings>
|
||||
</configuration>
|
BIN
Pulsar/Assets/EOCIcon-Presized.ico
Normal file
BIN
Pulsar/Assets/EOCIcon-Presized.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 210 KiB |
31
Pulsar/LoggingUtils.cs
Normal file
31
Pulsar/LoggingUtils.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Pulsar;
|
||||
|
||||
public static class LoggingUtils
|
||||
{
|
||||
internal static void LogError(Exception ex, string context)
|
||||
{
|
||||
var docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
var errorMessage = new StringBuilder();
|
||||
var timestamp = DateTime.Now.ToString("G");
|
||||
errorMessage
|
||||
.AppendLine($"[{timestamp}] Error encountered in Elite Observatory {context}")
|
||||
.AppendLine(FormatExceptionMessage(ex))
|
||||
.AppendLine();
|
||||
File.AppendAllText(docPath + Path.DirectorySeparatorChar + "ObservatoryCrashLog.txt",
|
||||
errorMessage.ToString());
|
||||
}
|
||||
|
||||
static string FormatExceptionMessage(Exception ex, bool inner = false)
|
||||
{
|
||||
var errorMessage = new StringBuilder();
|
||||
errorMessage
|
||||
.AppendLine($"{(inner ? "Inner e" : "E")}xception message: {ex.Message}")
|
||||
.AppendLine("Stack trace:")
|
||||
.AppendLine(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
errorMessage.AppendLine(FormatExceptionMessage(ex.InnerException, true));
|
||||
return errorMessage.ToString();
|
||||
}
|
||||
}
|
29
Pulsar/PluginManagement/PlaceholderPlugin.cs
Normal file
29
Pulsar/PluginManagement/PlaceholderPlugin.cs
Normal 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)
|
||||
{ }
|
||||
}
|
95
Pulsar/PluginManagement/PluginCore.cs
Normal file
95
Pulsar/PluginManagement/PluginCore.cs
Normal 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));
|
||||
}
|
||||
}
|
192
Pulsar/PluginManagement/PluginEventHandler.cs
Normal file
192
Pulsar/PluginManagement/PluginEventHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
329
Pulsar/PluginManagement/PluginManager.cs
Normal file
329
Pulsar/PluginManagement/PluginManager.cs
Normal 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
|
||||
}
|
||||
}
|
26
Pulsar/Program.cs
Normal file
26
Pulsar/Program.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
using Observatory;
|
||||
using Pulsar;
|
||||
using Pulsar.Utils;
|
||||
|
||||
SettingsManager.Load();
|
||||
|
||||
if (args.Length > 0 && File.Exists(args[0]))
|
||||
{
|
||||
var fileInfo = new FileInfo(args[0]);
|
||||
if (fileInfo.Extension == ".eop" || fileInfo.Extension == ".zip")
|
||||
File.Copy(
|
||||
fileInfo.FullName,
|
||||
$"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins{Path.DirectorySeparatorChar}{fileInfo.Name}");
|
||||
}
|
||||
|
||||
var version = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
|
||||
|
||||
try
|
||||
{
|
||||
//TODO: Start Application
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggingUtils.LogError(ex, version);
|
||||
}
|
15
Pulsar/Pulsar.csproj
Normal file
15
Pulsar/Pulsar.csproj
Normal file
@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Pulsar</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ObservatoryFramework\ObservatoryFramework.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
34
Pulsar/Utils/ErrorReporter.cs
Normal file
34
Pulsar/Utils/ErrorReporter.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Pulsar.Utils;
|
||||
|
||||
public static class ErrorReporter
|
||||
{
|
||||
public static void ShowErrorPopup(string title, List<(string error, string detail)> errorList)
|
||||
{
|
||||
// Limit number of errors displayed.
|
||||
StringBuilder displayMessage = new();
|
||||
displayMessage.AppendLine($"{errorList.Count} error{(errorList.Count > 1 ? "s" : string.Empty)} encountered.");
|
||||
var firstFiveErrors = errorList.Take(Math.Min(5, errorList.Count)).Select(e => e.error);
|
||||
displayMessage.AppendJoin(Environment.NewLine, firstFiveErrors);
|
||||
displayMessage.AppendLine();
|
||||
displayMessage.Append("Full error details logged to ObservatoryErrorLog file in your documents folder.");
|
||||
|
||||
//TODO: Winform error popup
|
||||
|
||||
// Log entirety of errors out to file.
|
||||
var timestamp = DateTime.Now.ToString("G");
|
||||
StringBuilder errorLog = new();
|
||||
foreach (var error in errorList)
|
||||
{
|
||||
errorLog.AppendLine($"[{timestamp}]:");
|
||||
errorLog.AppendLine($"{error.error} - {error.detail}");
|
||||
errorLog.AppendLine();
|
||||
}
|
||||
|
||||
var docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
File.AppendAllText(docPath + Path.DirectorySeparatorChar + "ObservatoryErrorLog.txt", errorLog.ToString());
|
||||
|
||||
errorList.Clear();
|
||||
}
|
||||
}
|
32
Pulsar/Utils/HttpClient.cs
Normal file
32
Pulsar/Utils/HttpClient.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Pulsar.Utils;
|
||||
|
||||
public sealed class HttpClient
|
||||
{
|
||||
private HttpClient()
|
||||
{ }
|
||||
|
||||
private static readonly Lazy<System.Net.Http.HttpClient> lazy = new Lazy<System.Net.Http.HttpClient>(() => new System.Net.Http.HttpClient());
|
||||
|
||||
public static System.Net.Http.HttpClient Client
|
||||
{
|
||||
get
|
||||
{
|
||||
return lazy.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetString(string url)
|
||||
{
|
||||
return lazy.Value.GetStringAsync(url).Result;
|
||||
}
|
||||
|
||||
public static HttpResponseMessage SendRequest(HttpRequestMessage request)
|
||||
{
|
||||
return lazy.Value.SendAsync(request).Result;
|
||||
}
|
||||
|
||||
public static Task<HttpResponseMessage> SendRequestAsync(HttpRequestMessage request)
|
||||
{
|
||||
return lazy.Value.SendAsync(request);
|
||||
}
|
||||
}
|
94
Pulsar/Utils/JournalReader.cs
Normal file
94
Pulsar/Utils/JournalReader.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Observatory.Framework.Files.Journal;
|
||||
using Observatory.Framework.Files.Journal.Exploration;
|
||||
|
||||
namespace Pulsar.Utils;
|
||||
|
||||
public class JournalReader
|
||||
{
|
||||
public static TJournal ObservatoryDeserializer<TJournal>(string json) where TJournal : JournalBase
|
||||
{
|
||||
TJournal deserialized;
|
||||
|
||||
if (typeof(TJournal) == typeof(InvalidJson))
|
||||
{
|
||||
InvalidJson invalidJson;
|
||||
try
|
||||
{
|
||||
var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json));
|
||||
var eventType = string.Empty;
|
||||
var timestamp = string.Empty;
|
||||
|
||||
while ((eventType == string.Empty || timestamp == string.Empty) && reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
{
|
||||
if (reader.GetString() == "event")
|
||||
{
|
||||
reader.Read();
|
||||
eventType = reader.GetString();
|
||||
}
|
||||
else if (reader.GetString() == "timestamp")
|
||||
{
|
||||
reader.Read();
|
||||
timestamp = reader.GetString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidJson = new InvalidJson
|
||||
{
|
||||
Event = "InvalidJson",
|
||||
Timestamp = timestamp,
|
||||
OriginalEvent = eventType
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
invalidJson = new InvalidJson
|
||||
{
|
||||
Event = "InvalidJson",
|
||||
Timestamp = string.Empty,
|
||||
OriginalEvent = "Invalid"
|
||||
};
|
||||
}
|
||||
|
||||
deserialized = (TJournal)Convert.ChangeType(invalidJson, typeof(TJournal));
|
||||
|
||||
}
|
||||
//Journal potentially had invalid JSON for a brief period in 2017, check for it and remove.
|
||||
//TODO: Check if this gets handled by InvalidJson now.
|
||||
else if (typeof(TJournal) == typeof(Scan) && json.Contains("\"RotationPeriod\":inf"))
|
||||
{
|
||||
deserialized = JsonSerializer.Deserialize<TJournal>(json.Replace("\"RotationPeriod\":inf,", ""));
|
||||
}
|
||||
else
|
||||
{
|
||||
deserialized = JsonSerializer.Deserialize<TJournal>(json);
|
||||
}
|
||||
deserialized.Json = json;
|
||||
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
|
||||
public static Dictionary<string, Type> PopulateEventClasses()
|
||||
{
|
||||
var eventClasses = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
var allTypes = Assembly.GetAssembly(typeof(JournalBase)).GetTypes();
|
||||
|
||||
var journalTypes = allTypes.Where(a => a.IsSubclassOf(typeof(JournalBase)));
|
||||
|
||||
foreach (var journalType in journalTypes)
|
||||
{
|
||||
eventClasses.Add(journalType.Name, journalType);
|
||||
}
|
||||
|
||||
eventClasses.Add("JournalBase", typeof(JournalBase));
|
||||
|
||||
return eventClasses;
|
||||
}
|
||||
}
|
451
Pulsar/Utils/LogMonitor.cs
Normal file
451
Pulsar/Utils/LogMonitor.cs
Normal file
@ -0,0 +1,451 @@
|
||||
using System.Diagnostics;
|
||||
using Observatory.Framework;
|
||||
using Observatory.Framework.Files;
|
||||
using Observatory.Framework.Files.Journal;
|
||||
|
||||
namespace Pulsar.Utils;
|
||||
|
||||
class LogMonitor
|
||||
{
|
||||
#region Singleton Instantiation
|
||||
|
||||
public static LogMonitor GetInstance
|
||||
{
|
||||
get
|
||||
{
|
||||
return _instance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Lazy<LogMonitor> _instance = new(NewLogMonitor);
|
||||
|
||||
private static LogMonitor NewLogMonitor()
|
||||
{
|
||||
return new LogMonitor();
|
||||
}
|
||||
|
||||
private LogMonitor()
|
||||
{
|
||||
currentLine = new();
|
||||
journalTypes = JournalReader.PopulateEventClasses();
|
||||
InitializeWatchers(string.Empty);
|
||||
SetLogMonitorState(LogMonitorState.Idle);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public properties
|
||||
public LogMonitorState CurrentState
|
||||
{
|
||||
get => currentState;
|
||||
}
|
||||
|
||||
public Status Status { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (firstStartMonitor)
|
||||
{
|
||||
// Only pre-read on first start monitor. Beyond that it's simply pause/resume.
|
||||
firstStartMonitor = false;
|
||||
PrereadJournals();
|
||||
}
|
||||
journalWatcher.EnableRaisingEvents = true;
|
||||
statusWatcher.EnableRaisingEvents = true;
|
||||
SetLogMonitorState(LogMonitorState.Realtime);
|
||||
JournalPoke();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
journalWatcher.EnableRaisingEvents = false;
|
||||
statusWatcher.EnableRaisingEvents = false;
|
||||
SetLogMonitorState(LogMonitorState.Idle);
|
||||
}
|
||||
|
||||
public void ChangeWatchedDirectory(string path)
|
||||
{
|
||||
journalWatcher.Dispose();
|
||||
statusWatcher.Dispose();
|
||||
InitializeWatchers(path);
|
||||
}
|
||||
|
||||
public bool IsMonitoring()
|
||||
{
|
||||
return currentState.HasFlag(LogMonitorState.Realtime);
|
||||
}
|
||||
|
||||
// TODO(fredjk_gh): Remove?
|
||||
public bool ReadAllInProgress()
|
||||
{
|
||||
return LogMonitorStateChangedEventArgs.IsBatchRead(currentState);
|
||||
}
|
||||
|
||||
public Func<IEnumerable<string>> ReadAllGenerator(out int fileCount)
|
||||
{
|
||||
// Prevent pre-reading when starting monitoring after reading all.
|
||||
firstStartMonitor = false;
|
||||
SetLogMonitorState(currentState | LogMonitorState.Batch);
|
||||
|
||||
var logDirectory = GetJournalFolder();
|
||||
var files = GetJournalFilesOrdered(logDirectory);
|
||||
fileCount = files.Count();
|
||||
|
||||
IEnumerable<string> ReadAllJournals()
|
||||
{
|
||||
var readErrors = new List<(Exception ex, string file, string line)>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
yield return file.Name;
|
||||
readErrors.AddRange(
|
||||
ProcessLines(ReadAllLines(file.FullName), file.Name));
|
||||
}
|
||||
|
||||
ReportErrors(readErrors);
|
||||
SetLogMonitorState(currentState & ~LogMonitorState.Batch);
|
||||
};
|
||||
|
||||
return ReadAllJournals;
|
||||
}
|
||||
|
||||
public void PrereadJournals()
|
||||
{
|
||||
SetLogMonitorState(currentState | LogMonitorState.PreRead);
|
||||
|
||||
var logDirectory = GetJournalFolder();
|
||||
var files = GetJournalFilesOrdered(logDirectory).ToList();
|
||||
|
||||
// Read at most the last two files (in case we were launched after the game and the latest
|
||||
// journal is mostly empty) but keeping only the lines since the last FSDJump.
|
||||
List<string> lastSystemLines = new();
|
||||
List<string> lastFileLines = new();
|
||||
List<string> fileHeaderLines = new();
|
||||
var sawFSDJump = false;
|
||||
foreach (var file in files.Skip(Math.Max(files.Count - 2, 0)))
|
||||
{
|
||||
var lines = ReadAllLines(file.FullName);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var eventType = JournalUtilities.GetEventType(line);
|
||||
if (eventType.Equals("FSDJump") || (eventType.Equals("CarrierJump") && (line.Contains("\"Docked\":true") || line.Contains("\"OnFoot\":true"))))
|
||||
{
|
||||
// Reset, start collecting again.
|
||||
lastSystemLines.Clear();
|
||||
sawFSDJump = true;
|
||||
}
|
||||
else if (eventType.Equals("Fileheader"))
|
||||
{
|
||||
lastFileLines.Clear();
|
||||
fileHeaderLines.Clear();
|
||||
fileHeaderLines.Add(line);
|
||||
}
|
||||
else if (eventType.Equals("LoadGame") || eventType.Equals("Statistics"))
|
||||
{
|
||||
// A few header lines to collect.
|
||||
fileHeaderLines.Add(line);
|
||||
}
|
||||
lastSystemLines.Add(line);
|
||||
lastFileLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't see a jump in the recent logs (Cmdr is stationary in a system for a while
|
||||
// ie. deep-space mining from a carrier), at very least, read from the beginning of the
|
||||
// current journal file which includes the important stuff like the last "LoadGame", etc. This
|
||||
// also helps out in cases where one forgets to hit "Start Monitor" until part-way into the
|
||||
// session (if auto-start is not enabled).
|
||||
List<string> linesToRead = lastFileLines;
|
||||
if (sawFSDJump)
|
||||
{
|
||||
// If we saw any relevant header lines, insert them as well. This ensures odyssey biologicials are properly
|
||||
// counted/presented, current Commander name is present, etc.
|
||||
if (fileHeaderLines.Count > 0)
|
||||
{
|
||||
lastSystemLines.InsertRange(0, fileHeaderLines);
|
||||
}
|
||||
linesToRead = lastSystemLines;
|
||||
}
|
||||
|
||||
ReportErrors(ProcessLines(linesToRead, "Pre-read"));
|
||||
SetLogMonitorState(currentState & ~LogMonitorState.PreRead);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Events
|
||||
|
||||
public event EventHandler<LogMonitorStateChangedEventArgs> LogMonitorStateChanged;
|
||||
|
||||
public event EventHandler<JournalEventArgs> JournalEntry;
|
||||
|
||||
public event EventHandler<JournalEventArgs> StatusUpdate;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private FileSystemWatcher? journalWatcher;
|
||||
private FileSystemWatcher? statusWatcher;
|
||||
private readonly Dictionary<string, Type> journalTypes;
|
||||
private readonly Dictionary<string, int> currentLine;
|
||||
private LogMonitorState currentState = LogMonitorState.Idle; // Change via #SetLogMonitorState
|
||||
private bool firstStartMonitor = true;
|
||||
private readonly string[] EventsWithAncillaryFile = {
|
||||
"Cargo",
|
||||
"NavRoute",
|
||||
"Market",
|
||||
"Outfitting",
|
||||
"Shipyard",
|
||||
"Backpack",
|
||||
"FCMaterials",
|
||||
"ModuleInfo",
|
||||
"ShipLocker"
|
||||
};
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void SetLogMonitorState(LogMonitorState newState)
|
||||
{
|
||||
var oldState = currentState;
|
||||
currentState = newState;
|
||||
LogMonitorStateChanged?.Invoke(this, new LogMonitorStateChangedEventArgs
|
||||
{
|
||||
PreviousState = oldState,
|
||||
NewState = newState
|
||||
}); ;
|
||||
|
||||
Debug.WriteLine("LogMonitor State change: {0} -> {1}", oldState, newState);
|
||||
}
|
||||
|
||||
private void InitializeWatchers(string path)
|
||||
{
|
||||
var logDirectory = GetJournalFolder();
|
||||
|
||||
journalWatcher = new FileSystemWatcher(logDirectory, "Journal.*.??.log")
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size |
|
||||
NotifyFilters.FileName | NotifyFilters.CreationTime
|
||||
};
|
||||
journalWatcher.Changed += LogChangedEvent;
|
||||
journalWatcher.Created += LogCreatedEvent;
|
||||
|
||||
statusWatcher = new FileSystemWatcher(logDirectory, "Status.json")
|
||||
{
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
|
||||
};
|
||||
statusWatcher.Changed += StatusUpdateEvent;
|
||||
}
|
||||
|
||||
private static string GetJournalFolder()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private List<(Exception ex, string file, string line)> ProcessLines(List<string> lines, string file)
|
||||
{
|
||||
var readErrors = new List<(Exception ex, string file, string line)>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
try
|
||||
{
|
||||
DeserializeAndInvoke(line);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
readErrors.Add((ex, file, line));
|
||||
}
|
||||
}
|
||||
return readErrors;
|
||||
}
|
||||
|
||||
private JournalEventArgs DeserializeToEventArgs(string eventType, string line)
|
||||
{
|
||||
|
||||
var eventClass = journalTypes[eventType];
|
||||
var journalRead = typeof(JournalReader).GetMethod(nameof(JournalReader.ObservatoryDeserializer));
|
||||
var journalGeneric = journalRead.MakeGenericMethod(eventClass);
|
||||
var entry = journalGeneric.Invoke(null, new object[] { line });
|
||||
return new JournalEventArgs { journalType = eventClass, journalEvent = entry };
|
||||
}
|
||||
|
||||
private void DeserializeAndInvoke(string line)
|
||||
{
|
||||
var eventType = JournalUtilities.GetEventType(line);
|
||||
if (!journalTypes.ContainsKey(eventType))
|
||||
{
|
||||
eventType = "JournalBase";
|
||||
}
|
||||
|
||||
var journalEvent = DeserializeToEventArgs(eventType, line);
|
||||
|
||||
JournalEntry?.Invoke(this, journalEvent);
|
||||
|
||||
// Files are only valid if realtime, otherwise they will be stale or empty.
|
||||
if (!currentState.HasFlag(LogMonitorState.Batch) && EventsWithAncillaryFile.Contains(eventType))
|
||||
{
|
||||
HandleAncillaryFile(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleAncillaryFile(string eventType)
|
||||
{
|
||||
var filename = eventType == "ModuleInfo"
|
||||
? "ModulesInfo.json" // Just FDev things
|
||||
: eventType + ".json";
|
||||
|
||||
// I have no idea what order Elite writes these files or if they're already written
|
||||
// by the time the journal updates.
|
||||
// Brief sleep to ensure the content is updated before we read it.
|
||||
|
||||
// Some files are still locked by another process after 50ms.
|
||||
// Retry every 50ms for 0.5 seconds before giving up.
|
||||
|
||||
string fileContent = null;
|
||||
var retryCount = 0;
|
||||
|
||||
while (fileContent == null && retryCount < 10)
|
||||
{
|
||||
Thread.Sleep(50);
|
||||
try
|
||||
{
|
||||
using var fileStream = File.Open(journalWatcher.Path + Path.DirectorySeparatorChar + filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(fileStream);
|
||||
fileContent = reader.ReadToEnd();
|
||||
var fileObject = DeserializeToEventArgs(eventType + "File", fileContent);
|
||||
JournalEntry?.Invoke(this, fileObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
|
||||
{
|
||||
if (readErrors.Any())
|
||||
{
|
||||
var errorList = readErrors.Select(error =>
|
||||
{
|
||||
string message;
|
||||
if (error.ex.InnerException == null)
|
||||
{
|
||||
message = error.ex.Message;
|
||||
}
|
||||
else
|
||||
{
|
||||
message = error.ex.InnerException.Message;
|
||||
}
|
||||
return ($"Error reading file {error.file}: {message}", error.line);
|
||||
});
|
||||
|
||||
ErrorReporter.ShowErrorPopup($"Journal Read Error{(readErrors.Count > 1 ? "s" : "")}", errorList.ToList());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void LogChangedEvent(object source, FileSystemEventArgs eventArgs)
|
||||
{
|
||||
var fileContent = ReadAllLines(eventArgs.FullPath);
|
||||
|
||||
if (!currentLine.ContainsKey(eventArgs.FullPath))
|
||||
{
|
||||
currentLine.Add(eventArgs.FullPath, fileContent.Count - 1);
|
||||
}
|
||||
|
||||
foreach (var line in fileContent.Skip(currentLine[eventArgs.FullPath]))
|
||||
{
|
||||
try
|
||||
{
|
||||
DeserializeAndInvoke(line);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReportErrors(new List<(Exception ex, string file, string line)> { (ex, eventArgs.Name ?? string.Empty, line) });
|
||||
}
|
||||
}
|
||||
|
||||
currentLine[eventArgs.FullPath] = fileContent.Count;
|
||||
}
|
||||
|
||||
private static List<string> ReadAllLines(string path)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
try
|
||||
{
|
||||
using StreamReader file = new(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
|
||||
while (!file.EndOfStream)
|
||||
{
|
||||
lines.Add(file.ReadLine() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
catch (IOException ioEx)
|
||||
{
|
||||
ReportErrors(new List<(Exception, string, string)> { (ioEx, path, "<reading all lines>") });
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private void LogCreatedEvent(object source, FileSystemEventArgs eventArgs)
|
||||
{
|
||||
currentLine.Add(eventArgs.FullPath, 0);
|
||||
LogChangedEvent(source, eventArgs);
|
||||
}
|
||||
|
||||
private void StatusUpdateEvent(object source, FileSystemEventArgs eventArgs)
|
||||
{
|
||||
var handler = StatusUpdate;
|
||||
var statusLines = ReadAllLines(eventArgs.FullPath);
|
||||
if (statusLines.Count > 0)
|
||||
{
|
||||
var status = JournalReader.ObservatoryDeserializer<Status>(statusLines[0]);
|
||||
Status = status;
|
||||
handler?.Invoke(this, new JournalEventArgs { journalType = typeof(Status), journalEvent = status });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Touches most recent journal file once every 250ms while LogMonitor is monitoring.
|
||||
/// Forces pending file writes to flush to disk and fires change events for new journal lines.
|
||||
/// </summary>
|
||||
private async void JournalPoke()
|
||||
{
|
||||
var journalFolder = GetJournalFolder();
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
while (IsMonitoring())
|
||||
{
|
||||
var journals = GetJournalFilesOrdered(journalFolder);
|
||||
|
||||
if (journals.Any())
|
||||
{
|
||||
var fileToPoke = GetJournalFilesOrdered(journalFolder).Last();
|
||||
using var stream = fileToPoke.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
stream.Close();
|
||||
}
|
||||
Thread.Sleep(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetSavedGamesPath()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static IEnumerable<FileInfo> GetJournalFilesOrdered(string path)
|
||||
{
|
||||
var journalFolder = new DirectoryInfo(path);
|
||||
return from file in journalFolder.GetFiles("Journal.*.??.log")
|
||||
orderby file.LastWriteTime
|
||||
select file;
|
||||
}
|
||||
#endregion
|
||||
}
|
14
Pulsar/Utils/SettingsManager.cs
Normal file
14
Pulsar/Utils/SettingsManager.cs
Normal file
@ -0,0 +1,14 @@
|
||||
namespace Pulsar.Utils;
|
||||
|
||||
internal static class SettingsManager
|
||||
{
|
||||
internal static void Save()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
internal static void Load()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user