diff --git a/Aurora/Proto/party.v2.proto b/Aurora/Proto/party.v2.proto index 55eb9b2..61c71b3 100644 --- a/Aurora/Proto/party.v2.proto +++ b/Aurora/Proto/party.v2.proto @@ -81,7 +81,7 @@ message Party { string description = 3; string hostIp = 4; Member hostMember = 5; - google.protobuf.Timestamp createdTime = 6; + google.protobuf.Timestamp createdOn = 6; } enum PartyJoinedStatusEnum { @@ -97,9 +97,8 @@ message Member { //Resource name of the party member to be returned string name = 1; string userName = 2; - string id = 3; - string ipAddress = 4; - int32 port = 5; + string ipAddress = 3; + google.protobuf.Timestamp addedOn = 4; } message ListMembersRequest { @@ -122,8 +121,7 @@ message GetMemberRequest { message CreateMemberRequest { //Resource name of the parent collection of the member to be created (The party) string parent = 1; - string memberId = 2; - Member member = 3; + Member member = 2; } message UpdateMemberRequest { @@ -217,7 +215,7 @@ message MemberCreatedEvent { } message MemberDeletedEvent { - Member member = 1; + string memberName = 1; } message EventSubscription { diff --git a/Aurora/Services/Server/Controllers/EventController.cs b/Aurora/Services/Server/Controllers/EventController.cs new file mode 100644 index 0000000..399312c --- /dev/null +++ b/Aurora/Services/Server/Controllers/EventController.cs @@ -0,0 +1,14 @@ +using System; +using System.Threading.Tasks; +using Aurora.Proto.PartyV2; + +namespace Aurora.Services.Server.Controllers +{ + public partial class RemotePartyController : RemotePartyService.RemotePartyServiceBase + { + public override Task GetEvents(GetEventsRequest request, Grpc.Core.IServerStreamWriter responseStream, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/Controllers/EventSubscriptionController.cs b/Aurora/Services/Server/Controllers/EventSubscriptionController.cs new file mode 100644 index 0000000..d934bfd --- /dev/null +++ b/Aurora/Services/Server/Controllers/EventSubscriptionController.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Aurora.Proto.PartyV2; +using Aurora.Proto.General; + +namespace Aurora.Services.Server.Controllers +{ + public partial class RemotePartyController : RemotePartyService.RemotePartyServiceBase + { + public override Task ListEventSubscriptions(ListEventSubscriptionsRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task CreateEventSubscription(CreateEventSubscriptionRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task DeleteEventSubscription(DeleteEventSubscriptionRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task DeleteAllEventSubscriptions(DeleteAllEventSubscriptionsRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/Controllers/MediaController.cs b/Aurora/Services/Server/Controllers/MediaController.cs new file mode 100644 index 0000000..7af204e --- /dev/null +++ b/Aurora/Services/Server/Controllers/MediaController.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Aurora.Proto.PartyV2; + +namespace Aurora.Services.Server.Controllers +{ + public partial class RemotePartyController : RemotePartyService.RemotePartyServiceBase + { + public override Task ListMedia(ListMediaRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task GetMedia(GetMediaRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task StreamMedia(StreamMediaRequest request, Grpc.Core.IServerStreamWriter responseStream, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task SyncMedia(SyncMediaRequest request, Grpc.Core.IServerStreamWriter responseStream, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/Controllers/MemberController.cs b/Aurora/Services/Server/Controllers/MemberController.cs new file mode 100644 index 0000000..8c12689 --- /dev/null +++ b/Aurora/Services/Server/Controllers/MemberController.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections; +using Aurora.Proto.PartyV2; +using Aurora.Proto.General; +using Aurora.Utils; +using Grpc.Core; + +namespace Aurora.Services.Server.Controllers +{ + public partial class RemotePartyController : RemotePartyService.RemotePartyServiceBase + { + private SortedList _memberList; + + public override Task ListMembers(ListMembersRequest request, Grpc.Core.ServerCallContext context) + { + //Ignoring parent field because there is only one instance of the party + ListMembersResponse resp = new ListMembersResponse(); + + //Determine start idx + int startIdx = 0; + if (!string.IsNullOrEmpty(request.PageToken)) + { + startIdx = _memberList.IndexOfKey(request.PageToken) + 1; + } + + //Gather page + List members = (List)_memberList.Values; + resp.Members.AddRange(members.GetRange(startIdx, request.PageSize)); + + //Set next page token + resp.NextPageToken = resp.Members[(startIdx + request.PageSize) - 1].Name; + + return Task.FromResult(resp); + } + + public override Task GetMember(GetMemberRequest request, Grpc.Core.ServerCallContext context) + { + _memberList.TryGetValue(request.Name, out Member member); + + if (member == null) + { + throw new KeyNotFoundException(); + } + + return Task.FromResult(member); + } + + public override Task UpdateMember(UpdateMemberRequest request, Grpc.Core.ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override Task CreateMember(CreateMemberRequest request, Grpc.Core.ServerCallContext context) + { + //Generate Guid + string memberNameGuid = HashUtil.GetHashGuid(new string[] { context.Peer, request.Member.UserName }).ToString(); + string resourceName = string.Format("{0}/members/{1}", request.Parent, memberNameGuid); + //Check if already added + if (_memberList.ContainsKey(resourceName)) + { + throw new RpcException(new Status(StatusCode.AlreadyExists, "Member already exists")); + } + + request.Member.Name = resourceName; + + _memberList.Add(resourceName, request.Member); + + BaseEvent @event = new BaseEvent + { + EventType = EventType.MemberCreated, + MemberCreatedEvent = new MemberCreatedEvent + { + Member = request.Member, + } + }; + + //Fire event manager event + this._eventManager.FireEvent(@event); + + return Task.FromResult(request.Member); + } + + public override Task DeleteMember(DeleteMemberRequest request, Grpc.Core.ServerCallContext context) + { + string memberResourceName = request.Name; + //Check if member exists + if (!_memberList.ContainsKey(request.Name)) + { + throw new RpcException(new Status(StatusCode.NotFound, "Member not found")); + } + + _memberList.Remove(memberResourceName); + + BaseEvent @event = new BaseEvent + { + EventType = EventType.MemberDeleted, + MemberDeletedEvent = new MemberDeletedEvent + { + MemberName = memberResourceName, + } + }; + + _eventManager.FireEvent(@event); + _eventManager.RemoveAllSubscriptions(memberResourceName); + _eventManager.CancelEventStream(memberResourceName); + + return Task.FromResult(new Empty()); + } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/Controllers/PartyController.cs b/Aurora/Services/Server/Controllers/PartyController.cs new file mode 100644 index 0000000..452cf68 --- /dev/null +++ b/Aurora/Services/Server/Controllers/PartyController.cs @@ -0,0 +1,56 @@ +using System; +using System.Threading.Tasks; +using System.Collections.Generic; +using Aurora.Proto.PartyV2; +using Google.Protobuf.WellKnownTypes; + + +namespace Aurora.Services.Server.Controllers +{ + public partial class RemotePartyController : RemotePartyService.RemotePartyServiceBase + { + private string _displayName; + private string _description; + private Member _hostMember; + private DateTime _startDateTime; + + private EventManager.EventManager _eventManager; + + /// + /// Constructor for partial class + /// + public RemotePartyController(string partyName, string description) + { + this._startDateTime = DateTime.Now; + this._displayName = partyName; + this._description = description; + this._memberList = new SortedList(); + + string userName = SettingsService.Instance.Username; + + this._eventManager = new EventManager.EventManager(); + + this._hostMember = new Member() + { + Name = userName, + UserName = userName, + IpAddress = ServerService.GetLocalIPAddress(), + }; + } + + public override Task GetParty(Proto.General.Empty request, Grpc.Core.ServerCallContext context) + { + Party party = new Party() + { + Name = "party/party1", + DisplayName = this._displayName, + Description = this._description, + HostIp = ServerService.GetLocalIPAddress(), + HostMember = this._hostMember, + CreatedOn = Timestamp.FromDateTime(_startDateTime) + }; + + return Task.FromResult(party); + } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/EventManager/EventAction.cs b/Aurora/Services/Server/EventManager/EventAction.cs new file mode 100644 index 0000000..eabd846 --- /dev/null +++ b/Aurora/Services/Server/EventManager/EventAction.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Aurora.Proto.PartyV2; + +namespace Aurora.Services.Server.EventManager +{ + public class EventAction + { + public EventAction(Action callback, Action cancel) + { + Callback = callback; + Cancel = cancel; + } + public Action Callback { get; set; } + public Action Cancel { get; set; } + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/EventManager/EventManager.cs b/Aurora/Services/Server/EventManager/EventManager.cs new file mode 100644 index 0000000..732b41b --- /dev/null +++ b/Aurora/Services/Server/EventManager/EventManager.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Aurora.Proto.PartyV2; + +namespace Aurora.Services.Server.EventManager +{ + public class EventManager + { + #region Fields + private Dictionary> _subscriptionList; + private Dictionary _actionList; + + #endregion Fields + public EventManager() + { + _subscriptionList = new Dictionary>(); + _actionList = new Dictionary(); + + } + + #region Private Methods + + + #endregion Private Methods + + #region Public Methods + /// + /// Get the list of event type subscriptions for a given sessionIdentifier id. + /// + /// sessionIdentifier Id + /// + public List GetSubscriptionList(string sessionIdentifier) + { + List eventList = new List(); + if (_subscriptionList.ContainsKey(sessionIdentifier)) + { + _subscriptionList.TryGetValue(sessionIdentifier, out eventList); + } + + return eventList; + } + + /// + /// Get the number of event subscriptions for a given sessionIdentifier + /// + /// sessionIdentifier Id + /// + public int GetSubscriptionCount(string sessionIdentifier) + { + List eventList = new List(); + if (_subscriptionList.ContainsKey(sessionIdentifier)) + { + _subscriptionList.TryGetValue(sessionIdentifier, out eventList); + } + + return eventList.Count(); + } + + /// + /// Add a new subscription + /// + /// + /// + public bool AddSubscription(string sessionIdentifier, EventType type) + { + bool success = false; + lock (_subscriptionList) + { + if (!_subscriptionList.ContainsKey(sessionIdentifier)) + { + //Add sessionIdentifier to subscription list + List eventList = new List(); + eventList.Add(type); + _subscriptionList.Add(sessionIdentifier, eventList); + success = true; + } + else + { + _subscriptionList.TryGetValue(sessionIdentifier, out List eventList); + if (eventList != null) + { + eventList.Add(type); + success = true; + } + } + } + + return success; + } + + /// + /// Add a list of subscriptions. This unsubscribes from unused events. + /// + /// The browser sessionIdentifier id. + /// The list of event types to subscribe to. + public void AddSubscriptionList(string sessionIdentifier, List types) + { + RemoveAllSubscriptions(sessionIdentifier); + + foreach (EventType e in types) + { + AddSubscription(sessionIdentifier, e); + } + } + + /// + /// Unsubscribe from a given event type. + /// + /// sessionIdentifier Id + /// Event Type to be removed + public void RemoveSubscription(string sessionIdentifier, EventType type) + { + lock (_subscriptionList) + { + if (_subscriptionList.ContainsKey(sessionIdentifier)) + { + List eventTypeList; + _subscriptionList.TryGetValue(sessionIdentifier, out eventTypeList); + if (eventTypeList != null && eventTypeList.Contains(type)) + { + eventTypeList.Remove(type); + //base.LogInformation(string.Format("Subscription removed for event type {0} subscription on sessionIdentifier {1}", type.ToString(), sessionIdentifier)); + } + } + } + } + + public void RemoveSubscriptionList(string sessionIdentifier, List types) + { + foreach (EventType e in types) + { + RemoveSubscription(sessionIdentifier, e); + } + } + + /// + /// Remove all subscriptons for a given sessionIdentifier. + /// + /// sessionIdentifier Id + public void RemoveAllSubscriptions(string sessionIdentifier) + { + if (_subscriptionList.ContainsKey(sessionIdentifier)) + { + _subscriptionList.Remove(sessionIdentifier); + } + } + + public void AddEventHandler(Action action, Action cancel, string sessionIdentifierId) + { + lock (_actionList) + { + _actionList.Add(sessionIdentifierId, new EventAction(action, cancel)); + } + } + + public void RemoveEventHandler(string sessionIdentifierId) + { + _actionList.Remove(sessionIdentifierId); + } + + public void CancelEventStream(string sessionIdentifierId) + { + _actionList.TryGetValue(sessionIdentifierId, out EventAction value); + value.Cancel(); + + RemoveEventHandler(sessionIdentifierId); + } + + public void FireEvent(BaseEvent bEvent) + { + Dictionary actionsCopy = new Dictionary(); + //Copy actions list + lock (_actionList) + { + foreach (KeyValuePair pair in _actionList) + { + actionsCopy.Add(pair.Key, pair.Value); + } + } + + lock (_subscriptionList) + { + foreach (KeyValuePair> pair in _subscriptionList) + { + Task.Delay(1000); + //If action list contains an action for id, invoke + if (actionsCopy.ContainsKey(pair.Key)) + { + actionsCopy.TryGetValue(pair.Key, out EventAction action); + Task executionTask = new Task(() => action.Callback(bEvent)); + + //Execute task with exception handler + executionTask.ContinueWith((Task task) => + { + var exception = executionTask.Exception; + Console.WriteLine(string.Format("SERVER --- Exception occurred firing event")); + this._actionList.Remove(pair.Key); + }, + TaskContinuationOptions.OnlyOnFaulted); + + executionTask.Start(); + } + } + } + + } + + #endregion Public Methods + + } +} \ No newline at end of file diff --git a/Aurora/Services/Server/ServerService.cs b/Aurora/Services/Server/ServerService.cs new file mode 100644 index 0000000..d1fcfcf --- /dev/null +++ b/Aurora/Services/Server/ServerService.cs @@ -0,0 +1,125 @@ +using System; +using System.Threading.Tasks; +using System.Net; +using System.Net.Sockets; +using Grpc.Core; +using Aurora.Services.Server.Controllers; +using Aurora.Proto.PartyV2; + + +namespace Aurora.Services.Server +{ + public class ServerService : BaseService + { + private int _port = SettingsService.Instance.DefaultPort; + private string _hostname; + private Grpc.Core.Server _server; + + //Implementation class declarations + private RemotePartyController _remotePartyController; + + /// + /// Constructor. Registers GRPC service implementations. + /// + public ServerService() + { + string host = GetLocalIPAddress(); + + if (string.IsNullOrWhiteSpace(host)) + { + throw new Exception("This device must have a valid IP address"); + } + + _hostname = host; + + _server = new Grpc.Core.Server + { + Ports = { new ServerPort(_hostname, _port, ServerCredentials.Insecure) } + }; + } + + public int Port + { + get { return _port; } + } + + public string Hostname + { + get { return _hostname; } + } + + public bool Initialized + { + get + { + return (_remotePartyController != null && + _server != null); + } + } + + + /// + /// Start Server + /// + public void Start(string partyName, string description) + { + try + { + + Console.WriteLine(string.Format("Starting gRPC server at hostname: {0}, port: {1}", _hostname, _port)); + + if (!Initialized) + { + //Construct implementations + _remotePartyController = new RemotePartyController(partyName, description); + + // Register grpc RemoteService with singleton server service + RegisterService(RemotePartyService.BindService(_remotePartyController)); + + } + _server.Start(); + } + catch (Exception ex) + { + Console.WriteLine(string.Format("Error starting gRPC server: {0}", ex.Message)); + } + } + + /// + /// Shutdown server async. + /// + /// Task + public async Task Stop() + { + await _server.ShutdownAsync(); + } + + public async Task Reset() + { + await Stop(); + _server = new Grpc.Core.Server + { + Ports = { new ServerPort("localhost", _port, ServerCredentials.Insecure) } + }; + } + + private void RegisterService(ServerServiceDefinition definition) + { + _server.Services.Add(definition); + } + + public static string GetLocalIPAddress() + { + string returnIp = ""; + var host = Dns.GetHostEntry(Dns.GetHostName()); + foreach (var ip in host.AddressList) + { + if (ip.AddressFamily == AddressFamily.InterNetwork) + { + returnIp = ip.ToString(); + } + } + return returnIp; + } + } +} \ No newline at end of file diff --git a/Aurora/Utils/HashUtil.cs b/Aurora/Utils/HashUtil.cs new file mode 100644 index 0000000..7d0e2fb --- /dev/null +++ b/Aurora/Utils/HashUtil.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text; +using System; + +namespace Aurora.Utils +{ + public class HashUtil + { + public static Guid GetHashGuid(string[] inputs) + { + string input = ""; + foreach (string str in inputs) + { + input += str; + } + + Guid result; + using (SHA256 sha = SHA256.Create()) + { + byte[] hash = sha.ComputeHash(Encoding.Default.GetBytes(input)); + result = new Guid(hash); + } + + return result; + } + } +}