Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for creating data providers in C# and apply a few Resharper suggestions #241

Merged
merged 5 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,8 @@ npm-debug.log.*
# Extra VS Code workspaces
*.code-workspace

# Extra Visual Studio files
launchSettings.json

# Test development user appdata files
dev-appdata/
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
94 changes: 94 additions & 0 deletions c-sharp/NetworkObjects/DataProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Collections.Concurrent;
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
{
// This is an internal class because nothing else should be instantiating it directly
private class MessageEventDataUpdated : MessageEventGeneric<string>
{
// A parameterless constructor is required for serialization to work
// ReSharper disable once UnusedMember.Local
public MessageEventDataUpdated()
: base(Enum<EventType>.Null) { }

public MessageEventDataUpdated(Enum<EventType> eventType, string dataScope)
: base(eventType, dataScope) { }
}

private readonly Enum<EventType> _eventType;
private readonly ConcurrentDictionary<
string,
MessageEventDataUpdated
> _updateEventsByScope = new();

protected DataProvider(string name, PapiClient papiClient)
: base(papiClient)
{
// "-data" is the suffix used by PAPI for data provider names
DataProviderName = name + "-data";

// "onDidUpdate" is the event name used by PAPI for data providers to notify consumers of updates
_eventType = new Enum<EventType>($"{DataProviderName}:onDidUpdate");
}

protected string DataProviderName { get; }

/// <summary>
/// Register this data provider on the network so that other services can use it
/// </summary>
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];
string[] parameters = arguments.Skip(1).ToArray();
return HandleRequest(functionName, parameters);
}

/// <summary>
/// Notify all processes on the network that this data provider has new data
/// </summary>
/// <param name="dataScope">Indicator of what data changed in the provider</param>
protected void SendDataUpdateEvent(string dataScope)
{
var dataUpdateEventMessage = _updateEventsByScope.GetOrAdd(
dataScope,
(scope) => new MessageEventDataUpdated(_eventType, scope)
);
PapiClient.SendEvent(dataUpdateEventMessage);
}

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

/// <summary>
/// Handle a request from a service using this data provider
/// </summary>
/// <param name="functionName">This would typically be "getXYZ" or "setXYZ", where "XYZ" is a type of data handled by this provider</param>
/// <param name="arguments">Optional arguments provided by the requester for the function indicated</param>
/// <returns>ResponseToRequest value that either contains a response for the function or an error message</returns>
protected abstract ResponseToRequest HandleRequest(string functionName, string[] arguments);
}
}
Loading