Skip to content

Commit

Permalink
Use TryGetCDNFallbackStream for certain CDN needs
Browse files Browse the repository at this point in the history
  • Loading branch information
neon-nyan committed Dec 11, 2023
1 parent 2aaa1bc commit d6fc30a
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 149 deletions.
11 changes: 2 additions & 9 deletions CollapseLauncher/Classes/EventsManagement/EventsHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,8 @@ public static async void StartCheckUpdate(bool forceUpdate)
private static async ValueTask<AppUpdateVersionProp> GetUpdateMetadata()
{
string relativePath = ConverterTool.CombineURLFromString(UpdateChannelName, "fileindex.json");

using (Http client = new Http(true))
using (MemoryStream ms = new MemoryStream())
{
await FallbackCDNUtil.DownloadCDNFallbackContent(client, ms, relativePath, default);
ms.Position = 0;

return await ms.DeserializeAsync<AppUpdateVersionProp>(InternalAppJSONContext.Default);
}
await using BridgedNetworkStream ms = await FallbackCDNUtil.TryGetCDNFallbackStream(relativePath, default);
return await ms.DeserializeAsync<AppUpdateVersionProp>(InternalAppJSONContext.Default);
}

public static bool CompareVersion(GameVersion? CurrentVer, GameVersion? ComparedVer)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
Expand All @@ -15,14 +16,11 @@ internal static partial class JSONSerializerHelper
internal static async ValueTask<T?> DeserializeAsync<T>(this Stream data, JsonSerializerContext context, CancellationToken token = default, T? defaultType = null)
where T : struct => await InnerDeserializeStreamAsync(data, context, token, defaultType);

private static async ValueTask<T?> InnerDeserializeStreamAsync<T>(Stream data, JsonSerializerContext context, CancellationToken token, T? defaultType)
{
// Check if the data length is 0, then return default value
if (data.Length == 0) return defaultType ?? default;

// Try deserialize. If it returns a null, then return the default value
return (T?)await JsonSerializer.DeserializeAsync(data, typeof(T), context, token);
}
private static async ValueTask<T?> InnerDeserializeStreamAsync<T>(Stream data, JsonSerializerContext context, CancellationToken token, T? defaultType) =>
// Check if the data cannot be read, then throw
!data.CanRead ? throw new NotSupportedException("Stream is not readable! Cannot deserialize the stream to JSON!") :
// Try deserialize. If it returns a null, then return the default value
(T?)await JsonSerializer.DeserializeAsync(data, typeof(T), context, token) ?? defaultType;
#nullable disable
}
}
16 changes: 5 additions & 11 deletions CollapseLauncher/Classes/Properties/InnerLauncherConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,8 @@ public static async Task<bool> CheckForNewConfigV2()

try
{
using (Http _http = new Http())
using (Stream s = new MemoryStream())
{
await FallbackCDNUtil.DownloadCDNFallbackContent(_http, s, string.Format(AppGameConfigV2URLPrefix, (IsPreview ? "preview" : "stable") + "stamp"), default).ConfigureAwait(false);
s.Position = 0;
ConfigStamp = (Stamp)JsonSerializer.Deserialize(s, typeof(Stamp), CoreLibraryJSONContext.Default);
}
await using BridgedNetworkStream s = await FallbackCDNUtil.TryGetCDNFallbackStream(string.Format(AppGameConfigV2URLPrefix, (IsPreview ? "preview" : "stable") + "stamp"), default);
ConfigStamp = await s.DeserializeAsync<Stamp>(CoreLibraryJSONContext.Default);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -201,10 +196,9 @@ private static async Task GetConfigV2Content(Http _httpClient, string prefix, st
{
string URL = string.Format(AppGameConfigV2URLPrefix, (IsPreview ? "preview" : "stable") + prefix);

using (FileStream fs = new FileStream(output, FileMode.Create, FileAccess.Write))
{
await FallbackCDNUtil.DownloadCDNFallbackContent(_httpClient, fs, URL, default).ConfigureAwait(false);
}
await using FileStream fs = new FileStream(output, FileMode.Create, FileAccess.Write);
await using BridgedNetworkStream networkStream = await FallbackCDNUtil.TryGetCDNFallbackStream(URL, default);
await networkStream.CopyToAsync(fs);
}
}
}
21 changes: 21 additions & 0 deletions CollapseLauncher/Classes/RegionManagement/BridgedNetworkStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ private int ReadBytes(byte[] buffer, int offset, int count)

