First pass at events almost buttoned up.

The goal is to get the members list to update when new users enter and leave the party.
This commit is contained in:
watsonb8 2019-07-07 17:12:13 -04:00
parent 74f5d3e9c6
commit 823e1341ca
14 changed files with 563 additions and 20 deletions

View File

@ -93,5 +93,7 @@
Include="Proto\party.proto"/>
<Protobuf
Include="Proto\playback.proto"/>
<Protobuf
Include="Proto\events.proto"/>
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Xamarin.Forms;
using Aurora.Proto;
using Aurora.Proto.Party;
namespace Aurora.Design.Components.MemberList
{

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.ObjectModel;
using Aurora.Executors;
using Aurora.Proto;
using Aurora.Proto.Party;
using Xamarin.Forms;
namespace Aurora.Design.Views.Party
@ -32,6 +32,15 @@ namespace Aurora.Design.Views.Party
State(PartyState.SelectingHost);
}
~PartyViewModel()
{
if (_executor != null)
{
_executor.Close();
}
}
#region Properties
public ObservableCollection<PartyMember> Members

View File

@ -2,8 +2,7 @@ using System;
using System.Reflection;
using System.Linq;
using System.Collections.ObjectModel;
using Aurora.Proto;
using Aurora.Proto.Party;
namespace Aurora.Executors
{
public abstract class BaseExecutor

View File

@ -1,8 +1,13 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using Grpc.Core;
using Aurora.Proto;
using Aurora.Models;
using wellKnown = Google.Protobuf.WellKnownTypes;
using Aurora.Proto.General;
using Aurora.Proto.Party;
using Aurora.Proto.Playback;
using Aurora.Proto.Events;
using Aurora.Services;
namespace Aurora.Executors
{
@ -11,6 +16,8 @@ namespace Aurora.Executors
private Channel _channel;
private RemotePartyService.RemotePartyServiceClient _remotePartyClient;
private RemotePlaybackService.RemotePlaybackServiceClient _remotePlaybackClient;
private RemoteEventService.RemoteEventServiceClient _remoteEventsClient;
private Task _eventsTask;
private ObservableCollection<PartyMember> _partyMembers;
@ -27,16 +34,28 @@ namespace Aurora.Executors
#endregion Properties
/// <summary>
/// Initiates the connection to a party.
/// </summary>
/// <param name="hostname">The hostname of the gRPC server</param>
public override void Connect(string hostname)
{
_channel = new Channel(string.Format("{0}:{1}", hostname, SettingsService.Instance.DefaultPort), ChannelCredentials.Insecure);
_remotePartyClient = new RemotePartyService.RemotePartyServiceClient(_channel);
_remotePlaybackClient = new RemotePlaybackService.RemotePlaybackServiceClient(_channel);
_remoteEventsClient = new RemoteEventService.RemoteEventServiceClient(_channel);
//Assign but don't start task
_eventsTask = new Task(GetEvents);
JoinParty();
}
/// <summary>
/// Shutdown Connections
/// </summary>
/// <returns></returns>
public override async void Close()
{
await _channel.ShutdownAsync();
@ -69,6 +88,28 @@ namespace Aurora.Executors
UserName = SettingsService.Instance.Username,
});
RefreshMembers();
//Subscribe to events
SubscribeRequest req = new SubscribeRequest();
req.EventTypes.Add(EventType.PartyMemberJoined);
req.EventTypes.Add(EventType.PartyMemberLeft);
try
{
_remoteEventsClient.SubscribeToEvents(req);
}
catch (Exception ex)
{
Console.WriteLine("Error subscribing to events: " + ex.Message);
}
_eventsTask.Start();
}
private void RefreshMembers()
{
MembersResponse resposne = _remotePartyClient.GetPartyMembers(new Empty());
//Add members
foreach (PartyMember member in resposne.Members)
@ -85,5 +126,36 @@ namespace Aurora.Executors
}
}
}
private async void GetEvents()
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
using (AsyncServerStreamingCall<BaseEvent> eventStream = _remoteEventsClient.GetEvents(new Empty()))
{
while (await eventStream.ResponseStream.MoveNext(cancellationTokenSource.Token))
{
//Convert derived event type
BaseEvent e = eventStream.ResponseStream.Current;
switch (e.DerivedEventCase)
{
case BaseEvent.DerivedEventOneofCase.None:
{
throw new InvalidOperationException();
}
case BaseEvent.DerivedEventOneofCase.PartyMemberJoinedEvent:
{
PartyMemberJoinedEvent derivedEvent = e.PartyMemberJoinedEvent;
break;
}
case BaseEvent.DerivedEventOneofCase.PartyMemberLeftEvent:
{
PartyMemberJoinedEvent derivedEvent = e.PartyMemberJoinedEvent;
break;
}
}
}
}
}
}
}

