106 lines
4.7 KiB
C#
106 lines
4.7 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Security;
|
|
using System.Text;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace game.PlayerDataStorage
|
|
{
|
|
// Internal seam for deterministic disk-failure tests; no production callback is supplied.
|
|
internal enum KVCacheWriteStage { BeforeWrite, AfterWrite, AfterFlush, BeforeReplace, AfterReplace }
|
|
|
|
internal sealed class PlayerKVCacheStore : IDisposable
|
|
{
|
|
const long MaxBytes = 64L * 1024 * 1024;
|
|
readonly KVIdentity identity;
|
|
readonly FileStream lease;
|
|
readonly Action<KVCacheWriteStage> fault;
|
|
readonly string main, temporary, backup;
|
|
bool disposed;
|
|
|
|
PlayerKVCacheStore(KVIdentity identity, FileStream lease, Action<KVCacheWriteStage> fault)
|
|
{
|
|
this.identity = identity; this.lease = lease; this.fault = fault;
|
|
main = Path.Combine(identity.DirectoryPath, "state.json");
|
|
temporary = main + ".tmp"; backup = main + ".bak";
|
|
}
|
|
|
|
internal static PlayerKVOpenError Open(KVIdentity identity, Action<KVCacheWriteStage> fault,
|
|
out PlayerKVCacheStore store, out KVSnapshot snapshot)
|
|
{
|
|
store = null; snapshot = null;
|
|
FileStream lease = null;
|
|
try
|
|
{
|
|
Directory.CreateDirectory(identity.DirectoryPath);
|
|
try { lease = new FileStream(Path.Combine(identity.DirectoryPath, "state.lock"), FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); }
|
|
catch (IOException ex) when ((ex.HResult & 0xffff) == 32 || (ex.HResult & 0xffff) == 33)
|
|
{ return PlayerKVOpenError.Busy; }
|
|
store = new PlayerKVCacheStore(identity, lease, fault);
|
|
if (!File.Exists(store.main))
|
|
{
|
|
if (File.Exists(store.backup) || File.Exists(store.temporary)) throw KVSnapshot.Invalid();
|
|
snapshot = new KVSnapshot();
|
|
}
|
|
else
|
|
{
|
|
if (new FileInfo(store.main).Length > MaxBytes) throw KVSnapshot.Invalid();
|
|
snapshot = KVSnapshot.Decode(File.ReadAllText(store.main, new UTF8Encoding(false, true)), identity);
|
|
}
|
|
return PlayerKVOpenError.None;
|
|
}
|
|
catch (Exception ex) when (ex is JsonException || ex is OverflowException || ex is FormatException || ex is DecoderFallbackException)
|
|
{
|
|
store?.Dispose(); lease?.Dispose(); store = null;
|
|
return PlayerKVOpenError.RecoveryRequired;
|
|
}
|
|
catch (Exception ex) when (DiskError(ex))
|
|
{
|
|
store?.Dispose(); lease?.Dispose(); store = null;
|
|
return PlayerKVOpenError.StorageError;
|
|
}
|
|
}
|
|
|
|
internal bool Save(KVSnapshot snapshot, out bool mayHaveCommitted)
|
|
{
|
|
mayHaveCommitted = false;
|
|
try
|
|
{
|
|
if (disposed) throw new ObjectDisposedException(nameof(PlayerKVCacheStore));
|
|
snapshot.Validate();
|
|
var bytes = new UTF8Encoding(false, true).GetBytes(snapshot.Encode(identity));
|
|
if (bytes.LongLength > MaxBytes) throw new IOException("KV cache capacity exceeded.");
|
|
fault?.Invoke(KVCacheWriteStage.BeforeWrite);
|
|
using (var stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None))
|
|
{
|
|
stream.Write(bytes, 0, bytes.Length);
|
|
fault?.Invoke(KVCacheWriteStage.AfterWrite);
|
|
stream.Flush(true);
|
|
fault?.Invoke(KVCacheWriteStage.AfterFlush);
|
|
}
|
|
fault?.Invoke(KVCacheWriteStage.BeforeReplace);
|
|
mayHaveCommitted = true;
|
|
// The temp file is on the same filesystem. Never delete the main file first.
|
|
if (File.Exists(main)) File.Replace(temporary, main, backup);
|
|
else File.Move(temporary, main);
|
|
fault?.Invoke(KVCacheWriteStage.AfterReplace);
|
|
mayHaveCommitted = false;
|
|
return true;
|
|
}
|
|
catch (Exception ex) when (DiskError(ex) || ex is JsonException || ex is EncoderFallbackException)
|
|
{ return false; }
|
|
}
|
|
|
|
static bool DiskError(Exception ex) => ex is IOException || ex is UnauthorizedAccessException ||
|
|
ex is SecurityException || ex is NotSupportedException || ex is ObjectDisposedException;
|
|
|
|
public void Dispose()
|
|
{
|
|
if (disposed) return;
|
|
disposed = true;
|
|
lease.Dispose();
|
|
// Keep files, including failed temporary writes, for explicit recovery diagnosis.
|
|
}
|
|
}
|
|
}
|