public override int Read(Span<byte> buffer) => ReadBytes(buffer);
public override int Read(byte[] buffer, int offset, int count) => ReadBytes(buffer, offset, count);
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
int totalRead = 0;
while (totalRead < buffer.Length)
{
int read = await ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false);
if (read == 0) return;

totalRead += read;
}
}

public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
Expand Down Expand Up @@ -92,5 +103,15 @@ protected override void Dispose(bool disposing)
_networkStream?.Dispose();
}
}

public override async ValueTask DisposeAsync()
{
_networkResponse?.Dispose();
if (_networkStream != null)
await _networkStream.DisposeAsync();

await base.DisposeAsync();
return;
}
}
}
116 changes: 73 additions & 43 deletions CollapseLauncher/Classes/RegionManagement/FallbackCDNUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,36 +40,18 @@ public async Task DownloadFile(string url, string targetFile, Action<int> progre

public async Task<byte[]> DownloadBytes(string url, string authorization = null, string accept = null)
{
Http _httpClient = new Http(true);
MemoryStream fs = new MemoryStream();
try
{
await FallbackCDNUtil.DownloadCDNFallbackContent(_httpClient, fs, GetRelativePathOnly(url), default);
return fs.ToArray();
}
catch { throw; }
finally
{
fs?.Dispose();
_httpClient?.Dispose();
}
await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(GetRelativePathOnly(url), default, true);
byte[] buffer = new byte[stream.Length];
await stream.ReadExactlyAsync(buffer);
return buffer;
}

public async Task<string> DownloadString(string url, string authorization = null, string accept = null)
{
Http _httpClient = new Http(true);
MemoryStream fs = new MemoryStream();
try
{
await FallbackCDNUtil.DownloadCDNFallbackContent(_httpClient, fs, GetRelativePathOnly(url), default);
return Encoding.UTF8.GetString(fs.ToArray());
}
catch { throw; }
finally
{
fs?.Dispose();
_httpClient?.Dispose();
}
await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(GetRelativePathOnly(url), default, true);
byte[] buffer = new byte[stream.Length];
await stream.ReadExactlyAsync(buffer);
return Encoding.UTF8.GetString(buffer);
}

private string GetRelativePathOnly(string url)
Expand All @@ -79,6 +61,21 @@ private string GetRelativePathOnly(string url)
}
}

internal readonly struct CDNUtilHTTPStatus
{
internal readonly HttpStatusCode StatusCode;
internal readonly bool IsSuccessStatusCode;
internal readonly Uri AbsoluteURL;
internal readonly HttpResponseMessage Message;
internal CDNUtilHTTPStatus(HttpResponseMessage message)
{
Message = message;
StatusCode = Message.StatusCode;
IsSuccessStatusCode = Message.IsSuccessStatusCode;
AbsoluteURL = Message.RequestMessage.RequestUri;
}
}

internal static class FallbackCDNUtil
{
private static readonly HttpClient _client = new HttpClient(new HttpClientHandler
Expand All @@ -92,6 +89,17 @@ internal static class FallbackCDNUtil
DefaultRequestVersion = HttpVersion.Version20,
Timeout = TimeSpan.FromMinutes(1)
};
private static readonly HttpClient _clientNoCompression = new HttpClient(new HttpClientHandler
{
AllowAutoRedirect = true,
MaxConnectionsPerServer = 16,
AutomaticDecompression = DecompressionMethods.None
})
{
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
DefaultRequestVersion = HttpVersion.Version20,
Timeout = TimeSpan.FromMinutes(1)
};
public static event EventHandler<DownloadEvent> DownloadProgress;

public static async Task DownloadCDNFallbackContent(Http httpInstance, string outputPath, int parallelThread, string relativeURL, CancellationToken token)
Expand Down Expand Up @@ -153,29 +161,33 @@ public static async Task DownloadCDNFallbackContent(Http httpInstance, Stream ou
}
}

