Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .kiro/specs/dotnet-api-diff/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Each task should follow this git workflow:
- **Git Workflow**: Create branch `feature/task-7.1-exit-codes`, commit, push, and create PR
- _Requirements: 2.1, 2.2, 2.4_

- [ ] 7.2 Add comprehensive error handling and logging
- [x] 7.2 Add comprehensive error handling and logging
- Implement structured logging throughout the application
- Add proper exception handling for assembly loading, configuration, and comparison errors
- Write integration tests for error scenarios and recovery
Expand Down
345 changes: 286 additions & 59 deletions src/DotNetApiDiff/AssemblyLoading/AssemblyLoader.cs

Large diffs are not rendered by default.

382 changes: 244 additions & 138 deletions src/DotNetApiDiff/Commands/CompareCommand.cs

Large diffs are not rendered by default.

193 changes: 193 additions & 0 deletions src/DotNetApiDiff/ExitCodes/GlobalExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Security;
using DotNetApiDiff.Interfaces;
using Microsoft.Extensions.Logging;

namespace DotNetApiDiff.ExitCodes
{
/// <summary>
/// Provides centralized exception handling for the application.
/// </summary>
public class GlobalExceptionHandler : IGlobalExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
private readonly IExitCodeManager _exitCodeManager;

/// <summary>
/// Initializes a new instance of the <see cref="GlobalExceptionHandler"/> class.
/// </summary>
/// <param name="logger">The logger to use for logging exceptions.</param>
/// <param name="exitCodeManager">The exit code manager to determine appropriate exit codes.</param>
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger, IExitCodeManager exitCodeManager)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_exitCodeManager = exitCodeManager ?? throw new ArgumentNullException(nameof(exitCodeManager));
}

/// <summary>
/// Handles an exception by logging it and determining the appropriate exit code.
/// </summary>
/// <param name="exception">The exception to handle.</param>
/// <param name="context">Optional context information about where the exception occurred.</param>
/// <returns>The appropriate exit code for the exception.</returns>
public int HandleException(Exception exception, string? context = null)
{
if (exception == null)
{
_logger.LogError("HandleException called with null exception");
return _exitCodeManager.GetExitCodeForException(new ArgumentNullException(nameof(exception)));
}

// Log the exception with context if provided
if (!string.IsNullOrEmpty(context))
{
_logger.LogError(exception, "Error in {Context}: {Message}", context, exception.Message);
}
else
{
_logger.LogError(exception, "Error: {Message}", exception.Message);
}

// Log additional details for specific exception types
LogExceptionDetails(exception);

// Determine the appropriate exit code
int exitCode = _exitCodeManager.GetExitCodeForException(exception);

_logger.LogInformation(
"Exiting with code {ExitCode}: {Description}",
exitCode,
_exitCodeManager.GetExitCodeDescription(exitCode));

return exitCode;
}

/// <summary>
/// Logs additional details for specific exception types.
/// </summary>
/// <param name="exception">The exception to log details for.</param>
private void LogExceptionDetails(Exception exception)
{
switch (exception)
{
case ReflectionTypeLoadException typeLoadEx:
LogReflectionTypeLoadException(typeLoadEx);
break;
case AggregateException aggregateEx:
LogAggregateException(aggregateEx);
break;
case FileNotFoundException fileNotFoundEx:
_logger.LogError("File not found: {FileName}", fileNotFoundEx.FileName);
break;
case BadImageFormatException badImageEx:
_logger.LogError("Bad image format: {FileName}", badImageEx.FileName);
break;
case SecurityException securityEx:
_logger.LogError("Security exception: {PermissionType}", securityEx.PermissionType);
break;
case InvalidOperationException:
// Log the stack trace for InvalidOperationException to help diagnose the issue
_logger.LogDebug("Stack trace: {StackTrace}", exception.StackTrace);
break;
}

// Log inner exception if present
if (exception.InnerException != null)
{
_logger.LogDebug("Inner exception: {Message}", exception.InnerException.Message);
}
}

