Files
UnrealEngineUWP/Engine/Source/Programs/Shared/EpicGames.Perforce/NativePerforceConnection.cs
ben marsh 79a25a6f7d Add support for setting passwords on the native client at creation time.
#ROBOMERGE-AUTHOR: ben.marsh
#ROBOMERGE-SOURCE: CL 18113267 in //UE5/Main/...
#ROBOMERGE-BOT: STARSHIP (Main -> Release-Engine-Test) (v889-18060218)

[CL 18113359 by ben marsh in ue5-release-engine-test branch]
2021-11-09 16:43:52 -05:00

503 lines
14 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Perforce;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace EpicGames.Perforce
{
/// <summary>
/// Experimental implementation of <see cref="IPerforceConnection"/> which wraps the native C++ API.
/// </summary>
public class NativePerforceConnection : IPerforceConnection, IDisposable
{
const string NativeDll = "EpicGames.Perforce.Native";
[StructLayout(LayoutKind.Sequential)]
class NativeSettings
{
[MarshalAs(UnmanagedType.LPStr)]
public string? ServerAndPort;
[MarshalAs(UnmanagedType.LPStr)]
public string? User;
[MarshalAs(UnmanagedType.LPStr)]
public string? Password;
[MarshalAs(UnmanagedType.LPStr)]
public string? Client;
[MarshalAs(UnmanagedType.LPStr)]
public string? AppName;
[MarshalAs(UnmanagedType.LPStr)]
public string? AppVersion;
}
[StructLayout(LayoutKind.Sequential)]
class NativeReadBuffer
{
public IntPtr Data;
public int Length;
public int Count;
public int MaxLength;
public int MaxCount;
};
[StructLayout(LayoutKind.Sequential)]
class NativeWriteBuffer
{
public IntPtr Data;
public int MaxLength;
public int MaxCount;
};
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void OnBufferReadyFn(NativeReadBuffer ReadBuffer, [In, Out] NativeWriteBuffer WriteBuffer);
[DllImport(NativeDll, CallingConvention = CallingConvention.Cdecl)]
static extern IntPtr Client_Create(NativeSettings? Settings, NativeWriteBuffer WriteBuffer, IntPtr OnBufferReadyFnPtr);
[DllImport(NativeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
static extern void Client_Login(IntPtr Client, [MarshalAs(UnmanagedType.LPStr)] string Password);
[DllImport(NativeDll, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)]
static extern void Client_Command(IntPtr Client, [MarshalAs(UnmanagedType.LPStr)] string Command, int NumArgs, string[] Args);
[DllImport(NativeDll, CallingConvention = CallingConvention.Cdecl)]
static extern void Client_Destroy(IntPtr Client);
/// <summary>
/// A buffer used for native code to stream data into
/// </summary>
class PinnedBuffer : IDisposable
{
public byte[] Data { get; private set; }
public GCHandle Handle { get; private set; }
public IntPtr BasePtr { get; private set; }
public int MaxLength => Data.Length;
public PinnedBuffer(int MaxLength)
{
Data = new byte[MaxLength];
Handle = GCHandle.Alloc(Data, GCHandleType.Pinned);
BasePtr = Handle.AddrOfPinnedObject();
}
public void Resize(int MaxLength)
{
byte[] OldData = Data;
Handle.Free();
Data = new byte[MaxLength];
Handle = GCHandle.Alloc(Data, GCHandleType.Pinned);
BasePtr = Handle.AddrOfPinnedObject();
OldData.CopyTo(Data, 0);
}
public void Dispose()
{
Handle.Free();
}
}
/// <summary>
/// Response object for a request
/// </summary>
class Response : IPerforceOutput
{
NativePerforceConnection Outer;
PinnedBuffer? Buffer;
int BufferPos;
int BufferLen;
PinnedBuffer? NextBuffer;
int NextBufferPos;
int NextBufferLen;
public Channel<(PinnedBuffer Buffer, int Length)> ReadBuffers = Channel.CreateUnbounded<(PinnedBuffer, int)>();
public ReadOnlyMemory<byte> Data => (Buffer == null) ? ReadOnlyMemory<byte>.Empty : Buffer.Data.AsMemory(BufferPos, BufferLen);
public Response(NativePerforceConnection Outer)
{
this.Outer = Outer;
}
public async ValueTask DisposeAsync()
{
if (Buffer != null)
{
Outer.WriteBuffers.Add(Buffer);
}
if (NextBuffer != null)
{
Outer.WriteBuffers.Add(NextBuffer);
}
while (await ReadBuffers.Reader.WaitToReadAsync())
{
(PinnedBuffer, int) Buffer;
if (ReadBuffers.Reader.TryRead(out Buffer))
{
Outer.WriteBuffers.Add(Buffer.Item1);
}
}
Outer.ResponseCompleteEvent.Set();
}
async Task<(PinnedBuffer?, int)> GetNextReadBufferAsync()
{
for (; ; )
{
if (!await ReadBuffers.Reader.WaitToReadAsync())
{
return (null, 0);
}
(PinnedBuffer, int) Pair;
if (ReadBuffers.Reader.TryRead(out Pair))
{
return Pair;
}
}
}
public async Task<bool> ReadAsync(CancellationToken Token)
{
// If we don't have any data yet, wait until a read completes
if (Buffer == null)
{
(Buffer, BufferLen) = await GetNextReadBufferAsync();
return Buffer != null;
}
// Ensure there's some space in the current buffer. In order to handle cases where we want to read data straddling both buffers, copy 16k chunks
// back to the first buffer until we can read entirely from the second buffer.
int MaxAppend = Buffer.MaxLength - BufferLen;
if (MaxAppend == 0)
{
if (BufferPos > 0)
{
Buffer.Data.AsSpan(BufferPos, BufferLen - BufferPos).CopyTo(Buffer.Data);
BufferLen -= BufferPos;
BufferPos = 0;
}
else
{
Buffer.Resize(Buffer.MaxLength + 16384);
}
MaxAppend = Buffer.MaxLength - BufferLen;
}
// Read the next buffer
if (NextBuffer == null)
{
(NextBuffer, NextBufferLen) = await GetNextReadBufferAsync();
if (NextBuffer == null)
{
return false;
}
}
// Try to copy some data from the next buffer
int CopyLen = Math.Min(NextBufferLen - NextBufferPos, Math.Min(MaxAppend, 16384));
NextBuffer.Data.AsSpan(NextBufferPos, CopyLen).CopyTo(Buffer.Data.AsSpan(BufferLen));
BufferLen += CopyLen;
NextBufferPos += CopyLen;
// If we've read everything from the next buffer, return it to the write list
if (NextBufferPos == NextBufferLen)
{
Outer.WriteBuffers.Add(NextBuffer);
NextBuffer = null;
NextBufferPos = 0;
NextBufferLen = 0;
}
return true;
}
public void Discard(int NumBytes)
{
if (NumBytes > 0)
{
// Update the read position
BufferPos += NumBytes;
Debug.Assert(BufferPos <= BufferLen);
// If we've used up all the data in the buffer, return it to the write list and move to the next one.
int OriginalBufferLen = BufferLen - NextBufferPos;
if (BufferPos >= OriginalBufferLen)
{
Outer.WriteBuffers.Add(Buffer!);
Buffer = NextBuffer;
BufferPos -= OriginalBufferLen;
BufferLen = NextBufferLen;
NextBuffer = null;
NextBufferPos = 0;
NextBufferLen = 0;
}
}
}
}
IntPtr Client;
PinnedBuffer[] Buffers;
OnBufferReadyFn OnBufferReadyInst;
IntPtr OnBufferReadyFnPtr;
Thread? BackgroundThread;
BlockingCollection<(Action, Response)?> Requests = new BlockingCollection<(Action, Response)?>();
BlockingCollection<PinnedBuffer> WriteBuffers = new BlockingCollection<PinnedBuffer>();
Response? CurrentResponse;
ManualResetEvent ResponseCompleteEvent;
/// <inheritdoc/>
public ILogger Logger { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="Logger">Logger for messages</param>
public NativePerforceConnection(ILogger Logger)
: this(2, 64 * 1024, Logger)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="BufferCount">Number of buffers to create for streaming response data</param>
/// <param name="BufferSize">Size of each buffer</param>
/// <param name="Logger">Logger for messages</param>
public NativePerforceConnection(int BufferCount, int BufferSize, ILogger Logger)
{
this.Logger = Logger;
Buffers = new PinnedBuffer[BufferCount];
for (int Idx = 0; Idx < BufferCount; Idx++)
{
Buffers[Idx] = new PinnedBuffer(BufferSize);
WriteBuffers.TryAdd(Buffers[Idx]);
}
OnBufferReadyInst = new OnBufferReadyFn(OnBufferReady);
OnBufferReadyFnPtr = Marshal.GetFunctionPointerForDelegate(OnBufferReadyInst);
ResponseCompleteEvent = new ManualResetEvent(false);
BackgroundThread = new Thread(BackgroundThreadProc);
BackgroundThread.Start();
}
void GetNextWriteBuffer(NativeWriteBuffer NativeWriteBuffer)
{
PinnedBuffer Buffer = WriteBuffers.Take();
NativeWriteBuffer.Data = Buffer.BasePtr;
NativeWriteBuffer.MaxLength = Buffer.Data.Length;
NativeWriteBuffer.MaxCount = int.MaxValue;
}
/// <summary>
/// Finalizer
/// </summary>
~NativePerforceConnection()
{
Dispose();
}
/// <inheritdoc/>
public void Dispose()
{
if (BackgroundThread != null)
{
Requests.Add(null);
Requests.CompleteAdding();
BackgroundThread.Join();
BackgroundThread = null!;
}
Requests.Dispose();
if (Client != IntPtr.Zero)
{
Client_Destroy(Client);
Client = IntPtr.Zero;
}
WriteBuffers.Dispose();
ResponseCompleteEvent.Dispose();
foreach (PinnedBuffer PinnedBuffer in Buffers)
{
PinnedBuffer.Dispose();
}
GC.SuppressFinalize(this);
}
/// <summary>
/// Initializes the connection, throwing an error on failure
/// </summary>
public async Task ConnectAsync(PerforceSettings? Settings)
{
PerforceError? Error = await TryConnectAsync(Settings);
if (Error != null)
{
throw new PerforceException(Error);
}
}
/// <summary>
/// Tries to initialize the connection
/// </summary>
/// <returns>Error returned when attempting to connect</returns>
public async Task<PerforceError?> TryConnectAsync(PerforceSettings? Settings)
{
await using Response Response = new Response(this);
NativeSettings? NativeSettings = null;
if (Settings != null)
{
NativeSettings = new NativeSettings();
NativeSettings.ServerAndPort = Settings.ServerAndPort;
NativeSettings.User = Settings.User;
NativeSettings.Password = Settings.Password;
NativeSettings.Client = Settings.Client;
NativeSettings.AppName = Settings.AppName;
NativeSettings.AppVersion = Settings.AppVersion;
}
Requests.Add((() =>
{
NativeWriteBuffer WriteBuffer = new NativeWriteBuffer();
GetNextWriteBuffer(WriteBuffer);
Client = Client_Create(NativeSettings, WriteBuffer, OnBufferReadyFnPtr);
}, Response));
List<PerforceResponse> Records = await ((IPerforceOutput)Response).ReadResponsesAsync(null, default);
if (Records.Count != 1)
{
throw new PerforceException("Expected at least one record to be returned from Init() call.");
}
PerforceError? Error = Records[0].Error;
if (Error == null)
{
throw new PerforceException("Unexpected response from init call");
}
if (Error.Severity != PerforceSeverityCode.Empty)
{
return Error;
}
return null;
}
/// <summary>
/// Background thread which sequences requests on a single thread. The Perforce API isn't async aware, but is primarily
/// I/O bound, so this thread will mostly be idle. All processing C#-side is done using async tasks, whereas this thread
/// blocks.
/// </summary>
void BackgroundThreadProc()
{
for (; ; )
{
(Action Action, Response Response)? Request = Requests.Take();
if (Request == null)
{
break;
}
CurrentResponse = Request.Value.Response;
ResponseCompleteEvent.Reset();
Request.Value.Action();
CurrentResponse.ReadBuffers.Writer.TryComplete();
ResponseCompleteEvent.WaitOne();
CurrentResponse = null;
}
}
/// <summary>
/// Callback for switching buffers
/// </summary>
/// <param name="ReadBuffer">The complete buffer</param>
/// <param name="WriteBuffer">Receives information about the next buffer to write to</param>
void OnBufferReady(NativeReadBuffer ReadBuffer, [In, Out] NativeWriteBuffer WriteBuffer)
{
PinnedBuffer Buffer = Buffers.First(x => x.BasePtr == ReadBuffer.Data);
CurrentResponse!.ReadBuffers.Writer.TryWrite((Buffer, ReadBuffer.Length)); // Unbounded; will always succeed
GetNextWriteBuffer(WriteBuffer);
}
/// <inheritdoc/>
public Task<IPerforceOutput> CommandAsync(string Command, IReadOnlyList<string> Arguments, IReadOnlyList<string>? FileArguments, byte[]? InputData)
{
if (InputData != null)
{
throw new NotImplementedException();
}
List<string> AllArguments = new List<string>(Arguments);
if (FileArguments != null)
{
AllArguments.AddRange(FileArguments);
}
Response Response = new Response(this);
Requests.Add((() => Client_Command(Client, Command, AllArguments.Count, AllArguments.ToArray()), Response));
return Task.FromResult<IPerforceOutput>(Response);
}
/// <inheritdoc/>
public async Task LoginAsync(string Password, CancellationToken CancellationToken = default)
{
await using Response Response = new Response(this);
Requests.Add((() => Client_Login(Client, Password), Response));
List<PerforceResponse> Records = await ((IPerforceOutput)Response).ReadResponsesAsync(null, default);
if (Records.Count != 1)
{
throw new PerforceException("Expected at least one record to be returned from Init() call.");
}
PerforceError? Error = Records[0].Error;
if (Error != null && Error.Severity != PerforceSeverityCode.Info)
{
throw new PerforceException(Error);
}
}
/// <inheritdoc/>
public Task SetAsync(string Name, string Value, CancellationToken CancellationToken = default)
{
throw new NotImplementedException();
}
/// <inheritdoc/>
public Task<string?> TryGetSettingAsync(string Name, CancellationToken CancellationToken = default)
{
throw new NotImplementedException();
}
}
}