Browse Source

Merge pull request #240 from PixiEditor/extendedCrashReport

Extended crash reports
CPKreuz 3 years ago
parent
commit
c4820e52ad

+ 2 - 1
PixiEditor/App.xaml

@@ -1,7 +1,8 @@
 <Application x:Class="PixiEditor.App"
 <Application x:Class="PixiEditor.App"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             StartupUri="Views/MainWindow.xaml">
+             >
+    <!--StartupUri="Views/MainWindow.xaml"-->
     <Application.Resources>
     <Application.Resources>
         <ResourceDictionary>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
             <ResourceDictionary.MergedDictionaries>

+ 36 - 1
PixiEditor/App.xaml.cs

@@ -1,7 +1,12 @@
-using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Enums;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
+using PixiEditor.Views.Dialogs;
+using System;
+using System.Diagnostics;
 using System.Linq;
 using System.Linq;
+using System.Text.RegularExpressions;
 using System.Windows;
 using System.Windows;
 
 
 namespace PixiEditor
 namespace PixiEditor
@@ -11,6 +16,23 @@ namespace PixiEditor
     /// </summary>
     /// </summary>
     public partial class App : Application
     public partial class App : Application
     {
     {
+        protected override void OnStartup(StartupEventArgs e)
+        {
+            string arguments = string.Join(' ', e.Args);
+
+            if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
+            {
+                CrashReport report = CrashReport.Parse(groups[2].Value);
+                MainWindow = new CrashReportDialog(report);
+            }
+            else
+            {
+                MainWindow = new MainWindow();
+            }
+
+            MainWindow.Show();
+        }
+
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
         protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
         {
         {
             base.OnSessionEnding(e);
             base.OnSessionEnding(e);
@@ -21,5 +43,18 @@ namespace PixiEditor
                 e.Cancel = confirmation != ConfirmationType.Yes;
                 e.Cancel = confirmation != ConfirmationType.Yes;
             }
             }
         }
         }
+
+        private bool ParseArgument(string pattern, string args, out Group[] groups)
+        {
+            Match match = Regex.Match(args, pattern, RegexOptions.IgnoreCase);
+            groups = null;
+
+            if (match.Success)
+            {
+                groups = match.Groups.Values.ToArray();
+            }
+
+            return match.Success;
+        }
     }
     }
 }
 }

+ 63 - 20
PixiEditor/Helpers/CrashHelper.cs

@@ -1,25 +1,76 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
+using ByteSizeLib;
+using Hardware.Info;
+using PixiEditor.Models.DataHolders;
+using System;
+using System.Globalization;
 using System.Text;
 using System.Text;
