2
0
Эх сурвалжийг харах

Always save files to temp and auto reopen on deadlock

CPKreuz 1 жил өмнө
parent
commit
15e4b13ce7

+ 6 - 0
src/PixiEditor.Extensions/Common/UserPreferences/PreferencesConstants.cs

@@ -19,4 +19,10 @@ public static class PreferencesConstants
     public const double AutosavePeriodDefault = 3;
 
     public const string UnsavedNextSessionFiles = nameof(UnsavedNextSessionFiles);
+
+    public const string AutosaveToDocumentPath = nameof(AutosaveToDocumentPath);
+    public const bool AutosaveToDocumentPathDefault = false;
+    
+    public const string SaveSessionStateEnabled = nameof(SaveSessionStateEnabled);
+    public const bool SaveSessionStateDefault = true;
 }

+ 6 - 1
src/PixiEditor/App.xaml.cs

@@ -71,8 +71,13 @@ internal partial class App : Application
             return;
         }
 
+        if (ParseArgument("--wait-before-init ([0-9]+)", arguments, out Group[] waitBeforeInitGroups))
+        {
+            Task.Delay(int.Parse(waitBeforeInitGroups[1].ValueSpan)).Wait();
+        }
+
 #if !STEAM
-        if (!HandleNewInstance())
+        if (!HandleNewInstance() && !arguments.Contains("--force-new-instance"))
         {
             return;
         }

+ 3 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -73,6 +73,8 @@
   "DUMP_ALL_COMMANDS_DESCRIPTIVE": "Dump all commands to a text file",
   "CRASH": "Crash",
   "CRASH_APP": "Crash application",
+  "FREEZE": "Freeze",
+  "FREEZE_APP": "Freeze application",
   "DELETE_USR_PREFS": "Delete user preferences (Roaming AppData)",
   "DELETE_SHORTCUT_FILE": "Delete shortcut file (Roaming AppData)",
   "DELETE_EDITOR_DATA": "Delete editor data (Local AppData)",
@@ -596,7 +598,7 @@
   "AUTOSAVE_SAVING_IN_MINUTE": "Saving in less than a minute",
   "AUTOSAVE_SAVING_IN": "Saving in about {0} {1}",
   "AUTOSAVE_WAITING_FOR_SAVE": "Waiting to autosave.",
-  "AUTOSAVE_PLEASE_RESAVE": "Error while saving. Save file manually to enable autosaving.",
+  "AUTOSAVE_PLEASE_RESAVE": "Error while saving. Please save manually.",
   "AUTOSAVE_FAILED_RETRYING": "Error while saving. Retrying in {0} {1}.",
   "AUTOSAVE_DISABLED": "Autosaving paused for document.",
   "AUTOSAVE_NOTHING_CHANGED": "Nothing changed, not saving.",

+ 58 - 9
src/PixiEditor/Helpers/DeadlockDetectionHelper.cs

@@ -37,7 +37,10 @@ public class DeadlockDetectionHelper
                 CheckStatus();
             }
             catch
-            { }
+            {
+                if (Debugger.IsAttached)
+                    Debugger.Break();
+            }
             
             Thread.Sleep(200);
         }
@@ -52,17 +55,51 @@ public class DeadlockDetectionHelper
 
         if (errorsReported < 5)
         {
-            var task = Task.Run(() => ReportProblem(mainThread.ManagedThreadId));
+            TryReportProblem();
+        }
+
+        errorsReported++;
 
-            if (!task.Wait(TimeSpan.FromSeconds(8)))
+        TrySaveFilesForNextSession();
+
+        if (Debugger.IsAttached)
+        {
+            CheckDispatcher(Timeout.Infinite, DispatcherPriority.Send);
+        }
+        else
+        {
+            bool close = !CheckDispatcher(20000, DispatcherPriority.Send);
+        
+            if (close)
             {
-                StartDeadlockHandlerProcess();
+                ForceNewProcess();
+                Environment.FailFast("Encountered deadlock. Reopening in new process");
             }
         }
