feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
@@ -58,6 +58,21 @@ namespace game.PlayerDataStorage
|
||||
internal PlayerKVAccountDescriptor AccountDescriptor =>
|
||||
Volatile.Read(ref prepared) != 0 && IsCurrent ? accountDescriptor : null;
|
||||
|
||||
// Only the coordinator may publish this original handle after Apply and Guard release.
|
||||
internal bool TryAcquireGameHandle(long expectedEpoch, out PlayerKVGameSessionHandle gameHandle)
|
||||
{
|
||||
gameHandle = null;
|
||||
// IsCurrent can invoke external code and synchronously reenter CloseAsync.
|
||||
if (expectedEpoch != epoch || !IsCurrent) return false;
|
||||
lock (gate)
|
||||
{
|
||||
if (closing != null || revoked != 0 || prepared != 1 || handle == null || handle.Epoch != expectedEpoch)
|
||||
return false;
|
||||
gameHandle = handle;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// All issuance and Close admission share gate. The editor factory may not block on Close.
|
||||
internal bool TryAcquireAccountLease(PlayerKVControlledLoadCapability capability,
|
||||
out PlayerKVAccountLease lease)
|
||||
@@ -317,21 +332,26 @@ namespace game.PlayerDataStorage
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Construction is absent from player builds; no default network handler is supplied.</summary>
|
||||
/// <summary>Controlled transport factory; formal and Editor validation modes remain separate.</summary>
|
||||
internal sealed class PlayerKVControlledLoadCapability
|
||||
{
|
||||
readonly Func<CookieContainer, HttpMessageHandler> leaseHandler;
|
||||
private PlayerKVControlledLoadCapability(Func<CookieContainer, HttpMessageHandler> leaseHandler)
|
||||
{ this.leaseHandler = leaseHandler ?? throw new ArgumentNullException(nameof(leaseHandler)); }
|
||||
readonly bool formal;
|
||||
private PlayerKVControlledLoadCapability(Func<CookieContainer, HttpMessageHandler> leaseHandler, bool formal)
|
||||
{ this.leaseHandler = leaseHandler ?? throw new ArgumentNullException(nameof(leaseHandler)); this.formal = formal; }
|
||||
internal bool IsFormal => formal;
|
||||
|
||||
internal static PlayerKVControlledLoadCapability CreateForGame(
|
||||
Func<CookieContainer, HttpMessageHandler> handler) => new PlayerKVControlledLoadCapability(handler, true);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
internal static PlayerKVControlledLoadCapability CreateForValidation(
|
||||
Func<CookieContainer, HttpMessageHandler> leaseHandler) => new PlayerKVControlledLoadCapability(leaseHandler);
|
||||
Func<CookieContainer, HttpMessageHandler> leaseHandler) => new PlayerKVControlledLoadCapability(leaseHandler, false);
|
||||
#endif
|
||||
|
||||
internal HttpMessageHandler CreateHandler(CookieContainer cookies)
|
||||
{
|
||||
var handler = leaseHandler(cookies) ?? throw new InvalidOperationException("Synthetic handler required.");
|
||||
var handler = leaseHandler(cookies) ?? throw new InvalidOperationException("Transport handler required.");
|
||||
var seen = new HashSet<HttpMessageHandler>();
|
||||
var node = handler;
|
||||
while (node != null)
|
||||
@@ -340,12 +360,23 @@ namespace game.PlayerDataStorage
|
||||
// It never becomes a lease; the rejecting factory retains responsibility for that graph.
|
||||
if (!seen.Add(node))
|
||||
throw new InvalidOperationException("Cyclic transport is not a validation handler.");
|
||||
if (node is HttpClientHandler ||
|
||||
node.GetType().FullName == "System.Net.Http.SocketsHttpHandler")
|
||||
if (!formal && (node is HttpClientHandler ||
|
||||
node.GetType().FullName == "System.Net.Http.SocketsHttpHandler"))
|
||||
{
|
||||
handler.Dispose();
|
||||
throw new InvalidOperationException("Real or cyclic transport is not a validation handler.");
|
||||
}
|
||||
if (formal && node is HttpClientHandler real &&
|
||||
(!real.UseCookies || !ReferenceEquals(real.CookieContainer, cookies) || real.AllowAutoRedirect))
|
||||
{
|
||||
handler.Dispose();
|
||||
throw new InvalidOperationException("Formal transport must share the session cookies and reject redirects.");
|
||||
}
|
||||
if (formal && node.GetType().FullName == "System.Net.Http.SocketsHttpHandler")
|
||||
{
|
||||
handler.Dispose();
|
||||
throw new InvalidOperationException("Unverified formal transport handler.");
|
||||
}
|
||||
node = (node as DelegatingHandler)?.InnerHandler;
|
||||
}
|
||||
return handler;
|
||||
@@ -366,6 +397,8 @@ namespace game.PlayerDataStorage
|
||||
{
|
||||
readonly object gate = new object();
|
||||
readonly HttpClient client;
|
||||
readonly CookieContainer cookies;
|
||||
readonly bool formal;
|
||||
readonly Func<bool> current;
|
||||
readonly Func<bool> admission;
|
||||
readonly CancellationTokenSource lifetime = new CancellationTokenSource();
|
||||
@@ -374,6 +407,8 @@ namespace game.PlayerDataStorage
|
||||
int active;
|
||||
internal Uri ApiBaseUri { get; }
|
||||
internal long Epoch { get; }
|
||||
internal bool IsFormal => formal;
|
||||
internal CookieContainer FormalCookies => formal && IsCurrent ? cookies : null;
|
||||
internal bool IsCurrent
|
||||
{
|
||||
get
|
||||
@@ -387,18 +422,24 @@ namespace game.PlayerDataStorage
|
||||
PlayerKVControlledLoadCapability capability, Func<bool> current, Func<bool> admission)
|
||||
{
|
||||
if (capability == null) throw new ArgumentNullException(nameof(capability));
|
||||
ApiBaseUri = api; Epoch = epoch; this.current = current; this.admission = admission;
|
||||
ApiBaseUri = api; Epoch = epoch; this.cookies = cookies;
|
||||
formal = capability.IsFormal; this.current = current; this.admission = admission;
|
||||
client = new HttpClient(capability.CreateHandler(cookies)) { BaseAddress = api };
|
||||
if (formal) client.Timeout = TimeSpan.FromSeconds(15);
|
||||
}
|
||||
|
||||
internal async Task<PlayerKVAccountResponse> SendAsync(HttpMethod method, string route,
|
||||
string body = null, string contentType = "application/json", CancellationToken token = default)
|
||||
{
|
||||
// Exact canonical relative paths: no query, fragment, encoding, traversal or absolute URL.
|
||||
if (route != "__kv_validation/ok" && route != "__kv_validation/unauthorized")
|
||||
if (formal)
|
||||
{
|
||||
if (!IsCanonicalBusinessRoute(route))
|
||||
throw new ArgumentException("Only canonical relative business routes are allowed.", nameof(route));
|
||||
}
|
||||
else if (route != "__kv_validation/ok" && route != "__kv_validation/unauthorized")
|
||||
throw new ArgumentException("Only canonical validation routes are allowed.", nameof(route));
|
||||
if (method != HttpMethod.Get && method != HttpMethod.Post)
|
||||
throw new ArgumentException("Unsupported validation method.", nameof(method));
|
||||
throw new ArgumentException("Unsupported lease method.", nameof(method));
|
||||
if (!IsCurrent) return new PlayerKVAccountResponse(false);
|
||||
lock (gate)
|
||||
{
|
||||
@@ -436,6 +477,27 @@ namespace game.PlayerDataStorage
|
||||
}
|
||||
}
|
||||
|
||||
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("__kv_validation", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.StartsWith("__kv_validation/", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.Equals("account/Login", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.Equals("playerdata", StringComparison.OrdinalIgnoreCase) &&
|
||||
!route.StartsWith("playerdata/", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal Task CloseAsync()
|
||||
{
|
||||
TaskCompletionSource<bool> completion;
|
||||
|
||||
Reference in New Issue
Block a user