Remote_Board_Game/RBG_Server.Core/CachedByteArray.cs

241 lines
9.3 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace RBG_Server
{
/// <summary>
/// Stores data in either memory or disk, depending on the required behaviour of the system, namely, if the
/// byte array exceeds the size of the Memory Limit (objects can be required to stay in RAM)
/// </summary>
public struct CachedByteArray : IDisposable
{
// Static event, so that every instance of CachedByteArray is affected by a change in memory limit
private static event EventHandler MemoryLimitModifiedEvent;
// Properties to keep track of the actual allocated cache amounts
private static long memoryAllocationTotal = 0;
private static long memoryAllocationRAM = 0;
// static fields
private static int memoryLimit = 1024 * 1024 * 1; // Start with a memory limit of 1 MB
private static readonly string folderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Remote Board Game\\Cache Objects");
// static properties
public static long MemoryAllocationRAM { get { return memoryAllocationRAM; } }
public static long MemoryAllocationTotal { get { return memoryAllocationTotal; } }
public static long MemoryAllocationDisk { get { return memoryAllocationTotal - memoryAllocationRAM; } }
private SemaphoreSlim accessSem;
public bool IsInRam { get; private set; }
// struct data
private bool useOnlyRAM;
private Stream dataStream;
private string dataHashCode;
public bool UseOnlyRAM { get => useOnlyRAM; set => useOnlyRAM = value; }
// Properties from the array itself
public long Length => dataStream.Length;
/// <summary>
/// Set the memory limit for keeping data in RAM, as opposed to storing on disk.
/// Will raise events if modified, so all instances will be refactored to disk if appropriate
/// </summary>
public static int MemoryLimit
{
get
{
return memoryLimit;
}
set
{
memoryLimit = value;
MemoryLimitModifiedEvent?.Invoke(null, EventArgs.Empty);
}
}
public CachedByteArray(byte[] b) : this(b, false)
{
}
public CachedByteArray(byte[] b, bool useOnlyRAM)
{
accessSem = new SemaphoreSlim(0,1);
this.useOnlyRAM = useOnlyRAM;
dataHashCode = GenerateDataHash(b);
if (!useOnlyRAM && b.Length > MemoryLimit)
{
// Save to a file; load the data
string path = Path.Combine(folderPath, dataHashCode);
dataStream = CopyIntoFile(b, path);
IsInRam = false;
}
else
{
dataStream = new MemoryStream(b);
memoryAllocationRAM += dataStream.Length;
IsInRam = true;
}
dataStream.Position = 0;
memoryAllocationTotal += dataStream.Length;
// Signal the semaphore is ready
accessSem.Release();
// Listen to changes in the memory limit
MemoryLimitModifiedEvent += CachedByteArray_memoryLimitModifiedEvent;
}
/// <summary>
/// Generates a SHA256 hash of the data in the array, and returns it in a safe string format
/// </summary>
/// <param name="byteData"></param>
/// <returns></returns>
private static string GenerateDataHash(byte[] byteData)
{
using SHA256 dataHash = SHA256.Create();
byte[] hashCode = dataHash.ComputeHash(byteData);
string generatedHashCode = "";
foreach (byte hashByte in hashCode)
{
generatedHashCode += $"{hashByte:X2}";
}
return generatedHashCode;
}
/// <summary>
/// Copies the given <see cref="byte[]"/> into the file at <paramref name="filePath"/>, opening the stream, and returning it
/// </summary>
/// <param name="byteData"></param>
/// <param name="filePath"></param>
/// <returns></returns>
private static Stream CopyIntoFile(byte[] byteData, string filePath)
{
FileStream s = File.OpenWrite(filePath);
foreach (byte b in byteData)
{
s.WriteByte(b);
}
return s;
}
private void CachedByteArray_memoryLimitModifiedEvent(object sender, EventArgs e)
{
if (!useOnlyRAM)
{
if (dataStream.Length > MemoryLimit && IsInRam)
{
// Memory stream exceeds the limit; create an FS and copy
accessSem.Wait();
// Get the underlying byte[]
byte[] dataBytes = ((MemoryStream)dataStream).ToArray();
// Generate the hashcode from the data
dataHashCode = GenerateDataHash(dataBytes);
// Create the file, and copy the data
dataStream = CopyIntoFile(dataBytes, Path.Combine(folderPath, dataHashCode));
// Disregard the allocation from RAM
memoryAllocationRAM -= dataStream.Length;
accessSem.Release();
}
else if (dataStream.Length <= MemoryLimit && !IsInRam)
{
// Data stream is no larger than the limit, but the file is saved on disk; load to RAM instead
// Obtain a lock on the stream
accessSem.Wait();
// Copy data to memory
dataStream.Position = 0;
MemoryStream ms = new();
dataStream.CopyTo(ms);
// Increase the amount noted in RAM
memoryAllocationRAM += ms.Length;
// Close the FileStream
dataStream.Close();
// Delete the file
DeleteFile(Path.Combine(folderPath, dataHashCode));
// Set the datastream to the memory location
dataStream = ms;
accessSem.Release();
// Check the loaded data matches the hashcode we already have
string loadedHashCode = GenerateDataHash(ms.ToArray());
if (loadedHashCode != dataHashCode)
{
throw new UnauthorizedAccessException("Data was modified");
}
}
// Otherwise, the data is already in the correct spot, so we don't need to do anything else
}
}
private static void DeleteFile(string filePath)
{
try
{
File.Delete(filePath);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
public static implicit operator byte[](CachedByteArray c)
{
byte[] dataBytes = new byte[c.dataStream.Length];
c.accessSem.Wait();
if (c.IsInRam)
{
// We can use the underlying array from the MemoryStream
dataBytes = (c.dataStream as MemoryStream).ToArray();
}
else
{
// Ensure we read from the beginning of the stream
c.dataStream.Position = 0;
c.dataStream.Read(dataBytes, 0, dataBytes.Length);
}
c.accessSem.Release();
return dataBytes;
}
public static explicit operator CachedByteArray(byte[] b) => new CachedByteArray(b);
/// <summary>
/// Array index operator, allows the CachedByteArray to act more completely as a byte[]
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public byte this[int index]
{
get
{
int b;
// Aquire the semaphore, so that the read is effectively atomic (ensures the read is performed on the expected data)
accessSem.Wait();
dataStream.Position = index;
b = dataStream.ReadByte();
accessSem.Release();
if (b == -1)
{
throw new IndexOutOfRangeException(index.ToString());
}
return (byte)b;
}
}
public void Dispose()
{
accessSem.Wait();
// Remove the event listener
MemoryLimitModifiedEvent -= CachedByteArray_memoryLimitModifiedEvent;
// Remove the allocation from the counter
memoryAllocationTotal -= dataStream.Length;
memoryAllocationRAM -= dataStream.Length;
// If the FileStream is opened, close and dispose it
dataStream?.Dispose();
// If the file is on disk, remove it
DeleteFile(Path.Combine(folderPath, dataHashCode));
// Release all resources obtained by the semaphore
accessSem.Dispose();
}
}
}