public static async Task<BridgedNetworkStream> DownloadCDNFallbackContent(Http httpInstance, string relativeURL, CancellationToken token)
public static async ValueTask<BridgedNetworkStream> TryGetCDNFallbackStream(string relativeURL, CancellationToken token = default, bool isForceUncompressRequest = false)
{
// Get the preferred CDN first and try get the content stream
// Get the preferred CDN first and try get the content
CDNURLProperty preferredCDN = GetPreferredCDN();
BridgedNetworkStream outputStream = await TryGetCDNContent(preferredCDN, httpInstance, relativeURL, token);
BridgedNetworkStream contentStream = await TryGetCDNContent(preferredCDN, relativeURL, token, isForceUncompressRequest);

// If stream is not null, then return
if (outputStream != null) return outputStream;
// If successful, then return
if (contentStream != null) return contentStream;

// If the fail return code occurred by the token, then throw cancellation exception
token.ThrowIfCancellationRequested();

// If not, then continue to get the content from another CDN
foreach (CDNURLProperty fallbackCDN in CDNList.Where(x => !x.Equals(preferredCDN)))
{
outputStream = await TryGetCDNContent(fallbackCDN, httpInstance, relativeURL, token);
// Dispose previous stream
await contentStream.DisposeAsync();
contentStream = await TryGetCDNContent(fallbackCDN, relativeURL, token, isForceUncompressRequest);

// If the stream is not null, then return
if (outputStream != null) return outputStream;
// If successful, then return
if (contentStream != null) return contentStream;
}

// If all of them failed, then return null
return null;
// If all of them failed, then throw an exception
// Dispose previous stream
await contentStream.DisposeAsync();
throw new AggregateException($"All available CDNs aren't reachable for your network while getting content: {relativeURL}. Please check your internet!");
}

private static void PerformStreamCheckAndSeek(Stream outputStream)
Expand All @@ -188,19 +200,18 @@ private static void PerformStreamCheckAndSeek(Stream outputStream)
outputStream.Position = 0;
}

private static async ValueTask<BridgedNetworkStream> TryGetCDNContent(CDNURLProperty cdnProp, Http httpInstance, string relativeURL, CancellationToken token)
private static async ValueTask<BridgedNetworkStream> TryGetCDNContent(CDNURLProperty cdnProp, string relativeURL, CancellationToken token, bool isForceUncompressRequest)
{
try
{
// Get the URL Status then return boolean and and URLStatus
(bool, string) urlStatus = await TryGetURLStatus(cdnProp, httpInstance, relativeURL, token);
CDNUtilHTTPStatus urlStatus = await TryGetURLStatus(cdnProp, relativeURL, token, isForceUncompressRequest);

// If URL status is false, then return null
if (!urlStatus.Item1) return null;
if (!urlStatus.IsSuccessStatusCode) return null;

// Continue to get the content and return the stream if successful
using HttpResponseMessage response = await GetURLHttpResponse(urlStatus.Item2, token);
return await GetHttpStreamFromResponse(response, token);
return await GetHttpStreamFromResponse(urlStatus.Message, token);
}
// Handle the error and log it. If fails, then log it and return false
catch (Exception ex)
Expand Down Expand Up @@ -298,6 +309,24 @@ private static async ValueTask<bool> TryGetCDNContent(CDNURLProperty cdnProp, Ht
return (true, absoluteURL);
}

private static async ValueTask<CDNUtilHTTPStatus> TryGetURLStatus(CDNURLProperty cdnProp, string relativeURL, CancellationToken token, bool isUncompressRequest)
{
// Concat the URL Prefix and Relative URL
string absoluteURL = ConverterTool.CombineURLFromString(cdnProp.URLPrefix, relativeURL);

LogWriteLine($"Getting CDN Content from: {cdnProp.Name} at URL: {absoluteURL}", LogType.Default, true);

// Try check the status of the URL
HttpResponseMessage responseMessage = await GetURLHttpResponse(absoluteURL, token, isUncompressRequest);

// If it's not a successful code, log the information
if (!responseMessage.IsSuccessStatusCode)
LogWriteLine($"CDN content from: {cdnProp.Name} (prefix: {cdnProp.URLPrefix}) (relPath: {relativeURL}) has returned an error code: {responseMessage.StatusCode} ({(int)responseMessage.StatusCode})", LogType.Error, true);

// Then return the status code
return new CDNUtilHTTPStatus(responseMessage);
}

public static CDNURLProperty GetPreferredCDN()
{
// Get the CurrentCDN index
Expand All @@ -317,7 +346,7 @@ public static CDNURLProperty GetPreferredCDN()
public static async Task<T> DownloadAsJSONType<T>(string URL, JsonSerializerContext context, CancellationToken token)
=> (T)await _client.GetFromJsonAsync(URL, typeof(T), context, token) ?? default;

public static async ValueTask<HttpResponseMessage> GetURLHttpResponse(string URL, CancellationToken token)
public static async ValueTask<HttpResponseMessage> GetURLHttpResponse(string URL, CancellationToken token, bool isForceUncompressRequest = false)
{
using HttpRequestMessage requestMsg = new HttpRequestMessage()
{
Expand All @@ -326,7 +355,8 @@ public static async ValueTask<HttpResponseMessage> GetURLHttpResponse(string URL
};
requestMsg.Headers.Range = new RangeHeaderValue(0, null);

return await _client.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, token);
return isForceUncompressRequest ? await _clientNoCompression.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, token)
: await _client.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, token);
}