View File

@ -4,7 +4,9 @@ using System.Collections.ObjectModel;
using Aurora.Models;
using Aurora.Executors;
using Aurora.Services;
using Aurora.Proto;
using Aurora.Proto.Party;
using Aurora.Proto.Playback;
using Aurora.Proto.Events;
using Aurora.RemoteImpl;
namespace Aurora.Executors
@ -13,10 +15,12 @@ namespace Aurora.Executors
{
RemotePartyServiceImpl _remotePartyServiceImpl;
RemotePlaybackServiceImpl _remotePlaybackImpl;
RemoteEventServiceImpl _remoteEventImpl;
public HostExecutor()
{
_remotePartyServiceImpl = new RemotePartyServiceImpl();
_remotePlaybackImpl = new RemotePlaybackServiceImpl();
_remoteEventImpl = new RemoteEventServiceImpl();
}
public override void Connect(string hostname)
@ -27,6 +31,7 @@ namespace Aurora.Executors
//Register grpc RemoteService with singleton server service
ServerService.Instance.RegisterService(RemotePartyService.BindService(_remotePartyServiceImpl));
ServerService.Instance.RegisterService(RemotePlaybackService.BindService(_remotePlaybackImpl));
ServerService.Instance.RegisterService(RemoteEventService.BindService(_remoteEventImpl));
//start gRPC server
ServerService.Instance.Start();

51
Aurora/Proto/events.proto Normal file
View File

@ -0,0 +1,51 @@
syntax = "proto3";
package Aurora.Proto.Events;
import "Proto/general.proto";
import "Proto/party.proto";
service RemoteEventService {
//Party Service
rpc GetEvents(Aurora.Proto.General.Empty) returns (stream BaseEvent) {};
rpc SubscribeToEvents(SubscribeRequest) returns(SubscriptionResponse);
rpc UnsubscribeFromEvents(UnsubscribeRequest) returns (SubscriptionResponse);
rpc UnsubscribeFromAll(UnsubscribeAllRequest) returns (SubscriptionResponse);
}
/* Subscription messages */
message SubscribeRequest {
repeated EventType eventTypes = 1;
}
message UnsubscribeRequest {
repeated EventType eventTypes = 1;
}
message UnsubscribeAllRequest {
}
message SubscriptionResponse {
bool successful = 1;
}
/* Event Types */
enum EventType {
PartyMemberJoined = 0;
PartyMemberLeft = 1;
}
message BaseEvent {
EventType eventType = 1;
oneof derivedEvent {
PartyMemberJoinedEvent partyMemberJoinedEvent = 2;
PartyMemberLeftEvent partyMemberLeftEvent = 3;
}
}
message PartyMemberJoinedEvent {
Aurora.Proto.Party.PartyMember member = 2;
}
message PartyMemberLeftEvent {
Aurora.Proto.Party.PartyMember member = 2;
}

View File

@ -1,10 +1,10 @@
syntax = "proto3";
package Aurora.Proto;
package Aurora.Proto.General;
message Chunk {
bytes Content = 1;
}
message Empty{
}
}

View File

