Skip to content

Commit

Permalink
Add support for creating data providers in C# and apply a few Resharper
Browse files Browse the repository at this point in the history
suggestions
  • Loading branch information
lyonsil committed Jun 14, 2023
1 parent 2cf15bc commit f9268a5
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 46 deletions.
3 changes: 2 additions & 1 deletion c-sharp/JsonUtils/MessageConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ static MessageConverter()

foreach (var evt in GetObjectsOfClosestSubtypes<MessageEvent>())
{
s_eventTypeMap.Add(evt.EventType, evt.GetType());
if (evt.EventType != Enum<EventType>.Null)
s_eventTypeMap.Add(evt.EventType, evt.GetType());
}
}

Expand Down
4 changes: 2 additions & 2 deletions c-sharp/MessageHandlers/MessageHandlerRequestByRequestType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public IEnumerable<Message> HandleMessage(Message message)
var response = handler(request.Contents);
if (response.Success)
{
yield return new MessageResponse(
yield return MessageResponse.Succeeded(
request.RequestType,
request.RequestId,
request.SenderId,
Expand All @@ -48,7 +48,7 @@ public IEnumerable<Message> HandleMessage(Message message)
}
else
{
yield return new MessageResponse(
yield return MessageResponse.Failed(
request.RequestType,
request.RequestId,
request.SenderId,
Expand Down
28 changes: 19 additions & 9 deletions c-sharp/MessageHandlers/ResponseToRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,34 @@ namespace Paranext.DataProvider.MessageHandlers;
/// <summary>
/// Internally represents a response we generated to an incoming request
/// </summary>
public sealed record class ResponseToRequest
public sealed record ResponseToRequest
{
private ResponseToRequest(bool success, dynamic? details)
{
Success = success;
if (success)
Contents = details;
else
ErrorMessage = details;
}

/// <summary>
/// Response when there was an error - no contents
/// Response when successful
/// </summary>
public ResponseToRequest(string errorMessage)
public static ResponseToRequest Succeeded(dynamic? contents = null)
{
Success = false;
ErrorMessage = errorMessage;
// If the contents are sent as null it is assumed to be a failed response regardless of the value of "Success".
// Replace null with an empty list to avoid this confusing behavior.
contents ??= new List<object>();
return new ResponseToRequest(true, contents);
}

/// <summary>
/// Response when successful
/// Response when there was an error
/// </summary>
public ResponseToRequest(dynamic? contents)
public static ResponseToRequest Failed(string errorMessage)
{
Success = true;
Contents = contents;
return new ResponseToRequest(false, errorMessage);
}

public bool Success { get; }
Expand Down
49 changes: 32 additions & 17 deletions c-sharp/MessageTransports/PapiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed class PapiClient : IDisposable
private const int DISCONNECT_TIMEOUT = 2000;
private const int RECEIVE_BUFFER_LENGTH = 2048;
private const int MAX_OUTGOING_MESSAGES = 10;
private static readonly Encoding s_utf8WithoutBOM = new UTF8Encoding();
private static readonly Encoding s_utf8WithoutBom = new UTF8Encoding();
private static readonly Uri s_connectionUri = new("ws://localhost:8876");
private static readonly JsonSerializerOptions s_serializationOptions;

Expand Down Expand Up @@ -79,6 +79,7 @@ public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool isDisposing)' method
Dispose(isDisposing: true);
// ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor
GC.SuppressFinalize(this);
}

Expand Down Expand Up @@ -140,7 +141,7 @@ public async Task<bool> ConnectAsync()

if (!_clientInitializationComplete.Wait(CONNECT_TIMEOUT))
{
Console.Error.WriteLine("PapiClient did not connect");
await Console.Error.WriteLineAsync("PapiClient did not connect");
await DisconnectAsync();
return false;
}
Expand All @@ -162,9 +163,13 @@ public async Task DisconnectAsync()
SignalToBeginGracefulDisconnect();

if (_incomingMessageThread.IsAlive && !_incomingMessageThread.Join(DISCONNECT_TIMEOUT))
Console.Error.WriteLine("Incoming message thread did not shut down properly");
await Console.Error.WriteLineAsync(
"Incoming message thread did not shut down properly"
);
if (_outgoingMessageThread.IsAlive && !_outgoingMessageThread.Join(DISCONNECT_TIMEOUT))
Console.Error.WriteLine("Outgoing message thread did not shut down properly");
await Console.Error.WriteLineAsync(
"Outgoing message thread did not shut down properly"
);

if (Connected)
{
Expand Down Expand Up @@ -194,12 +199,12 @@ public void BlockUntilMessageHandlingComplete()
/// </summary>
/// <param name="requestType">The request type to register</param>
/// <param name="requestHandler">Method that is called when a request of the specified type is received from the server</param>
/// <param name="responseTimeoutInMS">Number of milliseconds to wait for the registration response to be received</param>
/// <param name="responseTimeoutInMs">Number of milliseconds to wait for the registration response to be received</param>
/// <returns>True if the registration was successful</returns>
public bool RegisterRequestHandler(
Enum<RequestType> requestType,
Func<dynamic, ResponseToRequest> requestHandler,
int responseTimeoutInMS = 1000
int responseTimeoutInMs = 1000
)
{
ObjectDisposedException.ThrowIf(_isDisposed, this);
Expand All @@ -216,7 +221,7 @@ public bool RegisterRequestHandler(

_messageHandlersForMyRequests[requestMessage.RequestId] = new MessageHandlerResponse(
requestMessage,
(bool success, dynamic? data) =>
(bool success, dynamic? _) =>
{
if (!success)
{
Expand All @@ -235,12 +240,13 @@ public bool RegisterRequestHandler(
registrationSucceeded = true;
}
// ReSharper disable once AccessToDisposedClosure
registrationComplete.Set();
}
);

QueueOutgoingMessage(requestMessage);
if (!registrationComplete.Wait(responseTimeoutInMS))
if (!registrationComplete.Wait(responseTimeoutInMs))
{
Console.Error.WriteLine(
$"No response came back when registering request type \"{requestType}\""
Expand Down Expand Up @@ -284,7 +290,7 @@ public void UnregisterEventHandler(
}

/// <summary>
/// Send an event message to the server/>.
/// Send an event message to the server.
/// </summary>
/// <param name="message">Event message to send</param>
public void SendEvent(MessageEvent message)
Expand Down Expand Up @@ -346,7 +352,9 @@ private async void HandleOutgoingMessages()
}
catch (Exception ex)
{
Console.Error.WriteLine($"Exception while sending outgoing messages: {ex}");
await Console.Error.WriteLineAsync(
$"Exception while sending outgoing messages: {ex}"
);
}
} while (!_cancellationToken.IsCancellationRequested && Connected);

Expand All @@ -359,11 +367,13 @@ private async Task SendOutgoingMessageAsync(Message message)
{
message.SenderId = _clientId;
string jsonData = JsonSerializer.Serialize(message, s_serializationOptions);
/* Helpful for debugging
Console.WriteLine(
"Sending message over websocket: {0}",
StringUtils.LimitLength(jsonData, 180)
);
byte[] data = s_utf8WithoutBOM.GetBytes(jsonData);
*/
byte[] data = s_utf8WithoutBom.GetBytes(jsonData);
await _webSocket.SendAsync(data, WebSocketMessageType.Text, true, _cancellationToken);
}

Expand All @@ -387,18 +397,21 @@ private async void HandleIncomingMessages()
{
Message? message = await ReceiveIncomingMessageAsync();
// Handle each message asynchronously so we can keep receiving more messages
_ = Task.Run(() =>
{
HandleIncomingMessage(message);
});
_ = Task.Run(
() =>
{
HandleIncomingMessage(message);
},
_cancellationToken
);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Exception while handling messages: {ex}");
await Console.Error.WriteLineAsync($"Exception while handling messages: {ex}");
}
} while (!_cancellationToken.IsCancellationRequested && Connected);

Expand Down Expand Up @@ -431,11 +444,13 @@ private async void HandleIncomingMessages()
}
} while (!result.EndOfMessage);

string jsonData = s_utf8WithoutBOM.GetString(message.GetBuffer(), 0, (int)message.Position);
string jsonData = s_utf8WithoutBom.GetString(message.GetBuffer(), 0, (int)message.Position);
/* Helpful for debugging
Console.WriteLine(
"Received message over websocket: {0}",
StringUtils.LimitLength(jsonData, 180)
);
*/
return JsonSerializer.Deserialize<Message>(jsonData, s_serializationOptions);
}

Expand Down
30 changes: 18 additions & 12 deletions c-sharp/Messages/MessageResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,41 @@ private MessageResponse() { }
/// <summary>
/// Response when there was an error - no contents
/// </summary>
public MessageResponse(
public static MessageResponse Failed(
Enum<RequestType> requestType,
int requestId,
int requesterId,
string errorMessage
)
{
RequestType = requestType;
RequestId = requestId;
RequesterId = requesterId;
Success = false;
ErrorMessage = errorMessage;
return new MessageResponse
{
RequestType = requestType,
RequestId = requestId,
RequesterId = requesterId,
Success = false,
ErrorMessage = errorMessage
};
}

/// <summary>
/// Response when successful
/// </summary>
public MessageResponse(
public static MessageResponse Succeeded(
Enum<RequestType> requestType,
int requestId,
int requesterId,
dynamic? contents
)
{
RequestType = requestType;
RequestId = requestId;
RequesterId = requesterId;
Success = true;
Contents = contents;
return new MessageResponse
{
RequestType = requestType,
RequestId = requestId,
RequesterId = requesterId,
Success = true,
Contents = contents
};
}

public sealed override Enum<MessageType> Type => MessageType.Response;
Expand Down
75 changes: 75 additions & 0 deletions c-sharp/NetworkObjects/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Paranext.DataProvider.MessageHandlers;
using Paranext.DataProvider.Messages;
using Paranext.DataProvider.MessageTransports;
using PtxUtils;
using System.Text.Json;

namespace Paranext.DataProvider.NetworkObjects
{
internal abstract class DataProvider : NetworkObject
{
protected DataProvider(string name, PapiClient papiClient)
: base(papiClient)
{
DataProviderName = name + "-data";
}

protected string DataProviderName { get; }

public void RegisterDataProvider()
{
RegisterNetworkObject(DataProviderName, FunctionHandler);
StartDataProvider();
}

// An array of strings serialized as JSON will be sent here.
// The first item in the array is the name of the function to call.
// All remaining items are arguments to pass to the function.
// Data providers must provide "get" and "set" functions.
private ResponseToRequest FunctionHandler(dynamic? request)
{
string[] arguments = JsonSerializer.Deserialize<string[]>(request);
if (arguments.Length == 0)
return ResponseToRequest.Failed(
$"No function name provided when calling data provider {DataProviderName}"
);

string functionName = arguments[0].ToUpperInvariant();
string[] parameters = arguments.Skip(1).ToArray();
return functionName switch
{
"GET" => HandleGetRequest(parameters),
"SET" => HandleSetRequest(parameters),
_ => ResponseToRequest.Failed($"Unexpected function call: {functionName}"),
};
}

/// <summary>
/// Notify all processes on the network that this data provider has new data
/// </summary>
protected void ReportDataUpdate()
{
var dataUpdateEventType = new Enum<EventType>($"{DataProviderName}:onDidUpdate");
PapiClient.SendEvent(new DataUpdateEvent(dataUpdateEventType, true));
}

/// <summary>
/// Once a data provider has started, it should send out update events whenever its data changes.
/// </summary>
protected abstract void StartDataProvider();

/// <summary>
/// Read a copy of the requested data
/// </summary>
/// <param name="arguments">The first value in the array is meant to scope what kind of data was requested</param>
/// <returns>ResponseToRequest value that either contains the requested data or an error message</returns>
protected abstract ResponseToRequest HandleGetRequest(string[] arguments);

/// <summary>
/// Write data to the provided scope
/// </summary>
/// <param name="arguments">The first value in the array is meant to scope what kind of data was provided</param>
/// <returns>ResponseToRequest value that either notes success or an error message describing the failure</returns>
protected abstract ResponseToRequest HandleSetRequest(string[] arguments);
}
}
16 changes: 16 additions & 0 deletions c-sharp/NetworkObjects/DataUpdateEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Paranext.DataProvider.Messages;
using PtxUtils;

namespace Paranext.DataProvider.NetworkObjects
{
internal sealed class DataUpdateEvent : MessageEventGeneric<bool>
{
// A parameterless constructor is required for serialization to work, but we never need to deserialize this particular event. So just use a null event type.
// Because the event types are dynamic based on data provider names, we can't create every possible event type ahead of time.
public DataUpdateEvent()
: base(Enum<EventType>.Null) { }

public DataUpdateEvent(Enum<EventType> eventType, bool eventContents)
: base(eventType, eventContents) { }
}
}
Loading

0 comments on commit f9268a5

Please sign in to comment.