public static async ValueTask<BridgedNetworkStream> GetHttpStreamFromResponse(string URL, CancellationToken token)
Expand Down
34 changes: 11 additions & 23 deletions CollapseLauncher/Classes/RepairManagement/Honkai/Fetch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -396,36 +396,24 @@ private async Task<KianaAudioManifest> TryGetAudioManifest(Http _httpClient, str
#region XMFAndAssetIndex
private async Task<Dictionary<string, string>> FetchMetadata(Http _httpClient, CancellationToken token)
{
// Fetch metadata dictionary
using (MemoryStream mfs = new MemoryStream())
{
// Set metadata URL
string urlMetadata = string.Format(AppGameRepoIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName);

// Start downloading metadata using FallbackCDNUtil
await FallbackCDNUtil.DownloadCDNFallbackContent(_httpClient, mfs, urlMetadata, token);
// Set metadata URL
string urlMetadata = string.Format(AppGameRepoIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName);

// Deserialize metadata
mfs.Position = 0;
return await mfs.DeserializeAsync<Dictionary<string, string>>(CoreLibraryJSONContext.Default, token);
}
// Start downloading metadata using FallbackCDNUtil
await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlMetadata, token);
return await stream.DeserializeAsync<Dictionary<string, string>>(CoreLibraryJSONContext.Default, token);
}

private async Task FetchAssetIndex(Http _httpClient, List<FilePropertiesRemote> assetIndex, CancellationToken token)
{
// Initialize MemoryStream
using (MemoryStream mfs = new MemoryStream())
{
// Set asset index URL
string urlIndex = string.Format(AppGameRepairIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName, _gameVersion.VersionString) + ".bin";
// Set asset index URL
string urlIndex = string.Format(AppGameRepairIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName, _gameVersion.VersionString) + ".bin";

// Start downloading asset index using FallbackCDNUtil
await FallbackCDNUtil.DownloadCDNFallbackContent(_httpClient, mfs, urlIndex, token);
// Start downloading asset index using FallbackCDNUtil
await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token);

// Deserialize asset index and return
mfs.Position = 0;
DeserializeAssetIndex(mfs, assetIndex);
}
// Deserialize asset index and return
DeserializeAssetIndex(stream, assetIndex);
}

private void DeserializeAssetIndex(Stream stream, List<FilePropertiesRemote> assetIndex)
Expand Down
Loading

0 comments on commit d6fc30a

Please sign in to comment.