Implement centralized enhanced error handling

- Created MinIOErrorHandler utility with comprehensive error details
- Captures Message, ExceptionType, InnerException, StackTrace, Timestamp, Machine, ProcessId
- Provides detailed logging with Write-Warning for debugging
- Integrated with MinIOBaseCmdlet ExecuteOperation methods
- Fixed parameter name consistency (BucketName across all cmdlets)
- Supports both terminating and non-terminating error handling
- Automatic error category determination based on exception type

This addresses the requirement for centralized C# error handling that provides
detailed debugging information without requiring manual debug code additions.
This commit is contained in:
PSMinIO Developer
2025-07-11 14:53:41 -04:00
parent 396290e0e8
commit 140257d469
9 changed files with 379 additions and 36 deletions
+5 -5
View File
@@ -17,8 +17,8 @@ namespace PSMinIO.Cmdlets
/// </summary>
[Parameter(Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[SupportsWildcards]
[Alias("Bucket")]
public string? Name { get; set; }
[Alias("Bucket", "Name")]
public string? BucketName { get; set; }
/// <summary>
/// Include bucket statistics (object count and total size)
@@ -60,11 +60,11 @@ namespace PSMinIO.Cmdlets
WriteVerboseMessage("Retrieved {0} buckets", buckets.Count);
// Filter by name if specified
if (!string.IsNullOrEmpty(Name))
if (!string.IsNullOrEmpty(BucketName))
{
var wildcardPattern = new WildcardPattern(Name, WildcardOptions.IgnoreCase);
var wildcardPattern = new WildcardPattern(BucketName, WildcardOptions.IgnoreCase);
buckets = buckets.Where(b => b.Name != null && wildcardPattern.IsMatch(b.Name)).ToList();
WriteVerboseMessage("Filtered to {0} buckets matching pattern '{1}'", buckets.Count, Name);
WriteVerboseMessage("Filtered to {0} buckets matching pattern '{1}'", buckets.Count, BucketName);
}
// Enhance bucket information if requested
+7 -11
View File
@@ -115,11 +115,9 @@ namespace PSMinIO.Cmdlets
catch (Exception ex)
{
MinIOLogger.LogOperationFailure(this, operationName, ex, details);
// Determine appropriate error category
var category = GetErrorCategory(ex);
ThrowTerminatingError(new ErrorRecord(ex, $"{operationName}Failed", category, null));
// Use centralized enhanced error handling
MinIOErrorHandler.HandleError(this, ex, operationName, details);
}
}
@@ -146,12 +144,10 @@ namespace PSMinIO.Cmdlets
catch (Exception ex)
{
MinIOLogger.LogOperationFailure(this, operationName, ex, details);
// Determine appropriate error category
var category = GetErrorCategory(ex);
ThrowTerminatingError(new ErrorRecord(ex, $"{operationName}Failed", category, null));
// Use centralized enhanced error handling
MinIOErrorHandler.HandleError(this, ex, operationName, details);
// This line will never be reached, but is required for compilation
throw;
}
+9 -9
View File
@@ -14,26 +14,26 @@ namespace PSMinIO.Cmdlets
/// </summary>
[Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
[Alias("Bucket")]
public string Name { get; set; } = string.Empty;
[Alias("Bucket", "Name")]
public string BucketName { get; set; } = string.Empty;
/// <summary>
/// Processes the cmdlet
/// </summary>
protected override void ProcessRecord()
{
ValidateBucketName(Name);
ValidateBucketName(BucketName);
var exists = ExecuteOperation("CheckBucketExists", () =>
{
WriteVerboseMessage("Checking if bucket '{0}' exists", Name);
WriteVerboseMessage("Checking if bucket '{0}' exists", BucketName);
var bucketExists = S3Client.BucketExists(BucketName);
WriteVerboseMessage("Bucket '{0}' {1}", BucketName, bucketExists ? "exists" : "does not exist");
var bucketExists = S3Client.BucketExists(Name);
WriteVerboseMessage("Bucket '{0}' {1}", Name, bucketExists ? "exists" : "does not exist");
return bucketExists;
}, $"Bucket: {Name}");
}, $"Bucket: {BucketName}");
WriteObject(exists);
}
+13 -5
View File
@@ -73,14 +73,22 @@ namespace PSMinIO.Core.Http
Action<long>? progressCallback = null)
{
var request = CreateRequest(method, path, queryParameters, headers, content);
// Sign the request with AWS S3 signature
SignRequest(request);
// Execute the request synchronously
var response = _httpClient.SendAsync(request).GetAwaiter().GetResult();
return response;
try
{
// Execute the request synchronously
var response = _httpClient.SendAsync(request).GetAwaiter().GetResult();
return response;
}
catch (Exception ex)
{
// Add more detailed error information
var innerMessage = ex.InnerException?.Message ?? "No inner exception";
throw new InvalidOperationException($"HTTP request failed: {ex.Message}. Inner: {innerMessage}. URL: {request.RequestUri}", ex);
}
}
/// <summary>
+5 -4
View File
@@ -31,9 +31,10 @@ using System.Runtime.InteropServices;
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("2025.07.11.1421")]
[assembly: AssemblyVersion("2025.07.11.1421")]
[assembly: AssemblyFileVersion("2025.07.11.1421")]
[assembly: AssemblyInformationalVersion("2025.07.11.1421")]
// [assembly: AssemblyVersion("2025.07.11.1453")]
[assembly: AssemblyVersion("2025.07.11.1453")]
[assembly: AssemblyFileVersion("2025.07.11.1453")]
[assembly: AssemblyInformationalVersion("2025.07.11.1453")]
+233
View File
@@ -0,0 +1,233 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Management.Automation;
using System.Text;
namespace PSMinIO.Utils
{
/// <summary>
/// Centralized error handling utility for PSMinIO cmdlets
/// Provides comprehensive error information including script details, line numbers, and context
/// </summary>
public static class MinIOErrorHandler
{
/// <summary>
/// Handles and logs detailed error information for PSMinIO operations
/// </summary>
/// <param name="cmdlet">The cmdlet instance for logging</param>
/// <param name="exception">The exception that occurred</param>
/// <param name="operationName">Name of the operation that failed</param>
/// <param name="operationDetails">Additional details about the operation</param>
/// <param name="errorCategory">PowerShell error category</param>
/// <param name="targetObject">The target object related to the error</param>
public static void HandleError(
PSCmdlet cmdlet,
Exception exception,
string operationName,
string? operationDetails = null,
ErrorCategory errorCategory = ErrorCategory.NotSpecified,
object? targetObject = null)
{
if (cmdlet == null)
throw new ArgumentNullException(nameof(cmdlet));
if (exception == null)
throw new ArgumentNullException(nameof(exception));
if (string.IsNullOrEmpty(operationName))
throw new ArgumentException("Operation name cannot be null or empty", nameof(operationName));
// Create detailed error information
var errorDetails = CreateDetailedErrorInfo(exception, operationName, operationDetails);
// Log detailed error information
LogDetailedError(cmdlet, errorDetails);
// Create and throw PowerShell error record
var errorRecord = CreateErrorRecord(exception, operationName, errorCategory, targetObject);
cmdlet.ThrowTerminatingError(errorRecord);
}
/// <summary>
/// Handles and logs detailed error information for PSMinIO operations (non-terminating)
/// </summary>
/// <param name="cmdlet">The cmdlet instance for logging</param>
/// <param name="exception">The exception that occurred</param>
/// <param name="operationName">Name of the operation that failed</param>
/// <param name="operationDetails">Additional details about the operation</param>
/// <param name="errorCategory">PowerShell error category</param>
/// <param name="targetObject">The target object related to the error</param>
public static void HandleNonTerminatingError(
PSCmdlet cmdlet,
Exception exception,
string operationName,
string? operationDetails = null,
ErrorCategory errorCategory = ErrorCategory.NotSpecified,
object? targetObject = null)
{
if (cmdlet == null)
throw new ArgumentNullException(nameof(cmdlet));
if (exception == null)
throw new ArgumentNullException(nameof(exception));
if (string.IsNullOrEmpty(operationName))
throw new ArgumentException("Operation name cannot be null or empty", nameof(operationName));
// Create detailed error information
var errorDetails = CreateDetailedErrorInfo(exception, operationName, operationDetails);
// Log detailed error information
LogDetailedError(cmdlet, errorDetails);
// Create and write PowerShell error record
var errorRecord = CreateErrorRecord(exception, operationName, errorCategory, targetObject);
cmdlet.WriteError(errorRecord);
}
/// <summary>
/// Creates detailed error information dictionary
/// </summary>
private static OrderedDictionary CreateDetailedErrorInfo(Exception exception, string operationName, string? operationDetails)
{
var errorDetails = new OrderedDictionary();
// Basic error information
errorDetails.Add("Operation", operationName);
errorDetails.Add("Message", exception.Message);
errorDetails.Add("ExceptionType", exception.GetType().FullName);
// Operation details if provided
if (!string.IsNullOrEmpty(operationDetails))
{
errorDetails.Add("OperationDetails", operationDetails);
}
// Inner exception information
if (exception.InnerException != null)
{
errorDetails.Add("InnerExceptionType", exception.InnerException.GetType().FullName);
errorDetails.Add("InnerExceptionMessage", exception.InnerException.Message);
}
// Stack trace information
if (!string.IsNullOrEmpty(exception.StackTrace))
{
var stackLines = exception.StackTrace.Split('\n');
if (stackLines.Length > 0)
{
errorDetails.Add("StackTraceTop", stackLines[0].Trim());
}
}
// System information
errorDetails.Add("Timestamp", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC"));
errorDetails.Add("MachineName", Environment.MachineName);
errorDetails.Add("ProcessId", Process.GetCurrentProcess().Id);
return errorDetails;
}
/// <summary>
/// Logs detailed error information using PowerShell's Write-Warning
/// </summary>
private static void LogDetailedError(PSCmdlet cmdlet, OrderedDictionary errorDetails)
{
var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
// Log header
cmdlet.WriteWarning($"{timestamp} - ERROR: PSMinIO Operation Failed");
// Log each error detail
foreach (DictionaryEntry detail in errorDetails)
{
var key = detail.Key?.ToString() ?? "Unknown";
var value = detail.Value?.ToString() ?? "N/A";
// Truncate very long values for readability
if (value.Length > 200)
{
value = value.Substring(0, 197) + "...";
}
cmdlet.WriteWarning($"{timestamp} - ERROR: {key}: {value}");
}
}
/// <summary>
/// Creates a PowerShell ErrorRecord with appropriate categorization
/// </summary>
private static ErrorRecord CreateErrorRecord(Exception exception, string operationName, ErrorCategory errorCategory, object? targetObject)
{
// Determine error category if not specified
if (errorCategory == ErrorCategory.NotSpecified)
{
errorCategory = DetermineErrorCategory(exception);
}
// Create error ID
var errorId = $"PSMinIO.{operationName}.{exception.GetType().Name}";
return new ErrorRecord(exception, errorId, errorCategory, targetObject);
}
/// <summary>
/// Determines appropriate PowerShell error category based on exception type
/// </summary>
private static ErrorCategory DetermineErrorCategory(Exception exception)
{
return exception switch
{
ArgumentNullException => ErrorCategory.InvalidArgument,
ArgumentException => ErrorCategory.InvalidArgument,
UnauthorizedAccessException => ErrorCategory.PermissionDenied,
System.Net.Http.HttpRequestException => ErrorCategory.ConnectionError,
TimeoutException => ErrorCategory.OperationTimeout,
System.IO.FileNotFoundException => ErrorCategory.ObjectNotFound,
System.IO.DirectoryNotFoundException => ErrorCategory.ObjectNotFound,
System.IO.IOException => ErrorCategory.WriteError,
InvalidOperationException => ErrorCategory.InvalidOperation,
NotSupportedException => ErrorCategory.NotImplemented,
_ => ErrorCategory.NotSpecified
};
}
/// <summary>
/// Creates a formatted error message for operation failures
/// </summary>
/// <param name="operationName">Name of the failed operation</param>
/// <param name="details">Operation details</param>
/// <param name="exception">The exception that occurred</param>
/// <returns>Formatted error message</returns>
public static string CreateOperationErrorMessage(string operationName, string? details, Exception exception)
{
var message = new StringBuilder();
message.Append($"Operation failed: {operationName}");
if (!string.IsNullOrEmpty(details))
{
message.Append($" - {details}");
}
message.Append($": {exception.Message}");
return message.ToString();
}
/// <summary>
/// Validates common parameters and throws appropriate exceptions
/// </summary>
/// <param name="bucketName">Bucket name to validate</param>
/// <param name="objectName">Object name to validate (optional)</param>
public static void ValidateParameters(string? bucketName, string? objectName = null)
{
if (string.IsNullOrWhiteSpace(bucketName))
{
throw new ArgumentException("Bucket name cannot be null or empty", nameof(bucketName));
}
if (objectName != null && string.IsNullOrWhiteSpace(objectName))
{
throw new ArgumentException("Object name cannot be empty when specified", nameof(objectName));
}
}
}
}