+    }
 
-        errorsReported++;
+    private void TryReportProblem()
+    {
+        var thread = new Thread(() =>
+        {
+            ReportProblem(mainThread.ManagedThreadId);
+        });
+        
+        thread.Start();
+        thread.Join(7000);
+    }
 
-        CheckDispatcher(Timeout.Infinite, DispatcherPriority.Send);
+    private void TrySaveFilesForNextSession()
+    {
+        var thread = new Thread(() =>
+        {
+            var viewModel = ViewModelMain.Current;
+
+            viewModel.AutosaveAllForNextSession();
+        });
+        
+        thread.Start();
+        thread.Join(10000);
     }
 
     private void StartDeadlockHandlerProcess()
@@ -77,6 +114,19 @@ public class DeadlockDetectionHelper
 
         process.Start();
     }
+
+    private void ForceNewProcess()
+    {
+        Process process = new();
+
+        process.StartInfo = new()
+        {
+            FileName = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, "exe"),
+            Arguments = "--force-new-instance --wait-before-init 6000"
+        };
+
+        process.Start();
+    }
     
     private static void ReportProblem(int mainThreadId, int processId = -1)
     {
@@ -148,7 +198,6 @@ public class DeadlockDetectionHelper
 
         }
         
-        
         isFree = CheckDispatcher(1600, DispatcherPriority.Input);
 
         stopwatch.Restart();
@@ -158,7 +207,7 @@ public class DeadlockDetectionHelper
         if (isFree)
             return true;
 
-        isFree = CheckDispatcher(1000, DispatcherPriority.Send);
+        isFree = CheckDispatcher(2000, DispatcherPriority.Send);
 
         stopwatch.Restart();
         Debug.WriteLine($"-------- Fourth deadlock check time [0] {stopwatch.Elapsed}");
@@ -171,7 +220,7 @@ public class DeadlockDetectionHelper
     {
         var task = Task.Run(() => dispatcher.Invoke(ReturnTrue, TimeSpan.FromMilliseconds(timeout), priority) as bool? ?? false);
 
-        var waitTimeout = (int)(timeout != -1 ? timeout * 1.5 : timeout);
+        var waitTimeout = (int)(timeout != -1 ? timeout * 1.2 : timeout);
         
         return task.Wait(waitTimeout) && task.Result;
     }

+ 2 - 2
src/PixiEditor/Models/DataHolders/CrashFilePathInfo.cs → src/PixiEditor/Models/DataHolders/AutosaveFilePathInfo.cs

@@ -1,12 +1,12 @@
 namespace PixiEditor.Models.DataHolders;
 
