Compare commits
2 Commits
11863088e4
...
main
| 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_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>
|
||||||
@@ -11,7 +11,5 @@ namespace AlayaCore.Abstractions.Interfaces.Clients
|
|||||||
Uri uri,
|
Uri uri,
|
||||||
HttpCompletionOption completionOption,
|
HttpCompletionOption completionOption,
|
||||||
CancellationToken cancellationToken);
|
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
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
{
|
LauncherManifestSha512Hash = launcherManifestSha512Hash;
|
||||||
throw new ArgumentException("Manifest directory path cannot be null, empty, or whitespace.", nameof(manifestDirectoryPath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreManifestSha512Hash = coreManifestSha512Hash;
|
public static ManifestServiceOptions Default { get; } = new ManifestServiceOptions(
|
||||||
LauncherManifestSha512Hash = launcherManifestSha512Hash;
|
new Uri("INSERT-ALAYA-URL", UriKind.Absolute),
|
||||||
ManifestDirectoryPath = manifestDirectoryPath;
|
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 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;
|
||||||
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,10 +108,15 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
DownloadResult result = await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
|
DeleteFileIfExists(tempFilePath);
|
||||||
|
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
sourceUri,
|
sourceUri,
|
||||||
HttpCompletionOption.ResponseHeadersRead,
|
HttpCompletionOption.ResponseHeadersRead,
|
||||||
cancellationToken).ConfigureAwait(false);
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
@@ -144,7 +153,10 @@ namespace AlayaCore.Services
|
|||||||
byte[] buffer = new byte[BUFFER_SIZE];
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
Stopwatch stopwatch = Stopwatch.StartNew();
|
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)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -152,7 +164,7 @@ namespace AlayaCore.Services
|
|||||||
buffer,
|
buffer,
|
||||||
0,
|
0,
|
||||||
buffer.Length,
|
buffer.Length,
|
||||||
cancellationToken).ConfigureAwait(false);
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (bytesRead == 0)
|
if (bytesRead == 0)
|
||||||
{
|
{
|
||||||
@@ -163,7 +175,7 @@ namespace AlayaCore.Services
|
|||||||
buffer,
|
buffer,
|
||||||
0,
|
0,
|
||||||
bytesRead,
|
bytesRead,
|
||||||
cancellationToken).ConfigureAwait(false);
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
|
sha512.TransformBlock(buffer, 0, bytesRead, null, 0);
|
||||||
|
|
||||||
@@ -185,7 +197,7 @@ namespace AlayaCore.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
sha512.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
|
||||||
await fileStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
await fileStream.FlushAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
string actualHash = ConvertToLowerHex(sha512.Hash);
|
string actualHash = ConvertToLowerHex(sha512.Hash);
|
||||||
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(actualHash, normalizedExpectedHash, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -235,6 +247,11 @@ namespace AlayaCore.Services
|
|||||||
outcome,
|
outcome,
|
||||||
hashVerified: true,
|
hashVerified: true,
|
||||||
bytesDownloaded: bytesDownloaded);
|
bytesDownloaded: bytesDownloaded);
|
||||||
|
},
|
||||||
|
$"download:{fileName}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
await _downloadService.DownloadFileAsync(
|
await _downloadService.DownloadFileAsync(
|
||||||
manifestUri,
|
manifestUri,
|
||||||
destinationPath,
|
destinationPath,
|
||||||
sha512Hash,
|
sha512Hash,
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: token).ConfigureAwait(false);
|
||||||
|
},
|
||||||
|
$"manifest-download:{Path.GetFileName(destinationPath)}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!File.Exists(destinationPath))
|
if (!File.Exists(destinationPath))
|
||||||
{
|
{
|
||||||
@@ -368,10 +378,13 @@ namespace AlayaCore.Services
|
|||||||
|
|
||||||
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
|
_logger.LogDebug("Fetching remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
|
return await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
manifestUri,
|
manifestUri,
|
||||||
HttpCompletionOption.ResponseContentRead,
|
HttpCompletionOption.ResponseContentRead,
|
||||||
cancellationToken).ConfigureAwait(false);
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
@@ -393,6 +406,9 @@ namespace AlayaCore.Services
|
|||||||
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
|
_logger.LogDebug("Successfully fetched and mapped remote manifest from {ManifestUri}.", manifestUri);
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
|
},
|
||||||
|
$"manifest-fetch:{manifestUri.AbsolutePath}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private TModel? DeserializeAndMapManifest<TDto, TModel>(
|
private TModel? DeserializeAndMapManifest<TDto, TModel>(
|
||||||
|
|||||||
@@ -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,10 +217,13 @@ namespace AlayaCore.Services
|
|||||||
fileEntry.FileName,
|
fileEntry.FileName,
|
||||||
versionEndpoint);
|
versionEndpoint);
|
||||||
|
|
||||||
|
return await _retryPolicy.ExecuteAsync(
|
||||||
|
async token =>
|
||||||
|
{
|
||||||
using HttpResponseMessage response = await _httpClient.GetAsync(
|
using HttpResponseMessage response = await _httpClient.GetAsync(
|
||||||
new Uri(versionEndpoint, UriKind.Absolute),
|
new Uri(versionEndpoint, UriKind.Absolute),
|
||||||
HttpCompletionOption.ResponseContentRead,
|
HttpCompletionOption.ResponseContentRead,
|
||||||
cancellationToken).ConfigureAwait(false);
|
token).ConfigureAwait(false);
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
@@ -343,6 +350,9 @@ namespace AlayaCore.Services
|
|||||||
result);
|
result);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
},
|
||||||
|
$"mod-metadata:{fileEntry.FileName}",
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildVersionEndpoint(string sha512Hash)
|
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