nip/src/nimpak/errors.nim

412 lines
13 KiB
Nim

# NimPak Error Handling
#
# Comprehensive error handling utilities for the NimPak system.
# Provides formatted error messages, recovery suggestions, and error chaining.
# Task 37: Implement comprehensive error handling.
import std/[strformat, strutils, times, tables, terminal]
import ./types
# ############################################################################
# Error Formatting
# ############################################################################
proc errorCodeColor(code: ErrorCode): ForegroundColor =
## Get color for error code based on severity
case code
of PermissionDenied, ElevationRequired, ReadOnlyViolation,
AculViolation, PolicyViolation, SignatureInvalid, TrustViolation:
fgRed
of PackageNotFound, DependencyConflict, ChecksumMismatch,
InvalidMetadata, PackageCorrupted, VersionMismatch:
fgYellow
of NetworkError, DownloadFailed, RepositoryUnavailable, TimeoutError:
fgMagenta
of BuildFailed, CompilationError, MissingDependency:
fgCyan
else:
fgWhite
proc formatError*(err: NimPakError): string =
## Format a NimPakError into a human-readable string with context and suggestions
result = fmt"Error [{err.code}]: {err.msg}"
if err.context.len > 0:
result.add fmt"\n Context: {err.context}"
if err.suggestions.len > 0:
result.add "\n Suggestions:"
for suggestion in err.suggestions:
result.add fmt"\n • {suggestion}"
proc formatErrorColored*(err: NimPakError, useColor: bool = true): string =
## Format error with ANSI colors for terminal output
if not useColor:
return formatError(err)
let color = errorCodeColor(err.code)
result = "\e[" & $ord(color) & "mError [" & $err.code & "]\e[0m: " & err.msg
if err.context.len > 0:
result.add fmt"\n \e[90mContext:\e[0m {err.context}"
if err.suggestions.len > 0:
result.add "\n \e[32mSuggestions:\e[0m"
for suggestion in err.suggestions:
result.add fmt"\n \e[32m•\e[0m {suggestion}"
# ############################################################################
# Error Factory Functions
# ############################################################################
proc newNimPakError*(code: ErrorCode, message: string,
context: string = "",
suggestions: seq[string] = @[]): NimPakError =
## Create a new NimPakError with the specified details
result = NimPakError(
code: code,
msg: message,
context: context,
suggestions: suggestions
)
# --- Package Errors ---
proc packageNotFoundError*(packageName: string): NimPakError =
## Create a standardized package not found error
newNimPakError(
PackageNotFound,
fmt"Package '{packageName}' not found",
"The requested package could not be located in any configured repository",
@[
"Check the package name for typos",
"Verify that the package repository is accessible",
"Try updating the package index with 'nip update'"
]
)
proc dependencyConflictError*(pkg1, pkg2, dep: string): NimPakError =
newNimPakError(
DependencyConflict,
fmt"Dependency conflict between '{pkg1}' and '{pkg2}' for '{dep}'",
"Multiple packages require incompatible versions of the same dependency",
@[
"Try updating both packages to their latest versions",
"Check if one package can be downgraded",
"Consider using 'nip resolve --strategy=best-effort'"
]
)
proc checksumMismatchError*(filePath: string, expected: string, actual: string): NimPakError =
newNimPakError(
ChecksumMismatch,
fmt"Checksum verification failed for '{filePath}'",
fmt"Expected: {expected[0..min(15, expected.len-1)]}..., Got: {actual[0..min(15, actual.len-1)]}...",
@[
"The file may have been corrupted during download",
"Try re-downloading the package",
"Verify the integrity of your storage device"
]
)
proc packageCorruptedError*(packagePath: string): NimPakError =
newNimPakError(
PackageCorrupted,
fmt"Package at '{packagePath}' appears to be corrupted",
"The package structure or manifest is invalid",
@[
"Re-download the package from the source",
"Verify the package signature if available",
"Check for disk errors with 'fsck' or equivalent"
]
)
proc invalidMetadataError*(field: string, value: string, expected: string): NimPakError =
newNimPakError(
InvalidMetadata,
fmt"Invalid metadata: field '{field}' has invalid value",
fmt"Got: '{value}', Expected: {expected}",
@[
"Check the package manifest for syntax errors",
"Verify the package was created with a compatible version"
]
)
# --- Permission Errors ---
proc permissionDeniedError*(path: string, operation: string): NimPakError =
newNimPakError(
PermissionDenied,
fmt"Permission denied: cannot {operation} '{path}'",
"The current user lacks the required permissions",
@[
"Try running with elevated privileges (sudo)",
"Check file/directory ownership and permissions",
"Verify SELinux/AppArmor policies if applicable"
]
)
proc elevationRequiredError*(operation: string): NimPakError =
newNimPakError(
ElevationRequired,
fmt"Elevated privileges required for '{operation}'",
"This operation modifies system directories",
@[
"Run with 'sudo nip ...'",
"Configure polkit rules for non-interactive elevation",
"Use --user flag to install to user directory instead"
]
)
proc readOnlyViolationError*(path: string): NimPakError =
newNimPakError(
ReadOnlyViolation,
fmt"Cannot write to read-only path: '{path}'",
"The CAS or package directory is protected",
@[
"Use proper elevation to temporarily unlock",
"Check if nip-protect is active",
"Verify mount options for the filesystem"
]
)
# --- Network Errors ---
proc networkError*(url: string, details: string): NimPakError =
newNimPakError(
NetworkError,
fmt"Network error accessing '{url}'",
details,
@[
"Check your internet connection",
"Verify the repository URL is correct",
"Try using a different mirror"
]
)
proc downloadFailedError*(url: string, httpCode: int = 0): NimPakError =
let context = if httpCode > 0: fmt"HTTP status: {httpCode}" else: "Connection failed"
newNimPakError(
DownloadFailed,
fmt"Failed to download from '{url}'",
context,
@[
"Retry the download",
"Check if the file exists at the URL",
"Try using 'nip fetch --retry=3'"
]
)
proc repositoryUnavailableError*(repoUrl: string): NimPakError =
newNimPakError(
RepositoryUnavailable,
fmt"Repository at '{repoUrl}' is unavailable",
"Could not connect to the package repository",
@[
"Check repository status at the provider's site",
"Try an alternative mirror",
"Verify DNS resolution works"
]
)
proc timeoutError*(operation: string, timeoutSeconds: int): NimPakError =
newNimPakError(
TimeoutError,
fmt"Operation '{operation}' timed out after {timeoutSeconds}s",
"The operation took too long to complete",
@[
"Retry with a longer timeout: --timeout=<seconds>",
"Check network latency to the server",
"Try during off-peak hours"
]
)
# --- Build Errors ---
proc buildFailedError*(packageName: string, stage: string, output: string = ""): NimPakError =
let context = if output.len > 0: output[0..min(200, output.len-1)] else: stage
newNimPakError(
BuildFailed,
fmt"Build failed for package '{packageName}' at stage '{stage}'",
context,
@[
"Check the build log for details",
"Verify build dependencies are installed",
"Try 'nip build --verbose' for more information"
]
)
proc missingDependencyError*(packageName: string, depName: string): NimPakError =
newNimPakError(
MissingDependency,
fmt"Missing dependency '{depName}' for package '{packageName}'",
"A required dependency could not be resolved",
@[
fmt"Install the dependency first: nip install {depName}",
"Check if the dependency exists in configured repositories",
"Verify version constraints in the package manifest"
]
)
# --- ACUL/Policy Errors ---
proc signatureInvalidError*(packageName: string, keyId: string = ""): NimPakError =
let context = if keyId.len > 0: fmt"Key ID: {keyId}" else: "No valid signature found"
newNimPakError(
SignatureInvalid,
fmt"Signature verification failed for '{packageName}'",
context,
@[
"The package may have been tampered with",
"Update trusted keys with 'nip trust update'",
"Re-download from an official source"
]
)
proc trustViolationError*(keyId: string, reason: string): NimPakError =
newNimPakError(
TrustViolation,
fmt"Trust chain violation for key '{keyId}'",
reason,
@[
"Verify the key in your trust store",
"Check for key revocation",
"Contact the package maintainer"
]
)
proc aculViolationError*(packageName: string, violation: string): NimPakError =
newNimPakError(
AculViolation,
fmt"ACUL policy violation for package '{packageName}'",
violation,
@[
"Review the ACUL policy requirements",
"Check if the package meets compliance standards",
"Contact your administrator for policy exceptions"
]
)
# --- Storage Errors ---
proc objectNotFoundError*(hash: string): NimPakError =
newNimPakError(
ObjectNotFound,
fmt"Object not found in CAS: {hash[0..min(20, hash.len-1)]}...",
"The requested object does not exist in any CAS location",
@[
"Run garbage collection verification: nip gc --verify",
"Re-install the package that requires this object",
"Check CAS integrity with 'nip cas verify'"
]
)
proc storageFull*(path: string, required: int64, available: int64): NimPakError =
newNimPakError(
StorageFull,
"Insufficient storage space",
fmt"Required: {required div (1024*1024)}MB, Available: {available div (1024*1024)}MB at {path}",
@[
"Free up disk space",
"Run garbage collection: nip gc",
"Move CAS to a larger partition"
]
)
# --- GC Errors ---
proc gcFailedError*(reason: string): NimPakError =
newNimPakError(
GarbageCollectionFailed,
"Garbage collection failed",
reason,
@[
"Check for locked files or active operations",
"Retry with 'nip gc --force'",
"Verify CAS integrity first: nip cas verify"
]
)
proc referenceIntegrityError*(hash: string, expectedRefs: int, actualRefs: int): NimPakError =
newNimPakError(
ReferenceIntegrityError,
fmt"Reference count mismatch for object {hash[0..min(12, hash.len-1)]}...",
fmt"Expected: {expectedRefs}, Actual: {actualRefs}",
@[
"Run reference rebuild: nip gc --rebuild-refs",
"This may indicate storage corruption"
]
)
# --- Transaction Errors ---
proc transactionFailedError*(txId: string, operation: string): NimPakError =
newNimPakError(
TransactionFailed,
fmt"Transaction '{txId}' failed during '{operation}'",
"The atomic operation could not complete",
@[
"Automatic rollback was attempted",
"Check system logs for details",
"Retry the operation"
]
)
proc rollbackFailedError*(txId: string, reason: string): NimPakError =
newNimPakError(
RollbackFailed,
fmt"Rollback failed for transaction '{txId}'",
reason,
@[
"Manual intervention may be required",
"Check partial state in /var/lib/nip/transactions/",
"Contact support if data loss occurred"
]
)
# ############################################################################
# Error Recovery Strategies
# ############################################################################
type
RecoveryStrategy* = enum
Retry, Skip, Abort, Fallback, Manual
RecoveryAction* = object
strategy*: RecoveryStrategy
message*: string
action*: proc(): bool {.closure.}
proc suggestRecovery*(err: NimPakError): RecoveryStrategy =
## Suggest a recovery strategy based on error type
case err.code
of NetworkError, DownloadFailed, TimeoutError:
Retry
of PackageNotFound, ObjectNotFound:
Fallback
of PermissionDenied, ElevationRequired:
Manual
of ChecksumMismatch, SignatureInvalid:
Abort
else:
Abort
proc isRecoverable*(err: NimPakError): bool =
## Check if error can potentially be recovered from
err.code in {NetworkError, DownloadFailed, TimeoutError,
RepositoryUnavailable, PackageNotFound}
# ############################################################################
# Error Chaining
# ############################################################################
proc wrapError*(cause: NimPakError, code: ErrorCode, message: string): NimPakError =
## Wrap an existing error with additional context
result = newNimPakError(
code,
message,
fmt"Caused by: {cause.msg}",
cause.suggestions
)
proc chain*(errors: varargs[NimPakError]): string =
## Format a chain of related errors
result = "Error chain:\n"
for i, err in errors:
result.add fmt" {i+1}. [{err.code}] {err.msg}\n"