@ -1,6 +1,6 @@
syntax = "proto3";
package Aurora.Proto;
package Aurora.Proto.Party;
import "Proto/general.proto";
@ -8,7 +8,7 @@ service RemotePartyService {
//Party Service
rpc JoinParty(JoinPartyRequest) returns (JoinPartyResponse);
rpc LeaveParty(LeavePartyRequest) returns (LeavePartyResponse);
rpc GetPartyMembers(Empty) returns (MembersResponse);
rpc GetPartyMembers(Aurora.Proto.General.Empty) returns (MembersResponse);
}
message JoinPartyRequest {

View File

@ -1,12 +1,12 @@
syntax = "proto3";
package Aurora.Proto;
package Aurora.Proto.Playback;
import "Proto/general.proto";
service RemotePlaybackService {
//Playback Service
rpc GetPartyStream(Empty) returns (stream Chunk) {};
rpc GetPartyStream(Aurora.Proto.General.Empty) returns (stream Aurora.Proto.General.Chunk) {};
}
enum TransferStatusCode {

View File

@ -0,0 +1,80 @@
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Aurora.Services;
using Aurora.Proto.Events;
using Aurora.Proto.General;
namespace Aurora.RemoteImpl
{
public class RemoteEventServiceImpl : RemoteEventService.RemoteEventServiceBase
{
public RemoteEventServiceImpl()
{
}
/// <summary>
/// RPC for getting event stream for a particular client.
/// </summary>
/// <param name="request">Empty</param>
/// <param name="responseStream">The response stream</param>
/// <param name="context">gRPC client context</param>
/// <returns></returns>
public async override Task GetEvents(Empty request, Grpc.Core.IServerStreamWriter<BaseEvent> responseStream, Grpc.Core.ServerCallContext context)
{
while (EventManager.Instance.GetSubscriptionCount(context.Peer) > 0)
{
List<BaseEvent> events = EventManager.Instance.GetSessionEvents(context.Peer);
foreach (BaseEvent currentEvent in events)
{
await responseStream.WriteAsync(currentEvent);
}
}
}
/// <summary>
/// RPC for subscribing to remote events
/// </summary>
/// <param name="request">SubscribeRequest</param>
/// <param name="context">gRPC client context</param>
/// <returns></returns>
public override Task<SubscriptionResponse> SubscribeToEvents(SubscribeRequest request, Grpc.Core.ServerCallContext context)
{
EventManager.Instance.AddSubscriptionList(context.Peer, request.EventTypes.ToList());
return Task.FromResult(new SubscriptionResponse { Successful = true });
}
/// <summary>
/// RPC for unsubscibing from events
/// </summary>
/// <param name="request">UnsubscribeRequest</param>
/// <param name="context">gRPC client context</param>
/// <returns></returns>
public override Task<SubscriptionResponse> UnsubscribeFromEvents(UnsubscribeRequest request, Grpc.Core.ServerCallContext context)
{
EventType[] eventTypes = null;
request.EventTypes.CopyTo(eventTypes, 0);
EventManager.Instance.RemoveSubscriptionList(context.Peer, eventTypes.ToList());
return Task.FromResult(new SubscriptionResponse { Successful = true });
}
/// <summary>
/// RPC for unsubscribing from all events
/// </summary>
/// <param name="request">UnsubscribeAllRequest</param>
/// <param name="context">gRPC client context</param>
/// <returns></returns>
public override Task<SubscriptionResponse> UnsubscribeFromAll(UnsubscribeAllRequest request, Grpc.Core.ServerCallContext context)
{
EventManager.Instance.RemoveAllSubscriptions(context.Peer);
return Task.FromResult(new SubscriptionResponse { Successful = true });
}
}
}

View File

@ -2,7 +2,10 @@ using System;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using System.Linq;
using Aurora.Proto;
using Google.Protobuf.WellKnownTypes;
using Aurora.Proto.Party;
using Aurora.Proto.General;
using Aurora.Proto.Events;
using Aurora.Models;
using Aurora.Services;
@ -39,11 +42,24 @@ namespace Aurora.RemoteImpl
public override Task<JoinPartyResponse> JoinParty(JoinPartyRequest request, Grpc.Core.ServerCallContext context)
{
_partyMembers.Add(new PartyMember()
PartyMember partyMember = new PartyMember()
{
UserName = request.UserName,
IpAddress = context.Host,
});
};
_partyMembers.Add(partyMember);
BaseEvent e = new BaseEvent
{
EventType = EventType.PartyMemberJoined,
PartyMemberJoinedEvent = new PartyMemberJoinedEvent
{
Member = partyMember,
}
};
EventManager.Instance.PushEvent(e);
JoinPartyResponse response = new JoinPartyResponse() { Status = PartyJoinedStatusEnum.Connected };
return Task.FromResult(response);
@ -51,12 +67,26 @@ namespace Aurora.RemoteImpl
public override Task<LeavePartyResponse> LeaveParty(LeavePartyRequest request, Grpc.Core.ServerCallContext context)
{
_partyMembers.Remove(_partyMembers.Where(e => e.Id == request.ClientId).Single());
PartyMember partyMember = _partyMembers.Where(e => e.Id == request.ClientId).Single();
_partyMembers.Remove(partyMember);
BaseEvent bv = new BaseEvent
{
EventType = EventType.PartyMemberJoined,
PartyMemberLeftEvent = new PartyMemberLeftEvent
{
Member = partyMember,
}
};
EventManager.Instance.PushEvent(bv);
LeavePartyResponse response = new LeavePartyResponse() { Status = PartyJoinedStatusEnum.Disconnected };
return Task.FromResult(response);
}
public override Task<MembersResponse> GetPartyMembers(Empty empty, Grpc.Core.ServerCallContext context)
public override Task<MembersResponse> GetPartyMembers(Proto.General.Empty empty, Grpc.Core.ServerCallContext context)
{
MembersResponse response = new MembersResponse();
response.Members.AddRange(_partyMembers);

View File

@ -1,7 +1,8 @@
using System;
using System.Threading.Tasks;
using System.IO;
using Aurora.Proto;
using Aurora.Proto.Playback;
using Aurora.Proto.General;
using Aurora.Models;
namespace Aurora.RemoteImpl
@ -14,7 +15,7 @@ namespace Aurora.RemoteImpl
}
public override async Task GetPartyStream(Empty empty,
public override Task GetPartyStream(Empty empty,
Grpc.Core.IServerStreamWriter<Chunk> responseStream,
Grpc.Core.ServerCallContext context)
{

294
Aurora/Services/EventManager.cs Executable file
View File

@ -0,0 +1,294 @@
using System;
using System.IO;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Reflection;
using Aurora.Proto.Events;
using Aurora.Models;
namespace Aurora.Services
{
public class EventManager : BaseService<EventManager>
{
//TODO purge inactive sessions from the list
public EventManager()
{
_eventQueues = new ConcurrentDictionary<string, BlockingCollection<BaseEvent>>();
_subscriptionList = new Dictionary<string, List<EventType>>();
_unprocessedEventsList = new ObservableCollection<BaseEvent>();
_unprocessedEventsList.CollectionChanged += UnprocessedEventsList_Changed;
}
#region Fields
private ConcurrentDictionary<string, BlockingCollection<BaseEvent>> _eventQueues;
private Dictionary<string, List<EventType>> _subscriptionList;
private ObservableCollection<BaseEvent> _unprocessedEventsList;
#endregion Fields
#region Private Methods
/// <summary>
/// Add event to appropriate events list for subscribed sessions
/// </summary>
/// <param name="unprocessedEvent"></param>
private void ProcessEvent(BaseEvent unprocessedEvent)
{
lock (_subscriptionList)
{
lock (_unprocessedEventsList)
{
//Duplicate events into client event queues
foreach (KeyValuePair<string, List<EventType>> subscription in _subscriptionList)
{
//Active Events contains the session in question
if (_eventQueues.ContainsKey(subscription.Key))
{
//Add all of the events to active events that are subscribed
if (subscription.Value.Contains(unprocessedEvent.EventType))
{
BlockingCollection<BaseEvent> eventList;
_eventQueues.TryGetValue(subscription.Key, out eventList);
if (eventList != null)
{
eventList.Add(unprocessedEvent);
}
}
}
}
//Remove Event from list
_unprocessedEventsList.Remove(unprocessedEvent);
}
}
}
#endregion Private Methods
#region Public Methods
/// <summary>
/// Get the list of event type subscriptions for a given session id.
/// </summary>
/// <param name="session">Session Id</param>
/// <returns></returns>
public List<EventType> GetSubscriptionList(string session)
{
List<EventType> eventList = new List<EventType>();
if (_subscriptionList.ContainsKey(session))
{
_subscriptionList.TryGetValue(session, out eventList);
}
return eventList;
}
/// <summary>
/// Get the number of event subscriptions for a given session
/// </summary>
/// <param name="session">Session Id</param>
/// <returns></returns>
public int GetSubscriptionCount(string session)
{
List<EventType> eventList = new List<EventType>();
if (_subscriptionList.ContainsKey(session))
{
_subscriptionList.TryGetValue(session, out eventList);
}
return eventList.Count();
}
/// <summary>
/// Add a new subscription
/// </summary>
/// <param name="session"></param>
/// <param name="type"></param>
public bool AddSubscription(string session, EventType type)
{
bool success = false;
if (!_subscriptionList.ContainsKey(session))
{
//Add session to subscription list
List<EventType> eventList = new List<EventType>();
eventList.Add(type);
_subscriptionList.Add(session, eventList);
success = true;
//base.LogInformation(string.Format("Subscription removed for event type {0} subscription on session {1}", type.ToString(), session));
}
else
{
List<EventType> eventList;
_subscriptionList.TryGetValue(session, out eventList);
if (eventList != null)
{
eventList.Add(type);
success = true;
//base.LogInformation(string.Format("Subscription removed for event type {0} subscription on session {1}", type.ToString(), session));
}
}
//Add activeEvents if it doesn't exist
if (!_eventQueues.ContainsKey(session))
{
//Add session to active events
_eventQueues.TryAdd(session, new BlockingCollection<BaseEvent>());
}
return success;
}
/// <summary>
/// Add a list of subscriptions. This unsubscribes from unused events.
/// </summary>
/// <param name="session">The browser session id.</param>
/// <param name="types">The list of event types to subscribe to.</param>
public void AddSubscriptionList(string session, List<EventType> types)
{
RemoveAllSubscriptions(session);
foreach (EventType e in types)
{
AddSubscription(session, e);
}
}
/// <summary>
/// Unsubscribe from a given event type.
/// </summary>
/// <param name="session">Session Id</param>
/// <param name="type">Event Type to be removed</param>
public void RemoveSubscription(string session, EventType type)
{
if (_subscriptionList.ContainsKey(session))
{
List<EventType> eventTypeList;
_subscriptionList.TryGetValue(session, out eventTypeList);
if (eventTypeList != null && eventTypeList.Contains(type))
{
eventTypeList.Remove(type);
//base.LogInformation(string.Format("Subscription removed for event type {0} subscription on session {1}", type.ToString(), session));
}
}
}
public void RemoveSubscriptionList(string session, List<EventType> types)
{
foreach (EventType e in types)
{
RemoveSubscription(session, e);
}
}
/// <summary>
/// Remove all subscriptons for a given session.
/// </summary>
/// <param name="session">Session Id</param>
public void RemoveAllSubscriptions(string session)
{
if (_subscriptionList.ContainsKey(session))
{
_subscriptionList.Remove(session);
}
if (_eventQueues.ContainsKey(session))
{
BlockingCollection<BaseEvent> rem = null;
_eventQueues.TryRemove(session, out rem);
}
//base.LogInformation(string.Format("All subscriptions removed for event type {0}", session));
}
/// <summary>
/// Get a list of accumulated events for a given session. Timeout after 5 seconds if list is empty.
/// </summary>
/// <param name="session"></param>
/// <returns>List<BaseEvent></returns>
public List<BaseEvent> GetSessionEvents(string session)
{
BlockingCollection<BaseEvent> eList;
_eventQueues.TryGetValue(session, out eList);
List<BaseEvent> returnList = new List<BaseEvent>();
if (eList == null)
{
return returnList;
}
//Continue to take until the eList is not empty.
while (true)
{
BaseEvent e;
eList.TryTake(out e);
if (e != null)
{
returnList.Add(e);
}
else
{
break;
}
}
//In the event that eList was empty to begin with, wait for something to be appear or cancel
if (eList.Count == 0)
{
CancellationTokenSource tkSrc = new CancellationTokenSource(5000);
BaseEvent e = null;
try
{
e = eList.Take(tkSrc.Token);
}
catch (OperationCanceledException)
{
// eat this
}
catch (Exception)
{
}
if (e != null)
{
returnList.Add(e);
}
}
return returnList;
}
/// <summary>
/// Push a new event to the event queue.
/// </summary>
/// <param name="newEvent">The event to be pushed</param>
public void PushEvent(BaseEvent newEvent)
{
_unprocessedEventsList.Add(newEvent);
}
#endregion Public Methods
#region Events
private void UnprocessedEventsList_Changed(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
foreach (BaseEvent bEvent in e.NewItems)
{
ProcessEvent(bEvent);
}
break;
}
}
}
#endregion Events
}
}