Added Retry Policy and Launcher Error.

Included ErrorHelper for mapping.
This commit is contained in:
2026-04-06 20:47:13 +01:00
parent 491a3d420d
commit 6938635ee4
16 changed files with 539 additions and 342 deletions

View File

@@ -21,5 +21,6 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOperatingSystem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003Fb9_003Fd737a5e2_003FOperatingSystem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AOperatingSystem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003Fb9_003Fd737a5e2_003FOperatingSystem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProcessWrapper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F97_003F3a1ed5f7_003FProcessWrapper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AProcessWrapper_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003F97_003F3a1ed5f7_003FProcessWrapper_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicClientApplicationBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe7dcae430a36a6030304d2dec7149bc9e40cb379e3a8e0efb762d5d19da5c_003FPublicClientApplicationBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APublicClientApplicationBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fe7dcae430a36a6030304d2dec7149bc9e40cb379e3a8e0efb762d5d19da5c_003FPublicClientApplicationBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARandom_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb5d933e666c84a3394cfc49363e3e5bdd2b08_003F5f_003F5739d1f6_003FRandom_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F346166506159999f4fec5f7c475ba964d2495ee825dd6e4c48dedef117f086_003FVersionConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FSourcesCache_003F346166506159999f4fec5f7c475ba964d2495ee825dd6e4c48dedef117f086_003FVersionConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionMetadataCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ff7_003Fb6ecd842_003FVersionMetadataCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AVersionMetadataCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003FLibrary_003FApplication_0020Support_003FJetBrains_003FRider2026_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F11957ce69cb24b8a8e3979c0980ffeb33a400_003Ff7_003Fb6ecd842_003FVersionMetadataCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View File

@@ -11,7 +11,5 @@ namespace AlayaCore.Abstractions.Interfaces.Clients
Uri uri, Uri uri,
HttpCompletionOption completionOption, HttpCompletionOption completionOption,
CancellationToken cancellationToken); CancellationToken cancellationToken);
HttpClient HttpClient { get; }
} }
} }

View File

@@ -1,7 +1,19 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AlayaCore.Abstractions.Interfaces.Policies namespace AlayaCore.Abstractions.Interfaces.Policies
{ {
public class IRetryPolicy public interface IRetryPolicy
{ {
Task ExecuteAsync(
Func<CancellationToken, Task> operation,
string operationName,
CancellationToken cancellationToken = default);
Task<T> ExecuteAsync<T>(
Func<CancellationToken, Task<T>> operation,
string operationName,
CancellationToken cancellationToken = default);
} }
} }

View File

@@ -6,7 +6,7 @@ using AlayaCore.Abstractions.Interfaces.Clients;
namespace AlayaCore.Clients namespace AlayaCore.Clients
{ {
public sealed class DefaultHttpClient : IHttpClient, IDisposable public sealed class DefaultHttpClient : IHttpClient
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private bool _disposed; private bool _disposed;

View File

@@ -1,7 +1,19 @@
using System;
using AlayaCore.Utilities.Enums;
namespace AlayaCore.Errors namespace AlayaCore.Errors
{ {
public class LauncherError public sealed class LauncherError
{ {
public LauncherErrorType Type { get; }
public string Message { get; }
public Exception Exception { get; }
public LauncherError(LauncherErrorType type, string message, Exception exception)
{
Type = type;
Message = message;
Exception = exception;
}
} }
} }

View File

@@ -1,3 +1,6 @@
using System;
using System.IO;
namespace AlayaCore.Models.Configuration namespace AlayaCore.Models.Configuration
{ {
public sealed class LauncherUpdateServiceOptions public sealed class LauncherUpdateServiceOptions
@@ -10,5 +13,8 @@ namespace AlayaCore.Models.Configuration
public string AlayaUpdaterPath { get; set; } public string AlayaUpdaterPath { get; set; }
public bool ForceUpdate { get; set; } public bool ForceUpdate { get; set; }
public static LauncherUpdateServiceOptions Default { get; } =
new LauncherUpdateServiceOptions(Path.Combine(AppContext.BaseDirectory, "Data", "Updater"), false);
} }
} }

View File