-using System.Threading.Tasks;
 
 
 namespace PixiEditor.Helpers
 namespace PixiEditor.Helpers
 {
 {
-    public static class CrashHelper
+    public class CrashHelper
     {
     {
-        public static void SaveCrashInfo(Exception e)
+        private readonly IHardwareInfo hwInfo;
+
+        public static void SaveCrashInfo(Exception exception)
+        {
+            CrashReport report = CrashReport.Generate(exception);
+            report.TrySave();
+            report.RestartToCrashReport();
+        }
+
+        public CrashHelper()
+        {
+            hwInfo = new HardwareInfo();
+        }
+
+        public void GetCPUInformation(StringBuilder builder)
+        {
+            builder.AppendLine("CPU:");
+            hwInfo.RefreshCPUList(false);
+
+            foreach (var processor in hwInfo.CpuList)
+            {
+                builder
+                    .AppendLine($"  Name: {processor.Name}")
+                    .AppendLine($"  Speed: {(processor.CurrentClockSpeed / 1000f).ToString("F2", CultureInfo.InvariantCulture)} GHz")
+                    .AppendLine($"  Max Speed: {(processor.MaxClockSpeed / 1000f).ToString("F2", CultureInfo.InvariantCulture)} GHz")
+                    .AppendLine();
+            }
+        }
+
+        public void GetGPUInformation(StringBuilder builder)
         {
         {
-            StringBuilder builder = new System.Text.StringBuilder();
-            DateTime currentTime = DateTime.Now;
+            builder.AppendLine("GPU:");
+            hwInfo.RefreshVideoControllerList();
 
 
+            foreach (var gpu in hwInfo.VideoControllerList)
+            {
+                builder
+                    .AppendLine($"  Name: {gpu.Name}")
+                    .AppendLine($"  Driver: {gpu.DriverVersion}")
+                    .AppendLine();
+            }
+        }
+
+        public void GetMemoryInformation(StringBuilder builder)
+        {
+            builder.AppendLine("Memory:");
+            hwInfo.RefreshMemoryStatus();
+
+            var memInfo = hwInfo.MemoryStatus;
+
+            builder
+                .AppendLine($"  Available: {new ByteSize(memInfo.AvailablePhysical).ToString("", CultureInfo.InvariantCulture)}")
+                .AppendLine($"  Total: {new ByteSize(memInfo.TotalPhysical).ToString("", CultureInfo.InvariantCulture)}");
+        }
+
+        public static void AddExceptionMessage(StringBuilder builder, Exception e)
+        {
             builder
             builder
-                .Append($"PixiEditor crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n\n")
-                .Append("-------Crash message-------\n")
+                .AppendLine("\n-------Crash message-------")
                 .Append(e.GetType().ToString())
                 .Append(e.GetType().ToString())
                 .Append(": ")
                 .Append(": ")
-                .Append(e.Message);
+                .AppendLine(e.Message);
             {
             {
                 var innerException = e.InnerException;
                 var innerException = e.InnerException;
                 while (innerException != null)
                 while (innerException != null)
@@ -46,14 +97,6 @@ namespace PixiEditor.Helpers
                     innerException = innerException.InnerException;
                     innerException = innerException.InnerException;
                 }
                 }
             }
             }
-
-            string filename = $"crash-{currentTime:yyyy-MM-dd_HH-mm-ss_fff}.txt";
-            string path = Path.Combine(
-                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
-                "PixiEditor",
-                "crash_logs");
-            Directory.CreateDirectory(path);
-            File.WriteAllText(Path.Combine(path, filename), builder.ToString());
         }
         }
     }
     }
 }
 }

+ 19 - 0
PixiEditor/Helpers/ProcessHelpers.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Diagnostics;
+
+namespace PixiEditor.Helpers
+{
+    public static class ProcessHelpers
+    {
+        public static void ShellExecute(string url)
+        {
+            Process.Start(new ProcessStartInfo
+            {
+                FileName = url,
+                UseShellExecute = true
+            });
+        }
+
+        public static void ShellExecuteEV(string path) => ShellExecute(Environment.ExpandEnvironmentVariables(path));
+    }
+}

+ 192 - 0
PixiEditor/Models/DataHolders/CrashReport.cs

