Added dumpexceptions command (#3986)
authorIgor Bagdamyan <37334640+En3Tho@users.noreply.github.com>
Sun, 18 Jun 2023 15:53:20 +0000 (18:53 +0300)
committerGitHub <noreply@github.com>
Sun, 18 Jun 2023 15:53:20 +0000 (08:53 -0700)
Please look at this. Left some questions inside the code. Mainly about
ux/printing stuff.

Should messages be printed in multiple rows just for convenience?

Or alternatively should message be printed in another row? It's not like
a really long message can be shown anyway I guess. But at least more
chars can be shown. This way StackTrace can be partially shown too.

0262a3816710    7ffc556d2da0     System.InvalidOperationException
Message: The Settings Storage This computer was corrupted upon loading
from XML Settings Stor ... (more chars)?
StackTrace: little bit of stacktrace?

For tests should I somehow validate that all output lines except for
header and the last "Total" one contain "Exception" is the name?

P.S. Look at screenshots below first

src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs [new file with mode: 0644]

diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpExceptionsCommand.cs
new file mode 100644 (file)
index 0000000..4f57cf3
--- /dev/null
@@ -0,0 +1,122 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.Diagnostics.DebugServices;
+using Microsoft.Diagnostics.ExtensionCommands.Output;
+using Microsoft.Diagnostics.Runtime;
+
+#nullable enable
+
+namespace Microsoft.Diagnostics.ExtensionCommands
+{
+    [Command(Name = "dumpexceptions", Help = "Displays a list of all managed exceptions.")]
+    public class DumpExceptionsCommand : CommandBase
+    {
+        [ServiceImport]
+        public ClrRuntime Runtime { get; set; } = null!;
+
+        [ServiceImport]
+        public LiveObjectService LiveObjects { get; set; } = null!;
+
+        [Option(Name = "-live")]
+        public bool Live { get; set; }
+
+        [Option(Name = "-dead")]
+        public bool Dead{ get; set; }
+
+        [Option(Name = "-gen")]
+        public string? Generation { get; set; }
+
+        [Option(Name = "-type")]
+        public string? Type { get; set; }
+
+        public override void Invoke()
+        {
+            HeapWithFilters filteredHeap = ParseArguments();
+
+            IEnumerable<ClrObject> exceptionObjects =
+                filteredHeap.EnumerateFilteredObjects(Console.CancellationToken)
+                    .Where(obj => obj.IsException);
+
+            if (Live)
+            {
+                exceptionObjects = exceptionObjects.Where(obj => LiveObjects.IsLive(obj));
+            }
+
+            if (Dead)
+            {
+                exceptionObjects = exceptionObjects.Where(obj => !LiveObjects.IsLive(obj));
+            }
+
+            if (!string.IsNullOrWhiteSpace(Type))
+            {
+                string type = Type!;
+                exceptionObjects = exceptionObjects.Where(obj => obj.Type!.Name!.IndexOf(type, StringComparison.OrdinalIgnoreCase) >= 0);
+            }
+
+            PrintExceptions(exceptionObjects);
+        }
+
+        private void PrintExceptions(IEnumerable<ClrObject> exceptionObjects)
+        {
+            Table output = new(Console, ColumnKind.Pointer, ColumnKind.Pointer, ColumnKind.TypeName);
+            output.WriteHeader("Address", "MethodTable", "Message", "Name");
+
+            int totalExceptions = 0;
+            foreach (ClrObject exceptionObject in exceptionObjects)
+            {
+                totalExceptions++;
+
+                ClrException clrException = exceptionObject.AsException()!;
+                output.WriteRow(exceptionObject.Address, exceptionObject.Type!.MethodTable, exceptionObject.Type!.Name);
+
+                Console.Write("        Message: ");
+                Console.WriteLine(clrException.Message ?? "<null>");
+
+                ImmutableArray<ClrStackFrame> stackTrace = clrException.StackTrace;
+                if (stackTrace.Length > 0)
+                {
+                    Console.Write("        StackFrame: ");
+                    Console.WriteLine(stackTrace[0].ToString());
+                }
+            }
+
+            Console.WriteLine();
+            Console.WriteLine($"    Total: {totalExceptions} objects");
+        }
+
+        private HeapWithFilters ParseArguments()
+        {
+            HeapWithFilters filteredHeap = new(Runtime.Heap);
+
+            if (Live && Dead)
+            {
+                Live = false;
+                Dead = false;
+            }
+
+            // TODO: CR: this is the same as in dumpheap. Where to put this?
+            if (!string.IsNullOrWhiteSpace(Generation))
+            {
+                Generation generation = Generation!.ToLowerInvariant() switch
+                {
+                    "gen0" => Diagnostics.Runtime.Generation.Generation0,
+                    "gen1" => Diagnostics.Runtime.Generation.Generation1,
+                    "gen2" => Diagnostics.Runtime.Generation.Generation2,
+                    "loh" or "large" => Diagnostics.Runtime.Generation.Large,
+                    "poh" or "pinned" => Diagnostics.Runtime.Generation.Pinned,
+                    "foh" or "frozen" => Diagnostics.Runtime.Generation.Frozen,
+                    _ => throw new ArgumentException($"Unknown generation: {Generation}. Only gen0, gen1, gen2, loh (large), poh (pinned) and foh (frozen) are supported")
+                };
+
+                filteredHeap.Generation = generation;
+            }
+
+            return filteredHeap;
+        }
+    }
+}