|
@@ -1,12 +1,19 @@
|
|
|
using System.Diagnostics;
|
|
|
+using System.Globalization;
|
|
|
using System.IO;
|
|
|
using System.IO.Compression;
|
|
|
using System.Reflection;
|
|
|
using System.Text;
|
|
|
+using System.Windows.Input;
|
|
|
using Newtonsoft.Json;
|
|
|
+using PixiEditor.Extensions.Common.Localization;
|
|
|
+using PixiEditor.Extensions.Common.UserPreferences;
|
|
|
using PixiEditor.Helpers;
|
|
|
+using PixiEditor.Models.Commands;
|
|
|
+using PixiEditor.Models.Enums;
|
|
|
using PixiEditor.Parser;
|
|
|
using PixiEditor.ViewModels.SubViewModels.Document;
|
|
|
+using PixiEditor.Views;
|
|
|
|
|
|
namespace PixiEditor.Models.DataHolders;
|
|
|
|
|
@@ -20,14 +27,59 @@ internal class CrashReport : IDisposable
|
|
|
|
|
|
builder
|
|
|
.AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString(moreSpecific: true)} x{IntPtr.Size * 8} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss} {currentTime:zzz}")
|
|
|
+ .AppendLine($"Application started {GetFormatted(() => Process.GetCurrentProcess().StartTime, "yyyy.MM.dd HH:hh:ss")}, {GetFormatted(() => DateTime.Now - Process.GetCurrentProcess().StartTime, @"d\ hh\:mm\.ss")} ago")
|
|
|
.AppendLine($"Report: {Guid.NewGuid()}\n")
|
|
|
.AppendLine("-----System Information----")
|
|
|
.AppendLine("General:")
|
|
|
.AppendLine($" OS: {Environment.OSVersion.VersionString}")
|
|
|
+ .AppendLine($" Has Stylus Tablet Device: {GetFormatted(() => HasTabletDevice(TabletDeviceType.Stylus))}")
|
|
|
+ .AppendLine($" Has Touch Tablet Device: {GetFormatted(() => HasTabletDevice(TabletDeviceType.Touch))}")
|
|
|
.AppendLine();
|
|
|
|
|
|
CrashHelper helper = new();
|
|
|
|
|
|
+ AppendHardwareInfo(helper, builder);
|
|
|
+
|
|
|
+ builder.AppendLine("\n--------Command Log--------\n");
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ builder.Append(CommandController.Current.Log.GetSummary(currentTime.LocalDateTime));
|
|
|
+ }
|
|
|
+ catch (Exception cemLogException)
|
|
|
+ {
|
|
|
+ builder.AppendLine($"Error ({cemLogException.GetType().FullName}: {cemLogException.Message}) while gathering command log, skipping...");
|
|
|
+ }
|
|
|
+
|
|
|
+ builder.AppendLine("\n-----------State-----------");
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ AppendStateInfo(builder);
|
|
|
+ }
|
|
|
+ catch (Exception stateException)
|
|
|
+ {
|
|
|
+ builder.AppendLine($"Error ({stateException.GetType().FullName}: {stateException.Message}) while gathering state (Must be bug in GetPreferenceFormatted, GetFormatted or StringBuilder.AppendLine as these should not throw), skipping...");
|
|
|
+ }
|
|
|
+
|
|
|
+ CrashHelper.AddExceptionMessage(builder, exception);
|
|
|
+
|
|
|
+ string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.zip";
|
|
|
+ string path = Path.Combine(
|
|
|
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
+ "PixiEditor",
|
|
|
+ "crash_logs");
|
|
|
+ Directory.CreateDirectory(path);
|
|
|
+
|
|
|
+ CrashReport report = new();
|
|
|
+ report.FilePath = Path.Combine(path, filename);
|
|
|
+ report.ReportText = builder.ToString();
|
|
|
+
|
|
|
+ return report;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void AppendHardwareInfo(CrashHelper helper, StringBuilder builder)
|
|
|
+ {
|
|
|
try
|
|
|
{
|
|
|
helper.GetCPUInformation(builder);
|
|
@@ -54,21 +106,146 @@ internal class CrashReport : IDisposable
|
|
|
{
|
|
|
builder.AppendLine($"Error ({memE.GetType().FullName}: {memE.Message}) while gathering memory information, skipping...");
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- CrashHelper.AddExceptionMessage(builder, exception);
|
|
|
+ private static void AppendStateInfo(StringBuilder builder)
|
|
|
+ {
|
|
|
+ builder
|
|
|
+ .AppendLine("Environment:")
|
|
|
+ .AppendLine($" Thread Count: {GetFormatted(() => Process.GetCurrentProcess().Threads.Count)}")
|
|
|
+ .AppendLine("\nCulture:")
|
|
|
+ .AppendLine($" Selected language: {GetPreferenceFormatted("LanguageCode", true, "system")}")
|
|
|
+ .AppendLine($" Current Culture: {GetFormatted(() => CultureInfo.CurrentCulture)}")
|
|
|
+ .AppendLine($" Current UI Culture: {GetFormatted(() => CultureInfo.CurrentUICulture)}")
|
|
|
+ .AppendLine("\nPreferences:")
|
|
|
+ .AppendLine($" Has shared toolbar enabled: {GetPreferenceFormatted("EnableSharedToolbar", true, false)}")
|
|
|
+ .AppendLine($" Right click mode: {GetPreferenceFormatted<RightClickMode>("RightClickMode", true)}")
|
|
|
+ .AppendLine($" Has Rich presence enabled: {GetPreferenceFormatted("EnableRichPresence", true, true)}")
|
|
|
+ .AppendLine($" Debug Mode enabled: {GetPreferenceFormatted("IsDebugModeEnabled", true, false)}")
|
|
|
+ .AppendLine("\nUI:")
|
|
|
+ .AppendLine($" MainWindow not null: {GetFormatted(() => MainWindow.Current != null)}")
|
|
|
+ .AppendLine($" MainWindow Size: {GetFormatted(() => MainWindow.Current.RenderSize)}")
|
|
|
+ .AppendLine($" MainWindow State: {GetFormatted(() => MainWindow.Current.WindowState)}")
|
|
|
+ .AppendLine("\nViewModels:")
|
|
|
+ .AppendLine($" Has active updateable change: {GetFormatted(() => ViewModelMain.Current?.DocumentManagerSubViewModel?.ActiveDocument?.UpdateableChangeActive)}")
|
|
|
+ .AppendLine($" Current Tool: {GetFormattedFromViewModelMain(x => x.ToolsSubViewModel?.ActiveTool?.ToolName)}")
|
|
|
+ .AppendLine($" Primary Color: {GetFormattedFromViewModelMain(x => x.ColorsSubViewModel?.PrimaryColor)}")
|
|
|
+ .AppendLine($" Secondary Color: {GetFormattedFromViewModelMain(x => x.ColorsSubViewModel?.SecondaryColor)}")
|
|
|
+ .Append("\nActive Document: ");
|
|
|
|
|
|
- string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.zip";
|
|
|
- string path = Path.Combine(
|
|
|
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
|
- "PixiEditor",
|
|
|
- "crash_logs");
|
|
|
- Directory.CreateDirectory(path);
|
|
|
+ try
|
|
|
+ {
|
|
|
+ AppendActiveDocumentInfo(builder);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ builder.AppendLine($"Could not get active document info:\n{e}");
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- CrashReport report = new();
|
|
|
- report.FilePath = Path.Combine(path, filename);
|
|
|
- report.ReportText = builder.ToString();
|
|
|
+ private static void AppendActiveDocumentInfo(StringBuilder builder)
|
|
|
+ {
|
|
|
+ var main = ViewModelMain.Current;
|
|
|
+
|
|
|
+ if (main == null)
|
|
|
+ {
|
|
|
+ builder.AppendLine("{ ViewModelMain.Current is null }");
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- return report;
|
|
|
+ var manager = main.DocumentManagerSubViewModel;
|
|
|
+
|
|
|
+ if (manager == null)
|
|
|
+ {
|
|
|
+ builder.AppendLine("{ DocumentManagerSubViewModel is null }");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var document = manager.ActiveDocument;
|
|
|
+
|
|
|
+ if (document == null)
|
|
|
+ {
|
|
|
+ builder.AppendLine("null");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ builder
|
|
|
+ .AppendLine()
|
|
|
+ .AppendLine($" Size: {document.SizeBindable}")
|
|
|
+ .AppendLine($" Layer Count: {FormatObject(document.StructureHelper.GetAllLayers().Count)}")
|
|
|
+ .AppendLine($" Has all changes saved: {document.AllChangesSaved}")
|
|
|
+ .AppendLine($" Horizontal Symmetry Enabled: {document.HorizontalSymmetryAxisEnabledBindable}")
|
|
|
+ .AppendLine($" Horizontal Symmetry Value: {FormatObject(document.HorizontalSymmetryAxisYBindable)}")
|
|
|
+ .AppendLine($" Vertical Symmetry Enabled: {document.VerticalSymmetryAxisEnabledBindable}")
|
|
|
+ .AppendLine($" Vertical Symmetry Value: {FormatObject(document.VerticalSymmetryAxisXBindable)}")
|
|
|
+ .AppendLine($" Updateable Change Active: {FormatObject(document.UpdateableChangeActive)}")
|
|
|
+ .AppendLine($" Transform: {FormatObject(document.TransformViewModel)}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private static bool HasTabletDevice(TabletDeviceType type) => Tablet.TabletDevices.Cast<TabletDevice>().Any(tabletDevice => tabletDevice.Type == type);
|
|
|
+
|
|
|
+ private static string GetPreferenceFormatted<T>(string name, bool roaming, T defaultValue = default, string? format = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var preferences = IPreferences.Current;
|
|
|
+
|
|
|
+ if (preferences == null)
|
|
|
+ return "{ Preferences are null }";
|
|
|
+
|
|
|
+ var value = roaming
|
|
|
+ ? preferences.GetPreference(name, defaultValue)
|
|
|
+ : preferences.GetLocalPreference(name, defaultValue);
|
|
|
+
|
|
|
+ return FormatObject(value, format);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ return $$"""{ Failed getting preference: {{e.Message}} }""";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetFormattedFromViewModelMain<T>(Func<ViewModelMain, T?> getter, string? format = null)
|
|
|
+ {
|
|
|
+ var main = ViewModelMain.Current;
|
|
|
+
|
|
|
+ if (main == null)
|
|
|
+ return "{ ViewModelMain.Current is null }";
|
|
|
+
|
|
|
+ return GetFormatted(() => getter(main), format);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string GetFormatted<T>(Func<T?> getter, string? format = null)
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var value = getter();
|
|
|
+
|
|
|
+ return FormatObject(value, format);
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ return $$"""{ Failed retrieving: {{e.Message}} }""";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string FormatObject<T>(T? value, string? format = null)
|
|
|
+ {
|
|
|
+ return value switch
|
|
|
+ {
|
|
|
+ null => "null",
|
|
|
+ IFormattable formattable => formattable.ToString(format, CultureInfo.InvariantCulture),
|
|
|
+ LocalizedString localizedS => FormatLocalizedString(localizedS),
|
|
|
+ string s => $"\"{s}\"",
|
|
|
+ _ => value.ToString()
|
|
|
+ };
|
|
|
+
|
|
|
+ string FormatLocalizedString(LocalizedString localizedS)
|
|
|
+ {
|
|
|
+ return localizedS.Parameters != null
|
|
|
+ ? $"{localizedS.Key} @({string.Join(", ", localizedS.Parameters.Select(x => FormatObject(x, format)))})"
|
|
|
+ : localizedS.Key;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
public static CrashReport Parse(string path)
|
|
@@ -90,38 +267,66 @@ internal class CrashReport : IDisposable
|
|
|
|
|
|
public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
|
|
|
|
|
|
- public List<(string? originalPath, byte[] dotPixiBytes)> RecoverDocuments()
|
|
|
+ public bool TryRecoverDocuments(out List<RecoveredPixi> list)
|
|
|
{
|
|
|
- // Load .pixi files
|
|
|
- Dictionary<string, byte[]> recoveredDocuments = new();
|
|
|
- foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
|
|
|
+ try
|
|
|
{
|
|
|
- using Stream stream = entry.Open();
|
|
|
- using MemoryStream memStream = new();
|
|
|
- stream.CopyTo(memStream);
|
|
|
- recoveredDocuments.Add(entry.Name, memStream.ToArray());
|
|
|
+ list = RecoverDocuments();
|
|
|
+ }
|
|
|
+ catch (Exception e)
|
|
|
+ {
|
|
|
+ list = null;
|
|
|
+ CrashHelper.SendExceptionInfoToWebhook(e);
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
- ZipArchiveEntry? originalPathsEntry = ZipFile.Entries.Where(entry => entry.FullName == "DocumentInfo.json").FirstOrDefault();
|
|
|
- if (originalPathsEntry is null)
|
|
|
- return recoveredDocuments.Select<KeyValuePair<string, byte[]>, (string?, byte[])>(keyValue => (null, keyValue.Value)).ToList();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ public List<RecoveredPixi> RecoverDocuments()
|
|
|
+ {
|
|
|
+ List<RecoveredPixi> recoveredDocuments = new();
|
|
|
|
|
|
- // Load original paths
|
|
|
- Dictionary<string, string?> originalPaths;
|
|
|
+ var paths = TryGetOriginalPaths();
|
|
|
+ if (paths == null)
|
|
|
{
|
|
|
- using Stream stream = originalPathsEntry.Open();
|
|
|
- using StreamReader reader = new(stream);
|
|
|
- string json = reader.ReadToEnd();
|
|
|
- originalPaths = JsonConvert.DeserializeObject<Dictionary<string, string?>>(json);
|
|
|
+ recoveredDocuments.AddRange(
|
|
|
+ ZipFile.Entries
|
|
|
+ .Where(x =>
|
|
|
+ x.FullName.StartsWith("Documents") &&
|
|
|
+ x.FullName.EndsWith(".pixi"))
|
|
|
+ .Select(entry => new RecoveredPixi(null, entry)));
|
|
|
+
|
|
|
+ return recoveredDocuments;
|
|
|
}
|
|
|
|
|
|
- return (
|
|
|
- from docKeyValue in recoveredDocuments
|
|
|
- join pathKeyValue in originalPaths on docKeyValue.Key equals pathKeyValue.Key
|
|
|
- select (pathKeyValue.Value, docKeyValue.Value)
|
|
|
- ).ToList();
|
|
|
+ recoveredDocuments.AddRange(paths.Select(path => new RecoveredPixi(path.Value, ZipFile.GetEntry($"Documents/{path.Key}"))));
|
|
|
+
|
|
|
+ return recoveredDocuments;
|
|
|
+
|
|
|
+ Dictionary<string, string>? TryGetOriginalPaths()
|
|
|
+ {
|
|
|
+ var originalPathsEntry = ZipFile.Entries.FirstOrDefault(entry => entry.FullName == "DocumentInfo.json");
|
|
|
+
|
|
|
+ if (originalPathsEntry == null)
|
|
|
+ return null;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var stream = originalPathsEntry.Open();
|
|
|
+ using var reader = new StreamReader(stream);
|
|
|
+ string json = reader.ReadToEnd();
|
|
|
+
|
|
|
+ return JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+
|
|
|
public void Dispose()
|
|
|
{
|
|
|
ZipFile.Dispose();
|
|
@@ -169,13 +374,15 @@ internal class CrashReport : IDisposable
|
|
|
|
|
|
// Write the documents into zip
|
|
|
int counter = 0;
|
|
|
- Dictionary<string, string?> originalPaths = new();
|
|
|
- foreach (DocumentViewModel document in vm.DocumentManagerSubViewModel.Documents)
|
|
|
+ var originalPaths = new Dictionary<string, string>();
|
|
|
+ foreach (var document in vm.DocumentManagerSubViewModel.Documents)
|
|
|
{
|
|
|
try
|
|
|
{
|
|
|
string fileName = string.IsNullOrWhiteSpace(document.FullFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.FullFilePath);
|
|
|
- string nameInZip = $"{fileName}-{document.OpenedUTC}-{counter}.pixi".Replace(':', '_');
|
|
|
+ string nameInZip = $"{fileName}-{document.OpenedUTC.ToString(CultureInfo.InvariantCulture)}-{counter.ToString(CultureInfo.InvariantCulture)}.pixi"
|
|
|
+ .Replace(':', '_')
|
|
|
+ .Replace('/', '_');
|
|
|
|
|
|
byte[] serialized = PixiParser.Serialize(document.ToSerializable());
|
|
|
|
|
@@ -209,10 +416,27 @@ internal class CrashReport : IDisposable
|
|
|
ReportText = Encoding.UTF8.GetString(encodedReport);
|
|
|
}
|
|
|
|
|
|
- internal class CrashReportUserMessage
|
|
|
+
|
|
|
+ public class RecoveredPixi
|
|
|
{
|
|
|
- public string Message { get; set; }
|
|
|
+ public string? Path { get; }
|
|
|
+
|
|
|
+ public ZipArchiveEntry RecoveredEntry { get; }
|
|
|
+
|
|
|
+ public byte[] GetRecoveredBytes()
|
|
|
+ {
|
|
|
+ var buffer = new byte[RecoveredEntry.Length];
|
|
|
+ using var stream = RecoveredEntry.Open();
|
|
|
|
|
|
- public string Mail { get; set; }
|
|
|
+ stream.ReadExactly(buffer);
|
|
|
+
|
|
|
+ return buffer;
|
|
|
+ }
|
|
|
+
|
|
|
+ public RecoveredPixi(string? path, ZipArchiveEntry recoveredEntry)
|
|
|
+ {
|
|
|
+ Path = path;
|
|
|
+ RecoveredEntry = recoveredEntry;
|
|
|
+ }
|
|
|
}
|
|
|
}
|