@@ -0,0 +1,192 @@
+using PixiEditor.Helpers;
+using PixiEditor.Helpers.Extensions;
+using PixiEditor.Parser;
+using PixiEditor.ViewModels;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace PixiEditor.Models.DataHolders
+{
+    public class CrashReport : IDisposable
+    {
+        public static CrashReport Generate(Exception exception)
+        {
+            StringBuilder builder = new();
+            DateTime currentTime = DateTime.Now;
+
+            builder
+                .AppendLine($"PixiEditor {VersionHelpers.GetCurrentAssemblyVersionString()} crashed on {currentTime:yyyy.MM.dd} at {currentTime:HH:mm:ss}\n")
+                .AppendLine("-----System Information----")
+                .AppendLine("General:")
+                .AppendLine($"  OS: {Environment.OSVersion.VersionString}")
+                .AppendLine();
+
+            CrashHelper helper = new();
+
+            try
+            {
+                helper.GetCPUInformation(builder);
+            }
+            catch (Exception cpuE)
+            {
+                builder.AppendLine($"Error ({cpuE.GetType().FullName}: {cpuE.Message}) while gathering CPU information, skipping...");
+            }
+
+            try
+            {
+                helper.GetGPUInformation(builder);
+            }
+            catch (Exception gpuE)
+            {
+                builder.AppendLine($"Error ({gpuE.GetType().FullName}: {gpuE.Message}) while gathering GPU information, skipping...");
+            }
+
+            try
+            {
+                helper.GetMemoryInformation(builder);
+            }
+            catch (Exception memE)
+            {
+                builder.AppendLine($"Error ({memE.GetType().FullName}: {memE.Message}) while gathering memory information, 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;
+        }
+
+        public static CrashReport Parse(string path)
+        {
+            CrashReport report = new();
+            report.FilePath = path;
+
+            report.ZipFile = System.IO.Compression.ZipFile.Open(path, ZipArchiveMode.Read);
+            report.ExtractReport();
+
+            return report;
+        }
+
+        public string FilePath { get; set; }
+
+        public string ReportText { get; set; }
+
+        private ZipArchive ZipFile { get; set; }
+
+        public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
+
+        public IEnumerable<Document> RecoverDocuments()
+        {
+            foreach (ZipArchiveEntry entry in ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")))
+            {
+                using Stream stream = entry.Open();
+
+                Document document;
+
+                try
+                {
+                    document = PixiParser.Deserialize(stream).ToDocument();
+                    document.ChangesSaved = false;
+                }
+                catch
+                {
+                    continue;
+                }
+
+                yield return document;
+            }
+        }
+
+        public void Dispose()
+        {
+            ZipFile.Dispose();
+        }
+
+        public void RestartToCrashReport()
+        {
+            Process process = new();
+
+            process.StartInfo = new()
+            {
+                FileName = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "exe"),
+                Arguments = $"--crash \"{Path.GetFullPath(FilePath)}\""
+            };
+
+            process.Start();
+        }
+
+        public bool TrySave()
+        {
+            try
+            {
+                Save();
+                return true;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+
+        public void Save()
+        {
+            using FileStream zipStream = new(FilePath, FileMode.Create, FileAccess.Write);
+            using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
+
+            using (Stream reportStream = archive.CreateEntry("report.txt").Open())
+            {
+                reportStream.Write(Encoding.UTF8.GetBytes(ReportText));
+            }
+
+            foreach (Document document in ViewModelMain.Current.BitmapManager.Documents)
+            {
+                try
+                {
+                    string documentPath =
+                        $"{(string.IsNullOrWhiteSpace(document.DocumentFilePath) ? "Unsaved" : Path.GetFileNameWithoutExtension(document.DocumentFilePath))}-{document.OpenedUTC}.pixi";
+
+                    byte[] serialized = PixiParser.Serialize(document.ToSerializable());
+
+                    using Stream documentStream = archive.CreateEntry($"Documents/{documentPath}").Open();
+                    documentStream.Write(serialized);
+                }
+                catch
+                { }
+            }
+        }
+
+        private void ExtractReport()
+        {
+            ZipArchiveEntry entry = ZipFile.GetEntry("report.txt");
+            using Stream stream = entry.Open();
+
+            byte[] encodedReport = new byte[entry.Length];
+            stream.Read(encodedReport);
+
+            ReportText = Encoding.UTF8.GetString(encodedReport);
+        }
+
+        public class CrashReportUserMessage
+        {
+            public string Message { get; set; }
+
+            public string Mail { get; set; }
+        }
+    }
+}

+ 2 - 0
PixiEditor/PixiEditor.csproj

@@ -183,11 +183,13 @@
 	</ItemGroup>
 	</ItemGroup>
 	<ItemGroup>
 	<ItemGroup>
 		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.1" />
 		<PackageReference Include="Dirkster.AvalonDock" Version="4.60.1" />
+		<PackageReference Include="ByteSize" Version="2.1.1" />
 		<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
 		<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
 		<PackageReference Include="Expression.Blend.Sdk">
 		<PackageReference Include="Expression.Blend.Sdk">
 			<Version>1.0.2</Version>
 			<Version>1.0.2</Version>
 			<NoWarn>NU1701</NoWarn>
 			<NoWarn>NU1701</NoWarn>
 		</PackageReference>
 		</PackageReference>
+		<PackageReference Include="Hardware.Info" Version="1.1.1.1" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="MessagePack" Version="2.3.85" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">
 		<PackageReference Include="MvvmLightLibs" Version="5.4.1.1">

+ 62 - 0
PixiEditor/ViewModels/CrashReportViewModel.cs

@@ -0,0 +1,62 @@
+using GalaSoft.MvvmLight.CommandWpf;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Views.Dialogs;
+using System.Diagnostics;
+using System.Windows;
+
+namespace PixiEditor.ViewModels
+{
+    public class CrashReportViewModel : ViewModelBase
+    {
+        private bool hasRecoveredDocuments = true;
+
+        public CrashReport CrashReport { get; }
+
+        public string ReportText { get; }
+
+        public int DocumentCount { get; }
+
+        public RelayCommand OpenSendCrashReportCommand { get; }
+
+        public RelayCommand RecoverDocumentsCommand { get; }
+
+        public RelayCommand AttachDebuggerCommand { get; }
+
+        public bool IsDebugBuild { get; set; }
+
+        public CrashReportViewModel(CrashReport report)
+        {
+            SetIsDebug();
+
+            CrashReport = report;
+            ReportText = report.ReportText;
+            DocumentCount = report.GetDocumentCount();
+            OpenSendCrashReportCommand = new(() => new SendCrashReportWindow(CrashReport).Show());
+            RecoverDocumentsCommand = new(RecoverDocuments, () => hasRecoveredDocuments, false);
+            AttachDebuggerCommand = new(AttachDebugger);
+        }
+
+        public void RecoverDocuments()
+        {
+            MainWindow window = MainWindow.CreateWithDocuments(CrashReport.RecoverDocuments());
+
+            Application.Current.MainWindow = window;
+            window.Show();
+            hasRecoveredDocuments = false;
+        }
+
+        [Conditional("DEBUG")]
+        private void SetIsDebug()
+        {
+            IsDebugBuild = true;
+        }
+
+        private void AttachDebugger()
+        {
+            if (!Debugger.Launch())
+            {
+                MessageBox.Show("Starting debugger failed", "Starting debugger failed", MessageBoxButton.OK, MessageBoxImage.Error);
+            }
+        }
+    }
+}

+ 5 - 12
PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -1,6 +1,5 @@
 using PixiEditor.Helpers;
 using PixiEditor.Helpers;
 using System;
 using System;
-using System.Diagnostics;
 using System.IO;
 using System.IO;
 using System.Reflection;
 using System.Reflection;
 
 
@@ -12,30 +11,24 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         public RelayCommand OpenInstallLocationCommand { get; set; }
         public RelayCommand OpenInstallLocationCommand { get; set; }
 
 
+        public RelayCommand CrashCommand { get; set; }
+
         public DebugViewModel(ViewModelMain owner)
         public DebugViewModel(ViewModelMain owner)
             : base(owner)
             : base(owner)
         {
         {
             OpenFolderCommand = new RelayCommand(OpenFolder);
             OpenFolderCommand = new RelayCommand(OpenFolder);
             OpenInstallLocationCommand = new RelayCommand(OpenInstallLocation);
             OpenInstallLocationCommand = new RelayCommand(OpenInstallLocation);
+            CrashCommand = new RelayCommand(_ => throw new InvalidOperationException("Debug Crash"));
         }
         }
 
 
         public static void OpenFolder(object parameter)
         public static void OpenFolder(object parameter)
         {
         {
-            OpenShellExecute((string)parameter);
+            ProcessHelpers.ShellExecuteEV(parameter as string);
         }
         }
 
 
         public static void OpenInstallLocation(object parameter)
         public static void OpenInstallLocation(object parameter)
         {
         {
-            OpenShellExecute(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
-        }
-
-        private static void OpenShellExecute(string path)
-        {
-            ProcessStartInfo startInfo = new (Environment.ExpandEnvironmentVariables(path));
-
-            startInfo.UseShellExecute = true;
-
-            Process.Start(startInfo);
+            ProcessHelpers.ShellExecuteEV(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
         }
         }
     }
     }
 }
 }

+ 5 - 4
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -188,12 +188,13 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         private void Owner_OnStartupEvent(object sender, System.EventArgs e)
         {
         {
-            var lastArg = Environment.GetCommandLineArgs().Last();
-            if (Importer.IsSupportedFile(lastArg) && File.Exists(lastArg))
+            var args = Environment.GetCommandLineArgs();
+            var file = args.Last();
+            if (Importer.IsSupportedFile(file) && File.Exists(file))
             {
             {
-                Open(lastArg);
+                Open(file);
             }
             }
-            else
+            else if (Owner.BitmapManager.Documents.Count == 0 || !args.Contains("--crash"))
             {
             {
                 if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 if (IPreferences.Current.GetPreference("ShowStartupWindow", true))
                 {
                 {

+ 2 - 8
PixiEditor/ViewModels/SubViewModels/Main/MiscViewModel.cs

@@ -40,18 +40,12 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 
 
         private void OpenHyperlink(object parameter)
         private void OpenHyperlink(object parameter)
         {
         {
-            if (parameter == null)
+            if (parameter is not string s)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            var url = (string)parameter;
-            var processInfo = new ProcessStartInfo()
-            {
-                FileName = url,
-                UseShellExecute = true
-            };
-            Process.Start(processInfo);
+            ProcessHelpers.ShellExecute(s);
         }
         }
 
 
         private void OpenShortcutWindow(object parameter)
         private void OpenShortcutWindow(object parameter)

+ 5 - 11
PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -1,5 +1,5 @@
 using AvalonDock.Layout;
 using AvalonDock.Layout;
-using PixiEditor.Helpers;
+using GalaSoft.MvvmLight.CommandWpf;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 
 
@@ -7,9 +7,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
 {
 {
     public class WindowViewModel : SubViewModel<ViewModelMain>, ISettableOwner<ViewModelMain>
     public class WindowViewModel : SubViewModel<ViewModelMain>, ISettableOwner<ViewModelMain>
     {
     {
-        public MainWindow MainWindow { get; private set; }
-
-        public RelayCommand ShowAvalonDockWindowCommand { get; set; }
+        public RelayCommand<string> ShowAvalonDockWindowCommand { get; set; }
 
 
         public WindowViewModel()
         public WindowViewModel()
             : this(null)
             : this(null)
@@ -19,9 +17,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         public WindowViewModel(ViewModelMain owner)
         public WindowViewModel(ViewModelMain owner)
             : base(owner)
             : base(owner)
         {
         {
-            ShowAvalonDockWindowCommand = new RelayCommand(ShowAvalonDockWindow);
-
-            MainWindow = (MainWindow)System.Windows.Application.Current?.MainWindow;
+            ShowAvalonDockWindowCommand = new(ShowAvalonDockWindow);
         }
         }
 
 
         public void SetOwner(ViewModelMain owner)
         public void SetOwner(ViewModelMain owner)
@@ -29,11 +25,9 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
             Owner = owner;
             Owner = owner;
         }
         }
 
 
-        private void ShowAvalonDockWindow(object parameter)
+        private void ShowAvalonDockWindow(string id)
         {
         {
-            string id = (string)parameter;
-
-            var anchorables = new List<LayoutAnchorable>(MainWindow.LayoutRoot.Manager.Layout
+            var anchorables = new List<LayoutAnchorable>(MainWindow.Current.LayoutRoot.Manager.Layout
                     .Descendents()
                     .Descendents()
                     .OfType<LayoutAnchorable>());
                     .OfType<LayoutAnchorable>());
 
 

+ 55 - 0
PixiEditor/Views/Dialogs/CrashReportDialog.xaml

@@ -0,0 +1,55 @@
+<Window x:Class="PixiEditor.Views.Dialogs.CrashReportDialog"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:vm="clr-namespace:PixiEditor.ViewModels"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        d:DataContext="{d:DesignInstance vm:CrashReportViewModel}"
+        mc:Ignorable="d"
+        Background="{StaticResource AccentColor}" Foreground="White"
+        Title="PixiEditor has crashed!" WindowStyle="None"
+        MinWidth="450" MinHeight="195"
+        Width="480" Height="180">
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32" GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid>
+        <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
+            <RowDefinition/>
+        </Grid.RowDefinitions>
+        <dial:DialogTitleBar TitleText="PixiEditor has crashed!" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+        <Grid Grid.Row="1" Margin="30,30,30,0" >
+            <StackPanel>
+                <Grid Background="{StaticResource MainColor}">
+                    <StackPanel Margin="7" VerticalAlignment="Center">
+                        <TextBlock Text="{Binding DocumentCount, StringFormat={}{0} file(s) can be recovered}"
+                       d:Text="2 file(s) can be recovered"/>
+                        <TextBlock TextWrapping="Wrap">You can help the developers fixing this bug by sending a crash report that was generated</TextBlock>
+                    </StackPanel>
+                </Grid>
+
+                <WrapPanel Margin="0,20,0,5" Orientation="Horizontal" HorizontalAlignment="Center">
+                    <Button Command="{Binding OpenSendCrashReportCommand}"
+                        Width="120"
+                        Style="{StaticResource DarkRoundButton}">Send report</Button>
+                    <Button Margin="5,0,5,0" Width="120"
+                        Command="{Binding RecoverDocumentsCommand}"
+                        Style="{StaticResource DarkRoundButton}">Recover files</Button>
+                    <Button Visibility="{Binding IsDebugBuild, Converter={BoolToVisibilityConverter}}"
+                    Style="{StaticResource DarkRoundButton}" Width="170"
+                    Command="{Binding AttachDebuggerCommand}">(Re)Attach debugger</Button>
+                </WrapPanel>
+            </StackPanel>
+        </Grid>
+    </Grid>
+</Window>

+ 29 - 0
PixiEditor/Views/Dialogs/CrashReportDialog.xaml.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Models.DataHolders;
+using PixiEditor.ViewModels;
+using System.Windows;
+using System.Windows.Input;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for CrashReportDialog.xaml
+    /// </summary>
+    public partial class CrashReportDialog : Window
+    {
+        public CrashReportDialog(CrashReport report)
+        {
+            DataContext = new CrashReportViewModel(report);
+            InitializeComponent();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+    }
+}

+ 48 - 0
PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml

@@ -0,0 +1,48 @@
+<Window x:Class="PixiEditor.Views.Dialogs.SendCrashReportWindow"
+        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
+        mc:Ignorable="d"
+        Background="{StaticResource AccentColor}" Foreground="White"
+        Title="Send crash report"
+        WindowStyle="None"
+        Height="170" Width="340">
+    <Window.Resources>
+        <Style TargetType="TextBlock">
+            <Setter Property="HorizontalAlignment" Value="Center"/>
+        </Style>
+        <Style TargetType="Button" BasedOn="{StaticResource DarkRoundButton}">
+            <Setter Property="Margin" Value="5"/>
+            <Setter Property="Width" Value="100"/>
+        </Style>
+    </Window.Resources>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <StackPanel>
+        <dial:DialogTitleBar TitleText="Send crash report" CloseCommand="{x:Static SystemCommands.CloseWindowCommand}" />
+        <StackPanel Margin="10">
+            <TextBlock>You can find the report here:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Width="140" Click="OpenInExplorer">Open in Explorer</Button>
+            </StackPanel>
+            <TextBlock TextAlignment="Center">You can report your crash report here:</TextBlock>
+            <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
+                <Button Click="OpenHyperlink" Tag="github">GitHub</Button>
+                <Button Click="OpenHyperlink" Tag="discord">Discord</Button>
+                <Button Click="OpenHyperlink" Tag="email">E-Mail</Button>
+            </StackPanel>
+            <TextBlock TextWrapping="Wrap" FontSize="8">The report might contain the doucments that were opened when the crash happened</TextBlock>
+        </StackPanel>
+    </StackPanel>
+</Window>

+ 101 - 0
PixiEditor/Views/Dialogs/SendCrashReportWindow.xaml.cs

@@ -0,0 +1,101 @@
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using System;
+using System.IO;
+using System.Text;
+using System.Web;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace PixiEditor.Views.Dialogs
+{
+    /// <summary>
+    /// Interaction logic for SendCrashReportWindow.xaml
+    /// </summary>
+    public partial class SendCrashReportWindow : Window
+    {
+        const string DiscordInviteLink = "https://discord.gg/eh8gx6vNEp";
+
+        private readonly CrashReport report;
+
+        public SendCrashReportWindow(CrashReport report)
+        {
+            this.report = report;
+            InitializeComponent();
+        }
+
+        private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+        {
+            e.CanExecute = true;
+        }
+
+        private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+        {
+            SystemCommands.CloseWindow(this);
+        }
+
+        private void OpenInExplorer(object sender, RoutedEventArgs e)
+        {
+            string tempPath = Path.Combine(
+                Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                "PixiEditor",
+                "crash_logs",
+                "to-copy");
+
+            DirectoryInfo info = Directory.CreateDirectory(tempPath);
+
+            foreach (var file in info.EnumerateFiles())
+            {
+                file.Delete();
+            }
+
+            File.Copy(report.FilePath, Path.Combine(tempPath, Path.GetFileName(report.FilePath)), true);
+
+            ProcessHelpers.ShellExecute(tempPath);
+        }
+
+        private void OpenHyperlink(object sender, RoutedEventArgs e)
+        {
+            var button = sender as Button;
+            var tag = button.Tag as string;
+
+            string body = HttpUtility.UrlEncode($"** IMPORTANT: Drop the \"{Path.GetFileName(report.FilePath)}\" file in here **");
+
+            var result = tag switch
+            {
+                "github" => GetGitHubLink(),
+                "discord" => DiscordInviteLink,
+                "email" => GetMailtoLink(),
+                _ => throw new NotImplementedException()
+            };
+
+            OpenInExplorer(null, null);
+            ProcessHelpers.ShellExecute(result);
+
+            string GetGitHubLink()
+            {
+                StringBuilder builder = new();
+
+                builder.Append("https://github.com/PixiEditor/PixiEditor/issues/new?title=");
+                builder.Append(HttpUtility.UrlEncode($"Crash Report"));
+                builder.Append("&body=");
+                builder.Append(body);
+
+                return builder.ToString();
+            }
+
+            string GetMailtoLink()
+            {
+                StringBuilder builder = new();
+
+                builder.Append("mailto:[email protected]?subject=");
+                builder.Append(HttpUtility.UrlEncode($"Crash Report"));
+                builder.Append("&body=");
+                builder.Append(body);
+
+                return builder.ToString();
+            }
+        }
+    }
+}

+ 4 - 3
PixiEditor/Views/MainWindow.xaml

@@ -177,11 +177,12 @@
                               CommandParameter="https://pixieditor.net/docs/Third-party-licenses"/>
                               CommandParameter="https://pixieditor.net/docs/Third-party-licenses"/>
                 </MenuItem>
                 </MenuItem>
                 <MenuItem Header="_Debug" Visibility="{Binding IsDebug, Converter={StaticResource BoolToVisibilityConverter}}">
                 <MenuItem Header="_Debug" Visibility="{Binding IsDebug, Converter={StaticResource BoolToVisibilityConverter}}">
-                    <MenuItem Header="_Open Local App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
+                    <MenuItem Header="Open _Local App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
                               CommandParameter="%LocalAppData%/PixiEditor"/>
                               CommandParameter="%LocalAppData%/PixiEditor"/>
-                    <MenuItem Header="_Open Roaming App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
+                    <MenuItem Header="Open _Roaming App Data" Command="{Binding DebugSubViewModel.OpenFolderCommand}"
                               CommandParameter="%AppData%/PixiEditor"/>
                               CommandParameter="%AppData%/PixiEditor"/>
-                    <MenuItem Header="_Open Install Location"  Command="{Binding DebugSubViewModel.OpenInstallLocationCommand}"/>
+                    <MenuItem Header="Open _Install Location"  Command="{Binding DebugSubViewModel.OpenInstallLocationCommand}"/>
+                    <MenuItem Header="_Crash"  Command="{Binding DebugSubViewModel.CrashCommand}"/>
                 </MenuItem>
                 </MenuItem>
             </Menu>
             </Menu>
             <StackPanel DockPanel.Dock="Right" VerticalAlignment="Top" Orientation="Horizontal" Margin="0,-5,-5,0"
             <StackPanel DockPanel.Dock="Right" VerticalAlignment="Top" Orientation="Horizontal" Margin="0,-5,-5,0"

+ 23 - 1
PixiEditor/Views/MainWindow.xaml.cs

@@ -1,10 +1,12 @@
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Helpers.Extensions;
+using PixiEditor.Models.Controllers;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.Models.UserPreferences;
 using PixiEditor.ViewModels;
 using PixiEditor.ViewModels;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 using System;
 using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Diagnostics;
 using System.Linq;
 using System.Linq;
@@ -24,11 +26,15 @@ namespace PixiEditor
 
 
         private readonly IPreferences preferences;
         private readonly IPreferences preferences;
 
 
+        private readonly IServiceProvider services;
+
+        public static MainWindow Current { get; private set; }
+
         public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
         public new ViewModelMain DataContext { get => (ViewModelMain)base.DataContext; set => base.DataContext = value; }
 
 
         public MainWindow()
         public MainWindow()
         {
         {
-            IServiceProvider services = new ServiceCollection()
+            services = new ServiceCollection()
                 .AddPixiEditor()
                 .AddPixiEditor()
                 .BuildServiceProvider();
                 .BuildServiceProvider();
 
 
@@ -63,6 +69,22 @@ namespace PixiEditor
             OnReleaseBuild();
             OnReleaseBuild();
         }
         }
 
 
+        public static MainWindow CreateWithDocuments(IEnumerable<Document> documents)
+        {
+            MainWindow window = new();
+
+            BitmapManager bitmapManager = window.services.GetRequiredService<BitmapManager>();
+
+            foreach (Document document in documents)
+            {
+                bitmapManager.Documents.Add(document);
+            }
+
+            bitmapManager.ActiveDocument = bitmapManager.Documents.FirstOrDefault();
+
+            return window;
+        }
+
         protected override void OnClosing(CancelEventArgs e)
         protected override void OnClosing(CancelEventArgs e)
         {
         {
             DataContext.CloseWindow(e);
             DataContext.CloseWindow(e);