@@ -4,25 +4,23 @@ namespace AlayaCore.Models.Configuration
{ {
public sealed class ManifestServiceOptions public sealed class ManifestServiceOptions
{ {
public Uri CoreManifestUri { get; } public Uri AlayaManifestUri { get; }
public Uri LauncherManifestUri { get; } public Uri LauncherManifestUri { get; }
public string CoreManifestSha512Hash { get; } public string AlayaManifestSha512Hash { get; }
public string LauncherManifestSha512Hash { get; } public string LauncherManifestSha512Hash { get; }
public string ManifestDirectoryPath { get; }
public ManifestServiceOptions( public ManifestServiceOptions(
Uri coreManifestUri, Uri alayaManifestUri,
Uri launcherManifestUri, Uri launcherManifestUri,
string coreManifestSha512Hash, string alayaManifestSha512Hash,
string launcherManifestSha512Hash, string launcherManifestSha512Hash)
string manifestDirectoryPath)
{ {
CoreManifestUri = coreManifestUri ?? throw new ArgumentNullException(nameof(coreManifestUri)); AlayaManifestUri = alayaManifestUri ?? throw new ArgumentNullException(nameof(alayaManifestUri));
LauncherManifestUri = launcherManifestUri ?? throw new ArgumentNullException(nameof(launcherManifestUri)); LauncherManifestUri = launcherManifestUri ?? throw new ArgumentNullException(nameof(launcherManifestUri));
if (!CoreManifestUri.IsAbsoluteUri) if (!AlayaManifestUri.IsAbsoluteUri)
{ {
throw new ArgumentException("Core manifest URI must be absolute.", nameof(coreManifestUri)); throw new ArgumentException("Core manifest URI must be absolute.", nameof(alayaManifestUri));
} }
if (!LauncherManifestUri.IsAbsoluteUri) if (!LauncherManifestUri.IsAbsoluteUri)
@@ -30,9 +28,9 @@ namespace AlayaCore.Models.Configuration
throw new ArgumentException("Launcher manifest URI must be absolute.", nameof(launcherManifestUri)); throw new ArgumentException("Launcher manifest URI must be absolute.", nameof(launcherManifestUri));
} }
if (string.IsNullOrWhiteSpace(coreManifestSha512Hash)) if (string.IsNullOrWhiteSpace(alayaManifestSha512Hash))
{ {
throw new ArgumentException("Core manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(coreManifestSha512Hash)); throw new ArgumentException("Core manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(alayaManifestSha512Hash));
} }
if (string.IsNullOrWhiteSpace(launcherManifestSha512Hash)) if (string.IsNullOrWhiteSpace(launcherManifestSha512Hash))
@@ -40,14 +38,12 @@ namespace AlayaCore.Models.Configuration
throw new ArgumentException("Launcher manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(launcherManifestSha512Hash)); throw new ArgumentException("Launcher manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(launcherManifestSha512Hash));
} }
if (string.IsNullOrWhiteSpace(manifestDirectoryPath)) AlayaManifestSha512Hash = alayaManifestSha512Hash;
{
throw new ArgumentException("Manifest directory path cannot be null, empty, or whitespace.", nameof(manifestDirectoryPath));
}
CoreManifestSha512Hash = coreManifestSha512Hash;
LauncherManifestSha512Hash = launcherManifestSha512Hash; LauncherManifestSha512Hash = launcherManifestSha512Hash;
ManifestDirectoryPath = manifestDirectoryPath;
} }
public static ManifestServiceOptions Default { get; } = new ManifestServiceOptions(
new Uri("INSERT-ALAYA-URL", UriKind.Absolute),
new Uri("INSERT-LAUNCHER-URL", UriKind.Absolute), "INSERT-ALAYA-HASH", "INSERT-LAUNCHER-HASH");
} }
} }

View File

@@ -8,5 +8,8 @@ namespace AlayaCore.Models.Configuration
} }
public string BaseApiUrl { get; } public string BaseApiUrl { get; }
public static ModrinthConnectionOptions Default { get; } =
new ModrinthConnectionOptions("https://api.modrinth.com/v2/");
} }
} }

View File

@@ -1,7 +1,12 @@
namespace AlayaCore.Models.Configuration namespace AlayaCore.Models.Configuration
{ {
public class RetryPolicyOptions public sealed class RetryPolicyOptions
{ {
public int MaxAttempts { get; set; } = 3;
public int BaseDelayMilliseconds { get; set; } = 500;
public double BackoffMultiplier { get; set; } = 2.0;
public int MaxDelayMilliseconds { get; set; } = 3000;
public static RetryPolicyOptions Default { get; } = new();
} }
} }

View File

