refactor(kv): 整理存档接线并清理迁移冗余实现
This commit is contained in:
285
Client/Assets/Scripts/Services/PlayerData/KvAccountLease.cs
Normal file
285
Client/Assets/Scripts/Services/PlayerData/KvAccountLease.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using game.Account;
|
||||
|
||||
namespace game.KV
|
||||
{
|
||||
public enum KVLoginPrepareError
|
||||
{
|
||||
None,
|
||||
Validation,
|
||||
Cancelled,
|
||||
Stale,
|
||||
AuthenticationFailed,
|
||||
OwnerRejected,
|
||||
NotReady,
|
||||
Unexpected,
|
||||
CloseFailed
|
||||
}
|
||||
|
||||
internal sealed class KvAccountDescriptor
|
||||
{
|
||||
internal string PlayerId
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal long Epoch
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal uint InternalId
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal bool NewlyCreated
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal DateTime CreateTimeUtc
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal DateTime ServerTimeUtc
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvAccountDescriptor(string playerId, long epoch, uint internalId,
|
||||
bool newlyCreated, DateTime createTimeUtc, DateTime serverTimeUtc)
|
||||
{
|
||||
PlayerId = playerId;
|
||||
Epoch = epoch;
|
||||
InternalId = internalId;
|
||||
NewlyCreated = newlyCreated;
|
||||
CreateTimeUtc = createTimeUtc;
|
||||
ServerTimeUtc = serverTimeUtc;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal sealed class KvAccountResponse
|
||||
{
|
||||
internal bool IsCurrent
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal int? StatusCode
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal string Body
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvAccountResponse(bool isCurrent, int? statusCode = null, string body = null)
|
||||
{
|
||||
IsCurrent = isCurrent;
|
||||
StatusCode = isCurrent ? statusCode : null;
|
||||
Body = isCurrent ? body : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A context-owned account transport. Close drains before disposing its client.</summary>
|
||||
internal sealed class KvAccountLease
|
||||
{
|
||||
private readonly object gate = new object();
|
||||
private readonly HttpClient client;
|
||||
private readonly CookieContainer cookies;
|
||||
private readonly Func<bool> current;
|
||||
private readonly Func<bool> admission;
|
||||
private readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
||||
private TaskCompletionSource<bool> drained;
|
||||
private TaskCompletionSource<bool> closing;
|
||||
private int active;
|
||||
internal Uri ApiBaseUri
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal long Epoch
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal CookieContainer FormalCookies => IsCurrent ? cookies : null;
|
||||
internal bool IsCurrent
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (gate)
|
||||
if (closing != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return current();
|
||||
}
|
||||
}
|
||||
|
||||
internal KvAccountLease(Uri api, long epoch, CookieContainer cookies,
|
||||
Func<CookieContainer, HttpMessageHandler> capability, Func<bool> current, Func<bool> admission)
|
||||
{
|
||||
if (capability == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(capability));
|
||||
}
|
||||
ApiBaseUri = api;
|
||||
Epoch = epoch;
|
||||
this.cookies = cookies;
|
||||
this.current = current;
|
||||
this.admission = admission;
|
||||
client = new HttpClient(capability(cookies)) { BaseAddress = api };
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
}
|
||||
|
||||
internal async Task<KvAccountResponse> SendAsync(HttpMethod method, string route,
|
||||
string body = null, string contentType = "application/json", CancellationToken token = default)
|
||||
{
|
||||
if (!IsCanonicalBusinessRoute(route))
|
||||
{
|
||||
throw new ArgumentException("Only canonical relative business routes are allowed.", nameof(route));
|
||||
}
|
||||
if (method != HttpMethod.Get && method != HttpMethod.Post)
|
||||
{
|
||||
throw new ArgumentException("Unsupported lease method.", nameof(method));
|
||||
}
|
||||
if (!IsCurrent)
|
||||
{
|
||||
return new KvAccountResponse(false);
|
||||
}
|
||||
lock (gate)
|
||||
{
|
||||
if (closing != null || !admission())
|
||||
{
|
||||
return new KvAccountResponse(false);
|
||||
}
|
||||
active++;
|
||||
}
|
||||
try
|
||||
{
|
||||
using (var linked = CancellationTokenSource.CreateLinkedTokenSource(token, lifetime.Token))
|
||||
using (var request = new HttpRequestMessage(method, route))
|
||||
{
|
||||
if (body != null)
|
||||
{
|
||||
request.Content = new StringContent(body, System.Text.Encoding.UTF8, contentType);
|
||||
}
|
||||
using (var response = await client.SendAsync(request, linked.Token).ConfigureAwait(false))
|
||||
{
|
||||
if (!IsCurrent || linked.IsCancellationRequested)
|
||||
{
|
||||
return new KvAccountResponse(false);
|
||||
}
|
||||
var text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
// Do not follow redirects from the authenticated business endpoint.
|
||||
if ((int)response.StatusCode >= 300 && (int)response.StatusCode < 400)
|
||||
{
|
||||
throw new InvalidOperationException("Redirects are not accepted.");
|
||||
}
|
||||
return IsCurrent && !linked.IsCancellationRequested
|
||||
? new KvAccountResponse(true, (int)response.StatusCode, text)
|
||||
: new KvAccountResponse(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (!IsCurrent || token.IsCancellationRequested || lifetime.IsCancellationRequested)
|
||||
{
|
||||
return new KvAccountResponse(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
active--;
|
||||
if (active == 0)
|
||||
{
|
||||
drained?.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCanonicalBusinessRoute(string route)
|
||||
{
|
||||
if (string.IsNullOrEmpty(route) || route[0] == '/' || route[0] == '\\' ||
|
||||
Uri.TryCreate(route, UriKind.Absolute, out _) ||
|
||||
route.IndexOf("//", StringComparison.Ordinal) >= 0 ||
|
||||
route.IndexOfAny(new[] { '?', '#', '%', '\\' }) >= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var segments = route.Split('/');
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
if (segment.Length == 0 || segment == "." || segment == "..")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach (var character in segment)
|
||||
if (char.IsControl(character))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return !route.Equals("account/Login", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.Equals("playerdata", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.StartsWith("playerdata/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal Task CloseAsync()
|
||||
{
|
||||
TaskCompletionSource<bool> completion;
|
||||
lock (gate)
|
||||
{
|
||||
if (closing != null)
|
||||
{
|
||||
return closing.Task;
|
||||
}
|
||||
completion = closing = Signal();
|
||||
drained = Signal();
|
||||
if (active == 0)
|
||||
{
|
||||
drained.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
_ = completion.Task.ContinueWith(t =>
|
||||
{
|
||||
_ = t.Exception;
|
||||
}, CancellationToken.None,
|
||||
TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Exception cancellationFailure = null;
|
||||
try
|
||||
{
|
||||
lifetime.Cancel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
cancellationFailure = ex;
|
||||
}
|
||||
await drained.Task.ConfigureAwait(false);
|
||||
client.Dispose();
|
||||
lifetime.Dispose();
|
||||
if (cancellationFailure != null)
|
||||
{
|
||||
throw cancellationFailure;
|
||||
}
|
||||
completion.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completion.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
private static TaskCompletionSource<bool> Signal() =>
|
||||
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user