-public class CrashFilePathInfo
+public class AutosaveFilePathInfo
 {
     public string? OriginalPath { get; set; }
     
     public string? AutosavePath { get; set; }
     
-    public CrashFilePathInfo(string originalPath, string autosavePath)
+    public AutosaveFilePathInfo(string? originalPath, string? autosavePath)
     {
         OriginalPath = originalPath;
         AutosavePath = autosavePath;

+ 6 - 6
src/PixiEditor/Models/DataHolders/CrashReport.cs

@@ -209,7 +209,7 @@ internal class CrashReport : IDisposable
 
     public int GetDocumentCount() => ZipFile.Entries.Where(x => x.FullName.EndsWith(".pixi")).Count();
 
-    public List<(CrashFilePathInfo originalPath, byte[] dotPixiBytes)> RecoverDocuments()
+    public List<(AutosaveFilePathInfo originalPath, byte[] dotPixiBytes)> RecoverDocuments()
     {
         // Load .pixi files
         Dictionary<string, byte[]> recoveredDocuments = new();
@@ -224,15 +224,15 @@ internal class CrashReport : IDisposable
         var originalPathsEntry = ZipFile.Entries.First(entry => entry.FullName == "DocumentInfo.json");
         
         // Load original paths
-        Dictionary<string, CrashFilePathInfo> originalPaths;
+        Dictionary<string, AutosaveFilePathInfo> originalPaths;
         {
             using Stream stream = originalPathsEntry.Open();
             using StreamReader reader = new(stream);
             string json = reader.ReadToEnd();
-            originalPaths = JsonConvert.DeserializeObject<Dictionary<string, CrashFilePathInfo>>(json);
+            originalPaths = JsonConvert.DeserializeObject<Dictionary<string, AutosaveFilePathInfo>>(json);
         }
 
-        var list = new List<(CrashFilePathInfo originalPath, byte[] dotPixiBytes)>();
+        var list = new List<(AutosaveFilePathInfo originalPath, byte[] dotPixiBytes)>();
 
         foreach (var document in recoveredDocuments)
         {
@@ -290,7 +290,7 @@ internal class CrashReport : IDisposable
 
         // Write the documents into zip
         int counter = 0;
-        Dictionary<string, CrashFilePathInfo> originalPaths = new();
+        Dictionary<string, AutosaveFilePathInfo> originalPaths = new();
         foreach (DocumentViewModel document in vm.DocumentManagerSubViewModel.Documents)
         {
             try
@@ -305,7 +305,7 @@ internal class CrashReport : IDisposable
                 using Stream documentStream = archive.CreateEntry($"Documents/{nameInZip}").Open();
                 documentStream.Write(serialized);
 
-                originalPaths.Add(nameInZip, new CrashFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
+                originalPaths.Add(nameInZip, new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
             }
             catch { }
             counter++;

+ 0 - 1
src/PixiEditor/PixiEditor.csproj

@@ -19,7 +19,6 @@
     <RuntimeIdentifiers>win-x86;win-x64</RuntimeIdentifiers>
     <ImplicitUsings>true</ImplicitUsings>
     <AssemblyVersion></AssemblyVersion>
-    <LangVersion>11</LangVersion>
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
 	</PropertyGroup>
 

+ 60 - 48
src/PixiEditor/ViewModels/SubViewModels/Document/AutosaveDocumentViewModel.cs

@@ -17,7 +17,6 @@ internal class AutosaveDocumentViewModel : NotifyableObject
     private readonly Timer updateTextTimer;
     private readonly Timer busyTimer;
     private bool saveAfterNextFinish;
-    private bool reenableAfterNextSave;
     private int savingFailed;
     private DateTime nextSave;
     private Guid tempGuid;
@@ -39,6 +38,8 @@ internal class AutosaveDocumentViewModel : NotifyableObject
 
     private double AutosavePeriodMinutes { get; set; } = -1;
 
+    private bool SaveToDocumentPath => IPreferences.Current!.GetPreference(PreferencesConstants.AutosaveToDocumentPath, PreferencesConstants.AutosaveToDocumentPathDefault);
+
     private LocalizedString mainMenuText;
     
     public LocalizedString MainMenuText
@@ -95,7 +96,7 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         Document = document;
         tempGuid = Guid.NewGuid();
         savingTimer = new Timer();
-        updateTextTimer = new Timer(TimeSpan.FromSeconds(3));
+        updateTextTimer = new Timer(TimeSpan.FromSeconds(3.8));
 
         savingTimer.Elapsed += (_, _) => TryAutosave();
         savingTimer.AutoReset = false;
@@ -116,7 +117,9 @@ internal class AutosaveDocumentViewModel : NotifyableObject
     }
 
     public static bool AutosavingEnabled =>
-        (int)IPreferences.Current.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault) != -1;
+        (int)IPreferences.Current!.GetPreference(PreferencesConstants.AutosavePeriodMinutes, PreferencesConstants.AutosavePeriodDefault) != -1;
+
+    public static bool SaveStateEnabled => IPreferences.Current!.GetPreference(PreferencesConstants.SaveSessionStateEnabled, PreferencesConstants.SaveSessionStateDefault);
 
     public void HintFinishedAction()
     {
@@ -125,18 +128,7 @@ internal class AutosaveDocumentViewModel : NotifyableObject
 
         saveAfterNextFinish = false;
         
-        SafeAutosave();
-    }
-
-    public void HintSave()
-    {
-        if (!reenableAfterNextSave)
-            return;
-
-        reenableAfterNextSave = false;
-        
-        RestartTimers();
-        SetAutosaveText();
+        SafeAutosave(true);
     }
 
     private void SetAutosaveText()
@@ -158,7 +150,7 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_SAVING_IN", adjusted.Minutes.ToString(), minute), ClockIcon, InactiveBrush, false);
     }
 
-    public void TryAutosave()
+    public void TryAutosave(bool saveUserFileIfEnabled = true)
     {
         if (Document.UpdateableChangeActive)
         {
@@ -181,14 +173,14 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         }
 
         updateTextTimer.Stop();
-        SafeAutosave();
+        SafeAutosave(saveUserFileIfEnabled);
     }
 
-    private void SafeAutosave()
+    private void SafeAutosave(bool saveUserFile)
     {
         try
         {
-            Autosave();
+            Autosave(saveUserFile);
         }
         catch (Exception e)
         {
@@ -212,44 +204,32 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         }
     }
     
-    private void Autosave()
+    private void Autosave(bool saveUserFile)
     {
         saveAfterNextFinish = false;
         
         UpdateMainMenuTextSave("AUTOSAVE_SAVING", SavingIcon, ActiveBrush, true);
 
-        string filePath;
-        bool fileExists = true;
-
-        if (Document.FullFilePath == null || !Document.FullFilePath.EndsWith(".pixi"))
-        {
-            filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
-            Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
-        }
-        else
-        {
-            filePath = Document.FullFilePath;
-            fileExists = File.Exists(filePath);
-        }
+        string filePath = Path.Join(Paths.PathToUnsavedFilesFolder, $"autosave-{tempGuid}.pixi");
+        Directory.CreateDirectory(Directory.GetParent(filePath)!.FullName);
         
         busyTimer.Start();
         var result = Exporter.TrySave(Document, filePath);
 
-        if (result == SaveResult.Success && fileExists)
+        if (result == SaveResult.Success)
         {
-            savingFailed = 0;
-            UpdateMainMenuTextSave("AUTOSAVE_SAVED", SaveIcon, SuccessBrush, false);
-            Document.MarkAsSaved();
+            if (saveUserFile && SaveToDocumentPath && Document.FullFilePath != null)
+            {
+                CopyTemp(filePath);
+            }
+            else
+            {
+                UpdateMainMenuTextSave("AUTOSAVE_SAVED", SaveIcon, SuccessBrush, false);
+            }
+            
+            Document.MarkAsAutosaved();
             LastSavedPath = filePath;
         }
-        else if (result is SaveResult.InvalidPath or SaveResult.SecurityError || !fileExists)
-        {
-            UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", SaveIcon, ErrorBrush, true);
-            busyTimer.Stop();
-            Document.Busy = false;
-            reenableAfterNextSave = true;
-            return;
-        }
         else
         {
             busyTimer.Stop();
@@ -262,9 +242,9 @@ internal class AutosaveDocumentViewModel : NotifyableObject
             UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), WarnIcon, WarnBrush, true);
             savingFailed++;
 
-            if (savingFailed == 3)
+            if (savingFailed < 3)
             {
-                CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave 3 times in a row due to {result}"));
+                Task.Run(() => CrashHelper.SendExceptionInfoToWebhook(new Exception($"Failed to autosave for the {savingFailed}. time due to {result}")));
             }
         }
         
@@ -274,6 +254,38 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         RestartTimers();
     }
 
+    private void CopyTemp(string tempPath)
+    {
+        if (File.Exists(Document.FullFilePath))
+        {
+            UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", SaveIcon, ErrorBrush, true);
+        }
+        
+        Task.Run(Copy);
+        
+        void Copy()
+        {
+            try
+            {
+                File.Copy(tempPath, Document.FullFilePath!, true);
+                Document.MarkAsSaved();
+                UpdateMainMenuTextSave("AUTOSAVE_SAVED", SaveIcon, SuccessBrush, false);
+            }
+            catch (Exception e) when (e is UnauthorizedAccessException or DirectoryNotFoundException)
+            {
+                UpdateMainMenuTextSave("AUTOSAVE_PLEASE_RESAVE", SaveIcon, ErrorBrush, true);
+            }
+            catch
+            {
+                var minute = AutosavePeriodMinutes <= 1
+                    ? new LocalizedString("MINUTE_SINGULAR")
+                    : new LocalizedString("MINUTE_PLURAL");
+            
+                UpdateMainMenuTextSave(new LocalizedString("AUTOSAVE_FAILED_RETRYING", AutosavePeriodMinutes.ToString("0"), minute), WarnIcon, WarnBrush, true);
+            }
+        }
+    }
+
     private void RestartTimers()
     {
         savingTimer.Start();
@@ -321,7 +333,7 @@ internal class AutosaveDocumentViewModel : NotifyableObject
         }
     }
 