@@ -7,13 +7,14 @@ using AlayaCore.Abstractions.Interfaces.Policies;
using AlayaCore.Models.Configuration; using AlayaCore.Models.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace AlayaCore.Models.Policies namespace AlayaCore.Services
{ {
public sealed class RetryPolicy : IRetryPolicy public sealed class RetryPolicy : IRetryPolicy
{ {
private readonly RetryPolicyOptions _options; private readonly RetryPolicyOptions _options;
private readonly ILogger<RetryPolicy> _logger; private readonly ILogger<RetryPolicy> _logger;
private readonly Random _random;
private static readonly Random _random = new Random();
public RetryPolicy( public RetryPolicy(
RetryPolicyOptions options, RetryPolicyOptions options,
@@ -21,7 +22,6 @@ namespace AlayaCore.Models.Policies
{ {
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_random = new Random();
} }
public async Task ExecuteAsync( public async Task ExecuteAsync(
@@ -64,10 +64,7 @@ namespace AlayaCore.Models.Policies
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName)); throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
} }
if (_options.MaxAttempts <= 0) ValidateOptions();
{
throw new InvalidOperationException("RetryPolicyOptions.MaxAttempts must be greater than zero.");
}
Exception? lastException = null; Exception? lastException = null;
@@ -88,12 +85,38 @@ namespace AlayaCore.Models.Policies
return await operation(cancellationToken).ConfigureAwait(false); return await operation(cancellationToken).ConfigureAwait(false);
} }
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested && IsRetryable(ex))
{
lastException = ex;
if (attempt == _options.MaxAttempts)
{
_logger.LogError(
ex,
"Operation {OperationName} failed after {MaxAttempts} attempts due to repeated timeout or cancellation-like transient failures.",
operationName,
_options.MaxAttempts);
throw;
}
TimeSpan delay = CalculateDelay(attempt);
_logger.LogWarning(
ex,
"Operation {OperationName} timed out or was transiently cancelled on attempt {Attempt} of {MaxAttempts}. Retrying after {DelayMs}ms.",
operationName,
attempt,
_options.MaxAttempts,
(int)delay.TotalMilliseconds);
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
_logger.LogInformation( _logger.LogInformation(
"Operation {OperationName} was cancelled during attempt {Attempt}.", "Operation {OperationName} was cancelled by the caller.",
operationName, operationName);
attempt);
throw; throw;
} }
@@ -141,7 +164,30 @@ namespace AlayaCore.Models.Policies
lastException); lastException);
} }
private bool IsRetryable(Exception exception) private void ValidateOptions()
{
if (_options.MaxAttempts <= 0)
{
throw new InvalidOperationException("RetryPolicyOptions.MaxAttempts must be greater than zero.");
}
if (_options.BaseDelayMilliseconds < 0)
{
throw new InvalidOperationException("RetryPolicyOptions.BaseDelayMilliseconds cannot be negative.");
}
if (_options.BackoffMultiplier < 1d)
{
throw new InvalidOperationException("RetryPolicyOptions.BackoffMultiplier must be greater than or equal to 1.");
}
if (_options.MaxDelayMilliseconds < 0)
{
throw new InvalidOperationException("RetryPolicyOptions.MaxDelayMilliseconds cannot be negative.");
}
}
private static bool IsRetryable(Exception exception)
{ {
return exception switch return exception switch
{ {
@@ -157,11 +203,14 @@ namespace AlayaCore.Models.Policies
private TimeSpan CalculateDelay(int attempt) private TimeSpan CalculateDelay(int attempt)
{ {
double exponentialDelay = _options.BaseDelayMilliseconds * Math.Pow(_options.BackoffMultiplier, attempt - 1); double exponentialDelay = _options.BaseDelayMilliseconds *
Math.Pow(_options.BackoffMultiplier, attempt - 1);
double cappedDelay = Math.Min(exponentialDelay, _options.MaxDelayMilliseconds); double cappedDelay = Math.Min(exponentialDelay, _options.MaxDelayMilliseconds);
int jitter = _random.Next(0, 150); int jitterMilliseconds = _random.Next(0, 150);
return TimeSpan.FromMilliseconds(cappedDelay + jitter);
return TimeSpan.FromMilliseconds(cappedDelay + jitterMilliseconds);
} }
} }
} }

View File

