The MetadataMethod constructor cut the post-dot short name for every
static non-generic method whose name contains a dot, only to test it
for an op_ prefix that almost never matches. The prefix is now checked
on a span slice first, so the substring (still required by
OperatorDeclaration.GetOperatorType) is allocated only for actual
explicit-interface operator implementations.
Assisted-by: Claude:claude-fable-5:Claude Code
The "IL with C#" view cut up to four substrings (prefix, trimmed
prefix, highlighted range, suffix) out of every source line emitted
alongside an IL instruction. ISmartTextOutput now accepts a
ReadOnlySpan<char> - as a default interface method falling back to
Write(text.ToString()) so existing implementers keep working - and
AvaloniaEditTextOutput appends the span straight into its
StringBuilder. The overload lives on ISmartTextOutput rather than
ITextOutput because the latter is netstandard2.0 (no default interface
methods there), where a new member would break every external
implementer of the decompiler library; the highlighted-comment path is
typed against ISmartTextOutput already.
Assisted-by: Claude:claude-fable-5:Claude Code
CleanUpVariableName sits on the per-variable naming path of every
decompiled method and allocated up to three intermediates (backtick
cut, m_/_ prefix strip, lowercase-first concat) before producing its
result, and its callers added further throwaway substrings when
stripping get_/set_/Get/Set and interface-I prefixes. The cuts are now
slices over the original name: ContainsNonPrintableIdentifierChar and
IsValidName gained span overloads, and only the final lowered name is
materialized, in a single allocation via char[] (netstandard2.0 has no
string(span) constructor). IsKeyword keeps its string parameter - it
is called on that final string anyway, and the keyword HashSet has no
span lookup on netstandard2.0.
Assisted-by: Claude:claude-fable-5:Claude Code
EscapeIdentifier runs for every identifier token emitted, yet it built
a StringBuilder plus a fresh result string even for the overwhelmingly
common case of an identifier with no escapable characters. A pre-scan
now returns the original instance untouched in that case, and the
surrogate-pair copy appends the two chars directly instead of cutting
a two-char substring. New unit tests pin the escaping behavior and the
identity fast path.
Assisted-by: Claude:claude-fable-5:Claude Code
WriteOpCode allocated two strings per rendered ldarg.N/ldloc.N/stloc.N
instruction (the digit cut off the mnemonic plus the concatenated
local-reference key), even though the shortcut forms only ever produce
the indices 0-3. The digit text and the param_/loc_ reference keys now
come from static tables indexed by opcode arithmetic.
Assisted-by: Claude:claude-fable-5:Claude Code
SplitTypeParameterCountFromReflectionName allocated the digits after
the backtick only to feed int.TryParse and discard them, and the
TopLevelTypeName constructor cut the name part twice for generic
types (once at the dot, again at the backtick), dropping the first
cut. Both run for every generic reflection name parsed, e.g. for
typeof-valued attribute arguments and string-switch metadata. The
arity is now parsed in place with a digit loop (netstandard2.0 has
no span int.TryParse) and each final string is cut exactly once.
The digit loop only accepts plain ASCII digits, so suffixes like
`+1 that int.TryParse tolerated are now rejected; such names are not
legal reflection names. New unit tests pin the parse edge cases.
Assisted-by: Claude:claude-fable-5:Claude Code
AbstractSearchStrategy.IsMatch runs for every metadata row of every
loaded module on each keystroke, yet it re-stripped the operator prefix
per name and, for fuzzy terms, additionally lowercased both the name
and the term - the largest allocation source in interactive search.
The terms are invariant for the lifetime of a strategy (each keystroke
builds a new request and strategy), so the stripping and lowercasing
now happen once in the constructor, and the noncontiguous matcher
compares spans with per-char ToLowerInvariant. This switches fuzzy
matching from culture-sensitive to invariant lowercasing, which is the
appropriate semantic for matching metadata names.
The new IsMatchTests pin the +, -, =, ~ operator semantics (including
the pre-existing quirk that =Name compares against the backtick-
suffixed name of generic types) as observed before the change.
Assisted-by: Claude:claude-fable-5:Claude Code
The loop preceding the parse has already proven that the tail consists
solely of ASCII digits, so the Substring+int.TryParse pair only
re-validated them at the cost of a throwaway string allocation.
SplitName runs per variable and per reserved-name registration for
every decompiled method, making this one of the hottest Substring call
sites in the decompiler. Accumulating the digits inline keeps the
TryParse overflow semantics (fall back to number=1 and the unchanged
name) without allocating.
Assisted-by: Claude:claude-fable-5:Claude Code
The bookmark navigation tests asserted the one-shot line highlight by
polling the text view's renderer collection, but the adorner
self-dismisses after an ~800 ms lifetime driven by a DispatcherTimer.
On a loaded CI runner (the desktop job runs the UI and decompiler test
suites concurrently) the dispatcher can stall long enough that the
adorner registers and is dismissed again before the test's next
predicate check, so the wait misses the entire play and burns its full
60 s timeout; raising the timeout cannot help with that. Record the
last played line on DecompilerTextView as persistent evidence of the
one-shot highlight and assert that instead - it also pins the highlight
to the expected line, which the presence check never did.
Assisted-by: Claude:claude-fable-5:Claude Code
The legacy .NET Framework vbc lowers anonymous types differently from
Roslyn: ToString builds its result with a StringBuilder instead of one
String.Format call, no DebuggerBrowsable/DebuggerHidden attributes are
emitted even in debug builds, and in optimized builds the DebuggerDisplay
attribute precedes CompilerGenerated in metadata order. The None/Optimize
test configurations only run on machines where that compiler is
installed, which is why the fixture did not cover them yet.
Assisted-by: Claude:claude-fable-5:Claude Code
A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
The VB compiler carries the range variables of a query in $VB$It, $VB$It1,
$VB$It2 and $VB$ItAnonymous, its counterpart to C#'s <>h__TransparentIdentifier.
Unrecognized, they were left in place by CombineQueryExpressions, and since '$'
is not legal in a C# identifier every VB query with more than one range
variable decompiled to code that cannot be recompiled.
Assisted-by: Claude:claude-fable-5:Claude Code
Two predicates disagreed on what a generated name looks like. At the metadata
level a '$' in the name counts, so MemberIsHidden treated VB$AnonymousType_0
as an anonymous type and dropped its definition from the output. At the type
system level only '<' counted, so none of the anonymous-type translations in
CallBuilder and ExpressionBuilder fired. VB assemblies therefore lost the
definitions and kept the raw metadata names at every use site, which is not
valid C#.
Both levels now share one predicate and cannot drift apart again. It keeps the
metadata-level behaviour exactly: counting every name that merely contains
'<' would newly capture explicit implementations of generic interface members.
Assisted-by: Claude:claude-fable-5:Claude Code
VBPretty had no coverage of VB's anonymous types, so nothing caught that their
use sites decompiled to the raw metadata names while their definitions were
hidden from the output. The expected C# is written as it should read once the
generated-name predicates agree with each other; it fails until then.
Roslyn 2.10 targeting .NET Core 2.2 is branched off with #if: there the query
operator calls are not restored to extension-method syntax, so no query
expression is formed and the lowered form survives.
Assisted-by: Claude:claude-fable-5:Claude Code
FractionApprox rejects inputs above 0x7FFFFFFF because they cannot be stored
as a fraction, but the check was one-sided while the sign is stripped right
after it. A large negative value therefore reached the continued-fraction loop
and overflowed the terms it accumulates. ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so that aborted decompilation of the whole member
instead of wrapping.
Found by fuzzing nuget.org; reproduces on MathNet.Numerics, whose constants
reach the approximation through their ratio to PI.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
HandleSimpleArrayInitializer multiplies the array dimensions to size the list
it collects elements into. The dimensions come from the input assembly and
need not multiply within int range, and ICSharpCode.Decompiler is built with
CheckForOverflowUnderflow, so an implausible pair of dimensions aborted
decompilation of the whole member. The product is only a capacity hint, so it
can saturate.
Found by fuzzing nuget.org; reproduces on obfuscated assemblies.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
IsCopyConstructor required the copy constructor to be private on a sealed
record and protected otherwise, but IsGeneratedCopyConstructor in the same
class accepts protected regardless of sealedness. Since the former gates the
latter, a sealed record whose copy constructor stayed protected -- what you get
when a record is sealed after it was compiled -- was not recognized as one at
all: it fell through to the general constructor handling, where its base call
made it count as unchained, and the primary-constructor invariant then failed.
Found by fuzzing nuget.org. Beyond silencing the assertion this improves the
output, as the affected types now decompile to positional records.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The accessor forwarding-stub matcher bounded the IL body from above but not
from below. Reference assemblies keep the method RVA while stripping the body
to zero bytes, so an explicit interface accessor there sailed past the size
check and the first opcode read ran off the end of the blob, aborting the
whole type with a BadImageFormatException. The sibling matcher in
TransformDisplayClassUsage already guards its lower bound; this one did not.
Found by fuzzing nuget.org: every package resolving to the
Microsoft.NETFramework.ReferenceAssemblies packs was affected.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Roslyn compiles top-level statements into a synthesized Program class
whose entry point is called '<Main>$', a name that cannot be declared
in C#. Decompiled output kept it verbatim (escaped to
_003CMain_003E_0024 when exporting a project), and since C# accepts
only a method called 'Main' as an entry point, the exported executable
did not compile (CS5001).
Give that method the name 'Main'. Per the decision recorded in #829 we
do not reconstruct top-level statements, so this is the level of
support the output needs to compile.
An async top-level program needs the name in a different place: it
compiles to '<Main>$' holding the statements plus a '<Main>' entry
point that only awaits it. That wrapper carries the .entrypoint marker
but is hidden from the output, so the name goes to the method it
awaits instead - unless AsyncAwait is off, when the wrapper is
emitted and keeps the name itself.
Assisted-by: Claude:claude-fable-5:Claude Code
decimal has no IL literal: legacy csc compiles 0m to a Decimal.Zero
field load, which already decompiled to the literal, but Roslyn
compiles it to a zero-initialization, which decompiled to
default(decimal). The two forms are bit-identical for decimal, so the
literal is no less faithful to the IL and matches what a human writes;
it also removes the per-compiler split in the CompoundAssignmentTest
fixture.
Assisted-by: Claude:claude-fable-5:Claude Code
Operator precedence made the condition read
(setting && Add) || Subtract, so 'x -= 1' was converted to 'x--' even
with IntroduceIncrementAndDecrement disabled. Only observable with the
non-default setting, which no fixture configuration exercises, so no
test accompanies the fix.
Assisted-by: Claude:claude-fable-5:Claude Code
Reviving the 2020 test-cases-fp-types fixtures (compound assignment on
float, double and decimal) exposed an asymmetry: post-increment and
post-decrement on float/double round-tripped as x++/x--, but the pre
forms came back as x += 1f because the increment detection in
PrettifyAssignments only accepted integer constants. C# defines ++/--
on floating-point types as adding or subtracting exactly 1, so the
conversion is exact for a constant 1 operand. Decimal already works
through the op_Increment/op_Decrement path.
Assisted-by: Claude:claude-fable-5:Claude Code
Optimized code stores no temporary for a deconstruction element that is
used only once after the deconstruction. MatchAssignments handled that
for trailing elements, but a nested deconstruction copies the inner
element to a temporary, so the elements preceding it are also left
without an assignment; their external load then violated the
DeconstructInstruction invariant that all pattern variable loads are
descendants of the instruction. The forwarding fixup now covers all
unassigned elements and inserts in pattern order, because the statement
and expression builders pair pattern variables with assignments
positionally. This also fixes the nested tuple deconstruction crash
reported in #3388.
Also unwrap the address of the tested operand in
VisitDeconstructInstruction: deconstructing a struct passes the
receiver by reference, which was emitted as an invalid cast,
'var (x, y) = (S)(ref s);', even without nesting.
Fixes#3388.
Assisted-by: Claude:claude-fable-5:Claude Code
Widening the compiler matrix answers the open review question on the
Issue3230 fixture guard empirically: a class naming its own nested
interface in its base list is a Roslyn-era relaxation. The legacy csc
rejects every such shape with CS0146 (circular base class dependency,
it never reaches the accessibility check), mcs 2.6.4 rejects them with
CS0122/CS0146, and mcs 5.23 accepts them all, rejects naming a base
class's protected nested interface with the same CS0122 as Roslyn, and
emits the same transitive InterfaceImpl metadata. The fixtures are
therefore gated to ROSLYN || MCS5, which exercises the base-list filter
on mcs-generated metadata as well. The pre-existing class C needs
MCS2-specific expected output because mcs 2.6.4 reorders interface-impl
rows and explicit implementations in metadata.
Assisted-by: Claude:claude-fable-5:Claude Code
A class may name its own protected nested interface in its base list
(class F : F.IFoo), but referencing a protected interface nested in a
base class there (class SubF : F, F.IFoo) does not compile, even though
that interface is accessible inside the class body. The interface-impl
metadata still lists such interfaces (they are inherited through other
entries), so emitting every entry produced uncompilable sources. Skip
base types that are neither nested within the current type's nesting
chain nor accessible from the enclosing scope without the
protected-through-inheritance privilege. The check covers every type
the base-list reference names: type arguments, array and tuple
elements, and the declaring chain of the named type.
Assisted-by: Claude:claude-fable-5:Claude Code
TypeDefTableTreeNode and MethodTableTreeNode carried their own copies of
the FieldList/MethodList/ParamList width rule, and ImplMapTableTreeNode
and FieldMarshalTableTreeNode derived heap index widths from the heap
size; both rules diverge from SRM (the *Ptr row count only decides
whether the pointer table itself is large, and heap widths are declared
by the HeapSizes flags, not derived). Rather than fixing the same bugs
in two places, these four nodes now consume the shared readers, which
also verify their computed widths against the SRM row size. The
remaining raw-reading nodes only use plain simple-index widths and are
left for a follow-up.
Assisted-by: Claude:claude-fable-5:Claude Code
The GUI has the metadata-tables view; the CLI had nothing, so checking
e.g. which MethodSemantics rows reference a Property row required a
hand-written System.Reflection.Metadata script. --dump-table <name>
prints every row of a table (RID, token, resolved names, heap offsets,
coded indexes) as an aligned text table, or as JSON with --json, for
the same 39 Cor tables the GUI shows.
Row enumeration for tables without public SRM row access lives in
MetadataExtensions next to the existing GetMethodSemantics helper, so
the GUI's raw-reading table nodes can be folded onto the shared
readers later. Every table's columns are spelled out explicitly in
ECMA-335 declaration order: reflecting over the SRM row structs would
tie the output (and its column order) to runtime internals, and
deterministic output is the point of the feature. JSON uses
System.Text.Json from the shared framework, so no new package
reference is needed.
Assisted-by: Claude:claude-fable-5:Claude Code
The Semantics and Association columns were read from offsets relative
to the metadata root instead of the current row: Semantics always
decoded the first two bytes of the metadata header and Association a
constant offset near it, so only the Method column ever carried real
row data. The Association coded-index width also used the plain-index
threshold (2^16) instead of the coded one (2^15 for one tag bit).
Rewrite the loop with a BlobReader positioned at the table start,
reading each column in sequence, and introduce SimpleIndexSize and
CodedIndexSize helpers encoding the ECMA-335 II.24.2.6 width rules.
The GUI's MethodSemantics metadata table view consumes this helper and
displayed the garbage values.
Assisted-by: Claude:claude-fable-5:Claude Code
The interactive view intentionally shows verbatim metadata names
(EscapeInvalidIdentifiers only runs for save/export), so names compiled
from F# or obfuscators can contain apostrophes. The Char span in the C#
highlighting grammar opened at any apostrophe, coloring everything up to
the next one as a char literal. A negative lookbehind limits the span to
positions where a real char literal can occur.
Assisted-by: Claude:claude-fable-5:Claude Code
The resolver comments cited section numbers from the C# 4.0 spec (and a
few from C# 9.0 drafts), which no longer match the published ECMA-334
standard. Renumber them against dotnet/csharpstandard draft-v11; every
reference was checked against the actual section headings. The old
'better conversion from type' subclause (7.5.3.4) no longer exists as
such and its rules live in 12.6.4.5-12.6.4.7, so that comment now says
so instead of pointing at a dead number.
Assisted-by: Claude:claude-fable-5:Claude Code
CI versions derived from branch names can contain underscores (e.g.
'avalonia12_1_1'), which dpkg rejects in the Version field. Map every
character outside dpkg's allowed set to '.' after the existing '-' to
'~' substitution.
Assisted-by: Claude:claude-fable-5:Claude Code
The tab-opening test only verifies document tab and selection wiring. Using a tiny in-assembly fixture avoids cold framework decompilation work in CI and lowers the chance of unrelated timeout noise.
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Right-clicking inside the details editor inherited the metadata grid's
cell-oriented context menu, whose Copy entries are enabled only for a
hovered DataGridCell -- inside the details area there is none, so the
menu showed permanently disabled entries and selected text could not be
copied by mouse. The editor now carries the decompiler view's editor
menu shape: Copy (rich HTML copy with plain fallback, enabled while a
selection exists) and Select All. Enablement is decided in
ContextMenu.Opening, which only the context-request gesture raises;
the test therefore raises ContextRequested instead of calling Open().
Assisted-by: Claude:claude-fable-5:Claude Code
A one-line payload (short hex dump, tiny source-link document) rendered
as a squeezed strip barely taller than the row itself, which does not
read as an expandable details area. Floor the editor at 100px so the
details region stays visually recognisable regardless of payload size.
Assisted-by: Claude:claude-fable-5:Claude Code
The text-blob editor in the metadata row-details area hardcoded its font
and lacked the text view's flat selection highlight, so embedded source
looked different from the decompiled code right next to it and ignored
the user's font choice in the Options page. The decompiler-view look
(user-selected font applied live while attached, themed background,
square-cornered translucent selection) now lives in DecompilerTextEditor
itself, giving every surface hosting the editor the same appearance by
construction; DecompilerTextView and BuildTextBlob drop their now
redundant per-site styling.
Assisted-by: Claude:claude-fable-5:Claude Code
The CustomDebugInformation details area rendered decoded text payloads
(embedded source, source-link JSON, hex dumps) in a plain TextBox, which
shows code without any highlighting and materializes the whole formatted
text up front, so large embedded-source documents were expensive. Text
payloads now travel as a TextBlobDetail tagged with a file extension --
".json" for source link, the parent document's extension for embedded
source, none for hex -- and render in the theme-aware AvaloniaEdit
editor, which colours them via the existing highlighting registry and
virtualizes long documents. The editor's ThemeChanged subscription moves
from the constructor to OnAttachedToVisualTree so it stays paired with
the detach-time unsubscribe now that editors can leave and re-enter the
visual tree inside recycled row-details containers.
Assisted-by: Claude:claude-fable-5:Claude Code
The details content was pinned left and the text blob capped at 800px,
leaving dead space to the right of embedded-source text and the
flags/typed sub-grids. Let all three detail shapes stretch and give the
last sub-grid column the leftover width so the details area fills its
host row.
Assisted-by: Claude:claude-fable-5:Claude Code
The dotnet-hosted Roslyn 2.10 build cannot start its VBCSCompiler server
under a current dotnet host, so with /shared every test compilation first
waited out the client's full 20-second new-server connection timeout
before falling back to a sub-second in-process compile. Since the 2.10
configurations were enabled on non-Windows (#3914), that added ~29
minutes to the Linux CI job and ~43 minutes on macOS: ~340 affected
tests at ~21s each, versus ~0.2s for the toolsets whose server works.
Assisted-by: Claude:claude-fable-5:Claude Code
Review follow-up. A display-class field initialized from a non-this
parameter is now the only shape where propagation and a later mutation
coexist; it stays sound only because ResolveVariableToPropagate accepts
a parameter with LoadCount == 1, so the mutation can be redirected to
it. Nothing covered that, so Test12 pins it, and Test13 records the
neighbouring shape where the mutation happens inside a lambda - there
capturing the display class keeps it materialized and propagation never
arises. The guard predicate is renamed to say what it matches, since
'ReadOnly' reads like the C# keyword rather than 'a plain read'.
Assisted-by: Claude:claude-fable-5:Claude Code
A field that is propagated to the variable it was initialized from is
replaced by that variable, so re-emitting its initializer assigns the
variable to itself. Where the field was initialized from 'this' the
result does not even compile ('this = this'). The store is dropped
instead, which is what VisitStObj already does for initializer stores
that are not part of an object-initializer block; the insertion position
has to be tracked separately from the loop index, because skipping a
store would otherwise push the following ones past the end of the block.
Assisted-by: Claude:claude-fable-5:Claude Code
Aggressive scalar replacement propagated a display-class field to its
source variable even when the field is mutated after initialization,
aliasing two distinct source-level variables (Test9: thisField and
this). Propagation is now cancelled when the field sees a second store
or its address escapes, but only for propagation targets that cannot
absorb the store: 'this' and variables that are themselves
scalar-replaced display classes. Parameters continue to propagate,
because their remaining uses are already restricted by
ResolveVariableToPropagate and a captured parameter mutated inside a
lambda (DelegateConstruction's Bug951) must keep mapping to the
parameter. Checking CanPropagate first also keeps the guard away from
Mono state-machine fields, whose VariableToDeclare is pre-bound to a
state-machine variable that Propagate(null) would discard.
Re-enables Test9 and adds Test10 covering the escaping-address variant
(Interlocked.Exchange(ref displayClass.thisField, ...)).
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Assisted-by: Claude:claude-fable-5:Claude Code
The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.
Assisted-by: Claude:claude-fable-5:Claude Code
Avalonia windows default to ShowInTaskbar=true, so every owned modal
dialog (Open from GAC/NuGet feed/running process, Manage Assembly
Lists, Export Project, Create List, Set Target Framework) got its own
taskbar button, which is not standard Windows behavior. The assertion
and crash dialogs intentionally keep their buttons so an interrupted
session stays easy to find.
Assisted-by: Claude:claude-fable-5:Claude Code
ProDataGrid derives its scroll extent from the current scroll offset, so an
offset that is briefly too large inflates the extent, which permits a larger
offset again. A trackpad reaches that state within a few hundred sub-row
events: instrumented in the running app, a ~1500px list reported 8735px and
kept growing, the thumb collapsed to its minimum, and the end of the list ran
away from the user. A second defect slid the rows sideways by up to 10px - the
star-sized column is measured against the width including the vertical scroll
bar, leaving the grid convinced it has a scroll bar's worth of content to
reach.
Disabling the horizontal scroll bar, giving each grid its own
DefaultRowHeightEstimator, pinning RowHeight, forcing the vertical scroll bar
visible, and disabling scrolling on the template's inner ScrollViewer each
removed a symptom at most; none can break a loop that runs through the scroll
offset, and 12.0.4 is the newest release. A ListBox's virtualizing panel keeps
the extent a function of the items alone. The price is laying out the columns
here - so the header row and the item template have to be kept in step - and
losing sortable, resizable headers.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A collection that fails once the session has been granted - most plausibly
the target exiting before the stop command reaches it - left the task copying
the trace connection behind. That connection is torn down on the way out, the
copy faults, nobody awaits it, and the finalizer hands it to
TaskScheduler.UnobservedTaskException, which this app reports as a crash: a
second report of a failure the dialog's error bar had already explained
correctly, minutes later and detached from the gesture that caused it. The
CollectTracing2 fallback walks the same path, so one dying target produced two
of them.
The teardown order is the substance of the fix. The session connection has to
go first, because after the failure nothing else will ever end the read the
copy is parked on; the drain second, so that its own failure is observed
rather than abandoned; and the half-copied trace last, so nothing is still
writing into it when it is dropped.
The regression test pins the invariant rather than the symptom, because the
symptom is transport-specific: a Windows named pipe reports an aborted
overlapped read as cancellation, and a cancelled task is never unobserved, so
only the unix transport can produce the crash at all. What holds everywhere is
that no drain may still be running once the failure path is done with it.
Assisted-by: Claude:claude-opus-5:Claude Code
A refused unix socket raises SocketException, which derives from
Win32Exception rather than IOException, so it escaped the filter meant to
skip one unreachable process and failed the whole concurrent enumeration
instead: a machine where any .NET process exits between the port scan and
the connect showed an empty list. The classification is now a named
predicate covering both transports.
Two dialog defects shared a shape - state left behind by a query nobody is
waiting for any more. Rebuilding the bound collection on every filter
keystroke made the grid drop its selection and write that null back,
discarding the assemblies of a process the new filter still matched; and the
branch taken when nothing is selected cleared no loading flag, while the
query it superseded was no longer allowed to, so the progress bar animated
over an empty pane. Relatedly, the two-second command budget expired into a
process listed with null metadata, which made a slow machine look like a
runtime that answered with nothing; it is longer now, and expiry names the
process it gave up on, since the only thing that ever reaches the far end of
that budget is a runtime which will never answer.
The test gaps are closed the same way: a real dynamic assembly pins the
in-memory classification, the managed-only assertion is stated as the
property instead of a list of native names to exclude, and the .NET
Framework path gets live tests. Those showed that a desktop CLR process
mostly reports NGen native images rather than the IL assemblies behind them,
which is now recorded as the fidelity gap it is.
Assisted-by: Claude:claude-opus-5:Claude Code