-    public void SetTempFileGuiAndLastSavedPath(Guid guid, string lastSavedPath)
+    public void SetTempFileGuidAndLastSavedPath(Guid guid, string lastSavedPath)
     {
         tempGuid = guid;
         LastSavedPath = lastSavedPath;

+ 12 - 0
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -77,6 +77,12 @@ internal partial class DocumentViewModel : NotifyableObject
         }
     }
 
+    private Guid? lastChangeOnAutosave;
+    public bool AllChangesAutosaved
+    {
+        get => Internals.Tracker.LastChangeGuid == lastChangeOnSave;
+    }
+    
     public DateTime OpenedUTC { get; } = DateTime.UtcNow;
 
     private bool horizontalSymmetryAxisEnabled;
@@ -300,6 +306,12 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    public void MarkAsAutosaved()
+    {
+        lastChangeOnAutosave = Internals.Tracker.LastChangeGuid;
+        RaisePropertyChanged(nameof(AllChangesAutosaved));
+    }
+
     public void MarkAsUnsaved()
     {
         lastChangeOnSave = Guid.NewGuid();

+ 10 - 0
src/PixiEditor/ViewModels/SubViewModels/Main/DebugViewModel.cs

@@ -13,6 +13,7 @@ using PixiEditor.Models.Commands.Templates.Parsers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
 using PixiEditor.Models.Localization;
+using PixiEditor.Views;
 using PixiEditor.Views.Dialogs.DebugDialogs;
 using PixiEditor.Views.Dialogs.DebugDialogs.Localization;
 
@@ -228,6 +229,15 @@ internal class DebugViewModel : SubViewModel<ViewModelMain>
     [Command.Debug("PixiEditor.Debug.Crash", "CRASH", "CRASH_APP")]
     public static void Crash() => throw new InvalidOperationException("User requested to crash :c");
 
+    [Command.Debug("PixiEditor.Debug.Freeze", "FREEZE", "FREEZE_APP")]
+    public static void Freeze()
+    {
+        MainWindow.Current.Dispatcher.Invoke(() =>
+        {
+            Thread.Sleep(-1);
+        });
+    }
+
     [Command.Debug("PixiEditor.Debug.DeleteUserPreferences", @"%appdata%\PixiEditor\user_preferences.json", "DELETE_USR_PREFS", "DELETE_USR_PREFS")]
     [Command.Debug("PixiEditor.Debug.DeleteShortcutFile", @"%appdata%\PixiEditor\shortcuts.json", "DELETE_SHORTCUT_FILE", "DELETE_SHORTCUT_FILE")]
     [Command.Debug("PixiEditor.Debug.DeleteEditorData", @"%localappdata%\PixiEditor\editor_data.json", "DELETE_EDITOR_DATA", "DELETE_EDITOR_DATA")]

+ 11 - 11
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -102,7 +102,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
         if (!args.Contains("--crash"))
         {
-            ReopenUnsavedFiles();
+            ReopenTempFiles();
         }
         
         if (file != null)
@@ -118,28 +118,29 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
     }
 
