Browse Source

Skip missing session assemblies when navigating on launch

Navigating to a target on startup first eagerly loads every relevant
assembly's metadata so the entity search that follows can resolve it.
That pre-load used the throwing GetMetadataFileAsync, so a restored
session that still referenced an assembly whose file had since been
deleted or moved crashed startup with an unhandled
DirectoryNotFoundException instead of simply skipping the gone entry.

Use GetMetadataFileOrNullAsync there: a missing or unreadable assembly
now resolves to null and is skipped, which the entity search already
tolerates (it uses the OrNull variant too).

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3794/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
c649060fd0
  1. 31
      ILSpy.Tests/Commands/CommandLineArgumentsTests.cs
  2. 8
      ILSpy/AssemblyTree/AssemblyTreeModel.cs

31
ILSpy.Tests/Commands/CommandLineArgumentsTests.cs

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
@ -108,4 +110,33 @@ public class CommandLineArgumentsTests @@ -108,4 +110,33 @@ public class CommandLineArgumentsTests
// Assert — SelectedItem is still null.
((object?)vm.AssemblyTreeModel.SelectedItem).Should().BeNull();
}
[AvaloniaTest]
public async Task NavigateTo_Skips_A_Missing_Session_Assembly_Instead_Of_Crashing()
{
// A restored session can still list an assembly whose file has since been deleted or
// moved. Navigating on launch eagerly loads every relevant assembly's metadata before
// resolving the target; a gone file must be skipped, not abort startup with an
// unhandled exception. Regression test for a DirectoryNotFoundException thrown out of
// that pre-load loop.
// Arrange — boot, then inject a session assembly whose file does not exist.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var missingPath = Path.Combine(
Path.GetTempPath(), "ILSpyMissing_" + Guid.NewGuid().ToString("N"), "gone.dll");
vm.AssemblyTreeModel.AssemblyList!.OpenAssembly(missingPath);
var args = CommandLineArguments.Create(new[] { "--navigateto", "T:System.Linq.Enumerable" });
// Act — applying the args must complete without throwing despite the missing entry.
await vm.AssemblyTreeModel.HandleCommandLineArgumentsAsync(args);
// Assert — the missing assembly was skipped and the present target still resolved.
((object?)vm.AssemblyTreeModel.SelectedItem).Should().NotBeNull();
vm.AssemblyTreeModel.SelectedItem!.ToString().Should().Be("System.Linq.Enumerable");
}
}

8
ILSpy/AssemblyTree/AssemblyTreeModel.cs

@ -710,7 +710,9 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -710,7 +710,9 @@ namespace ICSharpCode.ILSpy.AssemblyTree
var assemblyNode = FindAssemblyNode(asm);
if (assemblyNode == null)
continue;
await asm.GetMetadataFileAsync().ConfigureAwait(true);
// A restored session can reference an assembly whose file is gone; loading it must
// not abort navigation, so skip a failed load rather than throw out of startup.
await asm.GetMetadataFileOrNullAsync().ConfigureAwait(true);
var nsNode = assemblyNode.FindNamespaceNode(namespaceName);
if (nsNode != null)
{
@ -721,8 +723,10 @@ namespace ICSharpCode.ILSpy.AssemblyTree @@ -721,8 +723,10 @@ namespace ICSharpCode.ILSpy.AssemblyTree
return;
}
// A gone or unreadable assembly resolves to null and is skipped by the entity search
// below; eagerly loading it here must not throw and crash navigation on launch.
foreach (var asm in relevant)
await asm.GetMetadataFileAsync().ConfigureAwait(true);
await asm.GetMetadataFileOrNullAsync().ConfigureAwait(true);
var entity = await Task.Run(() => FindEntityInRelevantAssemblies(navigateTo, relevant));
if (entity != null)

Loading…
Cancel
Save