/// <summary>
/// Logs details for a ReflectionTypeLoadException.
/// </summary>
/// <param name="exception">The ReflectionTypeLoadException to log details for.</param>
private void LogReflectionTypeLoadException(ReflectionTypeLoadException exception)
{
_logger.LogError("ReflectionTypeLoadException: Failed to load {Count} types", exception.Types?.Length ?? 0);

if (exception.LoaderExceptions != null)
{
int loaderExceptionCount = exception.LoaderExceptions.Length;
_logger.LogError("Loader exceptions count: {Count}", loaderExceptionCount);

// Log up to 5 loader exceptions to avoid excessive logging
int logCount = Math.Min(loaderExceptionCount, 5);
for (int i = 0; i < logCount; i++)
{
var loaderEx = exception.LoaderExceptions[i];
if (loaderEx != null)
{
_logger.LogError(loaderEx, "Loader exception {Index}: {Message}", i + 1, loaderEx.Message);
}
}

if (loaderExceptionCount > logCount)
{
_logger.LogError("... and {Count} more loader exceptions", loaderExceptionCount - logCount);
}
}
}

/// <summary>
/// Logs details for an AggregateException.
/// </summary>
/// <param name="exception">The AggregateException to log details for.</param>
private void LogAggregateException(AggregateException exception)
{
_logger.LogError("AggregateException with {Count} inner exceptions", exception.InnerExceptions.Count);

// Log up to 5 inner exceptions to avoid excessive logging
int logCount = Math.Min(exception.InnerExceptions.Count, 5);
for (int i = 0; i < logCount; i++)
{
var innerEx = exception.InnerExceptions[i];
_logger.LogError(innerEx, "Inner exception {Index}: {Message}", i + 1, innerEx.Message);
}

if (exception.InnerExceptions.Count > logCount)
{
_logger.LogError("... and {Count} more inner exceptions", exception.InnerExceptions.Count - logCount);
}
}

/// <summary>
/// Sets up global unhandled exception handling.
/// </summary>
public void SetupGlobalExceptionHandling()
{
// Handle unhandled exceptions in the current AppDomain
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
if (e.ExceptionObject is Exception ex)
{
_logger.LogCritical(ex, "Unhandled exception in AppDomain: {Message}", ex.Message);
}
else
{
_logger.LogCritical("Unhandled non-exception object in AppDomain: {Object}", e.ExceptionObject);
}
};

// Handle unhandled exceptions in tasks
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
_logger.LogCritical(e.Exception, "Unobserved task exception: {Message}", e.Exception.Message);
e.SetObserved(); // Mark as observed to prevent process termination
};

// Handle first-chance exceptions (useful for debugging)
if (_logger.IsEnabled(LogLevel.Debug))
{
// Register for FirstChanceException events only when debug logging is enabled
AppDomain.CurrentDomain.FirstChanceException += (sender, e) =>
{
// Only log first-chance exceptions at debug level to avoid noise
_logger.LogDebug(e.Exception, "First chance exception: {Message}", e.Exception.Message);
};
}
}
}
}
22 changes: 22 additions & 0 deletions src/DotNetApiDiff/Interfaces/IGlobalExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright DotNet API Diff Project Contributors - SPDX Identifier: MIT
namespace DotNetApiDiff.Interfaces
{
/// <summary>
/// Interface for global exception handling.
/// </summary>
public interface IGlobalExceptionHandler
{
/// <summary>
/// Handles an exception by logging it and determining the appropriate exit code.
/// </summary>
/// <param name="exception">The exception to handle.</param>
/// <param name="context">Optional context information about where the exception occurred.</param>
/// <returns>The appropriate exit code for the exception.</returns>
int HandleException(Exception exception, string? context = null);

/// <summary>
/// Sets up global unhandled exception handling.
/// </summary>
void SetupGlobalExceptionHandling();
}
}
Loading
Loading