-    private void ReopenUnsavedFiles()
+    private void ReopenTempFiles()
     {
         var preferences = Owner.Preferences;
-        var files = preferences.GetLocalPreference<string[]>(PreferencesConstants.UnsavedNextSessionFiles);
+        var files = preferences.GetLocalPreference<AutosaveFilePathInfo[]>(PreferencesConstants.UnsavedNextSessionFiles);
 
         if (files == null)
             return;
         
-        foreach (string file in files)
+        foreach (var file in files)
         {
             try
             {
-                if (file.StartsWith(Paths.PathToUnsavedFilesFolder))
+                if (file.AutosavePath != null)
                 {
-                    string guidString = Path.GetFileNameWithoutExtension(file)["autosave-".Length..];
-                    var document = OpenFromPath(file, false);
-
-                    document.AutosaveViewModel.SetTempFileGuiAndLastSavedPath(Guid.Parse(guidString), file);
+                    string guidString = Path.GetFileNameWithoutExtension(file.AutosavePath)["autosave-".Length..];
+                    var document = OpenFromPath(file.AutosavePath, false);
+                    document.FullFilePath = file.OriginalPath;
+                    
+                    document.AutosaveViewModel.SetTempFileGuidAndLastSavedPath(Guid.Parse(guidString), file.AutosavePath);
                 }
                 else
                 {
-                    OpenFromPath(file);
+                    OpenFromPath(file.OriginalPath);
                 }
             }
             catch (Exception e)
@@ -374,7 +375,6 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
         document.FullFilePath = finalPath;
         document.MarkAsSaved();
-        document.AutosaveViewModel.HintSave();
         return true;
     }
 

+ 7 - 10
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -249,24 +249,20 @@ internal class ViewModelMain : ViewModelBase
         return true;
     }
 
