mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-04-05 17:39:39 -04:00
Export, plugin archive install, and herald cache race condition fixes (#82)
* WIP: Grid export and plugin extraction * Tweak export process * Check for recursion of the same assembly load. * Individual screens aren't always primary? * Wait for cache to be writable * Export selection only. * Update built xml docs * Ignore invalid archives. * Need to ensure task is started.
This commit is contained in:
parent
a67cf7f6bb
commit
fb45b5c3e2
@ -174,12 +174,6 @@ namespace Observatory.PluginManagement
|
|||||||
Properties.Core.Default.Save();
|
Properties.Core.Default.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
//private static string GetSettingsFile(IObservatoryPlugin plugin)
|
|
||||||
//{
|
|
||||||
// var configDirectory = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory;
|
|
||||||
// return configDirectory.FullName + "\\" + plugin.Name + ".json";
|
|
||||||
//}
|
|
||||||
|
|
||||||
private static List<string> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers)
|
private static List<string> LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers)
|
||||||
{
|
{
|
||||||
observatoryWorkers = new();
|
observatoryWorkers = new();
|
||||||
@ -190,6 +184,8 @@ namespace Observatory.PluginManagement
|
|||||||
|
|
||||||
if (Directory.Exists(pluginPath))
|
if (Directory.Exists(pluginPath))
|
||||||
{
|
{
|
||||||
|
ExtractPlugins(pluginPath);
|
||||||
|
|
||||||
//Temporarily skipping signature checks. Need to do this the right way later.
|
//Temporarily skipping signature checks. Need to do this the right way later.
|
||||||
var pluginLibraries = Directory.GetFiles($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins", "*.dll");
|
var pluginLibraries = Directory.GetFiles($"{AppDomain.CurrentDomain.BaseDirectory}{Path.DirectorySeparatorChar}plugins", "*.dll");
|
||||||
//var coreToken = Assembly.GetExecutingAssembly().GetName().GetPublicKeyToken();
|
//var coreToken = Assembly.GetExecutingAssembly().GetName().GetPublicKeyToken();
|
||||||
@ -241,9 +237,32 @@ namespace Observatory.PluginManagement
|
|||||||
return errorList;
|
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
|
||||||
|
{
|
||||||
|
System.IO.Compression.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)
|
private static string LoadPluginAssembly(string dllPath, List<(IObservatoryWorker plugin, PluginStatus signed)> workers, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
string recursionGuard = string.Empty;
|
||||||
|
|
||||||
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, name) => {
|
System.Runtime.Loader.AssemblyLoadContext.Default.Resolving += (context, name) => {
|
||||||
|
|
||||||
if (name.Name.EndsWith("resources"))
|
if (name.Name.EndsWith("resources"))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -262,7 +281,17 @@ namespace Observatory.PluginManagement
|
|||||||
return context.LoadFromAssemblyPath(foundDlls[0]);
|
return context.LoadFromAssemblyPath(foundDlls[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name.Name != recursionGuard)
|
||||||
|
{
|
||||||
|
recursionGuard = name.Name;
|
||||||
|
|
||||||
return context.LoadFromAssemblyName(name);
|
return context.LoadFromAssemblyName(name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to load assembly " + name.Name);
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var pluginAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName);
|
var pluginAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName);
|
||||||
|
14
ObservatoryCore/Properties/Core.Designer.cs
generated
14
ObservatoryCore/Properties/Core.Designer.cs
generated
@ -12,7 +12,7 @@ namespace Observatory.Properties {
|
|||||||
|
|
||||||
|
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")]
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.1.0.0")]
|
||||||
internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase {
|
internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase {
|
||||||
|
|
||||||
private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core())));
|
private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core())));
|
||||||
@ -250,5 +250,17 @@ namespace Observatory.Properties {
|
|||||||
this["StartMonitor"] = value;
|
this["StartMonitor"] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||||
|
public string ExportFolder {
|
||||||
|
get {
|
||||||
|
return ((string)(this["ExportFolder"]));
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
this["ExportFolder"] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,5 +59,8 @@
|
|||||||
<Setting Name="StartMonitor" Type="System.Boolean" Scope="User">
|
<Setting Name="StartMonitor" Type="System.Boolean" Scope="User">
|
||||||
<Value Profile="(Default)">False</Value>
|
<Value Profile="(Default)">False</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
|
<Setting Name="ExportFolder" Type="System.String" Scope="User">
|
||||||
|
<Value Profile="(Default)" />
|
||||||
|
</Setting>
|
||||||
</Settings>
|
</Settings>
|
||||||
</SettingsFile>
|
</SettingsFile>
|
@ -16,6 +16,7 @@ namespace Observatory.UI.ViewModels
|
|||||||
{
|
{
|
||||||
private ObservableCollection<object> basicUIGrid;
|
private ObservableCollection<object> basicUIGrid;
|
||||||
|
|
||||||
|
public System.Collections.IList SelectedItems { get; set; }
|
||||||
|
|
||||||
public ObservableCollection<object> BasicUIGrid
|
public ObservableCollection<object> BasicUIGrid
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Observatory.Framework.Interfaces;
|
using Observatory.Framework.Interfaces;
|
||||||
using Observatory.UI.Models;
|
using Observatory.UI.Models;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@ -114,6 +115,104 @@ namespace Observatory.UI.ViewModels
|
|||||||
Process.Start(githubOpen);
|
Process.Start(githubOpen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void ExportGrid()
|
||||||
|
{
|
||||||
|
var exportFolder = Properties.Core.Default.ExportFolder;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(exportFolder))
|
||||||
|
{
|
||||||
|
exportFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenFolderDialog openFolderDialog = new()
|
||||||
|
{
|
||||||
|
Directory = exportFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
var application = (IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current.ApplicationLifetime;
|
||||||
|
|
||||||
|
var selectedFolder = await openFolderDialog.ShowAsync(application.MainWindow);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selectedFolder))
|
||||||
|
{
|
||||||
|
Properties.Core.Default.ExportFolder = selectedFolder;
|
||||||
|
Properties.Core.Default.Save();
|
||||||
|
exportFolder = selectedFolder;
|
||||||
|
|
||||||
|
foreach (var tab in tabs.Where(t => t.Name != "Core"))
|
||||||
|
{
|
||||||
|
var ui = (BasicUIViewModel)tab.UI;
|
||||||
|
List<object> selectedData;
|
||||||
|
bool specificallySelected = ui.SelectedItems?.Count > 1;
|
||||||
|
|
||||||
|
if (specificallySelected)
|
||||||
|
{
|
||||||
|
selectedData = new();
|
||||||
|
|
||||||
|
foreach (var item in ui.SelectedItems)
|
||||||
|
selectedData.Add(item);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectedData = ui.BasicUIGrid.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var columns = selectedData[0].GetType().GetProperties();
|
||||||
|
Dictionary<string, int> colSize = new();
|
||||||
|
Dictionary<string, List<string>> colContent = new();
|
||||||
|
|
||||||
|
foreach (var column in columns)
|
||||||
|
{
|
||||||
|
colSize.Add(column.Name, 0);
|
||||||
|
colContent.Add(column.Name, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineType = selectedData[0].GetType();
|
||||||
|
|
||||||
|
foreach (var line in selectedData)
|
||||||
|
{
|
||||||
|
foreach (var column in colContent)
|
||||||
|
{
|
||||||
|
var cellValue = lineType.GetProperty(column.Key).GetValue(line)?.ToString() ?? string.Empty;
|
||||||
|
column.Value.Add(cellValue);
|
||||||
|
if (colSize[column.Key] < cellValue.Length)
|
||||||
|
colSize[column.Key] = cellValue.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Text.StringBuilder exportData = new();
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var colTitle in colContent.Keys)
|
||||||
|
{
|
||||||
|
if (colSize[colTitle] < colTitle.Length)
|
||||||
|
colSize[colTitle] = colTitle.Length;
|
||||||
|
|
||||||
|
exportData.Append(colTitle.PadRight(colSize[colTitle]) + " ");
|
||||||
|
}
|
||||||
|
exportData.AppendLine();
|
||||||
|
|
||||||
|
for (int i = 0; i < colContent.First().Value.Count; i++)
|
||||||
|
{
|
||||||
|
foreach(var column in colContent)
|
||||||
|
{
|
||||||
|
if (column.Value[i].Length > 0 && !char.IsNumber(column.Value[i][0]) && column.Value[i].Count(char.IsLetter) / (float)column.Value[i].Length > 0.25)
|
||||||
|
exportData.Append(column.Value[i].PadRight(colSize[column.Key]) + " ");
|
||||||
|
else
|
||||||
|
exportData.Append(column.Value[i].PadLeft(colSize[column.Key]) + " ");
|
||||||
|
}
|
||||||
|
exportData.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
string exportPath = $"{exportFolder}{System.IO.Path.DirectorySeparatorChar}Observatory Export - {DateTime.UtcNow:yyyyMMdd-HHmmss} - {tab.Name}.txt";
|
||||||
|
|
||||||
|
System.IO.File.WriteAllText(exportPath, exportData.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string ToggleButtonText
|
public string ToggleButtonText
|
||||||
{
|
{
|
||||||
get => toggleButtonText;
|
get => toggleButtonText;
|
||||||
@ -158,7 +257,7 @@ namespace Observatory.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CheckUpdate()
|
private static bool CheckUpdate()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -179,7 +278,11 @@ namespace Observatory.UI.ViewModels
|
|||||||
|
|
||||||
foreach (var release in releases)
|
foreach (var release in releases)
|
||||||
{
|
{
|
||||||
var ver = release.GetProperty("tag_name").ToString()[1..].Split('.').Select(verString => int.Parse(verString)).ToArray();
|
var tag = release.GetProperty("tag_name").ToString();
|
||||||
|
var verstrings = tag[1..].Split('.');
|
||||||
|
var ver = verstrings.Select(verString => { _ = int.TryParse(verString, out int ver); return ver; }).ToArray();
|
||||||
|
if (ver.Length == 4)
|
||||||
|
{
|
||||||
Version version = new(ver[0], ver[1], ver[2], ver[3]);
|
Version version = new(ver[0], ver[1], ver[2], ver[3]);
|
||||||
if (version > System.Reflection.Assembly.GetEntryAssembly().GetName().Version)
|
if (version > System.Reflection.Assembly.GetEntryAssembly().GetName().Version)
|
||||||
{
|
{
|
||||||
@ -187,7 +290,7 @@ namespace Observatory.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
@ -82,10 +82,11 @@ namespace Observatory.UI.Views
|
|||||||
SelectionMode = DataGridSelectionMode.Extended,
|
SelectionMode = DataGridSelectionMode.Extended,
|
||||||
GridLinesVisibility = DataGridGridLinesVisibility.Vertical,
|
GridLinesVisibility = DataGridGridLinesVisibility.Vertical,
|
||||||
AutoGenerateColumns = true,
|
AutoGenerateColumns = true,
|
||||||
IsReadOnly = true,
|
IsReadOnly = true
|
||||||
};
|
};
|
||||||
dataGrid.AutoGeneratingColumn += ColumnGeneration;
|
dataGrid.AutoGeneratingColumn += ColumnGeneration;
|
||||||
dataGrid.DataContextChanged += OnDataContextSet;
|
dataGrid.DataContextChanged += OnDataContextSet;
|
||||||
|
dataGrid.SelectionChanged += OnSelectionChanged;
|
||||||
uiPanel.Children.Clear();
|
uiPanel.Children.Clear();
|
||||||
uiPanel.Children.Add(dataGrid);
|
uiPanel.Children.Add(dataGrid);
|
||||||
break;
|
break;
|
||||||
@ -103,6 +104,12 @@ namespace Observatory.UI.Views
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
((Observatory.UI.ViewModels.BasicUIViewModel)dataGrid.DataContext).SelectedItems = dataGrid.SelectedItems;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDataContextSet(object sender, EventArgs e)
|
private void OnDataContextSet(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (UIType != PluginUI.UIType.Basic || !(sender is DataGrid)) return;
|
if (UIType != PluginUI.UIType.Basic || !(sender is DataGrid)) return;
|
||||||
|
@ -73,6 +73,14 @@
|
|||||||
Cursor="Hand">
|
Cursor="Hand">
|
||||||
Update Available
|
Update Available
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="export"
|
||||||
|
Margin="10"
|
||||||
|
FontSize="15"
|
||||||
|
Command="{Binding ExportGrid}"
|
||||||
|
Content="Export">
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
Name="ToggleMonitor"
|
Name="ToggleMonitor"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
|
@ -130,6 +130,9 @@ namespace Observatory.UI.Views
|
|||||||
int corner = Properties.Core.Default.NativeNotifyCorner;
|
int corner = Properties.Core.Default.NativeNotifyCorner;
|
||||||
|
|
||||||
if (screen == -1 || screen > Screens.All.Count)
|
if (screen == -1 || screen > Screens.All.Count)
|
||||||
|
if (Screens.All.Count == 1)
|
||||||
|
screenBounds = Screens.All[0].Bounds;
|
||||||
|
else
|
||||||
screenBounds = Screens.Primary.Bounds;
|
screenBounds = Screens.Primary.Bounds;
|
||||||
else
|
else
|
||||||
screenBounds = Screens.All[screen - 1].Bounds;
|
screenBounds = Screens.All[screen - 1].Bounds;
|
||||||
|
@ -873,6 +873,13 @@
|
|||||||
Version string of Observatory Core.
|
Version string of Observatory Core.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="M:Observatory.Framework.Interfaces.IObservatoryCore.GetPluginErrorLogger(Observatory.Framework.Interfaces.IObservatoryPlugin)">
|
||||||
|
<summary>
|
||||||
|
Returns a delegate for logging an error for the calling plugin. A plugin can wrap this method
|
||||||
|
or pass it along to its collaborators.
|
||||||
|
</summary>
|
||||||
|
<param name="plugin">The calling plugin</param>
|
||||||
|
</member>
|
||||||
<member name="M:Observatory.Framework.Interfaces.IObservatoryCore.ExecuteOnUIThread(System.Action)">
|
<member name="M:Observatory.Framework.Interfaces.IObservatoryCore.ExecuteOnUIThread(System.Action)">
|
||||||
<summary>
|
<summary>
|
||||||
Perform an action on the current Avalonia UI thread.
|
Perform an action on the current Avalonia UI thread.
|
||||||
|
@ -229,7 +229,7 @@ namespace Observatory.Herald
|
|||||||
return demonym;
|
return demonym;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAndPruneCache(FileInfo currentFile)
|
private async void UpdateAndPruneCache(FileInfo currentFile)
|
||||||
{
|
{
|
||||||
Dictionary<string, CacheData> cacheIndex;
|
Dictionary<string, CacheData> cacheIndex;
|
||||||
|
|
||||||
@ -289,12 +289,40 @@ namespace Observatory.Herald
|
|||||||
cacheIndex.Remove(staleFile);
|
cacheIndex.Remove(staleFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Race conditions between title and detail speech make a collision here possible.
|
||||||
|
// Wait for file to become writable, but return control to call site while we wait.
|
||||||
|
System.Diagnostics.Stopwatch stopwatch = new();
|
||||||
|
stopwatch.Start();
|
||||||
|
|
||||||
|
while (!IsFileWritable(cacheIndexFile) && stopwatch.ElapsedMilliseconds < 1000)
|
||||||
|
await Task.Factory.StartNew(() => System.Threading.Thread.Sleep(100));
|
||||||
|
|
||||||
|
// 1000ms should be more than enough for a conflicting title or detail to complete,
|
||||||
|
// if we're still waiting something else is locking the file, just give up.
|
||||||
|
if (stopwatch.ElapsedMilliseconds < 1000)
|
||||||
|
{
|
||||||
File.WriteAllText(cacheIndexFile, JsonSerializer.Serialize(cacheIndex));
|
File.WriteAllText(cacheIndexFile, JsonSerializer.Serialize(cacheIndex));
|
||||||
|
|
||||||
// Purge cache from earlier versions, if still present.
|
// Purge cache from earlier versions, if still present.
|
||||||
var legacyCache = cacheLocation.GetFiles("*.wav");
|
var legacyCache = cacheLocation.GetFiles("*.wav");
|
||||||
Array.ForEach(legacyCache, file => File.Delete(file.FullName));
|
Array.ForEach(legacyCache, file => File.Delete(file.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFileWritable(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using FileStream fs = File.OpenWrite(path);
|
||||||
|
fs.Close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheData
|
public class CacheData
|
||||||
|
Loading…
x
Reference in New Issue
Block a user