@@ -6,6 +6,7 @@ using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces.Clients; using AlayaCore.Abstractions.Interfaces.Clients;
using AlayaCore.Abstractions.Interfaces.Policies;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Models.Progress; using AlayaCore.Models.Progress;
using AlayaCore.Models.Results; using AlayaCore.Models.Results;
@@ -18,13 +19,16 @@ namespace AlayaCore.Services
private const int BUFFER_SIZE = 81920; private const int BUFFER_SIZE = 81920;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IRetryPolicy _retryPolicy;
private readonly ILogger<HttpDownloadService> _logger; private readonly ILogger<HttpDownloadService> _logger;
public HttpDownloadService( public HttpDownloadService(
IHttpClient httpClient, IHttpClient httpClient,
IRetryPolicy retryPolicy,
ILogger<HttpDownloadService> logger) ILogger<HttpDownloadService> logger)
{ {
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -104,137 +108,150 @@ namespace AlayaCore.Services
try try
{ {
using HttpResponseMessage response = await _httpClient.GetAsync( DownloadResult result = await _retryPolicy.ExecuteAsync(
sourceUri, async token =>
HttpCompletionOption.ResponseHeadersRead, {
DeleteFileIfExists(tempFilePath);
using HttpResponseMessage response = await _httpClient.GetAsync(
sourceUri,
HttpCompletionOption.ResponseHeadersRead,
token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
long? totalBytes = response.Content.Headers.ContentLength;
long bytesDownloaded = 0;
_logger.LogInformation(
"Download response received for {FileName}. Content-Length: {TotalBytes}.",
fileName,
totalBytes);
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: 0,
totalBytes: totalBytes,
bytesPerSecond: null,
statusMessage: "Starting download..."));
await using Stream responseStream = await response.Content
.ReadAsStreamAsync()
.ConfigureAwait(false);
await using FileStream fileStream = new FileStream(
tempFilePath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
BUFFER_SIZE,
useAsync: true);
using SHA512 sha512 = SHA512.Create();
byte[] buffer = new byte[BUFFER_SIZE];
Stopwatch stopwatch = Stopwatch.StartNew();
_logger.LogDebug(
"Streaming download content for {FileName} into temporary file {TempFilePath}.",
fileName,
tempFilePath);
while (true)
{
int bytesRead = await responseStream.ReadAsync(
buffer,
0,
buffer.Length,
token).ConfigureAwait(false);
if (bytesRead == 0)
{
break;
}
await fileStream.WriteAsync(
buffer,
0,
bytesRead,
token).ConfigureAwait(false);
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
bytesDownloaded += bytesRead;
double? bytesPerSecond = null;
if (stopwatch.Elapsed.TotalSeconds > 0d)
{
bytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
}
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: bytesDownloaded,
totalBytes: totalBytes,
bytesPerSecond: bytesPerSecond,
statusMessage: "Downloading file..."));
}
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
await fileStream.FlushAsync(token).ConfigureAwait(false);
string actualHash = ConvertToLowerHex(sha512.Hash);
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"Hash verification failed for downloaded file {FileName}. Expected SHA-512: {ExpectedHash}. Actual SHA-512: {ActualHash}.",
fileName,
normalizedExpectedHash,
actualHash);
throw new InvalidDataException(
$"Downloaded file hash mismatch. Expected '{normalizedExpectedHash}', got '{actualHash}'.");
}
_logger.LogInformation(
"Hash verification succeeded for {FileName}. Replacing destination file with downloaded content.",
fileName);
ReplaceDestinationFile(tempFilePath, destinationPath);
double? finalBytesPerSecond = null;
if (stopwatch.Elapsed.TotalSeconds > 0d)
{
finalBytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
}
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: bytesDownloaded,
totalBytes: totalBytes ?? bytesDownloaded,
bytesPerSecond: finalBytesPerSecond,
statusMessage: "Download complete."));
DownloadOutcome outcome = destinationExisted
? DownloadOutcome.ReplacedInvalid
: DownloadOutcome.Downloaded;
_logger.LogInformation(
"Download completed successfully for {FileName}. Outcome: {Outcome}. Bytes downloaded: {BytesDownloaded}.",
fileName,
outcome,
bytesDownloaded);
return new DownloadResult(
destinationPath,
outcome,
hashVerified: true,
bytesDownloaded: bytesDownloaded);
},
$"download:{fileName}",
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); return result;
long? totalBytes = response.Content.Headers.ContentLength;
long bytesDownloaded = 0;
_logger.LogInformation(
"Download response received for {FileName}. Content-Length: {TotalBytes}.",
fileName,
totalBytes);
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: 0,
totalBytes: totalBytes,
bytesPerSecond: null,
statusMessage: "Starting download..."));
await using Stream responseStream = await response.Content
.ReadAsStreamAsync()
.ConfigureAwait(false);
await using FileStream fileStream = new FileStream(
tempFilePath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
BUFFER_SIZE,
useAsync: true);
using SHA512 sha512 = SHA512.Create();
byte[] buffer = new byte[BUFFER_SIZE];
Stopwatch stopwatch = Stopwatch.StartNew();
_logger.LogDebug("Streaming download content for {FileName} into temporary file {TempFilePath}.", fileName, tempFilePath);
while (true)
{
int bytesRead = await responseStream.ReadAsync(
buffer,
0,
buffer.Length,
cancellationToken).ConfigureAwait(false);
if (bytesRead == 0)
{
break;
}
await fileStream.WriteAsync(
buffer,
0,
bytesRead,
cancellationToken).ConfigureAwait(false);
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
bytesDownloaded += bytesRead;
double? bytesPerSecond = null;
if (stopwatch.Elapsed.TotalSeconds > 0d)
{
bytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
}
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: bytesDownloaded,
totalBytes: totalBytes,
bytesPerSecond: bytesPerSecond,
statusMessage: "Downloading file..."));
}
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
string actualHash = ConvertToLowerHex(sha512.Hash);
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"Hash verification failed for downloaded file {FileName}. Expected SHA-512: {ExpectedHash}. Actual SHA-512: {ActualHash}.",
fileName,
normalizedExpectedHash,
actualHash);
throw new InvalidDataException(
$"Downloaded file hash mismatch. Expected '{normalizedExpectedHash}', got '{actualHash}'.");
}
_logger.LogInformation(
"Hash verification succeeded for {FileName}. Replacing destination file with downloaded content.",
fileName);
ReplaceDestinationFile(tempFilePath, destinationPath);
double? finalBytesPerSecond = null;
if (stopwatch.Elapsed.TotalSeconds > 0d)
{
finalBytesPerSecond = bytesDownloaded / stopwatch.Elapsed.TotalSeconds;
}
progress?.Report(new DownloadProgress(
fileName: fileName,
destinationPath: destinationPath,
bytesDownloaded: bytesDownloaded,
totalBytes: totalBytes ?? bytesDownloaded,
bytesPerSecond: finalBytesPerSecond,
statusMessage: "Download complete."));
DownloadOutcome outcome = destinationExisted
? DownloadOutcome.ReplacedInvalid
: DownloadOutcome.Downloaded;
_logger.LogInformation(
"Download completed successfully for {FileName}. Outcome: {Outcome}. Bytes downloaded: {BytesDownloaded}.",
fileName,
outcome,
bytesDownloaded);
return new DownloadResult(
destinationPath,
outcome,
hashVerified: true,
bytesDownloaded: bytesDownloaded);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces; using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Clients; using AlayaCore.Abstractions.Interfaces.Clients;
using AlayaCore.Abstractions.Interfaces.Policies;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Models.Configuration; using AlayaCore.Models.Configuration;
using AlayaCore.Models.Manifests; using AlayaCore.Models.Manifests;
@@ -26,6 +27,7 @@ namespace AlayaCore.Services
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileStore _fileStore; private readonly IFileStore _fileStore;
private readonly ManifestServiceOptions _options; private readonly ManifestServiceOptions _options;
private readonly IRetryPolicy _retryPolicy;
private readonly ILogger<ManifestService> _logger; private readonly ILogger<ManifestService> _logger;
public ManifestService( public ManifestService(
@@ -33,12 +35,14 @@ namespace AlayaCore.Services
IHttpClient httpClient, IHttpClient httpClient,
IFileStore fileStore, IFileStore fileStore,
ManifestServiceOptions options, ManifestServiceOptions options,
IRetryPolicy retryPolicy,
ILogger<ManifestService> logger) ILogger<ManifestService> logger)
{ {
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService)); _downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore)); _fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -48,13 +52,13 @@ namespace AlayaCore.Services
_logger.LogInformation( _logger.LogInformation(
"Downloading and loading Alaya manifest from {ManifestUri} to {DestinationPath}.", "Downloading and loading Alaya manifest from {ManifestUri} to {DestinationPath}.",
_options.CoreManifestUri, _options.AlayaManifestUri,
destinationPath); destinationPath);
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>( return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
_options.CoreManifestUri, _options.AlayaManifestUri,
destinationPath, destinationPath,
_options.CoreManifestSha512Hash, _options.AlayaManifestSha512Hash,
static dto => dto.ToModel(), static dto => dto.ToModel(),
cancellationToken); cancellationToken);
} }
@@ -144,24 +148,24 @@ namespace AlayaCore.Services
public async Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default) public async Task<Version> GetRemoteCoreManifestVersionAsync(CancellationToken cancellationToken = default)
{ {
_logger.LogDebug("Fetching remote Alaya manifest version from {ManifestUri}.", _options.CoreManifestUri); _logger.LogDebug("Fetching remote Alaya manifest version from {ManifestUri}.", _options.AlayaManifestUri);
ManifestModel remoteManifest = await GetRemoteManifestAsync<ManifestDto, ManifestModel>( ManifestModel remoteManifest = await GetRemoteManifestAsync<ManifestDto, ManifestModel>(
_options.CoreManifestUri, _options.AlayaManifestUri,
static dto => dto.ToModel(), static dto => dto.ToModel(),
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (remoteManifest.AlayaVersion == null) if (remoteManifest.AlayaVersion == null)
{ {
_logger.LogError("Remote Alaya manifest from {ManifestUri} did not contain a valid version.", _options.CoreManifestUri); _logger.LogError("Remote Alaya manifest from {ManifestUri} did not contain a valid version.", _options.AlayaManifestUri);
throw new InvalidDataException( throw new InvalidDataException(
$"Remote core manifest from '{_options.CoreManifestUri}' does not contain a valid version."); $"Remote core manifest from '{_options.AlayaManifestUri}' does not contain a valid version.");
} }
_logger.LogInformation( _logger.LogInformation(
"Fetched remote Alaya manifest version {RemoteVersion} from {ManifestUri}.", "Fetched remote Alaya manifest version {RemoteVersion} from {ManifestUri}.",
remoteManifest.AlayaVersion, remoteManifest.AlayaVersion,
_options.CoreManifestUri); _options.AlayaManifestUri);
return remoteManifest.AlayaVersion; return remoteManifest.AlayaVersion;
} }
@@ -309,11 +313,17 @@ namespace AlayaCore.Services
manifestUri, manifestUri,
destinationPath); destinationPath);
await _downloadService.DownloadFileAsync( await _retryPolicy.ExecuteAsync(
manifestUri, async token =>
destinationPath, {
sha512Hash, await _downloadService.DownloadFileAsync(
cancellationToken: cancellationToken).ConfigureAwait(false); manifestUri,
destinationPath,
sha512Hash,
cancellationToken: token).ConfigureAwait(false);
},
$"manifest-download:{Path.GetFileName(destinationPath)}",
cancellationToken).ConfigureAwait(false);
if (!File.Exists(destinationPath)) if (!File.Exists(destinationPath))
{ {
@@ -368,31 +378,37 @@ namespace AlayaCore.Services
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri); _logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
using HttpResponseMessage response = await _httpClient.GetAsync( return await _retryPolicy.ExecuteAsync(
manifestUri, async token =>
HttpCompletionOption.ResponseContentRead, {
using HttpResponseMessage response = await _httpClient.GetAsync(
manifestUri,
HttpCompletionOption.ResponseContentRead,
token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(json))
{
_logger.LogError("Remote manifest response from {ManifestUri} was empty.", manifestUri);
throw new InvalidDataException(
$"Remote manifest response from '{manifestUri}' was empty.");
}
TModel model = DeserializeAndMapManifest(
json,
manifestUri.ToString(),
map,
swallowDeserializationErrors: false)!;
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
return model;
},
$"manifest-fetch:{manifestUri.AbsolutePath}",
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(json))
{
_logger.LogError("Remote manifest response from {ManifestUri} was empty.", manifestUri);
throw new InvalidDataException(
$"Remote manifest response from '{manifestUri}' was empty.");
}
TModel model = DeserializeAndMapManifest(
json,
manifestUri.ToString(),
map,
swallowDeserializationErrors: false)!;
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
return model;
} }
private TModel? DeserializeAndMapManifest<TDto, TModel>( private TModel? DeserializeAndMapManifest<TDto, TModel>(

View File

@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using AlayaCore.Abstractions.Interfaces; using AlayaCore.Abstractions.Interfaces;
using AlayaCore.Abstractions.Interfaces.Clients; using AlayaCore.Abstractions.Interfaces.Clients;
using AlayaCore.Abstractions.Interfaces.Policies;
using AlayaCore.Abstractions.Interfaces.Services; using AlayaCore.Abstractions.Interfaces.Services;
using AlayaCore.Installation; using AlayaCore.Installation;
using AlayaCore.Models; using AlayaCore.Models;
@@ -30,6 +31,7 @@ namespace AlayaCore.Services
private readonly ModrinthConnectionOptions _options; private readonly ModrinthConnectionOptions _options;
private readonly IHttpClient _httpClient; private readonly IHttpClient _httpClient;
private readonly IFileStore _fileStore; private readonly IFileStore _fileStore;
private readonly IRetryPolicy _retryPolicy;
private readonly ILogger<ModService> _logger; private readonly ILogger<ModService> _logger;
public ModService( public ModService(
@@ -37,12 +39,14 @@ namespace AlayaCore.Services
ModrinthConnectionOptions options, ModrinthConnectionOptions options,
IHttpClient httpClient, IHttpClient httpClient,
IFileStore fileStore, IFileStore fileStore,
IRetryPolicy retryPolicy,
ILogger<ModService> logger) ILogger<ModService> logger)
{ {
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService)); _downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
_options = options ?? throw new ArgumentNullException(nameof(options)); _options = options ?? throw new ArgumentNullException(nameof(options));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore)); _fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
@@ -213,136 +217,142 @@ namespace AlayaCore.Services
fileEntry.FileName, fileEntry.FileName,
versionEndpoint); versionEndpoint);
using HttpResponseMessage response = await _httpClient.GetAsync( return await _retryPolicy.ExecuteAsync(
new Uri(versionEndpoint, UriKind.Absolute), async token =>
HttpCompletionOption.ResponseContentRead, {
using HttpResponseMessage response = await _httpClient.GetAsync(
new Uri(versionEndpoint, UriKind.Absolute),
HttpCompletionOption.ResponseContentRead,
token).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(json))
{
_logger.LogError(
"Mod metadata response for {FileName} from {VersionEndpoint} was empty.",
fileEntry.FileName,
versionEndpoint);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' was empty.");
}
JObject jsonObject = JObject.Parse(json);
JArray? filesArray = jsonObject["files"] as JArray;
if (filesArray == null || filesArray.Count == 0)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain any files.",
fileEntry.FileName);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain any files.");
}
JObject? selectedFile = filesArray
.OfType<JObject>()
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
?? filesArray.OfType<JObject>().FirstOrDefault();
if (selectedFile == null)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a usable file entry.",
fileEntry.FileName);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
}
JObject? hashesObject = selectedFile["hashes"] as JObject;
if (hashesObject == null)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a hashes object.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a hashes object.");
}
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid SHA-512 hash.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid SHA-512 hash.");
}
if (!string.Equals(remoteSha512Hash, fileEntry.Sha512Hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"Mod metadata hash mismatch for {FileName}. Remote SHA-512: {RemoteHash}, Required SHA-512: {RequiredHash}",
fileEntry.FileName,
remoteSha512Hash,
fileEntry.Sha512Hash);
throw new InvalidDataException(
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
}
long? size = selectedFile.Value<long?>("size");
if (!size.HasValue || size.Value <= 0)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid file size.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
}
if (size.Value != fileEntry.Size)
{
_logger.LogError(
"Mod metadata size mismatch for {FileName}. Remote Size: {RemoteSize}, Required Size: {RequiredSize}",
fileEntry.FileName,
size.Value,
fileEntry.Size);
throw new InvalidDataException(
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
}
string? modUrl = selectedFile.Value<string>("url");
if (string.IsNullOrWhiteSpace(modUrl))
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid file URL.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
}
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
{
_logger.LogError(
"Mod metadata response for {FileName} contained an invalid file URL: {ModUrl}",
fileEntry.FileName,
modUrl);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
}
_logger.LogDebug(
"Resolved download URL for mod {FileName} to {ModUrl}.",
fileEntry.FileName,
result);
return result;
},
$"mod-metadata:{fileEntry.FileName}",
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(json))
{
_logger.LogError(
"Mod metadata response for {FileName} from {VersionEndpoint} was empty.",
fileEntry.FileName,
versionEndpoint);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' was empty.");
}
JObject jsonObject = JObject.Parse(json);
JArray? filesArray = jsonObject["files"] as JArray;
if (filesArray == null || filesArray.Count == 0)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain any files.",
fileEntry.FileName);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain any files.");
}
JObject? selectedFile = filesArray
.OfType<JObject>()
.FirstOrDefault(file => file.Value<bool?>("primary") == true)
?? filesArray.OfType<JObject>().FirstOrDefault();
if (selectedFile == null)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a usable file entry.",
fileEntry.FileName);
throw new InvalidDataException($"The mod metadata response for '{fileEntry.FileName}' did not contain a usable file entry.");
}
JObject? hashesObject = selectedFile["hashes"] as JObject;
if (hashesObject == null)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a hashes object.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a hashes object.");
}
string? remoteSha512Hash = hashesObject.Value<string>("sha512");
if (string.IsNullOrWhiteSpace(remoteSha512Hash))
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid SHA-512 hash.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid SHA-512 hash.");
}
if (!string.Equals(remoteSha512Hash, fileEntry.Sha512Hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"Mod metadata hash mismatch for {FileName}. Remote SHA-512: {RemoteHash}, Required SHA-512: {RequiredHash}",
fileEntry.FileName,
remoteSha512Hash,
fileEntry.Sha512Hash);
throw new InvalidDataException(
$"The mod metadata SHA-512 hash for '{fileEntry.FileName}' did not match the required manifest hash.");
}
long? size = selectedFile.Value<long?>("size");
if (!size.HasValue || size.Value <= 0)
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid file size.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file size.");
}
if (size.Value != fileEntry.Size)
{
_logger.LogError(
"Mod metadata size mismatch for {FileName}. Remote Size: {RemoteSize}, Required Size: {RequiredSize}",
fileEntry.FileName,
size.Value,
fileEntry.Size);
throw new InvalidDataException(
$"The mod metadata size for '{fileEntry.FileName}' did not match the required manifest size.");
}
string? modUrl = selectedFile.Value<string>("url");
if (string.IsNullOrWhiteSpace(modUrl))
{
_logger.LogError(
"Mod metadata response for {FileName} did not contain a valid file URL.",
fileEntry.FileName);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' did not contain a valid file URL.");
}
if (!Uri.TryCreate(modUrl, UriKind.Absolute, out Uri? result))
{
_logger.LogError(
"Mod metadata response for {FileName} contained an invalid file URL: {ModUrl}",
fileEntry.FileName,
modUrl);
throw new InvalidDataException(
$"The mod metadata response for '{fileEntry.FileName}' contained an invalid file URL.");
}
_logger.LogDebug(
"Resolved download URL for mod {FileName} to {ModUrl}.",
fileEntry.FileName,
result);
return result;
} }
private string BuildVersionEndpoint(string sha512Hash) private string BuildVersionEndpoint(string sha512Hash)

