Compare commits
2 Commits
11863088e4
...
6938635ee4
| Author | SHA1 | Date | |
|---|---|---|---|
| 6938635ee4 | |||
| 491a3d420d |
@@ -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_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_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_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>
|
||||
@@ -11,7 +11,5 @@ namespace AlayaCore.Abstractions.Interfaces.Clients
|
||||
Uri uri,
|
||||
HttpCompletionOption completionOption,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
HttpClient HttpClient { get; }
|
||||
}
|
||||
}
|
||||
19
AlayaCore/Abstractions/Interfaces/Policies/IRetryPolicy.cs
Normal file
19
AlayaCore/Abstractions/Interfaces/Policies/IRetryPolicy.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AlayaCore.Abstractions.Interfaces.Policies
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
58
AlayaCore/Clients/DefaultHttpClient.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||
|
||||
namespace AlayaCore.Clients
|
||||
{
|
||||
public sealed class DefaultHttpClient : IHttpClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool _disposed;
|
||||
|
||||
public DefaultHttpClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(
|
||||
Uri requestUri,
|
||||
HttpCompletionOption completionOption,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (requestUri == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(requestUri));
|
||||
}
|
||||
|
||||
if (!requestUri.IsAbsoluteUri)
|
||||
{
|
||||
throw new ArgumentException("Request URI must be absolute.", nameof(requestUri));
|
||||
}
|
||||
|
||||
ThrowIfDisposed();
|
||||
|
||||
return _httpClient.GetAsync(requestUri, completionOption, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_httpClient.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(DefaultHttpClient));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
AlayaCore/Errors/LauncherError.cs
Normal file
19
AlayaCore/Errors/LauncherError.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using AlayaCore.Utilities.Enums;
|
||||
|
||||
namespace AlayaCore.Errors
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace AlayaCore.Models.Configuration
|
||||
{
|
||||
public sealed class LauncherUpdateServiceOptions
|
||||
@@ -10,5 +13,8 @@ namespace AlayaCore.Models.Configuration
|
||||
|
||||
public string AlayaUpdaterPath { get; set; }
|
||||
public bool ForceUpdate { get; set; }
|
||||
|
||||
public static LauncherUpdateServiceOptions Default { get; } =
|
||||
new LauncherUpdateServiceOptions(Path.Combine(AppContext.BaseDirectory, "Data", "Updater"), false);
|
||||
}
|
||||
}
|
||||
@@ -4,25 +4,23 @@ namespace AlayaCore.Models.Configuration
|
||||
{
|
||||
public sealed class ManifestServiceOptions
|
||||
{
|
||||
public Uri CoreManifestUri { get; }
|
||||
public Uri AlayaManifestUri { get; }
|
||||
public Uri LauncherManifestUri { get; }
|
||||
public string CoreManifestSha512Hash { get; }
|
||||
public string AlayaManifestSha512Hash { get; }
|
||||
public string LauncherManifestSha512Hash { get; }
|
||||
public string ManifestDirectoryPath { get; }
|
||||
|
||||
public ManifestServiceOptions(
|
||||
Uri coreManifestUri,
|
||||
Uri alayaManifestUri,
|
||||
Uri launcherManifestUri,
|
||||
string coreManifestSha512Hash,
|
||||
string launcherManifestSha512Hash,
|
||||
string manifestDirectoryPath)
|
||||
string alayaManifestSha512Hash,
|
||||
string launcherManifestSha512Hash)
|
||||
{
|
||||
CoreManifestUri = coreManifestUri ?? throw new ArgumentNullException(nameof(coreManifestUri));
|
||||
AlayaManifestUri = alayaManifestUri ?? throw new ArgumentNullException(nameof(alayaManifestUri));
|
||||
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)
|
||||
@@ -30,9 +28,9 @@ namespace AlayaCore.Models.Configuration
|
||||
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))
|
||||
@@ -40,14 +38,12 @@ namespace AlayaCore.Models.Configuration
|
||||
throw new ArgumentException("Launcher manifest SHA-512 hash cannot be null, empty, or whitespace.", nameof(launcherManifestSha512Hash));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(manifestDirectoryPath))
|
||||
{
|
||||
throw new ArgumentException("Manifest directory path cannot be null, empty, or whitespace.", nameof(manifestDirectoryPath));
|
||||
AlayaManifestSha512Hash = alayaManifestSha512Hash;
|
||||
LauncherManifestSha512Hash = launcherManifestSha512Hash;
|
||||
}
|
||||
|
||||
CoreManifestSha512Hash = coreManifestSha512Hash;
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,8 @@ namespace AlayaCore.Models.Configuration
|
||||
}
|
||||
|
||||
public string BaseApiUrl { get; }
|
||||
|
||||
public static ModrinthConnectionOptions Default { get; } =
|
||||
new ModrinthConnectionOptions("https://api.modrinth.com/v2/");
|
||||
}
|
||||
}
|
||||
12
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
12
AlayaCore/Models/Configuration/RetryPolicyOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace AlayaCore.Models.Configuration
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
216
AlayaCore/Policies/RetryPolicy.cs
Normal file
216
AlayaCore/Policies/RetryPolicy.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||
using AlayaCore.Models.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AlayaCore.Services
|
||||
{
|
||||
public sealed class RetryPolicy : IRetryPolicy
|
||||
{
|
||||
private readonly RetryPolicyOptions _options;
|
||||
private readonly ILogger<RetryPolicy> _logger;
|
||||
|
||||
private static readonly Random _random = new Random();
|
||||
|
||||
public RetryPolicy(
|
||||
RetryPolicyOptions options,
|
||||
ILogger<RetryPolicy> logger)
|
||||
{
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(
|
||||
Func<CancellationToken, Task> operation,
|
||||
string operationName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (operation == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(operationName))
|
||||
{
|
||||
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||
}
|
||||
|
||||
await ExecuteAsync<object?>(
|
||||
async token =>
|
||||
{
|
||||
await operation(token).ConfigureAwait(false);
|
||||
return null;
|
||||
},
|
||||
operationName,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(
|
||||
Func<CancellationToken, Task<T>> operation,
|
||||
string operationName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (operation == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(operationName))
|
||||
{
|
||||
throw new ArgumentException("Operation name cannot be null, empty, or whitespace.", nameof(operationName));
|
||||
}
|
||||
|
||||
ValidateOptions();
|
||||
|
||||
Exception? lastException = null;
|
||||
|
||||
for (int attempt = 1; attempt <= _options.MaxAttempts; attempt++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (attempt > 1)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Retrying operation {OperationName}. Attempt {Attempt} of {MaxAttempts}.",
|
||||
operationName,
|
||||
attempt,
|
||||
_options.MaxAttempts);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Operation {OperationName} was cancelled by the caller.",
|
||||
operationName);
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (IsRetryable(ex))
|
||||
{
|
||||
lastException = ex;
|
||||
|
||||
if (attempt == _options.MaxAttempts)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Operation {OperationName} failed after {MaxAttempts} attempts.",
|
||||
operationName,
|
||||
_options.MaxAttempts);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
TimeSpan delay = CalculateDelay(attempt);
|
||||
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Operation {OperationName} failed with a transient error on attempt {Attempt} of {MaxAttempts}. Retrying after {DelayMs}ms.",
|
||||
operationName,
|
||||
attempt,
|
||||
_options.MaxAttempts,
|
||||
(int)delay.TotalMilliseconds);
|
||||
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Operation {OperationName} failed with a non-retryable error on attempt {Attempt}.",
|
||||
operationName,
|
||||
attempt);
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Retry policy exited unexpectedly for operation '{operationName}'.",
|
||||
lastException);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
HttpRequestException => true,
|
||||
IOException => true,
|
||||
TaskCanceledException => true,
|
||||
InvalidDataException => false,
|
||||
ArgumentException => false,
|
||||
InvalidOperationException => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private TimeSpan CalculateDelay(int attempt)
|
||||
{
|
||||
double exponentialDelay = _options.BaseDelayMilliseconds *
|
||||
Math.Pow(_options.BackoffMultiplier, attempt - 1);
|
||||
|
||||
double cappedDelay = Math.Min(exponentialDelay, _options.MaxDelayMilliseconds);
|
||||
|
||||
int jitterMilliseconds = _random.Next(0, 150);
|
||||
|
||||
return TimeSpan.FromMilliseconds(cappedDelay + jitterMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||
using AlayaCore.Abstractions.Interfaces.Services;
|
||||
using AlayaCore.Models.Progress;
|
||||
using AlayaCore.Models.Results;
|
||||
@@ -18,13 +19,16 @@ namespace AlayaCore.Services
|
||||
private const int BUFFER_SIZE = 81920;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IRetryPolicy _retryPolicy;
|
||||
private readonly ILogger<HttpDownloadService> _logger;
|
||||
|
||||
public HttpDownloadService(
|
||||
IHttpClient httpClient,
|
||||
IRetryPolicy retryPolicy,
|
||||
ILogger<HttpDownloadService> logger)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -104,10 +108,15 @@ namespace AlayaCore.Services
|
||||
|
||||
try
|
||||
{
|
||||
DownloadResult result = await _retryPolicy.ExecuteAsync(
|
||||
async token =>
|
||||
{
|
||||
DeleteFileIfExists(tempFilePath);
|
||||
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||
sourceUri,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -144,7 +153,10 @@ namespace AlayaCore.Services
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
||||
|
||||
_logger.LogDebug("Streaming download content for {FileName} into temporary file {TempFilePath}.", fileName, tempFilePath);
|
||||
_logger.LogDebug(
|
||||
"Streaming download content for {FileName} into temporary file {TempFilePath}.",
|
||||
fileName,
|
||||
tempFilePath);
|
||||
|
||||
while (true)
|
||||
{
|
||||
@@ -152,7 +164,7 @@ namespace AlayaCore.Services
|
||||
buffer,
|
||||
0,
|
||||
buffer.Length,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
@@ -163,7 +175,7 @@ namespace AlayaCore.Services
|
||||
buffer,
|
||||
0,
|
||||
bytesRead,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
|
||||
|
||||
@@ -185,7 +197,7 @@ namespace AlayaCore.Services
|
||||
}
|
||||
|
||||
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
await fileStream.FlushAsync(token).ConfigureAwait(false);
|
||||
|
||||
string actualHash = ConvertToLowerHex(sha512.Hash);
|
||||
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
@@ -235,6 +247,11 @@ namespace AlayaCore.Services
|
||||
outcome,
|
||||
hashVerified: true,
|
||||
bytesDownloaded: bytesDownloaded);
|
||||
},
|
||||
$"download:{fileName}",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces;
|
||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||
using AlayaCore.Abstractions.Interfaces.Services;
|
||||
using AlayaCore.Models.Configuration;
|
||||
using AlayaCore.Models.Manifests;
|
||||
@@ -26,6 +27,7 @@ namespace AlayaCore.Services
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileStore _fileStore;
|
||||
private readonly ManifestServiceOptions _options;
|
||||
private readonly IRetryPolicy _retryPolicy;
|
||||
private readonly ILogger<ManifestService> _logger;
|
||||
|
||||
public ManifestService(
|
||||
@@ -33,12 +35,14 @@ namespace AlayaCore.Services
|
||||
IHttpClient httpClient,
|
||||
IFileStore fileStore,
|
||||
ManifestServiceOptions options,
|
||||
IRetryPolicy retryPolicy,
|
||||
ILogger<ManifestService> logger)
|
||||
{
|
||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -48,13 +52,13 @@ namespace AlayaCore.Services
|
||||
|
||||
_logger.LogInformation(
|
||||
"Downloading and loading Alaya manifest from {ManifestUri} to {DestinationPath}.",
|
||||
_options.CoreManifestUri,
|
||||
_options.AlayaManifestUri,
|
||||
destinationPath);
|
||||
|
||||
return DownloadAndLoadManifestAsync<ManifestDto, ManifestModel>(
|
||||
_options.CoreManifestUri,
|
||||
_options.AlayaManifestUri,
|
||||
destinationPath,
|
||||
_options.CoreManifestSha512Hash,
|
||||
_options.AlayaManifestSha512Hash,
|
||||
static dto => dto.ToModel(),
|
||||
cancellationToken);
|
||||
}
|
||||
@@ -144,24 +148,24 @@ namespace AlayaCore.Services
|
||||
|
||||
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>(
|
||||
_options.CoreManifestUri,
|
||||
_options.AlayaManifestUri,
|
||||
static dto => dto.ToModel(),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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(
|
||||
$"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(
|
||||
"Fetched remote Alaya manifest version {RemoteVersion} from {ManifestUri}.",
|
||||
remoteManifest.AlayaVersion,
|
||||
_options.CoreManifestUri);
|
||||
_options.AlayaManifestUri);
|
||||
|
||||
return remoteManifest.AlayaVersion;
|
||||
}
|
||||
@@ -309,11 +313,17 @@ namespace AlayaCore.Services
|
||||
manifestUri,
|
||||
destinationPath);
|
||||
|
||||
await _retryPolicy.ExecuteAsync(
|
||||
async token =>
|
||||
{
|
||||
await _downloadService.DownloadFileAsync(
|
||||
manifestUri,
|
||||
destinationPath,
|
||||
sha512Hash,
|
||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken: token).ConfigureAwait(false);
|
||||
},
|
||||
$"manifest-download:{Path.GetFileName(destinationPath)}",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!File.Exists(destinationPath))
|
||||
{
|
||||
@@ -368,10 +378,13 @@ namespace AlayaCore.Services
|
||||
|
||||
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
|
||||
|
||||
return await _retryPolicy.ExecuteAsync(
|
||||
async token =>
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||
manifestUri,
|
||||
HttpCompletionOption.ResponseContentRead,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -393,6 +406,9 @@ namespace AlayaCore.Services
|
||||
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
|
||||
|
||||
return model;
|
||||
},
|
||||
$"manifest-fetch:{manifestUri.AbsolutePath}",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private TModel? DeserializeAndMapManifest<TDto, TModel>(
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AlayaCore.Abstractions.Interfaces;
|
||||
using AlayaCore.Abstractions.Interfaces.Clients;
|
||||
using AlayaCore.Abstractions.Interfaces.Policies;
|
||||
using AlayaCore.Abstractions.Interfaces.Services;
|
||||
using AlayaCore.Installation;
|
||||
using AlayaCore.Models;
|
||||
@@ -30,6 +31,7 @@ namespace AlayaCore.Services
|
||||
private readonly ModrinthConnectionOptions _options;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly IFileStore _fileStore;
|
||||
private readonly IRetryPolicy _retryPolicy;
|
||||
private readonly ILogger<ModService> _logger;
|
||||
|
||||
public ModService(
|
||||
@@ -37,12 +39,14 @@ namespace AlayaCore.Services
|
||||
ModrinthConnectionOptions options,
|
||||
IHttpClient httpClient,
|
||||
IFileStore fileStore,
|
||||
IRetryPolicy retryPolicy,
|
||||
ILogger<ModService> logger)
|
||||
{
|
||||
_downloadService = downloadService ?? throw new ArgumentNullException(nameof(downloadService));
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_fileStore = fileStore ?? throw new ArgumentNullException(nameof(fileStore));
|
||||
_retryPolicy = retryPolicy ?? throw new ArgumentNullException(nameof(retryPolicy));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -213,10 +217,13 @@ namespace AlayaCore.Services
|
||||
fileEntry.FileName,
|
||||
versionEndpoint);
|
||||
|
||||
return await _retryPolicy.ExecuteAsync(
|
||||
async token =>
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||
new Uri(versionEndpoint, UriKind.Absolute),
|
||||
HttpCompletionOption.ResponseContentRead,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
token).ConfigureAwait(false);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
@@ -343,6 +350,9 @@ namespace AlayaCore.Services
|
||||
result);
|
||||
|
||||
return result;
|
||||
},
|
||||
$"mod-metadata:{fileEntry.FileName}",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string BuildVersionEndpoint(string sha512Hash)
|
||||
|
||||
16
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
16
AlayaCore/Utilities/Enums/LauncherErrorType.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace AlayaCore.Utilities.Enums
|
||||
{
|
||||
public enum LauncherErrorType
|
||||
{
|
||||
Unknown,
|
||||
Network,
|
||||
Manifest,
|
||||
Authentication,
|
||||
Download,
|
||||
Installation,
|
||||
Update,
|
||||
Launch,
|
||||
Configuration,
|
||||
Cancelled
|
||||
}
|
||||
}
|
||||
56
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
56
AlayaCore/Utilities/Helpers/ErrorHelper.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using AlayaCore.Utilities.Enums;
|
||||
|
||||
namespace AlayaCore.Errors
|
||||
{
|
||||
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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
21
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
21
AlayaCore/Utilities/Helpers/OptionsHelper.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using AlayaCore.Models.Configuration;
|
||||
|
||||
namespace AlayaCore.Utilities.Helpers
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user