-    private void AutosaveAllForNextSession()
+    public void AutosaveAllForNextSession()
     {
-        if (!AutosaveDocumentViewModel.AutosavingEnabled)
+        if (!AutosaveDocumentViewModel.SaveStateEnabled)
         {
             return;
         }
         
-        var list = new List<string>();
+        var list = new List<AutosaveFilePathInfo>();
         foreach (var document in DocumentManagerSubViewModel.Documents)
         {
             document.AutosaveViewModel.TryAutosave();
-            if (document.AutosaveViewModel.LastSavedPath != null)
+            if (document.AutosaveViewModel.LastSavedPath != null || document.FullFilePath != null)
             {
-                list.Add(document.AutosaveViewModel.LastSavedPath);
-            }
-            else if (document.FullFilePath != null)
-            {
-                list.Add(document.FullFilePath);
+                list.Add(new AutosaveFilePathInfo(document.FullFilePath, document.AutosaveViewModel.LastSavedPath));
             }
         }
         
@@ -290,7 +286,8 @@ internal class ViewModelMain : ViewModelBase
         const string ConfirmationDialogMessage = "DOCUMENT_MODIFIED_SAVE";
 
         ConfirmationType result = ConfirmationType.No;
-        if (!document.AllChangesSaved)
+        var hasUnsavedChanges = !(document.AllChangesSaved || document.AllChangesAutosaved);
+        if (hasUnsavedChanges)
         {
             result = ConfirmationDialog.Show(ConfirmationDialogMessage, ConfirmationDialogTitle);
             if (result == ConfirmationType.Yes)

+ 3 - 0
src/PixiEditor/Views/MainWindow.xaml

@@ -418,6 +418,9 @@
                         <MenuItem
                             ui1:Translator.Key="CRASH"
                             cmds:Menu.Command="PixiEditor.Debug.Crash" />
+                        <MenuItem
+                            ui1:Translator.Key="FREEZE"
+                            cmds:Menu.Command="PixiEditor.Debug.Freeze" />
                         <MenuItem
                             ui1:Translator.Key="DELETE">
                             <MenuItem