View File

@@ -2,6 +2,15 @@ namespace AlayaCore.Utilities.Enums
{ {
public enum LauncherErrorType public enum LauncherErrorType
{ {
Unknown,
Network,
Manifest,
Authentication,
Download,
Installation,
Update,
Launch,
Configuration,
Cancelled
} }
} }

View File

@@ -1,7 +1,56 @@
namespace AlayaCore.Utilities.Helpers using System;
using System.IO;
using System.Net.Http;
using AlayaCore.Utilities.Enums;
namespace AlayaCore.Errors
{ {
public class ErrorHelper public static class ErrorHelper
{ {
public static LauncherError Map(Exception exception)
{
if (exception == null)
{
throw new ArgumentNullException(nameof(exception));
}
return exception switch
{
OperationCanceledException => new LauncherError(
LauncherErrorType.Cancelled,
"The operation was cancelled.",
exception),
HttpRequestException => new LauncherError(
LauncherErrorType.Network,
"A network error occurred.",
exception),
IOException => new LauncherError(
LauncherErrorType.Network,
"A file or network I/O error occurred.",
exception),
InvalidDataException => new LauncherError(
LauncherErrorType.Manifest,
"Invalid or corrupt data was encountered.",
exception),
ArgumentException => new LauncherError(
LauncherErrorType.Configuration,
"Invalid configuration or input.",
exception),
InvalidOperationException => new LauncherError(
LauncherErrorType.Launch,
"The operation could not be completed due to an invalid state.",
exception),
_ => new LauncherError(
LauncherErrorType.Unknown,
"An unexpected error occurred.",
exception)
};
}
} }
} }

View File

@@ -1,7 +1,21 @@
using AlayaCore.Models.Configuration;
namespace AlayaCore.Utilities.Helpers namespace AlayaCore.Utilities.Helpers
{ {
public class OptionsHelper public static class OptionsHelper
{ {
public static (LauncherUpdateServiceOptions LaunchUpdater, LauncherOptions Launcher,
GameOptions Game, ManifestServiceOptions Manifest, ModrinthConnectionOptions Modrinth,
RetryPolicyOptions RetryPolicy) GetDefaultOptions()
{
LauncherUpdateServiceOptions lso = LauncherUpdateServiceOptions.Default;
LauncherOptions lo = LauncherOptions.Default;
GameOptions go = GameOptions.Default;
ManifestServiceOptions mso = ManifestServiceOptions.Default;
ModrinthConnectionOptions mco = ModrinthConnectionOptions.Default;
RetryPolicyOptions rpo = RetryPolicyOptions.Default;
return (lso, lo, go, mso, mco, rpo);
}
} }
} }